def axis_range(x_data_axis: np.ndarray) -> Tuple[float, float]:
            """
            Given the entries of one axis of the dependent variable, determine a min/max range over which to plot the fit.
            Args:
                x_data_axis: The entries of one axis of the dependent variable, i.e. x_data["x1"].

            Returns: A tuple representing the (min, max) value over which to plot that axis.
            """
            minval = np.min(x_data_axis)
            maxval = np.max(x_data_axis)

            return (minval, maxval)
Exemple #2
0
    def draw(self, draw_mcl=True, backend="matplotlib", show=True):
        """
        Draw the airfoil object.
        :param draw_mcl: Should we draw the mean camber line (MCL)? [boolean]
        :param backend: Which backend should we use? "plotly" or "matplotlib"
        :return: None
        """
        x = np.array(self.x()).reshape(-1)
        y = np.array(self.y()).reshape(-1)
        if draw_mcl:
            x_mcl = np.linspace(np.min(x), np.max(x), len(x))
            y_mcl = self.local_camber(x_mcl)

        if backend == "matplotlib":
            color = '#280887'
            plt.plot(x, y, ".-", zorder=11, color=color)
            plt.fill(x, y, zorder=10, color=color, alpha=0.2)
            if draw_mcl:
                plt.plot(x_mcl, y_mcl, "-", zorder=4, color=color, alpha=0.4)
            plt.axis("equal")
            plt.xlabel(r"$x/c$")
            plt.ylabel(r"$y/c$")
            plt.title(f"{self.name} Airfoil")
            plt.tight_layout()
            if show:
                plt.show()

        elif backend == "plotly":
            from aerosandbox.visualization.plotly import go
            fig = go.Figure()
            fig.add_trace(
                go.Scatter(x=x,
                           y=y,
                           mode="lines+markers",
                           name="Airfoil",
                           fill="toself",
                           line=dict(color="blue")), )
            if draw_mcl:
                fig.add_trace(
                    go.Scatter(x=x_mcl,
                               y=y_mcl,
                               mode="lines+markers",
                               name="Mean Camber Line (MCL)",
                               line=dict(color="navy")))
            fig.update_layout(xaxis_title="x/c",
                              yaxis_title="y/c",
                              yaxis=dict(scaleanchor="x", scaleratio=1),
                              title=f"{self.name} Airfoil")
            if show:
                fig.show()
            else:
                return fig
Exemple #3
0
 def draw(self, draw_mcl=True, backend="plotly", show=True):
     """
     Draw the airfoil object.
     :param draw_mcl: Should we draw the mean camber line (MCL)? [boolean]
     :param backend: Which backend should we use? "plotly" or "matplotlib"
     :return: None
     """
     x = np.array(self.x()).reshape(-1)
     y = np.array(self.y()).reshape(-1)
     if draw_mcl:
         x_mcl = np.linspace(np.min(x), np.max(x), len(x))
         y_mcl = self.local_camber(x_mcl)
     if backend == "plotly":
         fig = go.Figure()
         fig.add_trace(
             go.Scatter(x=x,
                        y=y,
                        mode="lines+markers",
                        name="Airfoil",
                        fill="toself",
                        line=dict(color="blue")), )
         if draw_mcl:
             fig.add_trace(
                 go.Scatter(x=x_mcl,
                            y=y_mcl,
                            mode="lines+markers",
                            name="Mean Camber Line (MCL)",
                            line=dict(color="navy")))
         fig.update_layout(xaxis_title="x/c",
                           yaxis_title="y/c",
                           yaxis=dict(scaleanchor="x", scaleratio=1),
                           title="%s Airfoil" % self.name)
         if show:
             fig.show()
         else:
             return fig
     elif backend == "matplotlib":
         fig, ax = plt.subplots(1, 1, figsize=(6.4, 4.8), dpi=200)
         plt.plot(x, y, ".-", zorder=11, color='#280887')
         if draw_mcl:
             plt.plot(x_mcl, y_mcl, "-", zorder=4, color='#28088744')
         plt.axis("equal")
         plt.xlabel(r"$x/c$")
         plt.ylabel(r"$y/c$")
         plt.title("%s Airfoil" % self.name)
         plt.tight_layout()
         if show:
             plt.show()
         else:
             return fig, ax
Exemple #4
0
    def alpha(
        self,
        alpha: Union[float, np.ndarray],
        start_at: Union[float, None] = 0,
    ) -> Dict[str, np.ndarray]:
        """
        Execute XFoil at a given angle of attack, or at a sequence of angles of attack.

        Args:

            alpha: The angle of attack [degrees]. Can be either a float or an iterable of floats, such as an array.

            start_at: Chooses whether to split a large sweep into two runs that diverge away from some central value,
            to improve convergence. As an example, if you wanted to sweep from alpha=-20 to alpha=20, you might want
            to instead do two sweeps and stitch them together: 0 to 20, and 0 to -20. `start_at` can be either:

                * None, in which case the alpha inputs are run as a single sequence in the order given.

                * A float that corresponds to an angle of attack (in degrees), in which case the alpha inputs are
                split into two sequences that diverge from the `start_at` value. Successful runs are then sorted by
                `alpha` before returning.

        Returns: A dictionary with the XFoil results. Dictionary values are arrays; they may not be the same shape as
        your input array if some points did not converge.

        """
        alphas = np.array(alpha).reshape(-1)

        if np.length(alphas) > 1:
            if start_at is not None:
                if np.min(alphas) < start_at < np.max(alphas):
                    alphas = np.sort(alphas)
                    alphas_upper = alphas[alphas > start_at]
                    alphas_lower = alphas[alpha <= start_at][::-1]

                    output = self._run_xfoil(
                        "\n".join([f"a {a}" for a in alphas_upper] + ["init"] +
                                  [f"a {a}" for a in alphas_lower]))

                    sort_order = np.argsort(output['alpha'])
                    output = {k: v[sort_order] for k, v in output.items()}
                    return output

        return self._run_xfoil("\n".join([f"a {a}" for a in alphas]))
Exemple #5
0
    def __init__(self,
                 x_data: Union[np.ndarray, Dict[str, np.ndarray]],
                 y_data: np.ndarray,
                 x_data_resample: Union[int, Dict[str, Union[int, np.ndarray]]] = 10,
                 resampling_interpolator: object = interpolate.RBFInterpolator,
                 resampling_interpolator_kwargs: Dict[str, Any] = None,
                 fill_value=np.NaN,  # Default behavior: return NaN for all inputs outside data range.
                 interpolated_model_kwargs: Dict[str, Any] = None,
                 ):
        """
        Creates the interpolator. Note that data must be unstructured (i.e., point cloud) for general N-dimensional
        interpolation.

        Note that if data is either 1D or structured,

        Args:

            x_data: Values of the dependent variable(s) in the dataset to be fitted. This is a dictionary; syntax is {
            var_name:var_data}.

                * If the model is one-dimensional (e.g. f(x1) instead of f(x1, x2, x3...)), you can instead supply x_data
                as a 1D ndarray. (If you do this, just treat `x` as an array in your model, not a dict.)

            y_data: Values of the independent variable in the dataset to be fitted. [1D ndarray of length n]

            x_data_resample: A parameter that guides how the x_data should be resampled onto a structured grid.

                * If this is an int, we look at each axis of the `x_data` (here, we'll call this `xi`),
                and we resample onto a linearly-spaced grid between `min(xi)` and `max(xi)` with `x_data_resample`
                points.

                * If this is a dict, it must be a dict where the keys are strings matching the keys of (the
                dictionary) `x_data`. The values can either be ints or 1D np.ndarrays.

                    * If the values are ints, then that axis is linearly spaced between `min(xi)` and `max(xi)` with
                    `x_data_resample` points.

                    * If the values are 1D np.ndarrays, then those 1D np.ndarrays are used as the resampled spacing
                    for the given axis.

            resampling_interpolator: Indicates the interpolator to use in order to resample the unstructured data
            onto a structured grid. Should be analogous to scipy.interpolate.RBFInterpolator in __init__ and __call__
            syntax. See reference here:

                * https://docs.scipy.org/doc/scipy/reference/generated/scipy.interpolate.RBFInterpolator.html

            resampling_interpolator_kwargs: Indicates keyword arguments (keyword-value pairs, as a dictionary) to
            pass into the resampling interpolator.

            fill_value: Gives the value that the interpolator should return for points outside of the interpolation
            domain. The interpolation domain is defined as the hypercube bounded by the coordinates specified in
            `x_data_resample`. By default, these coordinates are the tightest axis-aligned hypercube that bounds the
            point cloud data. If fill_value is None, then the interpolator will attempt to extrapolate if the interpolation method allows.

            interpolated_model_kwargs: Indicates keyword arguments to pass into the (structured) InterpolatedModel.
            Also a dictionary. See aerosandbox.InterpolatedModel for documentation on possible inputs here.

        """
        if resampling_interpolator_kwargs is None:
            resampling_interpolator_kwargs = {}
        if interpolated_model_kwargs is None:
            interpolated_model_kwargs = {}

        try:  # Try to use the InterpolatedModel initializer. If it doesn't work, then move on.
            super().__init__(
                x_data_coordinates=x_data,
                y_data_structured=y_data,
            )
            return
        except ValueError:
            pass

        # If it didn't work, this implies that x_data is multidimensional, and hence a dict-like object. Validate this.
        try:  # Determine type of `x_data`
            x_data.keys()
            x_data.values()
            x_data.items()
        except AttributeError:
            raise TypeError("`x_data` must be a dict-like object!")

        # Make the interpolator, based on x_data and y_data.
        if resampling_interpolator == interpolate.RBFInterpolator:
            resampling_interpolator_kwargs = {
                "kernel": "thin_plate_spline",
                "degree": 1,
                **resampling_interpolator_kwargs
            }

        interpolator = resampling_interpolator(
            y=np.stack(tuple(x_data.values()), axis=1),
            d=y_data,
            **resampling_interpolator_kwargs
        )

        # If x_data_resample is an int, make it into a dict that matches x_data.
        if isinstance(x_data_resample, int):
            x_data_resample = {
                k: x_data_resample
                for k in x_data.keys()
            }

        # Now, x_data_resample should be dict-like. Validate this.
        try:
            x_data_resample.keys()
            x_data_resample.values()
            x_data_resample.items()
        except AttributeError:
            raise TypeError("`x_data_resample` must be a dict-like object!")

        # Go through x_data_resample, and replace any values that are ints with linspaced arrays.
        for k, v in x_data_resample.items():
            if isinstance(v, int):
                x_data_resample[k] = np.linspace(
                    np.min(x_data[k]),
                    np.max(x_data[k]),
                    v
                )

        x_data_coordinates: Dict = x_data_resample

        x_data_structured_values = [
            xi.flatten()
            for xi in np.meshgrid(*x_data_coordinates.values(), indexing="ij")
        ]
        x_data_structured = {
            k: xi
            for k, xi in zip(x_data.keys(), x_data_structured_values)
        }

        y_data_structured = interpolator(
            np.stack(tuple(x_data_structured_values), axis=1)
        )
        y_data_structured = y_data_structured.reshape([
            np.length(xi)
            for xi in x_data_coordinates.values()
        ])

        interpolated_model_kwargs = {
            "fill_value": fill_value,
            **interpolated_model_kwargs
        }

        super().__init__(
            x_data_coordinates=x_data_coordinates,
            y_data_structured=y_data_structured,
            **interpolated_model_kwargs,
        )

        self.x_data_raw_unstructured = x_data
        self.y_data_raw = y_data
Exemple #6
0
def test_min():
    a = cas.SX([1, 2, 3])
    b = [1, 2, 3]

    assert int(np.min(a)) == int(np.min(b))
Exemple #7
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 #8
0
def contour(
        X: np.ndarray,
        Y: np.ndarray,
        Z: np.ndarray,
        levels: Union[int, List, np.ndarray] = 31,
        colorbar: bool = True,
        linelabels: bool = True,
        cmap=mpl.cm.get_cmap('viridis'),
        alpha: float = 0.7,
        extend: str = "both",
        linecolor="k",
        linewidths: float = 0.5,
        extendrect: bool = True,
        linelabels_format: Union[str, Callable[[float], str]] = eng_string,
        linelabels_fontsize: float = 8,
        colorbar_label: str = None,
        z_log_scale: bool = False,
        contour_kwargs: Dict = None,
        contourf_kwargs: Dict = None,
        colorbar_kwargs: Dict = None,
        linelabels_kwargs: Dict = None,
        **kwargs,
):
    """
    An analogue for plt.contour and plt.tricontour and friends that produces a much prettier default graph.

    Can take inputs with either contour or tricontour syntax.

    See syntax here:
        https://matplotlib.org/stable/api/_as_gen/matplotlib.pyplot.contour.html
        https://matplotlib.org/stable/api/_as_gen/matplotlib.pyplot.contourf.html
        https://matplotlib.org/stable/api/_as_gen/matplotlib.pyplot.tricontour.html
        https://matplotlib.org/stable/api/_as_gen/matplotlib.pyplot.tricontourf.html

    Args:
        X: See contour docs.

        Y: See contour docs.

        Z: See contour docs.

        levels: See contour docs.

        colorbar: Should we draw a colorbar?

        linelabels: Should we add line labels?

        cmap: What colormap should we use?

        alpha: What transparency should all plot elements be?

        extend: See contour docs.

        linecolor: What color should the line labels be?

        linewidths: See contour docs.

        extendrect: See colorbar docs.

        linelabels_format: See ax.clabel docs.

        linelabels_fontsize: See ax.clabel docs.

        contour_kwargs: Additional keyword arguments for contour.

        contourf_kwargs: Additional keyword arguments for contourf.

        colorbar_kwargs: Additional keyword arguments for colorbar.

        linelabels_kwargs: Additional keyword arguments for the line labels (ax.clabel).

        **kwargs: Additional keywords, which are passed to both contour and contourf.


    Returns: A tuple of (contour, contourf, colorbar) objects.

    """

    if contour_kwargs is None:
        contour_kwargs = {}
    if contourf_kwargs is None:
        contourf_kwargs = {}
    if colorbar_kwargs is None:
        colorbar_kwargs = {}
    if linelabels_kwargs is None:
        linelabels_kwargs = {}

    args = [
        X,
        Y,
        Z,
    ]

    shared_kwargs = kwargs
    if levels is not None:
        shared_kwargs["levels"] = levels
    if alpha is not None:
        shared_kwargs["alpha"] = alpha
    if extend is not None:
        shared_kwargs["extend"] = extend
    if z_log_scale:

        shared_kwargs = {
            "norm"   : mpl.colors.LogNorm(),
            "locator": mpl.ticker.LogLocator(
                subs=np.geomspace(1, 10, 4 + 1)[:-1]
            ),
            **shared_kwargs
        }

        if np.min(Z) <= 0:
            import warnings
            warnings.warn(
                "Warning: All values of the `Z` input to `contour()` should be nonnegative if `z_log_scale` is True!",
                stacklevel=2
            )
            Z = np.maximum(Z, 1e-300)  # Make all values nonnegative

    if colorbar_label is not None:
        colorbar_kwargs["label"] = colorbar_label

    contour_kwargs = {
        "colors"    : linecolor,
        "linewidths": linewidths,
        **shared_kwargs,
        **contour_kwargs
    }
    contourf_kwargs = {
        "cmap": cmap,
        **shared_kwargs,
        **contourf_kwargs
    }

    colorbar_kwargs = {
        "extendrect": extendrect,
        **colorbar_kwargs
    }

    linelabels_kwargs = {
        "inline"  : 1,
        "fontsize": linelabels_fontsize,
        "fmt"     : linelabels_format,
        **linelabels_kwargs
    }

    try:
        cont = plt.contour(*args, **contour_kwargs)
        contf = plt.contourf(*args, **contourf_kwargs)
    except TypeError as e:
        try:
            cont = plt.tricontour(*args, **contour_kwargs)
            contf = plt.tricontourf(*args, **contourf_kwargs)
        except TypeError:
            raise e

    if colorbar:
        cbar = plt.colorbar(**colorbar_kwargs)

        if z_log_scale:
            cbar.ax.yaxis.set_major_locator(mpl.ticker.LogLocator())
            cbar.ax.yaxis.set_major_formatter(mpl.ticker.LogFormatter())
    else:
        cbar = None

    if linelabels:
        plt.gca().clabel(cont, **linelabels_kwargs)

    return cont, contf, cbar