def max_thickness( self, x_over_c_sample: np.ndarray = np.linspace(0, 1, 101)) -> float: """ Returns the maximum thickness of the airfoil. Args: x_over_c_sample: Where should the airfoil be sampled to determine the max thickness? Returns: The maximum thickness, as a fraction of chord. """ return np.max(self.local_thickness(x_over_c=x_over_c_sample))
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)
def test_block_move_fixed_time(): opti = asb.Opti() n_timesteps = 300 time = np.linspace(0, 1, n_timesteps) dyn = asb.DynamicsPointMass1DHorizontal( mass_props=asb.MassProperties(mass=1), x_e=opti.variable(init_guess=np.linspace(0, 1, n_timesteps)), u_e=opti.variable(init_guess=1, n_vars=n_timesteps), ) u = opti.variable(init_guess=np.linspace(1, -1, n_timesteps)) dyn.add_force( Fx=u ) dyn.constrain_derivatives( opti=opti, time=time ) opti.subject_to([ dyn.x_e[0] == 0, dyn.x_e[-1] == 1, dyn.u_e[0] == 0, dyn.u_e[-1] == 0, ]) # effort = np.sum( # np.trapz(dyn.X ** 2) * np.diff(time) # ) effort = np.sum( # More sophisticated integral-of-squares integration (closed form correct) np.diff(time) / 3 * (u[:-1] ** 2 + u[:-1] * u[1:] + u[1:] ** 2) ) opti.minimize(effort) sol = opti.solve() dyn.substitute_solution(sol) assert dyn.x_e[0] == pytest.approx(0) assert dyn.x_e[-1] == pytest.approx(1) assert dyn.u_e[0] == pytest.approx(0) assert dyn.u_e[-1] == pytest.approx(0) assert np.max(dyn.u_e) == pytest.approx(1.5, abs=0.01) assert sol.value(u)[0] == pytest.approx(6, abs=0.05) assert sol.value(u)[-1] == pytest.approx(-6, abs=0.05)
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
def test_block_move_minimum_time(): opti = asb.Opti() n_timesteps = 300 time = np.linspace( 0, opti.variable(init_guess=1, lower_bound=0), n_timesteps, ) dyn = asb.DynamicsPointMass1DHorizontal( mass_props=asb.MassProperties(mass=1), x_e=opti.variable(init_guess=np.linspace(0, 1, n_timesteps)), u_e=opti.variable(init_guess=1, n_vars=n_timesteps), ) u = opti.variable(init_guess=np.linspace(1, -1, n_timesteps), lower_bound=-1, upper_bound=1) dyn.add_force( Fx=u ) dyn.constrain_derivatives( opti=opti, time=time ) opti.subject_to([ dyn.x_e[0] == 0, dyn.x_e[-1] == 1, dyn.u_e[0] == 0, dyn.u_e[-1] == 0, ]) opti.minimize( time[-1] ) sol = opti.solve() dyn.substitute_solution(sol) assert dyn.x_e[0] == pytest.approx(0) assert dyn.x_e[-1] == pytest.approx(1) assert dyn.u_e[0] == pytest.approx(0) assert dyn.u_e[-1] == pytest.approx(0) assert np.max(dyn.u_e) == pytest.approx(1, abs=0.01) assert sol.value(u)[0] == pytest.approx(1, abs=0.05) assert sol.value(u)[-1] == pytest.approx(-1, abs=0.05) assert np.mean(np.abs(sol.value(u))) == pytest.approx(1, abs=0.01)
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
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]))
def goodness_of_fit(self, type="R^2"): """ Returns a metric of the goodness of the fit. Args: type: Type of metric to use for goodness of fit. One of: * "R^2": The coefficient of determination. Strictly speaking only mathematically rigorous to use this for linear fits. https://en.wikipedia.org/wiki/Coefficient_of_determination * "deviation" or "Linf": The maximum deviation of the fit from any of the data points. Returns: The metric of the goodness of the fit. """ if type == "R^2": y_mean = np.mean(self.y_data) SS_tot = np.sum((self.y_data - y_mean)**2) y_model = self(self.x_data) SS_res = np.sum((self.y_data - y_model)**2) R_squared = 1 - SS_res / SS_tot return R_squared elif type == "deviation" or type == "Linf": return np.max(np.abs(self.y_data - self(self.x_data))) else: raise ValueError("Bad value of `type`!")
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
def test_max(): a = cas.SX([1, 2, 3]) b = [1, 2, 3] assert int(np.max(a)) == int(np.max(b))
def draw( self, vehicle_model: Airplane = None, backend: str = "pyvista", draw_axes: bool = True, scale_vehicle_model: Union[float, None] = None, n_vehicles_to_draw: int = 10, cg_axes: str = "geometry", show: bool = True, ): if backend == "pyvista": import pyvista as pv import aerosandbox.tools.pretty_plots as p if vehicle_model is None: default_vehicle_stl = _asb_root / "dynamics/visualization/default_assets/yf23.stl" vehicle_model = pv.read(str(default_vehicle_stl)) elif isinstance(vehicle_model, pv.PolyData): pass elif isinstance(vehicle_model, Airplane): vehicle_model = vehicle_model.draw(backend="pyvista", show=False) vehicle_model.rotate_y( 180) # Rotate from geometry axes to body axes. elif isinstance( vehicle_model, str ): # Interpret the string as a filepath to a .stl or similar try: pv.read(filename=vehicle_model) except: raise ValueError("Could not parse `vehicle_model`!") else: raise TypeError( "`vehicle_model` should be an Airplane or PolyData object." ) x_e = np.array(self.x_e) y_e = np.array(self.y_e) z_e = np.array(self.z_e) if np.length(x_e) == 1: x_e = x_e * np.ones(len(self)) if np.length(y_e) == 1: y_e = y_e * np.ones(len(self)) if np.length(z_e) == 1: z_e = z_e * np.ones(len(self)) if scale_vehicle_model is None: trajectory_bounds = np.array([ [x_e.min(), x_e.max()], [y_e.min(), y_e.max()], [z_e.min(), z_e.max()], ]) trajectory_size = np.max(np.diff(trajectory_bounds, axis=1)) vehicle_bounds = np.array(vehicle_model.bounds).reshape((3, 2)) vehicle_size = np.max(np.diff(vehicle_bounds, axis=1)) scale_vehicle_model = 0.1 * trajectory_size / vehicle_size ### Initialize the plotter plotter = pv.Plotter() # Set the window title title = "ASB Dynamics" addenda = [] if scale_vehicle_model != 1: addenda.append( f"Vehicle drawn at {scale_vehicle_model:.2g}x scale") addenda.append(f"{self.__class__.__name__} Engine") if len(addenda) != 0: title = title + f" ({'; '.join(addenda)})" plotter.title = title # Draw axes and grid plotter.add_axes() plotter.show_grid(color='gray') ### Draw the vehicle for i in np.unique( np.round(np.linspace(0, len(self) - 1, n_vehicles_to_draw))).astype(int): dyn = self[i] try: phi = dyn.phi except AttributeError: phi = dyn.bank try: theta = dyn.theta except AttributeError: theta = dyn.gamma try: psi = dyn.psi except AttributeError: psi = dyn.track x_cg_b, y_cg_b, z_cg_b = dyn.convert_axes(dyn.mass_props.x_cg, dyn.mass_props.y_cg, dyn.mass_props.z_cg, from_axes=cg_axes, to_axes="body") this_vehicle = copy.deepcopy(vehicle_model) this_vehicle.translate([ -x_cg_b, -y_cg_b, -z_cg_b, ], inplace=True) this_vehicle.points *= scale_vehicle_model this_vehicle.rotate_x(np.degrees(phi), inplace=True) this_vehicle.rotate_y(np.degrees(theta), inplace=True) this_vehicle.rotate_z(np.degrees(psi), inplace=True) this_vehicle.translate([ dyn.x_e, dyn.y_e, dyn.z_e, ], inplace=True) plotter.add_mesh(this_vehicle, ) if draw_axes: rot = np.rotation_matrix_from_euler_angles(phi, theta, psi) axes_scale = 0.5 * np.max( np.diff(np.array(this_vehicle.bounds).reshape((3, -1)), axis=1)) origin = np.array([ dyn.x_e, dyn.y_e, dyn.z_e, ]) for i, c in enumerate(["r", "g", "b"]): plotter.add_mesh( pv.Spline( np.array( [origin, origin + rot[:, i] * axes_scale])), color=c, line_width=2.5, ) for i in range(len(self)): ### Draw the trajectory line polyline = pv.Spline(np.array([x_e, y_e, z_e]).T) plotter.add_mesh( polyline, color=p.adjust_lightness(p.palettes["categorical"][0], 1.2), line_width=3, ) ### Finalize the plotter plotter.camera.up = (0, 0, -1) plotter.camera.Azimuth(90) plotter.camera.Elevation(60) if show: plotter.show() return plotter
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