def plot_Cf_flat_plates(): from aerosandbox.tools.pretty_plots import plt, show_plot Res = np.geomspace(1e3, 1e8, 500) for method in [ "blasius", "turbulent", "hybrid-cengel", "hybrid-schlichting", "hybrid-sharpe-convex", "hybrid-sharpe-nonconvex", ]: plt.loglog(Res, aero.Cf_flat_plate(Res, method=method), label=method) plt.ylim(1e-3, 1e-1) show_plot( "Models for Mean Skin Friction Coefficient of Flat Plate", r"$Re$", r"$C_f$", )
def plot_Cf_flat_plates(): sns.set(palette=sns.color_palette("husl")) fig, ax = plt.subplots(1, 1, figsize=(6.4, 4.8), dpi=200) Res = np.geomspace(1e3, 1e8, 500) for method in [ "blasius", "turbulent", "hybrid-cengel", "hybrid-schlichting", "hybrid-sharpe-convex", "hybrid-sharpe-nonconvex", ]: plt.loglog( Res, aero.Cf_flat_plate(Res, method=method), label=method ) plt.xlabel(r"$Re$") plt.ylabel(r"$C_f$") plt.ylim(1e-3, 1e-1) plt.title(r"Models for Mean Skin Friction Coefficient of Flat Plate") plt.tight_layout() plt.legend() plt.show()
import aerosandbox as asb import aerosandbox.numpy as np if __name__ == '__main__': af = asb.Airfoil("dae11") af.generate_polars() alpha = np.linspace(-40, 40, 300) re = np.geomspace(1e4, 1e12, 100) Alpha, Re = np.meshgrid(alpha, re) af.CL_function(alpha=0, Re=1e6) CL = af.CL_function(Alpha.flatten(), Re.flatten()).reshape(Alpha.shape) CD = af.CD_function(Alpha.flatten(), Re.flatten()).reshape(Alpha.shape) CM = af.CM_function(Alpha.flatten(), Re.flatten()).reshape(Alpha.shape) ##### Plot alpha-Re contours from aerosandbox.tools.pretty_plots import plt, show_plot, contour fig, ax = plt.subplots() contour(Alpha, Re, CL, levels=30, colorbar_label=r"$C_L$") plt.scatter(af.xfoil_data["alpha"], af.xfoil_data["Re"], color="k", alpha=0.2) plt.yscale('log') show_plot( f"Auto-generated Polar for {af.name} Airfoil", "Angle of Attack [deg]", "Reynolds Number [-]", )
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
5e3, 10e3, 13e3, 18e3, 22e3, 30e3, 34e3, 45e3, 49e3, 53e3, 69e3, 73e3, 77e3, 83e3, 87e3, ] + list(87e3 + np.geomspace(5e3, 2000e3, 11)) + list(0 - np.geomspace(5e3, 5000e3, 11))) altitude_knot_points = np.sort(np.unique(altitude_knot_points)) temperature_knot_points = temperature_isa(altitude_knot_points) pressure_knot_points = pressure_isa(altitude_knot_points) # creates interpolated model for temperature and pressure interpolated_temperature = InterpolatedModel( x_data_coordinates=altitude_knot_points, y_data_structured=temperature_knot_points, ) interpolated_log_pressure = InterpolatedModel( x_data_coordinates=altitude_knot_points, y_data_structured=np.log(pressure_knot_points),
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
import os, sys from pathlib import Path this_dir = Path(__file__).parent sys.path.insert(0, str(this_dir.parent)) from bike import Bike import aerosandbox.numpy as np from aerosandbox.tools.pretty_plots import plt, show_plot, set_ticks from scipy import optimize speed = 24 / 2.24 fig, ax = plt.subplots() t = np.linspace(0, 10, 500) gear_ratios = np.geomspace(0.020 / 0.700, 0.700 / 0.700, 300) def get_efficiency(gear_ratio): bike = Bike(gear_ratio=gear_ratio) try: perf = bike.steady_state_performance(speed=speed) except ValueError: return np.NaN return perf['motor state']['efficiency'] eff = np.array([get_efficiency(gear_ratio) for gear_ratio in gear_ratios]) plt.plot(gear_ratios, eff * 100)