diff --git a/aerosandbox/__init__.py b/aerosandbox/__init__.py index 624640676..5384178bc 100644 --- a/aerosandbox/__init__.py +++ b/aerosandbox/__init__.py @@ -22,6 +22,7 @@ def docs(): Opens the AeroSandbox documentation. """ import webbrowser + webbrowser.open_new( "https://github.com/peterdsharpe/AeroSandbox/tree/master/aerosandbox" ) # TODO: make this redirect to a hosted ReadTheDocs, or similar. @@ -34,9 +35,12 @@ def run_tests(): try: import pytest except ModuleNotFoundError: - raise ModuleNotFoundError("Please install `pytest` (`pip install pytest`) to run AeroSandbox unit tests.") + raise ModuleNotFoundError( + "Please install `pytest` (`pip install pytest`) to run AeroSandbox unit tests." + ) import matplotlib.pyplot as plt + with plt.ion(): # Disable blocking plotting pytest.main([str(_asb_root)]) diff --git a/aerosandbox/aerodynamics/aero_2D/IBL2.py b/aerosandbox/aerodynamics/aero_2D/IBL2.py index 1eb00a45a..0bad15a95 100644 --- a/aerosandbox/aerodynamics/aero_2D/IBL2.py +++ b/aerosandbox/aerodynamics/aero_2D/IBL2.py @@ -18,12 +18,13 @@ class IBL2(ImplicitAnalysis): """ @ImplicitAnalysis.initialize - def __init__(self, - streamwise_coordinate: np.ndarray, - edge_velocity: np.ndarray, - viscosity: float, - theta_0: float, - H_0: float = 2.6, - opti: Opti = None, - ): + def __init__( + self, + streamwise_coordinate: np.ndarray, + edge_velocity: np.ndarray, + viscosity: float, + theta_0: float, + H_0: float = 2.6, + opti: Opti = None, + ): pass diff --git a/aerosandbox/aerodynamics/aero_2D/airfoil_inviscid.py b/aerosandbox/aerodynamics/aero_2D/airfoil_inviscid.py index 98ae3b946..34426bdbe 100644 --- a/aerosandbox/aerodynamics/aero_2D/airfoil_inviscid.py +++ b/aerosandbox/aerodynamics/aero_2D/airfoil_inviscid.py @@ -1,7 +1,9 @@ from aerosandbox.common import * from aerosandbox.geometry import Airfoil from aerosandbox.performance import OperatingPoint -from aerosandbox.aerodynamics.aero_2D.singularities import calculate_induced_velocity_line_singularities +from aerosandbox.aerodynamics.aero_2D.singularities import ( + calculate_induced_velocity_line_singularities, +) import aerosandbox.numpy as np from typing import Union, List, Optional @@ -17,11 +19,12 @@ class AirfoilInviscid(ImplicitAnalysis): """ @ImplicitAnalysis.initialize - def __init__(self, - airfoil: Union[Airfoil, List[Airfoil]], - op_point: OperatingPoint, - ground_effect: bool = False, - ): + def __init__( + self, + airfoil: Union[Airfoil, List[Airfoil]], + op_point: OperatingPoint, + ground_effect: bool = False, + ): if isinstance(airfoil, Airfoil): self.airfoils = [airfoil] else: @@ -36,24 +39,30 @@ def __init__(self, self._calculate_forces() def __repr__(self): - return self.__class__.__name__ + "(\n\t" + "\n\t".join([ - f"airfoils={self.airfoils}", - f"op_point={self.op_point}", - ]) + "\n)" + return ( + self.__class__.__name__ + + "(\n\t" + + "\n\t".join( + [ + f"airfoils={self.airfoils}", + f"op_point={self.op_point}", + ] + ) + + "\n)" + ) def _setup_unknowns(self): for airfoil in self.airfoils: airfoil.gamma = self.opti.variable( - init_guess=0, - scale=self.op_point.velocity, - n_vars=airfoil.n_points() + init_guess=0, scale=self.op_point.velocity, n_vars=airfoil.n_points() ) airfoil.sigma = np.zeros(airfoil.n_points()) - def calculate_velocity(self, - x_field, - y_field, - ) -> [np.ndarray, np.ndarray]: + def calculate_velocity( + self, + x_field, + y_field, + ) -> [np.ndarray, np.ndarray]: ### Analyze the freestream u_freestream = self.op_point.velocity * np.cosd(self.op_point.alpha) v_freestream = self.op_point.velocity * np.sind(self.op_point.alpha) @@ -64,13 +73,15 @@ def calculate_velocity(self, for airfoil in self.airfoils: ### Add in the influence of the vortices and sources on the airfoil surface - u_field_induced, v_field_induced = calculate_induced_velocity_line_singularities( - x_field=x_field, - y_field=y_field, - x_panels=airfoil.x(), - y_panels=airfoil.y(), - gamma=airfoil.gamma, - sigma=airfoil.sigma, + u_field_induced, v_field_induced = ( + calculate_induced_velocity_line_singularities( + x_field=x_field, + y_field=y_field, + x_panels=airfoil.x(), + y_panels=airfoil.y(), + gamma=airfoil.gamma, + sigma=airfoil.sigma, + ) ) u_field = u_field + u_field_induced @@ -78,13 +89,15 @@ def calculate_velocity(self, ### Add in the influence of a source across the open trailing-edge panel. if airfoil.TE_thickness() != 0: - u_field_induced_TE, v_field_induced_TE = calculate_induced_velocity_line_singularities( - x_field=x_field, - y_field=y_field, - x_panels=[airfoil.x()[0], airfoil.x()[-1]], - y_panels=[airfoil.y()[0], airfoil.y()[-1]], - gamma=[0, 0], - sigma=[airfoil.gamma[0], airfoil.gamma[0]] + u_field_induced_TE, v_field_induced_TE = ( + calculate_induced_velocity_line_singularities( + x_field=x_field, + y_field=y_field, + x_panels=[airfoil.x()[0], airfoil.x()[-1]], + y_panels=[airfoil.y()[0], airfoil.y()[-1]], + gamma=[0, 0], + sigma=[airfoil.gamma[0], airfoil.gamma[0]], + ) ) u_field = u_field + u_field_induced_TE @@ -93,13 +106,15 @@ def calculate_velocity(self, if self.ground_effect: ### Add in the influence of the vortices and sources on the airfoil surface - u_field_induced, v_field_induced = calculate_induced_velocity_line_singularities( - x_field=x_field, - y_field=y_field, - x_panels=airfoil.x(), - y_panels=-airfoil.y(), - gamma=-airfoil.gamma, - sigma=airfoil.sigma, + u_field_induced, v_field_induced = ( + calculate_induced_velocity_line_singularities( + x_field=x_field, + y_field=y_field, + x_panels=airfoil.x(), + y_panels=-airfoil.y(), + gamma=-airfoil.gamma, + sigma=airfoil.sigma, + ) ) u_field = u_field + u_field_induced @@ -107,13 +122,15 @@ def calculate_velocity(self, ### Add in the influence of a source across the open trailing-edge panel. if airfoil.TE_thickness() != 0: - u_field_induced_TE, v_field_induced_TE = calculate_induced_velocity_line_singularities( - x_field=x_field, - y_field=y_field, - x_panels=[airfoil.x()[0], airfoil.x()[-1]], - y_panels=-1 * np.array([airfoil.y()[0], airfoil.y()[-1]]), - gamma=[0, 0], - sigma=[airfoil.gamma[0], airfoil.gamma[0]] + u_field_induced_TE, v_field_induced_TE = ( + calculate_induced_velocity_line_singularities( + x_field=x_field, + y_field=y_field, + x_panels=[airfoil.x()[0], airfoil.x()[-1]], + y_panels=-1 * np.array([airfoil.y()[0], airfoil.y()[-1]]), + gamma=[0, 0], + sigma=[airfoil.gamma[0], airfoil.gamma[0]], + ) ) u_field = u_field + u_field_induced_TE @@ -135,7 +152,7 @@ def _enforce_governing_equations(self): panel_dx = np.diff(airfoil.x()) panel_dy = np.diff(airfoil.y()) - panel_length = (panel_dx ** 2 + panel_dy ** 2) ** 0.5 + panel_length = (panel_dx**2 + panel_dy**2) ** 0.5 xp_hat_x = panel_dx / panel_length # x-coordinate of the xp_hat vector xp_hat_y = panel_dy / panel_length # y-coordinate of the yp_hat vector @@ -156,15 +173,16 @@ def _calculate_forces(self): for airfoil in self.airfoils: panel_dx = np.diff(airfoil.x()) panel_dy = np.diff(airfoil.y()) - panel_length = (panel_dx ** 2 + panel_dy ** 2) ** 0.5 + panel_length = (panel_dx**2 + panel_dy**2) ** 0.5 ### Sum up the vorticity on this airfoil by integrating airfoil.vorticity = np.sum( - (airfoil.gamma[1:] + airfoil.gamma[:-1]) / 2 * - panel_length + (airfoil.gamma[1:] + airfoil.gamma[:-1]) / 2 * panel_length ) - airfoil.Cl = 2 * airfoil.vorticity # TODO normalize by chord and freestream velocity etc. + airfoil.Cl = ( + 2 * airfoil.vorticity + ) # TODO normalize by chord and freestream velocity etc. self.total_vorticity = sum([airfoil.vorticity for airfoil in self.airfoils]) self.Cl = 2 * self.total_vorticity @@ -200,8 +218,8 @@ def draw_streamlines(self, res=200, show=True): U[contains] = np.nan V[contains] = np.nan - speed = (U ** 2 + V ** 2) ** 0.5 - Cp = 1 - speed ** 2 + speed = (U**2 + V**2) ** 0.5 + Cp = 1 - speed**2 ### Draw the airfoils for airfoil in self.airfoils: @@ -215,7 +233,7 @@ def draw_streamlines(self, res=200, show=True): color=speed, density=2.5, arrowsize=0, - cmap=p.mpl.colormaps.get_cmap('coolwarm_r'), + cmap=p.mpl.colormaps.get_cmap("coolwarm_r"), ) CB = plt.colorbar( orientation="horizontal", @@ -225,7 +243,7 @@ def draw_streamlines(self, res=200, show=True): CB.set_label(r"Relative Airspeed ($U/U_\infty$)") plt.clim(0.6, 1.4) - plt.gca().set_aspect('equal', adjustable='box') + plt.gca().set_aspect("equal", adjustable="box") plt.xlabel(r"$x/c$") plt.ylabel(r"$y/c$") plt.title(rf"Inviscid Airfoil: Flow Field") @@ -235,10 +253,11 @@ def draw_streamlines(self, res=200, show=True): def draw_cp(self, show=True): import matplotlib.pyplot as plt + fig, ax = plt.subplots(1, 1, figsize=(6.4, 4.8), dpi=200) for airfoil in self.airfoils: surface_speeds = airfoil.gamma - C_p = 1 - surface_speeds ** 2 + C_p = 1 - surface_speeds**2 plt.plot(airfoil.x(), C_p) @@ -252,13 +271,12 @@ def draw_cp(self, show=True): plt.show() -if __name__ == '__main__': +if __name__ == "__main__": a = AirfoilInviscid( airfoil=[ # Airfoil("naca4408") # .repanel(50) - Airfoil("e423") - .repanel(n_points_per_side=50), + Airfoil("e423").repanel(n_points_per_side=50), Airfoil("naca6408") .repanel(n_points_per_side=50) .scale(0.4, 0.4) @@ -268,7 +286,7 @@ def draw_cp(self, show=True): op_point=OperatingPoint( velocity=1, alpha=5, - ) + ), ) a.draw_streamlines() a.draw_cp() @@ -278,9 +296,6 @@ def draw_cp(self, show=True): opti2 = Opti() b = AirfoilInviscid( airfoil=Airfoil("naca4408"), - op_point=OperatingPoint( - velocity=1, - alpha=5 - ), - opti=opti2 + op_point=OperatingPoint(velocity=1, alpha=5), + opti=opti2, ) diff --git a/aerosandbox/aerodynamics/aero_2D/airfoil_optimizer/airfoil_optimizer.py b/aerosandbox/aerodynamics/aero_2D/airfoil_optimizer/airfoil_optimizer.py index 3546f4de2..129b3bcc3 100644 --- a/aerosandbox/aerodynamics/aero_2D/airfoil_optimizer/airfoil_optimizer.py +++ b/aerosandbox/aerodynamics/aero_2D/airfoil_optimizer/airfoil_optimizer.py @@ -5,7 +5,7 @@ import matplotlib.pyplot as plt import aerosandbox.tools.pretty_plots as p -if __name__ == '__main__': +if __name__ == "__main__": ### Design Conditions Re_des = 3e5 # Re to design to @@ -13,7 +13,9 @@ Cl_end = 1.5 # Upper bound of CLs that you care about (Effectively, CL_max) Cm_min = -0.08 # Worst-allowable pitching moment that you'll allow TE_thickness = 0.0015 # Sets trailing edge thickness - enforce_continuous_LE_radius = True # Should we force the leading edge to have continous curvature? + enforce_continuous_LE_radius = ( + True # Should we force the leading edge to have continous curvature? + ) ### Guesses for airfoil CST parameters; you usually don't need to change these lower_guess = -0.05 * np.ones(30) @@ -40,7 +42,6 @@ pack = lambda lower, upper: np.concatenate((lower, upper)) unpack = lambda pack: (pack[:n_lower], pack[n_lower:]) - def make_airfoil(x): """ A function that constructs an airfoil from a packed design vector. @@ -55,11 +56,10 @@ def make_airfoil(x): upper_weights=upper, enforce_continuous_LE_radius=enforce_continuous_LE_radius, TE_thickness=TE_thickness, - n_points_per_side=80 - ) + n_points_per_side=80, + ), ) - ### Initial guess construction x0 = pack(lower_guess, upper_guess) initial_airfoil = make_airfoil(x0) @@ -67,17 +67,17 @@ def make_airfoil(x): ### Initialize plotting fig = plt.figure(figsize=(15, 2.5)) ax = fig.add_subplot(111) - trace_initial, = ax.plot( + (trace_initial,) = ax.plot( initial_airfoil.coordinates[:, 0], initial_airfoil.coordinates[:, 1], - ':r', - label="Initial Airfoil" + ":r", + label="Initial Airfoil", ) - trace_current, = ax.plot( + (trace_current,) = ax.plot( initial_airfoil.coordinates[:, 0], initial_airfoil.coordinates[:, 1], "-b", - label="Current Airfoil" + label="Current Airfoil", ) plt.axis("equal") plt.xlabel(r"$x/c$") @@ -85,9 +85,8 @@ def make_airfoil(x): plt.title("Airfoil Optimization") plt.legend() - def draw( - airfoil # type: Airfoil + airfoil, # type: Airfoil ): """ Updates the "current airfoil" line on the plot with the given airfoil. @@ -99,13 +98,11 @@ def draw( plt.draw() plt.pause(0.001) - ### Utilities for tracking the design vector and objective throughout the optimization run iteration = 0 xs = [] fs = [] - def augmented_objective(x): """ Objective function with constraints added via a multiplicative external penalty method @@ -120,7 +117,7 @@ def augmented_objective(x): Re=Re_des, verbose=False, max_iter=40, - repanel=False + repanel=False, ) if np.isnan(xfoil["Cd"]).any(): return np.inf @@ -128,36 +125,37 @@ def augmented_objective(x): objective = np.sqrt(np.mean(xfoil["Cd"] ** 2)) # RMS penalty = 0 - penalty += np.sum(np.minimum(0, (xfoil["Cm"] - Cm_min) / 0.01) ** 2) # Cm constraint - penalty += np.minimum(0, (airfoil.TE_angle() - 5) / 1) ** 2 # TE angle constraint - penalty += np.minimum(0, (airfoil.local_thickness(0.90) - 0.015) / 0.005) ** 2 # Spar thickness constraint - penalty += np.minimum(0, (airfoil.local_thickness(0.30) - 0.12) / 0.005) ** 2 # Spar thickness constraint + penalty += np.sum( + np.minimum(0, (xfoil["Cm"] - Cm_min) / 0.01) ** 2 + ) # Cm constraint + penalty += ( + np.minimum(0, (airfoil.TE_angle() - 5) / 1) ** 2 + ) # TE angle constraint + penalty += ( + np.minimum(0, (airfoil.local_thickness(0.90) - 0.015) / 0.005) ** 2 + ) # Spar thickness constraint + penalty += ( + np.minimum(0, (airfoil.local_thickness(0.30) - 0.12) / 0.005) ** 2 + ) # Spar thickness constraint xs.append(x) fs.append(objective) return objective * (1 + penalty) - def callback(x): global iteration iteration += 1 - print( - f"Iteration {iteration}: Cd = {fs[-1]:.6f}" - ) + print(f"Iteration {iteration}: Cd = {fs[-1]:.6f}") if iteration % 1 == 0: airfoil = make_airfoil(x) draw(airfoil) ax.set_title(f"Airfoil Optimization: Iteration {iteration}") airfoil.write_dat("optimized_airfoil.dat") - draw(initial_airfoil) - initial_simplex = ( - (0.5 + 1 * np.random.random((len(x0) + 1, len(x0)))) - * x0 - ) + initial_simplex = (0.5 + 1 * np.random.random((len(x0) + 1, len(x0)))) * x0 initial_simplex[0, :] = x0 # Include x0 in the simplex print("Initializing simplex (give this a few minutes)...") res = optimize.minimize( @@ -166,12 +164,12 @@ def callback(x): method="Nelder-Mead", callback=callback, options={ - 'maxiter' : 10 ** 6, - 'initial_simplex': initial_simplex, - 'xatol' : 1e-8, - 'fatol' : 1e-6, - 'adaptive' : False, - } + "maxiter": 10**6, + "initial_simplex": initial_simplex, + "xatol": 1e-8, + "fatol": 1e-6, + "adaptive": False, + }, ) final_airfoil = make_airfoil(res.x) diff --git a/aerosandbox/aerodynamics/aero_2D/airfoil_polar_functions.py b/aerosandbox/aerodynamics/aero_2D/airfoil_polar_functions.py index 69f39ac61..a3d919df1 100644 --- a/aerosandbox/aerodynamics/aero_2D/airfoil_polar_functions.py +++ b/aerosandbox/aerodynamics/aero_2D/airfoil_polar_functions.py @@ -5,8 +5,8 @@ def airfoil_coefficients_post_stall( - airfoil: Airfoil, - alpha: float, + airfoil: Airfoil, + alpha: float, ): """ Estimates post-stall aerodynamics of an airfoil. @@ -39,11 +39,11 @@ def airfoil_coefficients_post_stall( pt2_star = -1.78e-1 pt3_star = -2.98e-1 - Cd90 = Cd90_0 + pn2_star * cosa + pn3_star * cosa ** 2 + Cd90 = Cd90_0 + pn2_star * cosa + pn3_star * cosa**2 CN = Cd90 * sina ##### Tangential force calculation - CT = (pt1_star + pt2_star * cosa + pt3_star * cosa ** 3) * sina ** 2 + CT = (pt1_star + pt2_star * cosa + pt3_star * cosa**3) * sina**2 ##### Conversion to wind axes CL = CN * cosa + CT * sina @@ -53,12 +53,10 @@ def airfoil_coefficients_post_stall( return CL, CD, CM -if __name__ == '__main__': +if __name__ == "__main__": af = Airfoil("naca0012") alpha = np.linspace(0, 360, 721) - CL, CD, CM = airfoil_coefficients_post_stall( - af, alpha - ) + CL, CD, CM = airfoil_coefficients_post_stall(af, alpha) from aerosandbox.tools.pretty_plots import plt, show_plot, set_ticks fig, ax = plt.subplots(1, 2, figsize=(8, 5)) diff --git a/aerosandbox/aerodynamics/aero_2D/ignore/viscous analysis.py b/aerosandbox/aerodynamics/aero_2D/ignore/viscous analysis.py index ac20386fd..bc005807c 100644 --- a/aerosandbox/aerodynamics/aero_2D/ignore/viscous analysis.py +++ b/aerosandbox/aerodynamics/aero_2D/ignore/viscous analysis.py @@ -23,27 +23,29 @@ Re_theta = ue * theta / nu H_star = np.where( - H < 4, - 1.515 + 0.076 * (H - 4) ** 2 / H, - 1.515 + 0.040 * (H - 4) ** 2 / H + H < 4, 1.515 + 0.076 * (H - 4) ** 2 / H, 1.515 + 0.040 * (H - 4) ** 2 / H ) # From AVF Eq. 4.53 -c_f = 2 / Re_theta * np.where( - H < 6.2, - -0.066 + 0.066 * np.abs(6.2 - H) ** 1.5 / (H - 1), - -0.066 + 0.066 * (H - 6.2) ** 2 / (H - 4) ** 2 +c_f = ( + 2 + / Re_theta + * np.where( + H < 6.2, + -0.066 + 0.066 * np.abs(6.2 - H) ** 1.5 / (H - 1), + -0.066 + 0.066 * (H - 6.2) ** 2 / (H - 4) ** 2, + ) ) # From AVF Eq. 4.54 -c_D = H_star / 2 / Re_theta * np.where( - H < 4, - 0.207 + 0.00205 * np.abs(4 - H) ** 5.5, - 0.207 - 0.100 * (H - 4) ** 2 / H ** 2 +c_D = ( + H_star + / 2 + / Re_theta + * np.where( + H < 4, + 0.207 + 0.00205 * np.abs(4 - H) ** 5.5, + 0.207 - 0.100 * (H - 4) ** 2 / H**2, + ) ) # From AVF Eq. 4.55 Re_theta_o = 10 ** ( - 2.492 / (H - 1) ** 0.43 + - 0.7 * ( - np.tanh( - 14 / (H - 1) - 9.24 - ) + 1 - ) + 2.492 / (H - 1) ** 0.43 + 0.7 * (np.tanh(14 / (H - 1) - 9.24) + 1) ) # From AVF Eq. 6.38 d_theta_dx = np.diff(theta) / np.diff(x) @@ -58,30 +60,21 @@ def int(x): def logint(x): # return int(x) logx = np.log(x) - return np.exp( - (logx[1:] + logx[:-1]) / 2 - ) + return np.exp((logx[1:] + logx[:-1]) / 2) ### Add governing equations opti.subject_to( - d_theta_dx == int(c_f) / 2 - int( - (H + 2) * theta / ue - ) * d_ue_dx, # From AVF Eq. 4.51 + d_theta_dx + == int(c_f) / 2 - int((H + 2) * theta / ue) * d_ue_dx, # From AVF Eq. 4.51 ) opti.subject_to( - logint(theta / H_star) * d_H_star_dx == - int(2 * c_D / H_star - c_f / 2) + - logint( - (H - 1) * theta / ue - ) * d_ue_dx + logint(theta / H_star) * d_H_star_dx + == int(2 * c_D / H_star - c_f / 2) + logint((H - 1) * theta / ue) * d_ue_dx ) ### Add initial conditions -opti.subject_to([ - theta[0] == theta_0, - H[0] == H_0 # Equilibrium value -]) +opti.subject_to([theta[0] == theta_0, H[0] == H_0]) # Equilibrium value ### Solve sol = opti.solve() diff --git a/aerosandbox/aerodynamics/aero_2D/mses.py b/aerosandbox/aerodynamics/aero_2D/mses.py index 8f40d8a32..94232cd97 100644 --- a/aerosandbox/aerodynamics/aero_2D/mses.py +++ b/aerosandbox/aerodynamics/aero_2D/mses.py @@ -63,31 +63,32 @@ class MSES(ExplicitAnalysis): """ - def __init__(self, - airfoil: Airfoil, - n_crit: float = 9., - xtr_upper: float = 1., - xtr_lower: float = 1., - max_iter: int = 100, - mset_command: str = "mset", - mses_command: str = "mses", - mplot_command: str = "mplot", - use_xvfb: bool = None, - xvfb_command: str = "xvfb-run -a", - verbosity: int = 1, - timeout_mset: Union[float, int, None] = 10, - timeout_mses: Union[float, int, None] = 60, - timeout_mplot: Union[float, int, None] = 10, - working_directory: str = None, - behavior_after_unconverged_run: str = "reinitialize", - mset_alpha: float = 0, - mset_n: int = 141, - mset_e: float = 0.4, - mset_io: int = 37, - mset_x: float = 0.850, - mses_mcrit: float = 0.99, - mses_mucon: float = -1.0, - ): + def __init__( + self, + airfoil: Airfoil, + n_crit: float = 9.0, + xtr_upper: float = 1.0, + xtr_lower: float = 1.0, + max_iter: int = 100, + mset_command: str = "mset", + mses_command: str = "mses", + mplot_command: str = "mplot", + use_xvfb: bool = None, + xvfb_command: str = "xvfb-run -a", + verbosity: int = 1, + timeout_mset: Union[float, int, None] = 10, + timeout_mses: Union[float, int, None] = 60, + timeout_mplot: Union[float, int, None] = 10, + working_directory: str = None, + behavior_after_unconverged_run: str = "reinitialize", + mset_alpha: float = 0, + mset_n: int = 141, + mset_e: float = 0.4, + mset_io: int = 37, + mset_x: float = 0.850, + mses_mcrit: float = 0.99, + mses_mucon: float = -1.0, + ): """ Interface to XFoil. Compatible with both XFoil v6.xx (public) and XFoil v7.xx (private, contact Mark Drela at MIT for a copy.) @@ -152,8 +153,11 @@ def __init__(self, shell=True, text=True, ) - expected_result = 'xvfb-run: usage error:' - use_xvfb = expected_result in trial_run.stderr or expected_result in trial_run.stdout + expected_result = "xvfb-run: usage error:" + use_xvfb = ( + expected_result in trial_run.stderr + or expected_result in trial_run.stdout + ) if not use_xvfb: xvfb_command = "" @@ -182,11 +186,12 @@ def __init__(self, self.mses_mcrit = mses_mcrit self.mses_mucon = mses_mucon - def run(self, - alpha: Union[float, np.ndarray, List] = 0., - Re: Union[float, np.ndarray, List] = 0., - mach: Union[float, np.ndarray, List] = 0.01, - ): + def run( + self, + alpha: Union[float, np.ndarray, List] = 0.0, + Re: Union[float, np.ndarray, List] = 0.0, + mach: Union[float, np.ndarray, List] = 0.01, + ): ### Make all inputs iterables: alphas, Res, machs = np.broadcast_arrays( np.ravel(alpha), @@ -207,7 +212,8 @@ def run(self, self.airfoil.write_dat(directory / airfoil_file) def mset(mset_alpha): - mset_keystrokes = dedent(f"""\ + mset_keystrokes = dedent( + f"""\ 15 case 7 @@ -224,7 +230,8 @@ def mset(mset_alpha): 3 4 0 - """) + """ + ) if self.verbosity >= 1: print(f"Generating mesh at alpha = {mset_alpha} with MSES...") @@ -237,7 +244,7 @@ def mset(mset_alpha): text=True, shell=True, check=True, - timeout=self.timeout_mset + timeout=self.timeout_mset, ) try: @@ -246,19 +253,25 @@ def mset(mset_alpha): print(e.stdout) print(e.stderr) if "BadName (named color or font does not exist)" in e.stderr: - raise RuntimeError("MSET via AeroSandbox errored becausee it couldn't launch an X11 window.\n" - "Try either installing a typical X11 client, or install Xvfb, which is\n" - "a virtual X11 server. More details in the AeroSandbox MSES docstring.") + raise RuntimeError( + "MSET via AeroSandbox errored becausee it couldn't launch an X11 window.\n" + "Try either installing a typical X11 client, or install Xvfb, which is\n" + "a virtual X11 server. More details in the AeroSandbox MSES docstring." + ) runs_output = {} for i, (alpha, mach, Re) in enumerate(zip(alphas, machs, Res)): if self.verbosity >= 1: - print(f"Solving alpha = {alpha:.3f}, mach = {mach:.4f}, Re = {Re:.3e} with MSES...") + print( + f"Solving alpha = {alpha:.3f}, mach = {mach:.4f}, Re = {Re:.3e} with MSES..." + ) with open(directory / "mses.case", "w+") as f: - f.write(dedent(f"""\ + f.write( + dedent( + f"""\ 3 4 5 7 3 4 5 7 {mach} 0.0 {alpha} | MACHin CLIFin ALFAin @@ -269,12 +282,16 @@ def mset(mset_alpha): 0 0 | ISMOVE ISPRES 0 0 | NMODN NPOSN - """)) + """ + ) + ) - mses_keystrokes = dedent(f"""\ + mses_keystrokes = dedent( + f"""\ {self.max_iter} 0 - """) + """ + ) mses_run = subprocess.run( f'{self.xvfb_command} "{self.mses_command}" case', @@ -284,7 +301,7 @@ def mset(mset_alpha): text=True, shell=True, check=True, - timeout=self.timeout_mses + timeout=self.timeout_mses, ) if self.verbosity >= 2: print(mses_run.stdout) @@ -294,7 +311,9 @@ def mset(mset_alpha): if not converged: if self.behavior_after_unconverged_run == "reinitialize": if self.verbosity >= 1: - print("Run did not converge. Reinitializing mesh and continuing...") + print( + "Run did not converge. Reinitializing mesh and continuing..." + ) try: next_alpha = alphas[i + 1] except IndexError: @@ -302,17 +321,21 @@ def mset(mset_alpha): mset_run = mset(mset_alpha=next_alpha) elif self.behavior_after_unconverged_run == "terminate": if self.verbosity >= 1: - print("Run did not converge. Skipping all subsequent runs...") + print( + "Run did not converge. Skipping all subsequent runs..." + ) break continue - mplot_keystrokes = dedent(f"""\ + mplot_keystrokes = dedent( + f"""\ 1 12 0 0 - """) + """ + ) mplot_run = subprocess.run( f'{self.xvfb_command} "{self.mplot_command}" case', @@ -322,40 +345,36 @@ def mset(mset_alpha): text=True, shell=True, check=True, - timeout=self.timeout_mplot + timeout=self.timeout_mplot, ) if self.verbosity >= 2: print(mplot_run.stdout) print(mplot_run.stderr) - raw_output = mplot_run.stdout. \ - replace("top Xtr", "xtr_top"). \ - replace("bot Xtr", "xtr_bot"). \ - replace("at x,y", "x_ac") + raw_output = ( + mplot_run.stdout.replace("top Xtr", "xtr_top") + .replace("bot Xtr", "xtr_bot") + .replace("at x,y", "x_ac") + ) run_output = AVL.parse_unformatted_data_output(raw_output) # Merge runs_output and run_output for k in run_output.keys(): try: - runs_output[k].append( - run_output[k] - ) + runs_output[k].append(run_output[k]) except KeyError: # List not created yet runs_output[k] = [run_output[k]] # Clean up the dictionary runs_output = {k: np.array(v) for k, v in runs_output.items()} # runs_output["mach"] = runs_output.pop("Ma") - runs_output = { - "mach": runs_output.pop("Ma"), - **runs_output - } + runs_output = {"mach": runs_output.pop("Ma"), **runs_output} return runs_output -if __name__ == '__main__': +if __name__ == "__main__": from pathlib import Path from pprint import pprint @@ -384,5 +403,5 @@ def mset(mset_alpha): import aerosandbox.tools.pretty_plots as p fig, ax = plt.subplots() - plt.plot(res['mach'], res['CD'], ".-") + plt.plot(res["mach"], res["CD"], ".-") p.show_plot() diff --git a/aerosandbox/aerodynamics/aero_2D/singularities/__init__.py b/aerosandbox/aerodynamics/aero_2D/singularities/__init__.py index 18fa52e2e..541cc3a56 100644 --- a/aerosandbox/aerodynamics/aero_2D/singularities/__init__.py +++ b/aerosandbox/aerodynamics/aero_2D/singularities/__init__.py @@ -1 +1,3 @@ -from .linear_strength_line_singularities import calculate_induced_velocity_line_singularities +from .linear_strength_line_singularities import ( + calculate_induced_velocity_line_singularities, +) diff --git a/aerosandbox/aerodynamics/aero_2D/singularities/linear_strength_line_singularities.py b/aerosandbox/aerodynamics/aero_2D/singularities/linear_strength_line_singularities.py index c51c9b79b..04df6602d 100644 --- a/aerosandbox/aerodynamics/aero_2D/singularities/linear_strength_line_singularities.py +++ b/aerosandbox/aerodynamics/aero_2D/singularities/linear_strength_line_singularities.py @@ -4,13 +4,13 @@ def _calculate_induced_velocity_line_singularity_panel_coordinates( - xp_field: Union[float, np.ndarray], - yp_field: Union[float, np.ndarray], - gamma_start: float = 0., - gamma_end: float = 0., - sigma_start: float = 0., - sigma_end: float = 0., - xp_panel_end: float = 1., + xp_field: Union[float, np.ndarray], + yp_field: Union[float, np.ndarray], + gamma_start: float = 0.0, + gamma_end: float = 0.0, + sigma_start: float = 0.0, + sigma_end: float = 0.0, + xp_panel_end: float = 1.0, ) -> [Union[float, np.ndarray], Union[float, np.ndarray]]: """ Calculates the induced velocity at a point (xp_field, yp_field) in a 2D potential-flow flowfield. @@ -25,7 +25,7 @@ def _calculate_induced_velocity_line_singularity_panel_coordinates( * gamma_end at (xp_panel_end, 0). # TODO update paragraph By convention here, positive gamma induces clockwise swirl in the flow field. - + Function returns the 2D velocity u, v in the local coordinate system of the panel. Inputs x and y can be 1D ndarrays representing various field points, @@ -48,42 +48,32 @@ def _calculate_induced_velocity_line_singularity_panel_coordinates( yp_field = np.array([yp_field]) ### Determine if you can skip either the vortex or source parts - skip_vortex_math = not ( - isinstance(gamma_start, cas.MX) or - isinstance(gamma_end, cas.MX) - ) and gamma_start == 0 and gamma_end == 0 - skip_source_math = not ( - isinstance(sigma_start, cas.MX) or - isinstance(sigma_end, cas.MX) - ) and sigma_start == 0 and sigma_end == 0 + skip_vortex_math = ( + not (isinstance(gamma_start, cas.MX) or isinstance(gamma_end, cas.MX)) + and gamma_start == 0 + and gamma_end == 0 + ) + skip_source_math = ( + not (isinstance(sigma_start, cas.MX) or isinstance(sigma_end, cas.MX)) + and sigma_start == 0 + and sigma_end == 0 + ) ### Determine which points are effectively on the panel, necessitating different math: is_on_panel = np.fabs(yp_field) <= 1e-8 ### Do some geometry calculation - r_1 = ( - xp_field ** 2 + - yp_field ** 2 - ) ** 0.5 - r_2 = ( - (xp_field - xp_panel_end) ** 2 + - yp_field ** 2 - ) ** 0.5 + r_1 = (xp_field**2 + yp_field**2) ** 0.5 + r_2 = ((xp_field - xp_panel_end) ** 2 + yp_field**2) ** 0.5 ### Regularize - is_on_endpoint = ( - (r_1 == 0) | (r_2 == 0) - ) + is_on_endpoint = (r_1 == 0) | (r_2 == 0) r_1 = np.where( r_1 == 0, 1, r_1, ) - r_2 = np.where( - r_2 == 0, - 1, - r_2 - ) + r_2 = np.where(r_2 == 0, 1, r_2) ### Continue geometry calculation theta_1 = np.arctan2(yp_field, xp_field) @@ -93,11 +83,7 @@ def _calculate_induced_velocity_line_singularity_panel_coordinates( tau = 2 * np.pi ### Regularize if the point is on the panel. - yp_field_regularized = np.where( - is_on_panel, - 1, - yp_field - ) + yp_field_regularized = np.where(is_on_panel, 1, yp_field) ### VORTEX MATH if skip_vortex_math: @@ -105,16 +91,10 @@ def _calculate_induced_velocity_line_singularity_panel_coordinates( v_vortex = 0 else: d_gamma = gamma_end - gamma_start - u_vortex_term_1_quantity = (yp_field - / tau - * d_gamma - / xp_panel_end - ) - u_vortex_term_2_quantity = ( - gamma_start * xp_panel_end + d_gamma * xp_field - ) / ( - tau * xp_panel_end - ) + u_vortex_term_1_quantity = yp_field / tau * d_gamma / xp_panel_end + u_vortex_term_2_quantity = (gamma_start * xp_panel_end + d_gamma * xp_field) / ( + tau * xp_panel_end + ) # Calculate u_vortex u_vortex_term_1 = u_vortex_term_1_quantity * ln_r_2_r_1 @@ -122,11 +102,7 @@ def _calculate_induced_velocity_line_singularity_panel_coordinates( u_vortex = u_vortex_term_1 + u_vortex_term_2 # Correct the u-velocity if field point is on the panel - u_vortex = np.where( - is_on_panel, - 0, - u_vortex - ) + u_vortex = np.where(is_on_panel, 0, u_vortex) # Calculate v_vortex v_vortex_term_1 = u_vortex_term_2_quantity * ln_r_2_r_1 @@ -134,10 +110,7 @@ def _calculate_induced_velocity_line_singularity_panel_coordinates( v_vortex_term_2 = np.where( is_on_panel, d_gamma / tau, - u_vortex_term_1_quantity * ( - xp_panel_end / yp_field_regularized - - d_theta - ), + u_vortex_term_1_quantity * (xp_panel_end / yp_field_regularized - d_theta), ) v_vortex = v_vortex_term_1 + v_vortex_term_2 @@ -148,27 +121,17 @@ def _calculate_induced_velocity_line_singularity_panel_coordinates( v_source = 0 else: d_sigma = sigma_end - sigma_start - v_source_term_1_quantity = (yp_field - / tau - * d_sigma - / xp_panel_end - ) - v_source_term_2_quantity = ( - sigma_start * xp_panel_end + d_sigma * xp_field - ) / ( - tau * xp_panel_end - ) + v_source_term_1_quantity = yp_field / tau * d_sigma / xp_panel_end + v_source_term_2_quantity = (sigma_start * xp_panel_end + d_sigma * xp_field) / ( + tau * xp_panel_end + ) # Calculate v_source v_source_term_1 = -v_source_term_1_quantity * ln_r_2_r_1 v_source_term_2 = v_source_term_2_quantity * d_theta v_source = v_source_term_1 + v_source_term_2 # Correct the v-velocity if field point is on the panel - v_source = np.where( - is_on_panel, - 0, - v_source - ) + v_source = np.where(is_on_panel, 0, v_source) # Calculate u_source u_source_term_1 = -v_source_term_2_quantity * ln_r_2_r_1 @@ -176,10 +139,7 @@ def _calculate_induced_velocity_line_singularity_panel_coordinates( u_source_term_2 = np.where( is_on_panel, -d_sigma / tau, - -v_source_term_1_quantity * ( - xp_panel_end / yp_field_regularized - - d_theta - ), + -v_source_term_1_quantity * (xp_panel_end / yp_field_regularized - d_theta), ) u_source = u_source_term_1 + u_source_term_2 @@ -189,31 +149,23 @@ def _calculate_induced_velocity_line_singularity_panel_coordinates( v = v_vortex + v_source ### If the field point is on the endpoint of the panel, replace the NaN with a zero. - u = np.where( - is_on_endpoint, - 0, - u - ) - v = np.where( - is_on_endpoint, - 0, - v - ) + u = np.where(is_on_endpoint, 0, u) + v = np.where(is_on_endpoint, 0, v) return u, v def _calculate_induced_velocity_line_singularity( - x_field: Union[float, np.ndarray], - y_field: Union[float, np.ndarray], - x_panel_start: float, - y_panel_start: float, - x_panel_end: float, - y_panel_end: float, - gamma_start: float = 0., - gamma_end: float = 0., - sigma_start: float = 0., - sigma_end: float = 0., + x_field: Union[float, np.ndarray], + y_field: Union[float, np.ndarray], + x_panel_start: float, + y_panel_start: float, + x_panel_end: float, + y_panel_end: float, + gamma_start: float = 0.0, + gamma_end: float = 0.0, + sigma_start: float = 0.0, + sigma_end: float = 0.0, ) -> [Union[float, np.ndarray], Union[float, np.ndarray]]: """ Calculates the induced velocity at a point (x_field, y_field) in a 2D potential-flow flowfield. @@ -235,7 +187,7 @@ def _calculate_induced_velocity_line_singularity( ### Calculate the panel coordinate transform (x -> xp, y -> yp), where panel_dx = x_panel_end - x_panel_start panel_dy = y_panel_end - y_panel_start - panel_length = (panel_dx ** 2 + panel_dy ** 2) ** 0.5 + panel_length = (panel_dx**2 + panel_dy**2) ** 0.5 panel_length = np.fmax(panel_length, 1e-16) @@ -249,8 +201,12 @@ def _calculate_induced_velocity_line_singularity( x_field_relative = x_field - x_panel_start y_field_relative = y_field - y_panel_start - xp_field = x_field_relative * xp_hat_x + y_field_relative * xp_hat_y # dot product with the xp unit vector - yp_field = x_field_relative * yp_hat_x + y_field_relative * yp_hat_y # dot product with the xp unit vector + xp_field = ( + x_field_relative * xp_hat_x + y_field_relative * xp_hat_y + ) # dot product with the xp unit vector + yp_field = ( + x_field_relative * yp_hat_x + y_field_relative * yp_hat_y + ) # dot product with the xp unit vector ### Do the vortex math up, vp = _calculate_induced_velocity_line_singularity_panel_coordinates( @@ -272,12 +228,12 @@ def _calculate_induced_velocity_line_singularity( def calculate_induced_velocity_line_singularities( - x_field: Union[float, np.ndarray], - y_field: Union[float, np.ndarray], - x_panels: np.ndarray, - y_panels: np.ndarray, - gamma: np.ndarray, - sigma: np.ndarray, + x_field: Union[float, np.ndarray], + y_field: Union[float, np.ndarray], + x_panels: np.ndarray, + y_panels: np.ndarray, + gamma: np.ndarray, + sigma: np.ndarray, ) -> [Union[float, np.ndarray], Union[float, np.ndarray]]: """ Calculates the induced velocity at a point (x_field, y_field) in a 2D potential-flow flowfield. @@ -301,8 +257,8 @@ def calculate_induced_velocity_line_singularities( except TypeError: N = x_panels.shape[0] - u_field = 0. - v_field = 0. + u_field = 0.0 + v_field = 0.0 for i in range(N - 1): u, v = _calculate_induced_velocity_line_singularity( @@ -323,7 +279,7 @@ def calculate_induced_velocity_line_singularities( return u_field, v_field -if __name__ == '__main__': +if __name__ == "__main__": X, Y = np.meshgrid( np.linspace(-2, 2, 50), @@ -341,7 +297,7 @@ def calculate_induced_velocity_line_singularities( x_panels=x_panels, y_panels=y_panels, gamma=1 * np.ones_like(x_panels), - sigma=1 * np.ones_like(x_panels) + sigma=1 * np.ones_like(x_panels), ) import matplotlib.pyplot as plt @@ -350,11 +306,7 @@ def calculate_induced_velocity_line_singularities( sns.set(palette=sns.color_palette("husl")) fig, ax = plt.subplots(1, 1, figsize=(6, 6), dpi=200) - plt.quiver( - X, Y, U, V, - (U ** 2 + V ** 2) ** 0.5, - scale=10 - ) + plt.quiver(X, Y, U, V, (U**2 + V**2) ** 0.5, scale=10) plt.axis("equal") plt.xlabel(r"$x$") plt.ylabel(r"$z$") diff --git a/aerosandbox/aerodynamics/aero_2D/singularities/test_singularities/test_linear_vortex_strength.py b/aerosandbox/aerodynamics/aero_2D/singularities/test_singularities/test_linear_vortex_strength.py index c62d89c3f..c964cd20f 100644 --- a/aerosandbox/aerodynamics/aero_2D/singularities/test_singularities/test_linear_vortex_strength.py +++ b/aerosandbox/aerodynamics/aero_2D/singularities/test_singularities/test_linear_vortex_strength.py @@ -8,7 +8,7 @@ def test_calculate_induced_velocity_panel_coordinates(): X, Y = np.meshgrid( np.linspace(-1, 2, 50), np.linspace(-1, 1, 50), - indexing='ij', + indexing="ij", ) X = X.flatten() Y = Y.flatten() @@ -33,8 +33,8 @@ def test_vortex_limit(): gamma=[1 / eps, 1 / eps], sigma=[0, 0], ) - assert u == pytest.approx(0, abs=eps ** 0.5) - assert v == pytest.approx(-1 / (2 * pi), abs=eps ** 0.5) + assert u == pytest.approx(0, abs=eps**0.5) + assert v == pytest.approx(-1 / (2 * pi), abs=eps**0.5) def test_source_limit(): @@ -47,8 +47,8 @@ def test_source_limit(): sigma=[1 / eps, 1 / eps], gamma=[0, 0], ) - assert u == pytest.approx(1 / (2 * pi), abs=eps ** 0.5) - assert v == pytest.approx(0, abs=eps ** 0.5) + assert u == pytest.approx(1 / (2 * pi), abs=eps**0.5) + assert v == pytest.approx(0, abs=eps**0.5) def test_zero_length_case(): @@ -65,6 +65,6 @@ def test_zero_length_case(): assert v == pytest.approx(0) -if __name__ == '__main__': +if __name__ == "__main__": test_zero_length_case() pytest.main() diff --git a/aerosandbox/aerodynamics/aero_2D/test_aero_2D/test_airfoil_inviscid.py b/aerosandbox/aerodynamics/aero_2D/test_aero_2D/test_airfoil_inviscid.py index e77d8c4a8..d6d52c37a 100644 --- a/aerosandbox/aerodynamics/aero_2D/test_aero_2D/test_airfoil_inviscid.py +++ b/aerosandbox/aerodynamics/aero_2D/test_aero_2D/test_airfoil_inviscid.py @@ -6,24 +6,18 @@ def test_airfoil_with_TE_gap(): a = asb.AirfoilInviscid( airfoil=asb.Airfoil("naca4408").repanel(100), - op_point=asb.OperatingPoint( - velocity=1, - alpha=5 - ) + op_point=asb.OperatingPoint(velocity=1, alpha=5), ) assert a.Cl == pytest.approx(1.0754, abs=0.01) # From XFoil def test_airfoil_symmetric_NACA(): a = asb.AirfoilInviscid( - airfoil=[ - asb.Airfoil("naca0012") - .repanel(50) - ], + airfoil=[asb.Airfoil("naca0012").repanel(50)], op_point=asb.OperatingPoint( velocity=1, alpha=0, - ) + ), ) assert a.Cl == pytest.approx(0, abs=1e-8) @@ -31,10 +25,7 @@ def test_airfoil_symmetric_NACA(): def test_airfoil_without_TE_gap(): a = asb.AirfoilInviscid( airfoil=asb.Airfoil("e423").repanel(100), - op_point=asb.OperatingPoint( - velocity=1, - alpha=5 - ) + op_point=asb.OperatingPoint(velocity=1, alpha=5), ) assert a.Cl == pytest.approx(1.9304, abs=0.01) # From XFoil @@ -42,33 +33,26 @@ def test_airfoil_without_TE_gap(): def test_airfoil_multielement(): a = asb.AirfoilInviscid( airfoil=[ - asb.Airfoil("e423") - .repanel(n_points_per_side=50), + asb.Airfoil("e423").repanel(n_points_per_side=50), asb.Airfoil("naca6408") .repanel(n_points_per_side=25) .scale(0.4, 0.4) .rotate(np.radians(-20)) .translate(0.9, -0.05), ], - op_point=asb.OperatingPoint( - velocity=1, - alpha=5 - ) + op_point=asb.OperatingPoint(velocity=1, alpha=5), ) def test_airfoil_ground_effect(): a = asb.AirfoilInviscid( airfoil=asb.Airfoil("naca4408").repanel(100).translate(0, 0.2), - op_point=asb.OperatingPoint( - velocity=1, - alpha=0 - ), - ground_effect=True + op_point=asb.OperatingPoint(velocity=1, alpha=0), + ground_effect=True, ) assert a.calculate_velocity(0, 0)[1] == pytest.approx(0) -if __name__ == '__main__': +if __name__ == "__main__": # pass pytest.main() diff --git a/aerosandbox/aerodynamics/aero_2D/test_aero_2D/test_neuralfoil_optimization.py b/aerosandbox/aerodynamics/aero_2D/test_aero_2D/test_neuralfoil_optimization.py index a368d90ff..b55174214 100644 --- a/aerosandbox/aerodynamics/aero_2D/test_aero_2D/test_neuralfoil_optimization.py +++ b/aerosandbox/aerodynamics/aero_2D/test_aero_2D/test_neuralfoil_optimization.py @@ -2,6 +2,7 @@ import aerosandbox.numpy as np import pytest + def test_single_point_optimization(): opti = asb.Opti() @@ -16,15 +17,14 @@ def test_single_point_optimization(): mach=0, ) - opti.subject_to( - aero["CL"] == 0.5 - ) + opti.subject_to(aero["CL"] == 0.5) sol = opti.solve() assert sol(alpha) == pytest.approx(4.52, abs=0.5) assert sol(aero["CL"]) == pytest.approx(0.5, abs=0.01) + def test_multi_point_optimization(): opti = asb.Opti() @@ -39,17 +39,15 @@ def test_multi_point_optimization(): mach=0, ) - opti.subject_to( - aero["CL"] == 0.5 - ) + opti.subject_to(aero["CL"] == 0.5) sol = opti.solve() - assert sol(alpha)[0] == pytest.approx(4.52, abs=0.5) assert sol(aero["CL"])[0] == pytest.approx(0.5, abs=0.01) -if __name__ == '__main__': + +if __name__ == "__main__": test_single_point_optimization() test_multi_point_optimization() - pytest.main() \ No newline at end of file + pytest.main() diff --git a/aerosandbox/aerodynamics/aero_2D/xfoil.py b/aerosandbox/aerodynamics/aero_2D/xfoil.py index ff4177e81..b83f5a10c 100644 --- a/aerosandbox/aerodynamics/aero_2D/xfoil.py +++ b/aerosandbox/aerodynamics/aero_2D/xfoil.py @@ -38,24 +38,25 @@ class XFoil(ExplicitAnalysis): class XFoilError(Exception): pass - def __init__(self, - airfoil: Airfoil, - Re: float = 0., - mach: float = 0., - n_crit: float = 9., - xtr_upper: float = 1., - xtr_lower: float = 1., - hinge_point_x: float = 0.75, - full_potential: bool = False, - max_iter: int = 100, - xfoil_command: str = "xfoil", - xfoil_repanel: bool = True, - xfoil_repanel_n_points: int = 279, - include_bl_data: bool = False, - verbose: bool = False, - timeout: Union[float, int, None] = 30, - working_directory: Union[Path, str] = None, - ): + def __init__( + self, + airfoil: Airfoil, + Re: float = 0.0, + mach: float = 0.0, + n_crit: float = 9.0, + xtr_upper: float = 1.0, + xtr_lower: float = 1.0, + hinge_point_x: float = 0.75, + full_potential: bool = False, + max_iter: int = 100, + xfoil_command: str = "xfoil", + xfoil_repanel: bool = True, + xfoil_repanel_n_points: int = 279, + include_bl_data: bool = False, + verbose: bool = False, + timeout: Union[float, int, None] = 30, + working_directory: Union[Path, str] = None, + ): """ Interface to XFoil. Compatible with both XFoil v6.xx (public) and XFoil v7.xx (private, contact Mark Drela at MIT for a copy.) @@ -140,7 +141,9 @@ def __init__(self, """ if mach >= 1: - raise ValueError("XFoil will terminate if a supersonic freestream Mach number is given.") + raise ValueError( + "XFoil will terminate if a supersonic freestream Mach number is given." + ) self.airfoil = airfoil self.Re = Re @@ -166,10 +169,11 @@ def __init__(self, def __repr__(self): return f"XFoil(airfoil={self.airfoil}, Re={self.Re}, mach={self.mach}, n_crit={self.n_crit})" - def _default_keystrokes(self, - airfoil_filename: str, - output_filename: str, - ) -> List[str]: + def _default_keystrokes( + self, + airfoil_filename: str, + output_filename: str, + ) -> List[str]: """ Returns a list of XFoil keystrokes that are common to all XFoil runs. @@ -254,16 +258,15 @@ def _default_keystrokes(self, ] # Include more data in polar - run_file_contents += [ - "cinc" # include minimum Cp - ] + run_file_contents += ["cinc"] # include minimum Cp return run_file_contents - def _run_xfoil(self, - run_command: str, - read_bl_data_from: str = None, - ) -> Dict[str, np.ndarray]: + def _run_xfoil( + self, + run_command: str, + read_bl_data_from: str = None, + ) -> Dict[str, np.ndarray]: """ Private function to run XFoil. @@ -291,15 +294,10 @@ def _run_xfoil(self, # Handle the keystroke file keystrokes = self._default_keystrokes( - airfoil_filename=airfoil_file, - output_filename=output_filename + airfoil_filename=airfoil_file, output_filename=output_filename ) keystrokes += [run_command] - keystrokes += [ - "pacc", # End polar accumulation - "", - "quit" - ] + keystrokes += ["pacc", "", "quit"] # End polar accumulation # Remove an old output file, if one exists: try: @@ -322,8 +320,7 @@ def _run_xfoil(self, # check=True ) outs, errs = proc.communicate( - input="\n".join(keystrokes), - timeout=self.timeout + input="\n".join(keystrokes), timeout=self.timeout ) return_code = proc.poll() @@ -335,20 +332,22 @@ def _run_xfoil(self, "XFoil run timed out!\n" "If this was not expected, try increasing the `timeout` parameter\n" "when you create this AeroSandbox XFoil instance.", - stacklevel=2 + stacklevel=2, ) except subprocess.CalledProcessError as e: if e.returncode == 11: raise self.XFoilError( "XFoil segmentation-faulted. This is likely because your input airfoil has too many points.\n" "Try repaneling your airfoil with `Airfoil.repanel()` before passing it into XFoil.\n" - "For further debugging, turn on the `verbose` flag when creating this AeroSandbox XFoil instance.") + "For further debugging, turn on the `verbose` flag when creating this AeroSandbox XFoil instance." + ) elif e.returncode == 8 or e.returncode == 136: raise self.XFoilError( "XFoil returned a floating point exception. This is probably because you are trying to start\n" "your analysis at an operating point where the viscous boundary layer can't be initialized based\n" "on the computed inviscid flow. (You're probably hitting a Goldstein singularity.) Try starting\n" - "your XFoil run at a less-aggressive (alpha closer to 0, higher Re) operating point.") + "your XFoil run at a less-aggressive (alpha closer to 0, higher Re) operating point." + ) elif e.returncode == 1: raise self.XFoilError( f"Command '{self.xfoil_command}' returned non-zero exit status 1.\n" @@ -389,15 +388,17 @@ def _run_xfoil(self, title_line = lines[i - 1] columns = title_line.split() - data_lines = lines[i + 1:] + data_lines = lines[i + 1 :] except IndexError: raise self.XFoilError( "XFoil output file is malformed; it doesn't have the expected number of lines.\n" "For debugging, the raw output file from XFoil is printed below:\n" + "\n".join(lines) - + "\nTitle line: " + title_line - + "\nColumns: " + str(columns) + + "\nTitle line: " + + title_line + + "\nColumns: " + + str(columns) ) def str_to_float(s: str) -> float: @@ -449,23 +450,24 @@ def str_to_float(s: str) -> float: "In previous testing, this occurs due to a bug in XFoil itself, with certain input combos.\n" "For debugging, the raw output file from XFoil is printed below:\n" + "\n".join(lines) - + "\nTitle line: " + title_line + + "\nTitle line: " + + title_line + f"\nIdentified {len(data)} data columns and {len(columns)} header columns." - + "\nColumns: " + str(columns) - + "\nData: " + str(data) + + "\nColumns: " + + str(columns) + + "\nData: " + + str(data) ) for i in range(len(columns)): output[columns[i]].append(data[i]) - output = { - k: np.array(v, dtype=float) - for k, v in output.items() - } + output = {k: np.array(v, dtype=float) for k, v in output.items()} # Read the BL data if read_bl_data_from is not None: import pandas as pd + bl_datas: List[pd.DataFrame] = [] if read_bl_data_from == "alpha": @@ -476,14 +478,26 @@ def str_to_float(s: str) -> float: for alpha in output["alpha"]: dump_filename = alpha_to_dump_mapping[ - min(alpha_to_dump_mapping.keys(), key=lambda x: abs(x - alpha)) + min( + alpha_to_dump_mapping.keys(), + key=lambda x: abs(x - alpha), + ) ] bl_datas.append( pd.read_csv( dump_filename, sep=r"\s+", - names=["s", "x", "y", "ue/vinf", "dstar", "theta", "cf", "H"], + names=[ + "s", + "x", + "y", + "ue/vinf", + "dstar", + "theta", + "cf", + "H", + ], skiprows=1, ) ) @@ -503,32 +517,42 @@ def str_to_float(s: str) -> float: pd.read_csv( dump_filename, sep=r"\s+", - names=["s", "x", "y", "ue/vinf", "dstar", "theta", "cf", "H"], + names=[ + "s", + "x", + "y", + "ue/vinf", + "dstar", + "theta", + "cf", + "H", + ], skiprows=1, ) ) else: - raise ValueError("The `read_bl_data_from` parameter must be 'alpha', 'cl', or None.") + raise ValueError( + "The `read_bl_data_from` parameter must be 'alpha', 'cl', or None." + ) # Augment the output data for each BL for bl_data in bl_datas: # Get Cp via Karman-Tsien compressibility correction, same as XFoil - Cp_0 = (1 - bl_data["ue/vinf"] ** 2) - bl_data["Cp"] = (Cp_0 / - ( - np.sqrt(1 - self.mach ** 2) - + ( - (self.mach ** 2) - / (1 + np.sqrt(1 - self.mach ** 2)) - * (Cp_0 / 2) - ) - - ) - ) + Cp_0 = 1 - bl_data["ue/vinf"] ** 2 + bl_data["Cp"] = Cp_0 / ( + np.sqrt(1 - self.mach**2) + + ( + (self.mach**2) + / (1 + np.sqrt(1 - self.mach**2)) + * (Cp_0 / 2) + ) + ) # Get Re_theta - bl_data["Re_theta"] = np.abs(bl_data["ue/vinf"]) * bl_data["theta"] * self.Re + bl_data["Re_theta"] = ( + np.abs(bl_data["ue/vinf"]) * bl_data["theta"] * self.Re + ) output["bl_data"] = np.fromiter(bl_datas, dtype="O") @@ -553,16 +577,17 @@ def open_interactive(self) -> None: ### Open up AVL import sys, os + if sys.platform == "win32": # Run XFoil - print("Running XFoil interactively in a new window, quit it to continue...") + print( + "Running XFoil interactively in a new window, quit it to continue..." + ) command = f'cmd /k "{self.xfoil_command} {airfoil_file}"' process = subprocess.Popen( - command, - cwd=directory, - creationflags=subprocess.CREATE_NEW_CONSOLE + command, cwd=directory, creationflags=subprocess.CREATE_NEW_CONSOLE ) process.wait() @@ -571,10 +596,11 @@ def open_interactive(self) -> None: "Ability to auto-launch interactive XFoil sessions isn't yet implemented for non-Windows OSes." ) - def alpha(self, - alpha: Union[float, np.ndarray], - start_at: Union[float, None] = 0, - ) -> Dict[str, np.ndarray]: + 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. @@ -609,20 +635,22 @@ def schedule_run(alpha: float): commands.append("fmom") if self.include_bl_data: - commands.extend([ - f"dump dump_a_{alpha:.8f}.txt", - # "vplo", - # "cd", # Dissipation coefficient - # f"dump cdis_a_{alpha:.8f}.txt", - # f"n", # Amplification ratio - # f"dump n_a_{alpha:.8f}.txt", - # "", - ]) + commands.extend( + [ + f"dump dump_a_{alpha:.8f}.txt", + # "vplo", + # "cd", # Dissipation coefficient + # f"dump cdis_a_{alpha:.8f}.txt", + # f"n", # Amplification ratio + # f"dump n_a_{alpha:.8f}.txt", + # "", + ] + ) if ( - len(alphas) > 1 and - (start_at is not None) and - (np.min(alphas) < start_at < np.max(alphas)) + len(alphas) > 1 + and (start_at is not None) + and (np.min(alphas) < start_at < np.max(alphas)) ): alphas_upper = alphas[alphas > start_at] alphas_lower = alphas[alpha <= start_at][::-1] @@ -640,20 +668,18 @@ def schedule_run(alpha: float): output = self._run_xfoil( "\n".join(commands), - read_bl_data_from="alpha" if self.include_bl_data else None + read_bl_data_from="alpha" if self.include_bl_data else None, ) - sort_order = np.argsort(output['alpha']) - output = { - k: v[sort_order] - for k, v in output.items() - } + sort_order = np.argsort(output["alpha"]) + output = {k: v[sort_order] for k, v in output.items()} return output - def cl(self, - cl: Union[float, np.ndarray], - start_at: Union[float, None] = 0, - ) -> Dict[str, np.ndarray]: + def cl( + self, + cl: Union[float, np.ndarray], + start_at: Union[float, None] = 0, + ) -> Dict[str, np.ndarray]: """ Execute XFoil at a given lift coefficient, or at a sequence of lift coefficients. @@ -688,20 +714,22 @@ def schedule_run(cl: float): commands.append("fmom") if self.include_bl_data: - commands.extend([ - f"dump dump_cl_{cl:.8f}.txt", - # "vplo", - # "cd", # Dissipation coefficient - # f"dump cdis_cl_{cl:.8f}.txt", - # f"n", # Amplification ratio - # f"dump n_cl_{cl:.8f}.txt", - # "", - ]) + commands.extend( + [ + f"dump dump_cl_{cl:.8f}.txt", + # "vplo", + # "cd", # Dissipation coefficient + # f"dump cdis_cl_{cl:.8f}.txt", + # f"n", # Amplification ratio + # f"dump n_cl_{cl:.8f}.txt", + # "", + ] + ) if ( - len(cls) > 1 and - (start_at is not None) and - (np.min(cls) < start_at < np.max(cls)) + len(cls) > 1 + and (start_at is not None) + and (np.min(cls) < start_at < np.max(cls)) ): cls_upper = cls[cls > start_at] cls_lower = cls[cls <= start_at][::-1] @@ -719,18 +747,15 @@ def schedule_run(cl: float): output = self._run_xfoil( "\n".join(commands), - read_bl_data_from="cl" if self.include_bl_data else None + read_bl_data_from="cl" if self.include_bl_data else None, ) - sort_order = np.argsort(output['alpha']) - output = { - k: v[sort_order] - for k, v in output.items() - } + sort_order = np.argsort(output["alpha"]) + output = {k: v[sort_order] for k, v in output.items()} return output -if __name__ == '__main__': +if __name__ == "__main__": af = Airfoil("naca2412").repanel(n_points_per_side=100) xf = XFoil( diff --git a/aerosandbox/aerodynamics/aero_3D/aero_buildup.py b/aerosandbox/aerodynamics/aero_3D/aero_buildup.py index 6a888d4af..27bd8079b 100644 --- a/aerosandbox/aerodynamics/aero_3D/aero_buildup.py +++ b/aerosandbox/aerodynamics/aero_3D/aero_buildup.py @@ -8,7 +8,9 @@ import aerosandbox.library.aerodynamics as aerolib import copy from typing import Union, List, Dict, Any -from aerosandbox.aerodynamics.aero_3D.aero_buildup_submodels.softmax_scalefree import softmax_scalefree +from aerosandbox.aerodynamics.aero_3D.aero_buildup_submodels.softmax_scalefree import ( + softmax_scalefree, +) from dataclasses import dataclass from functools import cached_property @@ -29,10 +31,10 @@ class AeroBuildup(ExplicitAnalysis): >>> aero_with_stability_derivs = ab.run_with_stability_derivatives() # Same, but also gets stability derivatives. """ + default_analysis_specific_options = { Fuselage: dict( E_wave_drag=2.5, # Wave drag efficiency factor - # Defined by Raymer, "Aircraft Design: A Conceptual Approach", 2nd Ed. Chap. 12.5.9 "Supersonic Parasite Drag". # Notated there as "E_WD". # @@ -42,20 +44,19 @@ class AeroBuildup(ExplicitAnalysis): # * For a "more typical supersonic...", 1.8 - 2.2 # * For a "poor supersonic design", 2.5 - 3.0 # * The F-15 has E_WD = 2.9. - nose_fineness_ratio=3, # Fineness ratio (length / diameter) of the nose section of the fuselage. - # Impacts wave drag calculations, among other things. ), } - def __init__(self, - airplane: Airplane, - op_point: OperatingPoint, - xyz_ref: Union[np.ndarray, List[float]] = None, - model_size: str = "small", - include_wave_drag: bool = True, - ): + def __init__( + self, + airplane: Airplane, + op_point: OperatingPoint, + xyz_ref: Union[np.ndarray, List[float]] = None, + model_size: str = "small", + include_wave_drag: bool = True, + ): """ Initializes a new AeroBuildup analysis as an object. @@ -89,11 +90,18 @@ def __init__(self, self.include_wave_drag = include_wave_drag def __repr__(self): - return self.__class__.__name__ + "(\n\t" + "\n\t".join([ - f"airplane={self.airplane}", - f"op_point={self.op_point}", - f"xyz_ref={self.xyz_ref}", - ]) + "\n)" + return ( + self.__class__.__name__ + + "(\n\t" + + "\n\t".join( + [ + f"airplane={self.airplane}", + f"op_point={self.op_point}", + f"xyz_ref={self.xyz_ref}", + ] + ) + + "\n)" + ) @dataclass class AeroComponentResults: @@ -101,8 +109,12 @@ class AeroComponentResults: c_ref: float # Reference chord [m] b_ref: float # Reference span [m] op_point: OperatingPoint - F_g: List[Union[float, np.ndarray]] # An [x, y, z] list of forces in geometry axes [N] - M_g: List[Union[float, np.ndarray]] # An [x, y, z] list of moments about geometry axes [Nm] + F_g: List[ + Union[float, np.ndarray] + ] # An [x, y, z] list of forces in geometry axes [N] + M_g: List[ + Union[float, np.ndarray] + ] # An [x, y, z] list of moments about geometry axes [Nm] span_effective: float # The effective span of the component's Trefftz-plane wake, used for induced drag calculations. [m] oswalds_efficiency: float # Oswald's efficiency factor [-] @@ -110,43 +122,58 @@ class AeroComponentResults: def __repr__(self): F_w = self.F_w M_b = self.M_b - return self.__class__.__name__ + "(\n\t" + "\n\t".join([ - f"L={-F_w[2]},", - f"Y={F_w[1]},", - f"D={-F_w[0]},", - f"l_b={M_b[0]},", - f"m_b={M_b[1]},", - f"n_b={M_b[2]},", - f"span_effective={self.span_effective}, oswalds_efficiency={self.oswalds_efficiency},", - ]) + "\n)" + return ( + self.__class__.__name__ + + "(\n\t" + + "\n\t".join( + [ + f"L={-F_w[2]},", + f"Y={F_w[1]},", + f"D={-F_w[0]},", + f"l_b={M_b[0]},", + f"m_b={M_b[1]},", + f"n_b={M_b[2]},", + f"span_effective={self.span_effective}, oswalds_efficiency={self.oswalds_efficiency},", + ] + ) + + "\n)" + ) @property def F_b(self) -> List[Union[float, np.ndarray]]: """ An [x, y, z] list of forces in body axes [N] """ - return self.op_point.convert_axes(*self.F_g, from_axes="geometry", to_axes="body") + return self.op_point.convert_axes( + *self.F_g, from_axes="geometry", to_axes="body" + ) @property def F_w(self) -> List[Union[float, np.ndarray]]: """ An [x, y, z] list of forces in wind axes [N] """ - return self.op_point.convert_axes(*self.F_g, from_axes="geometry", to_axes="wind") + return self.op_point.convert_axes( + *self.F_g, from_axes="geometry", to_axes="wind" + ) @property def M_b(self) -> List[Union[float, np.ndarray]]: """ An [x, y, z] list of moments about body axes [Nm] """ - return self.op_point.convert_axes(*self.M_g, from_axes="geometry", to_axes="body") + return self.op_point.convert_axes( + *self.M_g, from_axes="geometry", to_axes="body" + ) @property def M_w(self) -> List[Union[float, np.ndarray]]: """ An [x, y, z] list of moments about wind axes [Nm] """ - return self.op_point.convert_axes(*self.M_g, from_axes="geometry", to_axes="wind") + return self.op_point.convert_axes( + *self.M_g, from_axes="geometry", to_axes="wind" + ) @property def L(self) -> Union[float, np.ndarray]: @@ -190,7 +217,9 @@ def n_b(self) -> Union[float, np.ndarray]: """ return self.M_b[2] - def run(self) -> Dict[str, Union[Union[float, np.ndarray], List[Union[float, np.ndarray]]]]: + def run( + self, + ) -> Dict[str, Union[Union[float, np.ndarray], List[Union[float, np.ndarray]]]]: """ Computes the aerodynamic forces and moments on the airplane. @@ -232,55 +261,38 @@ def run(self) -> Dict[str, Union[Union[float, np.ndarray], List[Union[float, np. ### Compute the forces on each component wing_aero_components = [ - self.wing_aerodynamics( - wing=wing, - include_induced_drag=False - ) + self.wing_aerodynamics(wing=wing, include_induced_drag=False) for wing in self.airplane.wings ] fuselage_aero_components = [ - self.fuselage_aerodynamics( - fuselage=fuse, - include_induced_drag=False - ) + self.fuselage_aerodynamics(fuselage=fuse, include_induced_drag=False) for fuse in self.airplane.fuselages ] aero_components = wing_aero_components + fuselage_aero_components ### Sum up the forces - F_g_total = [ - sum([comp.F_g[i] for comp in aero_components]) - for i in range(3) - ] - M_g_total = [ - sum([comp.M_g[i] for comp in aero_components]) - for i in range(3) - ] + F_g_total = [sum([comp.F_g[i] for comp in aero_components]) for i in range(3)] + M_g_total = [sum([comp.M_g[i] for comp in aero_components]) for i in range(3)] ##### Add in the induced drag Q = self.op_point.dynamic_pressure() - span_effective_squared = softmax_scalefree([ - comp.span_effective ** 2 * comp.oswalds_efficiency - for comp in aero_components - ]) + span_effective_squared = softmax_scalefree( + [ + comp.span_effective**2 * comp.oswalds_efficiency + for comp in aero_components + ] + ) _, sideforce, lift = self.op_point.convert_axes( - *F_g_total, - from_axes="geometry", - to_axes="wind" + *F_g_total, from_axes="geometry", to_axes="wind" ) - D_induced = ( - (lift ** 2 + sideforce ** 2) / - (Q * np.pi * span_effective_squared) - ) + D_induced = (lift**2 + sideforce**2) / (Q * np.pi * span_effective_squared) D_induced_g = self.op_point.convert_axes( - -D_induced, 0, 0, - from_axes="wind", - to_axes="geometry" + -D_induced, 0, 0, from_axes="wind", to_axes="geometry" ) for i in range(3): @@ -294,24 +306,16 @@ def run(self) -> Dict[str, Union[Union[float, np.ndarray], List[Union[float, np. ##### Add in other metrics output["F_b"] = self.op_point.convert_axes( - *F_g_total, - from_axes="geometry", - to_axes="body" + *F_g_total, from_axes="geometry", to_axes="body" ) output["F_w"] = self.op_point.convert_axes( - *F_g_total, - from_axes="geometry", - to_axes="wind" + *F_g_total, from_axes="geometry", to_axes="wind" ) output["M_b"] = self.op_point.convert_axes( - *M_g_total, - from_axes="geometry", - to_axes="body" + *M_g_total, from_axes="geometry", to_axes="body" ) output["M_w"] = self.op_point.convert_axes( - *M_g_total, - from_axes="geometry", - to_axes="wind" + *M_g_total, from_axes="geometry", to_axes="wind" ) output["L"] = -output["F_w"][2] @@ -339,20 +343,19 @@ def run(self) -> Dict[str, Union[Union[float, np.ndarray], List[Union[float, np. output["fuselage_aero_components"] = fuselage_aero_components ##### Add the drag breakdown - output["D_profile"] = sum([ - comp.D for comp in aero_components - ]) + output["D_profile"] = sum([comp.D for comp in aero_components]) output["D_induced"] = D_induced return output - def run_with_stability_derivatives(self, - alpha=True, - beta=True, - p=True, - q=True, - r=True, - ) -> Dict[str, Union[Union[float, np.ndarray], List[Union[float, np.ndarray]]]]: + def run_with_stability_derivatives( + self, + alpha=True, + beta=True, + p=True, + q=True, + r=True, + ) -> Dict[str, Union[Union[float, np.ndarray], List[Union[float, np.ndarray]]]]: """ Computes the aerodynamic forces and moments on the airplane, and the stability derivatives. @@ -416,32 +419,32 @@ def run_with_stability_derivatives(self, """ do_analysis: Dict[str, bool] = { "alpha": alpha, - "beta" : beta, - "p" : p, - "q" : q, - "r" : r, + "beta": beta, + "p": p, + "q": q, + "r": r, } abbreviations: Dict[str, str] = { "alpha": "a", - "beta" : "b", - "p" : "p", - "q" : "q", - "r" : "r", + "beta": "b", + "p": "p", + "q": "q", + "r": "r", } finite_difference_amounts: Dict[str, float] = { "alpha": 0.001, - "beta" : 0.001, - "p" : 0.001 * (2 * self.op_point.velocity) / self.airplane.b_ref, - "q" : 0.001 * (2 * self.op_point.velocity) / self.airplane.c_ref, - "r" : 0.001 * (2 * self.op_point.velocity) / self.airplane.b_ref, + "beta": 0.001, + "p": 0.001 * (2 * self.op_point.velocity) / self.airplane.b_ref, + "q": 0.001 * (2 * self.op_point.velocity) / self.airplane.c_ref, + "r": 0.001 * (2 * self.op_point.velocity) / self.airplane.b_ref, } scaling_factors: Dict[str, float] = { "alpha": np.degrees(1), - "beta" : np.degrees(1), - "p" : (2 * self.op_point.velocity) / self.airplane.b_ref, - "q" : (2 * self.op_point.velocity) / self.airplane.c_ref, - "r" : (2 * self.op_point.velocity) / self.airplane.b_ref, + "beta": np.degrees(1), + "p": (2 * self.op_point.velocity) / self.airplane.b_ref, + "q": (2 * self.op_point.velocity) / self.airplane.c_ref, + "r": (2 * self.op_point.velocity) / self.airplane.b_ref, } original_op_point = self.op_point @@ -457,7 +460,9 @@ def run_with_stability_derivatives(self, # of integration".) for d in do_analysis.keys(): - if not do_analysis[d]: # Basically, if the parameter from the function input is not True, + if not do_analysis[ + d + ]: # Basically, if the parameter from the function input is not True, continue # Skip this run. # This way, you can (optionally) speed up this routine if you only need static derivatives, # or longitudinal derivatives, etc. @@ -492,10 +497,12 @@ def run_with_stability_derivatives(self, ]: derivative_name = derivative_numerator + abbreviations[d] # Gives "CLa" run_base[derivative_name] = ( - ( # Finite-difference out the derivatives - run_incremented[derivative_numerator] - run_base[derivative_numerator] - ) / finite_difference_amounts[d] - * scaling_factors[d] + ( # Finite-difference out the derivatives + run_incremented[derivative_numerator] + - run_base[derivative_numerator] + ) + / finite_difference_amounts[d] + * scaling_factors[d] ) ### Try to compute and append neutral point, if possible @@ -517,14 +524,17 @@ def run_with_stability_derivatives(self, run_base["CYb"], ) - run_base["x_np_lateral"] = self.xyz_ref[0] - (Cnb / CYb * self.airplane.b_ref) + run_base["x_np_lateral"] = self.xyz_ref[0] - ( + Cnb / CYb * self.airplane.b_ref + ) return run_base - def wing_aerodynamics(self, - wing: Wing, - include_induced_drag: bool = True, - ) -> AeroComponentResults: + def wing_aerodynamics( + self, + wing: Wing, + include_induced_drag: bool = True, + ) -> AeroComponentResults: """ Estimates the aerodynamic forces, moments, and derivatives on a wing in isolation. @@ -561,10 +571,9 @@ def wing_aerodynamics(self, for i in range(len(wing.xsecs)): span_inboard_to_YZ_plane = np.minimum( span_inboard_to_YZ_plane, - np.abs(wing._compute_xyz_of_WingXSec( - i, - x_nondim=0.25, z_nondim=0 - )[1]) + np.abs( + wing._compute_xyz_of_WingXSec(i, x_nondim=0.25, z_nondim=0)[1] + ), ) else: span_inboard_to_YZ_plane = 0 @@ -573,17 +582,10 @@ def wing_aerodynamics(self, xsec_chords = [xsec.chord for xsec in wing.xsecs] sectional_chords = [ (inner_chord + outer_chord) / 2 - for inner_chord, outer_chord in zip( - xsec_chords[1:], - xsec_chords[:-1] - ) + for inner_chord, outer_chord in zip(xsec_chords[1:], xsec_chords[:-1]) ] sectional_areas = [ - span * chord - for span, chord in zip( - sectional_spans, - sectional_chords - ) + span * chord for span, chord in zip(sectional_spans, sectional_chords) ] half_area = sum(sectional_areas) area_inboard_to_YZ_plane = span_inboard_to_YZ_plane * wing_MAC @@ -599,20 +601,18 @@ def wing_aerodynamics(self, dihedral_factor = np.sind(wing_dihedral) ** 2 span_effective = ( - span_0_dihedral + - (span_90_dihedral - span_0_dihedral) * dihedral_factor + span_0_dihedral + (span_90_dihedral - span_0_dihedral) * dihedral_factor ) area_effective = ( - area_0_dihedral + - (area_90_dihedral - area_0_dihedral) * dihedral_factor + area_0_dihedral + (area_90_dihedral - area_0_dihedral) * dihedral_factor ) else: span_effective = half_span area_effective = half_area - AR_effective = span_effective ** 2 / area_effective + AR_effective = span_effective**2 / area_effective mach = op_point.mach() # mach_normal = mach * np.cosd(sweep) @@ -620,13 +620,13 @@ def wing_aerodynamics(self, aspect_ratio=AR_effective, mach=mach, sweep=wing_sweep, - Cl_is_compressible=True + Cl_is_compressible=True, ) oswalds_efficiency = aerolib.oswalds_efficiency( taper_ratio=wing_taper, aspect_ratio=AR_effective, sweep=wing_sweep, - fuselage_diameter_to_span_ratio=0 # an assumption + fuselage_diameter_to_span_ratio=0, # an assumption ) areas = wing.area(_sectional=True) @@ -637,10 +637,21 @@ def wing_aerodynamics(self, a = AR_effective / (AR_effective + 2) s = np.radians(wing_sweep) t = np.exp(-wing_taper) - neutral_point_deviation_due_to_unsweep = -( - ((((3.557726 ** (a ** 2.8443985)) * ((((s * a) + (t * 1.9149417)) + -1.4449639) * s)) + ( - a + -0.89228547)) * -0.16073418) - ) * wing_MAC + neutral_point_deviation_due_to_unsweep = ( + -( + ( + ( + ( + (3.557726 ** (a**2.8443985)) + * ((((s * a) + (t * 1.9149417)) + -1.4449639) * s) + ) + + (a + -0.89228547) + ) + * -0.16073418 + ) + ) + * wing_MAC + ) aerodynamic_centers = [ ac + np.array([neutral_point_deviation_due_to_unsweep, 0, 0]) for ac in aerodynamic_centers @@ -655,10 +666,7 @@ def wing_aerodynamics(self, for i in range(len(wing.xsecs)) ] - def compute_section_aerodynamics( - sect_id: int, - mirror_across_XZ: bool = False - ): + def compute_section_aerodynamics(sect_id: int, mirror_across_XZ: bool = False): """ Computes the forces and moments about self.xyz_ref on a given wing section. Args: @@ -692,42 +700,46 @@ def compute_section_aerodynamics( if mirror_across_XZ: xg_local[1] *= -1 yg_local[1] *= -1 - zg_local[1] *= -1 # Note: if mirrored, this results in a left-handed coordinate system. + zg_local[ + 1 + ] *= ( + -1 + ) # Note: if mirrored, this results in a left-handed coordinate system. ##### Compute the moment arm from the section AC sect_AC_raw = aerodynamic_centers[sect_id] if mirror_across_XZ: sect_AC_raw[1] *= -1 - sect_AC = [ - sect_AC_raw[i] - self.xyz_ref[i] - for i in range(3) - ] + sect_AC = [sect_AC_raw[i] - self.xyz_ref[i] for i in range(3)] ##### Compute the generalized angle of attack, which is the geometric alpha that the wing section "sees". - vel_vector_g_from_freestream = op_point.convert_axes( # Points backwards (with relative wind) - x_from=-op_point.velocity, y_from=0, z_from=0, - from_axes="wind", - to_axes="geometry" + vel_vector_g_from_freestream = ( + op_point.convert_axes( # Points backwards (with relative wind) + x_from=-op_point.velocity, + y_from=0, + z_from=0, + from_axes="wind", + to_axes="geometry", + ) ) vel_vector_g_from_rotation = np.cross( sect_AC, op_point.convert_axes( - op_point.p, op_point.q, op_point.r, + op_point.p, + op_point.q, + op_point.r, from_axes="body", - to_axes="geometry" + to_axes="geometry", ), - manual=True + manual=True, ) vel_vector_g = [ vel_vector_g_from_freestream[i] + vel_vector_g_from_rotation[i] for i in range(3) ] - vel_mag_g = np.sqrt(sum(comp ** 2 for comp in vel_vector_g)) - vel_dir_g = [ - vel_vector_g[i] / vel_mag_g - for i in range(3) - ] + vel_mag_g = np.sqrt(sum(comp**2 for comp in vel_vector_g)) + vel_dir_g = [vel_vector_g[i] / vel_mag_g for i in range(3)] vel_dot_x = np.dot(vel_dir_g, xg_local, manual=True) vel_dot_z = np.dot(vel_dir_g, zg_local, manual=True) @@ -735,7 +747,7 @@ def compute_section_aerodynamics( alpha_generalized = np.where( vel_dot_x > 0, 90 - np.arccosd(np.clip(vel_dot_z, -1, 1)), # In range (-90 to 90) - 90 + np.arccosd(np.clip(vel_dot_z, -1, 1)) # In range (90 to 270) + 90 + np.arccosd(np.clip(vel_dot_z, -1, 1)), # In range (90 to 270) ) ##### Compute the effective generalized angle of attack, which roughly accounts for self-downwash @@ -743,8 +755,11 @@ def compute_section_aerodynamics( # it is surprisingly accurate! (<20% lift coefficient error against wind tunnel experiment, even at as # low as AR = 0.5.) alpha_generalized_effective = ( - alpha_generalized - - (1 - AR_3D_factor ** 0.8) * np.sind(2 * alpha_generalized) / 2 * (180 / np.pi) + alpha_generalized + - (1 - AR_3D_factor**0.8) + * np.sind(2 * alpha_generalized) + / 2 + * (180 / np.pi) # TODO: "center" this scaling around alpha = alpha_{airfoil, Cl=0}, not around alpha = 0. # TODO Can estimate airfoil's alpha_{Cl=0} by camber + thin airfoil theory + viscous decambering knockdown. ) # Models finite-wing increase in alpha_{CL_max}. @@ -753,16 +768,15 @@ def compute_section_aerodynamics( xsec_a_quarter_chord = xsec_quarter_chords[sect_id] xsec_b_quarter_chord = xsec_quarter_chords[sect_id + 1] quarter_chord_vector_g = xsec_b_quarter_chord - xsec_a_quarter_chord - quarter_chord_dir_g = quarter_chord_vector_g / np.linalg.norm(quarter_chord_vector_g) + quarter_chord_dir_g = quarter_chord_vector_g / np.linalg.norm( + quarter_chord_vector_g + ) quarter_chord_dir_g = [ # Convert to list - quarter_chord_dir_g[i] - for i in range(3) + quarter_chord_dir_g[i] for i in range(3) ] vel_dir_dot_quarter_chord_dir = np.dot( - vel_dir_g, - quarter_chord_dir_g, - manual=True + vel_dir_g, quarter_chord_dir_g, manual=True ) sweep_rad = np.arcsin(vel_dir_dot_quarter_chord_dir) @@ -786,50 +800,37 @@ def compute_section_aerodynamics( ##### Compute sectional lift at cross-sections using lookup functions. Merge them linearly to get section CL. kwargs = dict( alpha=alpha_generalized_effective, - mach=mach_normal if self.include_wave_drag else 0., + mach=mach_normal if self.include_wave_drag else 0.0, control_surfaces=symmetry_treated_control_surfaces, model_size=self.model_size, ) xsec_a_airfoil_aero = xsec_a.airfoil.get_aero_from_neuralfoil( - Re=Re_a, - **kwargs + Re=Re_a, **kwargs ) xsec_b_airfoil_aero = xsec_b.airfoil.get_aero_from_neuralfoil( - Re=Re_b, - **kwargs + Re=Re_b, **kwargs ) xsec_a_Cl = xsec_a_airfoil_aero["CL"] xsec_b_Cl = xsec_b_airfoil_aero["CL"] sect_CL = ( - xsec_a_Cl * a_weight + - xsec_b_Cl * b_weight - ) * AR_3D_factor ** 0.2 # Models slight decrease in finite-wing CL_max. + xsec_a_Cl * a_weight + xsec_b_Cl * b_weight + ) * AR_3D_factor**0.2 # Models slight decrease in finite-wing CL_max. ##### Compute sectional drag at cross-sections using lookup functions. Merge them linearly to get section CD. xsec_a_Cdp = xsec_a_airfoil_aero["CD"] xsec_b_Cdp = xsec_b_airfoil_aero["CD"] - sect_CDp = ( - ( - xsec_a_Cdp * a_weight + - xsec_b_Cdp * b_weight - ) - ) + sect_CDp = xsec_a_Cdp * a_weight + xsec_b_Cdp * b_weight ##### Compute sectional moment at cross-sections using lookup functions. Merge them linearly to get section CM. xsec_a_Cm = xsec_a_airfoil_aero["CM"] xsec_b_Cm = xsec_b_airfoil_aero["CM"] - sect_CM = ( - xsec_a_Cm * a_weight + - xsec_b_Cm * b_weight - ) + sect_CM = xsec_a_Cm * a_weight + xsec_b_Cm * b_weight ##### Compute induced drag from local CL and full-wing properties (AR, e) if include_induced_drag: - sect_CDi = ( - sect_CL ** 2 / (np.pi * AR_effective * oswalds_efficiency) - ) + sect_CDi = sect_CL**2 / (np.pi * AR_effective * oswalds_efficiency) sect_CD = sect_CDp + sect_CDi else: @@ -837,15 +838,14 @@ def compute_section_aerodynamics( ##### Go to dimensional quantities using the area. area = areas[sect_id] - q_local = 0.5 * op_point.atmosphere.density() * vel_mag_g ** 2 + q_local = 0.5 * op_point.atmosphere.density() * vel_mag_g**2 sect_L = q_local * area * sect_CL sect_D = q_local * area * sect_CD sect_M = q_local * area * sect_CM * mean_chord ##### Compute the direction of the lift by projecting the section's normal vector into the plane orthogonal to the local freestream. L_direction_g_unnormalized = [ - zg_local[i] - vel_dot_z * vel_dir_g[i] - for i in range(3) + zg_local[i] - vel_dot_z * vel_dir_g[i] for i in range(3) ] L_direction_g_unnormalized = [ # Handles the 90 degree to 270 degree cases np.where( @@ -855,10 +855,11 @@ def compute_section_aerodynamics( ) for i in range(3) ] - L_direction_g_mag = np.sqrt(sum(comp ** 2 for comp in L_direction_g_unnormalized)) + L_direction_g_mag = np.sqrt( + sum(comp**2 for comp in L_direction_g_unnormalized) + ) L_direction_g = [ - L_direction_g_unnormalized[i] / L_direction_g_mag - for i in range(3) + L_direction_g_unnormalized[i] / L_direction_g_mag for i in range(3) ] ##### Compute the direction of the drag by aligning the drag vector with the freestream vector. @@ -866,31 +867,20 @@ def compute_section_aerodynamics( ##### Compute the force vector in geometry axes. sect_F_g = [ - sect_L * L_direction_g[i] + sect_D * D_direction_g[i] - for i in range(3) + sect_L * L_direction_g[i] + sect_D * D_direction_g[i] for i in range(3) ] ##### Compute the moment vector in geometry axes. - M_g_lift = np.cross( - sect_AC, - sect_F_g, - manual=True - ) + M_g_lift = np.cross(sect_AC, sect_F_g, manual=True) M_direction_g = np.cross(L_direction_g, D_direction_g, manual=True) - M_g_pitching_moment = [ - M_direction_g[i] * sect_M - for i in range(3) - ] - sect_M_g = [ - M_g_lift[i] + M_g_pitching_moment[i] - for i in range(3) - ] + M_g_pitching_moment = [M_direction_g[i] * sect_M for i in range(3)] + sect_M_g = [M_g_lift[i] + M_g_pitching_moment[i] for i in range(3)] return sect_F_g, sect_M_g ##### Iterate through all sections and add up all forces/moments. - F_g = [0., 0., 0.] - M_g = [0., 0., 0.] + F_g = [0.0, 0.0, 0.0] + M_g = [0.0, 0.0, 0.0] for sect_id in range(len(wing.xsecs) - 1): sect_F_g, sect_M_g = compute_section_aerodynamics(sect_id=sect_id) @@ -900,7 +890,9 @@ def compute_section_aerodynamics( M_g[i] += sect_M_g[i] if wing.symmetric: - sect_F_g, sect_M_g = compute_section_aerodynamics(sect_id=sect_id, mirror_across_XZ=True) + sect_F_g, sect_M_g = compute_section_aerodynamics( + sect_id=sect_id, mirror_across_XZ=True + ) for i in range(3): F_g[i] += sect_F_g[i] @@ -914,13 +906,12 @@ def compute_section_aerodynamics( F_g=F_g, M_g=M_g, span_effective=span_effective, - oswalds_efficiency=oswalds_efficiency + oswalds_efficiency=oswalds_efficiency, ) - def fuselage_aerodynamics(self, - fuselage: Fuselage, - include_induced_drag: bool = True - ) -> AeroComponentResults: + def fuselage_aerodynamics( + self, fuselage: Fuselage, include_induced_drag: bool = True + ) -> AeroComponentResults: """ Estimates the aerodynamic forces, moments, and derivatives on a fuselage in isolation. @@ -952,34 +943,20 @@ def fuselage_aerodynamics(self, eta = jorgensen_eta(fuselage.fineness_ratio()) span_effective = softmax_scalefree( - [ - xsec.width - for xsec in fuselage.xsecs - ] + [ - xsec.height - for xsec in fuselage.xsecs - ], + [xsec.width for xsec in fuselage.xsecs] + + [xsec.height for xsec in fuselage.xsecs], ) ##### Initialize storage for total forces and moments, in geometry axes. - F_g = [0., 0., 0.] - M_g = [0., 0., 0.] + F_g = [0.0, 0.0, 0.0] + M_g = [0.0, 0.0, 0.0] ##### Compute the inviscid aerodynamics using slender body theory - xsec_areas = [ - xsec.xsec_area() - for xsec in fuselage.xsecs - ] + xsec_areas = [xsec.xsec_area() for xsec in fuselage.xsecs] - sect_xyz_a = [ - xsec.xyz_c - for xsec in fuselage.xsecs[:-1] - ] + sect_xyz_a = [xsec.xyz_c for xsec in fuselage.xsecs[:-1]] - sect_xyz_b = [ - xsec.xyz_c - for xsec in fuselage.xsecs[1:] - ] + sect_xyz_b = [xsec.xyz_c for xsec in fuselage.xsecs[1:]] sect_xyz_midpoints = [ [(xyz_a[i] + xyz_b[i]) / 2 for i in range(3)] @@ -992,11 +969,12 @@ def fuselage_aerodynamics(self, ] sect_directions = [ - [np.where( - sect_lengths[i] != 0, - (xyz_b[j] - xyz_a[j]) / (sect_lengths[i] + 1e-100), - 1 if j == 0 else 0 # Default to [1, 0, 0] - ) + [ + np.where( + sect_lengths[i] != 0, + (xyz_b[j] - xyz_a[j]) / (sect_lengths[i] + 1e-100), + 1 if j == 0 else 0, # Default to [1, 0, 0] + ) for j in range(3) ] for i, (xyz_a, xyz_b) in enumerate(zip(sect_xyz_a, sect_xyz_b)) @@ -1007,7 +985,9 @@ def fuselage_aerodynamics(self, for area_a, area_b in zip(xsec_areas[:-1], xsec_areas[1:]) ] - vel_direction_g = op_point.convert_axes(-1, 0, 0, from_axes="wind", to_axes="geometry") + vel_direction_g = op_point.convert_axes( + -1, 0, 0, from_axes="wind", to_axes="geometry" + ) sin_local_alpha_force_direction = [ [ @@ -1017,28 +997,23 @@ def fuselage_aerodynamics(self, for s in sect_directions ] - rho_V_squared = op_point.atmosphere.density() * op_point.velocity ** 2 + rho_V_squared = op_point.atmosphere.density() * op_point.velocity**2 sin_local_alpha_moment_direction = [ - np.cross( - vel_direction_g, - sect_direction, - manual=True - ) + np.cross(vel_direction_g, sect_direction, manual=True) for sect_direction in sect_directions ] # Drela, Flight Vehicle Aerodynamics Eq. 6.77 lift_force_at_nose = [ - rho_V_squared - * xsec_areas[-1] - * sin_local_alpha_force_direction[-1][i] + rho_V_squared * xsec_areas[-1] * sin_local_alpha_force_direction[-1][i] for i in range(3) ] # Drela, Flight Vehicle Aerodynamics Eq. 6.78 moment_at_nose_due_to_open_tail = [ - -1 * rho_V_squared + -1 + * rho_V_squared * sum(sect_lengths) * xsec_areas[-1] * sin_local_alpha_moment_direction[-1][i] @@ -1049,28 +1024,27 @@ def fuselage_aerodynamics(self, * sum( area * moment_direction[i] * length for area, moment_direction, length in zip( - sect_areas, - sin_local_alpha_moment_direction, - sect_lengths + sect_areas, sin_local_alpha_moment_direction, sect_lengths ) ) for i in range(3) ] lift_moment_arm = [ - fuselage.xsecs[0].xyz_c[i] - self.xyz_ref[i] - for i in range(3) + fuselage.xsecs[0].xyz_c[i] - self.xyz_ref[i] for i in range(3) ] moment_due_to_lift_force = np.cross( - lift_moment_arm, - lift_force_at_nose, - manual=True + lift_moment_arm, lift_force_at_nose, manual=True ) ### Total the invsicid forces and moments for i in range(3): F_g[i] += lift_force_at_nose[i] - M_g[i] += moment_at_nose_due_to_open_tail[i] + moment_at_nose_due_to_shape[i] + moment_due_to_lift_force[i] + M_g[i] += ( + moment_at_nose_due_to_open_tail[i] + + moment_at_nose_due_to_shape[i] + + moment_due_to_lift_force[i] + ) ##### Now, need to add in viscous aerodynamics from profile drag sources ### Base Drag @@ -1080,14 +1054,15 @@ def fuselage_aerodynamics(self, ### Skin friction drag form_factor = fuselage_form_factor( fineness_ratio=fuselage.fineness_ratio(), - ratio_of_corner_radius_to_body_width=0.5 + ratio_of_corner_radius_to_body_width=0.5, ) C_f_ideal = ( - # From the same study as the `fuselage_form_factor` function above. This is done on purpose - # as the form factor in this particular paper is a fit that correlates best using this precise - # definition of C_f_ideal. - 3.46 * np.log10(Re) - 5.6 - ) ** -2 + # From the same study as the `fuselage_form_factor` function above. This is done on purpose + # as the form factor in this particular paper is a fit that correlates best using this precise + # definition of C_f_ideal. + 3.46 * np.log10(Re) + - 5.6 + ) ** -2 C_f = C_f_ideal * form_factor drag_skin = C_f * fuselage.area_wetted() * q @@ -1096,8 +1071,7 @@ def fuselage_aerodynamics(self, if self.include_wave_drag: sears_haack_drag_area = transonic.sears_haack_drag_from_volume( - volume=fuselage.volume(), - length=length + volume=fuselage.volume(), length=length ) # Units of area sears_haack_C_D_wave = sears_haack_drag_area / S_ref @@ -1106,7 +1080,8 @@ def fuselage_aerodynamics(self, mach_crit=critical_mach( fineness_ratio_nose=fuse_options["nose_fineness_ratio"] ), - CD_wave_at_fully_supersonic=fuse_options["E_wave_drag"] * sears_haack_C_D_wave, + CD_wave_at_fully_supersonic=fuse_options["E_wave_drag"] + * sears_haack_C_D_wave, ) else: C_D_wave = 0 @@ -1117,20 +1092,17 @@ def fuselage_aerodynamics(self, drag_profile = drag_base + drag_skin + drag_wave drag_profile_g = op_point.convert_axes( - -drag_profile, 0, 0, - from_axes="wind", - to_axes="geometry" + -drag_profile, 0, 0, from_axes="wind", to_axes="geometry" ) drag_moment_arm = [ - (fuselage.xsecs[0].xyz_c[i] + fuselage.xsecs[-1].xyz_c[i]) / 2 - self.xyz_ref[i] + (fuselage.xsecs[0].xyz_c[i] + fuselage.xsecs[-1].xyz_c[i]) / 2 + - self.xyz_ref[i] for i in range(3) ] moment_due_to_drag_profile = np.cross( - drag_moment_arm, - drag_profile_g, - manual=True + drag_moment_arm, drag_profile_g, manual=True ) for i in range(3): @@ -1139,31 +1111,21 @@ def fuselage_aerodynamics(self, ##### Now, we need to add in the viscous aerodynamics from crossflow sources sin_generalized_alphas = [ - sum(comp ** 2 for comp in s) ** 0.5 - for s in sin_local_alpha_force_direction - ] - mean_aerodynamic_radii = [ - (area / np.pi + 1e-100) ** 0.5 - for area in sect_areas + sum(comp**2 for comp in s) ** 0.5 for s in sin_local_alpha_force_direction ] + mean_aerodynamic_radii = [(area / np.pi + 1e-100) ** 0.5 for area in sect_areas] Re_n_sect = [ - s * op_point.reynolds( - reference_length=2 * radius - ) + s * op_point.reynolds(reference_length=2 * radius) for s, radius in zip(sin_generalized_alphas, mean_aerodynamic_radii) ] - mach_n_sect = [ - s * mach - for s in sin_generalized_alphas - ] + mach_n_sect = [s * mach for s in sin_generalized_alphas] C_d_n_sect = [ np.where( Re_n_sect[i] != 0, aerolib.Cd_cylinder( - Re_D=Re_n_sect[i], - mach=mach_n_sect[i] + Re_D=Re_n_sect[i], mach=mach_n_sect[i] ), # Replace with 1.20 from Jorgensen Table 1 if this isn't working well 0, ) @@ -1171,11 +1133,7 @@ def fuselage_aerodynamics(self, ] vel_dot_x = [ - np.dot( - vel_direction_g, - sect_directions[i], - manual=True - ) + np.dot(vel_direction_g, sect_directions[i], manual=True) for i in range(len(fuselage.xsecs) - 1) ] normal_directions_g_unnormalized = [ @@ -1186,10 +1144,12 @@ def fuselage_aerodynamics(self, for i in range(len(fuselage.xsecs) - 1) ] for i in range(len(fuselage.xsecs) - 1): - normal_directions_g_unnormalized[i][2] += 1e-100 # Hack to avoid divide-by-zero in 0-AoA case + normal_directions_g_unnormalized[i][ + 2 + ] += 1e-100 # Hack to avoid divide-by-zero in 0-AoA case normal_directions_g_mag = [ - (sum(comp ** 2 for comp in n) + 1e-100) ** 0.5 + (sum(comp**2 for comp in n) + 1e-100) ** 0.5 for n in normal_directions_g_unnormalized ] normal_directions_g = [ @@ -1206,7 +1166,8 @@ def fuselage_aerodynamics(self, * rho_V_squared * eta * C_d_n_sect[i] - * normal_directions_g[i][j] * sum(c ** 2 for c in sin_local_alpha_force_direction[i]) + * normal_directions_g[i][j] + * sum(c**2 for c in sin_local_alpha_force_direction[i]) * mean_aerodynamic_radii[i] for j in range(3) ] @@ -1217,7 +1178,7 @@ def fuselage_aerodynamics(self, np.cross( [sect_xyz_midpoints[i][j] - self.xyz_ref[j] for j in range(3)], lift_viscous_crossflow[i], - manual=True + manual=True, ) for i in range(len(fuselage.xsecs) - 1) ] @@ -1239,18 +1200,17 @@ def fuselage_aerodynamics(self, ### Compute the induced drag, if relevant if include_induced_drag: _, sideforce, lift = op_point.convert_axes( - *F_g, - from_axes="geometry", - to_axes="wind" + *F_g, from_axes="geometry", to_axes="wind" ) - D_induced = ( - (lift ** 2 + sideforce ** 2) / - (op_point.dynamic_pressure() * np.pi * span_effective ** 2) + D_induced = (lift**2 + sideforce**2) / ( + op_point.dynamic_pressure() * np.pi * span_effective**2 ) D_induced_g = op_point.convert_axes( - -D_induced, 0, 0, + -D_induced, + 0, + 0, from_axes="wind", to_axes="geometry", ) @@ -1270,8 +1230,10 @@ def fuselage_aerodynamics(self, ) -if __name__ == '__main__': - from aerosandbox.aerodynamics.aero_3D.test_aero_3D.geometries.conventional import airplane +if __name__ == "__main__": + from aerosandbox.aerodynamics.aero_3D.test_aero_3D.geometries.conventional import ( + airplane, + ) import matplotlib.pyplot as plt import aerosandbox.tools.pretty_plots as p @@ -1284,11 +1246,7 @@ def fuselage_aerodynamics(self, alpha = np.linspace(-20, 20, 1000) aero = AeroBuildup( airplane=airplane, - op_point=OperatingPoint( - velocity=10, - alpha=alpha, - beta=0 - ), + op_point=OperatingPoint(velocity=10, alpha=alpha, beta=0), ).run() plt.sca(ax[0, 0]) @@ -1316,64 +1274,64 @@ def fuselage_aerodynamics(self, plt.ylabel(r"$C_L/C_D$") p.set_ticks(5, 1, 10, 2) - p.show_plot( - "`asb.AeroBuildup` Aircraft Aerodynamics" - ) + p.show_plot("`asb.AeroBuildup` Aircraft Aerodynamics") Beta, Alpha = np.meshgrid(np.linspace(-90, 90, 200), np.linspace(-90, 90, 200)) aero = AeroBuildup( airplane=airplane, op_point=OperatingPoint( - velocity=10, - alpha=Alpha.flatten(), - beta=Beta.flatten() + velocity=10, alpha=Alpha.flatten(), beta=Beta.flatten() ), ).run() - def show(): p.set_ticks(15, 5, 15, 5) p.equal() p.show_plot( "`asb.AeroBuildup` Aircraft Aerodynamics", r"Sideslip angle $\beta$ [deg]", - r"Angle of Attack $\alpha$ [deg]" + r"Angle of Attack $\alpha$ [deg]", ) - fig, ax = plt.subplots(figsize=(6, 5)) p.contour( - Beta, Alpha, aero["CL"].reshape(Alpha.shape), + Beta, + Alpha, + aero["CL"].reshape(Alpha.shape), colorbar_label="Lift Coefficient $C_L$ [-]", linelabels_format=lambda x: f"{x:.2f}", linelabels_fontsize=7, cmap="RdBu", - alpha=0.6 + alpha=0.6, ) plt.clim(*np.array([-1, 1]) * np.max(np.abs(aero["CL"]))) show() fig, ax = plt.subplots(figsize=(6, 5)) p.contour( - Beta, Alpha, aero["CD"].reshape(Alpha.shape), + Beta, + Alpha, + aero["CD"].reshape(Alpha.shape), colorbar_label="Drag Coefficient $C_D$ [-]", linelabels_format=lambda x: f"{x:.2f}", linelabels_fontsize=7, z_log_scale=True, cmap="YlOrRd", - alpha=0.6 + alpha=0.6, ) show() fig, ax = plt.subplots(figsize=(6, 5)) p.contour( - Beta, Alpha, (aero["CL"] / aero["CD"]).reshape(Alpha.shape), + Beta, + Alpha, + (aero["CL"] / aero["CD"]).reshape(Alpha.shape), levels=15, colorbar_label="$C_L / C_D$ [-]", linelabels_format=lambda x: f"{x:.0f}", linelabels_fontsize=7, cmap="RdBu", - alpha=0.6 + alpha=0.6, ) plt.clim(*np.array([-1, 1]) * np.max(np.abs(aero["CL"] / aero["CD"]))) show() diff --git a/aerosandbox/aerodynamics/aero_3D/aero_buildup_submodels/fuselage_aerodynamics_utilities.py b/aerosandbox/aerodynamics/aero_3D/aero_buildup_submodels/fuselage_aerodynamics_utilities.py index 8b44794e4..949f086a0 100644 --- a/aerosandbox/aerodynamics/aero_3D/aero_buildup_submodels/fuselage_aerodynamics_utilities.py +++ b/aerosandbox/aerodynamics/aero_3D/aero_buildup_submodels/fuselage_aerodynamics_utilities.py @@ -22,11 +22,7 @@ def critical_mach(fineness_ratio_nose: float) -> float: Returns: The critical Mach number """ - p = { - 'a': 11.087202397070559, - 'b': 13.469755774708842, - 'c': 4.034476257077558 - } + p = {"a": 11.087202397070559, "b": 13.469755774708842, "c": 4.034476257077558} mach_dd = 1 - (p["a"] / (2 * fineness_ratio_nose + p["b"])) ** p["c"] @@ -55,10 +51,10 @@ def jorgensen_eta(fineness_ratio: float) -> float: """ x = fineness_ratio p = { - '1scl': 23.009059965179222, - '1cen': -122.76900250914575, - '2scl': 13.006453125841258, - '2cen': -24.367562906887436 + "1scl": 23.009059965179222, + "1cen": -122.76900250914575, + "2scl": 13.006453125841258, + "2cen": -24.367562906887436, } return 1 - p["1scl"] / (x - p["1cen"]) - (p["2scl"] / (x - p["2cen"])) ** 2 @@ -81,24 +77,25 @@ def fuselage_base_drag_coefficient(mach: float) -> float: """ m = mach - p = {'a' : 0.18024110740341143, - 'center_sup': -0.21737019935624047, - 'm_trans' : 0.9985447737532848, - 'pc_sub' : 0.15922582283573747, - 'pc_sup' : 0.04698820458826384, - 'scale_sup' : 0.34978926411193456, - 'trans_str' : 9.999987483414937} + p = { + "a": 0.18024110740341143, + "center_sup": -0.21737019935624047, + "m_trans": 0.9985447737532848, + "pc_sub": 0.15922582283573747, + "pc_sup": 0.04698820458826384, + "scale_sup": 0.34978926411193456, + "trans_str": 9.999987483414937, + } return np.blend( p["trans_str"] * (m - p["m_trans"]), - p["pc_sup"] + p["a"] * np.exp(-(p["scale_sup"] * (m - p["center_sup"])) ** 2), - p["pc_sub"] + p["pc_sup"] + p["a"] * np.exp(-((p["scale_sup"] * (m - p["center_sup"])) ** 2)), + p["pc_sub"], ) def fuselage_form_factor( - fineness_ratio: float, - ratio_of_corner_radius_to_body_width: float = 0.5 + fineness_ratio: float, ratio_of_corner_radius_to_body_width: float = 0.5 ): """ Computes the form factor of a fuselage as a function of various geometrical parameters. @@ -139,10 +136,10 @@ def fuselage_form_factor( fr = fineness_ratio r = 2 * ratio_of_corner_radius_to_body_width - cs1 = -0.825885 * r ** 0.411795 + 4.0001 - cs2 = -0.340977 * r ** 7.54327 - 2.27920 - cs3 = -0.013846 * r ** 1.34253 + 1.11029 + cs1 = -0.825885 * r**0.411795 + 4.0001 + cs2 = -0.340977 * r**7.54327 - 2.27920 + cs3 = -0.013846 * r**1.34253 + 1.11029 - form_factor = cs1 * fr ** cs2 + cs3 + form_factor = cs1 * fr**cs2 + cs3 return form_factor diff --git a/aerosandbox/aerodynamics/aero_3D/aero_buildup_submodels/softmax_scalefree.py b/aerosandbox/aerodynamics/aero_3D/aero_buildup_submodels/softmax_scalefree.py index e842a370e..5c9b27e29 100644 --- a/aerosandbox/aerodynamics/aero_3D/aero_buildup_submodels/softmax_scalefree.py +++ b/aerosandbox/aerodynamics/aero_3D/aero_buildup_submodels/softmax_scalefree.py @@ -6,11 +6,6 @@ def softmax_scalefree(x: List[float]) -> float: if len(x) == 1: return x[0] else: - softness = np.max(np.array( - [1e-6] + x - )) * 0.01 + softness = np.max(np.array([1e-6] + x)) * 0.01 - return np.softmax( - *x, - softness=softness - ) + return np.softmax(*x, softness=softness) diff --git a/aerosandbox/aerodynamics/aero_3D/avl.py b/aerosandbox/aerodynamics/aero_3D/avl.py index ad034e599..67b32a895 100644 --- a/aerosandbox/aerodynamics/aero_3D/avl.py +++ b/aerosandbox/aerodynamics/aero_3D/avl.py @@ -37,11 +37,10 @@ class AVL(ExplicitAnalysis): >>> outputs = avl.run() """ + default_analysis_specific_options = { - Airplane: dict( - profile_drag_coefficient=0 - ), - Wing : dict( + Airplane: dict(profile_drag_coefficient=0), + Wing: dict( wing_level_spanwise_spacing=True, spanwise_resolution=12, spanwise_spacing="cosine", @@ -71,33 +70,31 @@ class AVL(ExplicitAnalysis): CD2=0, CL3=0, CD3=0, - ) + ), ), - Fuselage: dict( - panel_resolution=24, - panel_spacing="cosine" - ) + Fuselage: dict(panel_resolution=24, panel_spacing="cosine"), } AVL_spacing_parameters = { "uniform": 0, - "cosine" : 1, - "sine" : 2, - "-sine" : -2, - "equal" : 0, # "uniform" is preferred + "cosine": 1, + "sine": 2, + "-sine": -2, + "equal": 0, # "uniform" is preferred } - def __init__(self, - airplane: Airplane, - op_point: OperatingPoint, - xyz_ref: List[float] = None, - avl_command: str = "avl", - verbose: bool = False, - timeout: Union[float, int, None] = 5, - working_directory: str = None, - ground_effect: bool = False, - ground_effect_height: float = 0 - ): + def __init__( + self, + airplane: Airplane, + op_point: OperatingPoint, + xyz_ref: List[float] = None, + avl_command: str = "avl", + verbose: bool = False, + timeout: Union[float, int, None] = 5, + working_directory: str = None, + ground_effect: bool = False, + ground_effect_height: float = 0, + ): """ Interface to AVL. @@ -151,11 +148,18 @@ def __init__(self, self.ground_effect_height = ground_effect_height def __repr__(self): - return self.__class__.__name__ + "(\n\t" + "\n\t".join([ - f"airplane={self.airplane}", - f"op_point={self.op_point}", - f"xyz_ref={self.xyz_ref}", - ]) + "\n)" + return ( + self.__class__.__name__ + + "(\n\t" + + "\n\t".join( + [ + f"airplane={self.airplane}", + f"op_point={self.op_point}", + f"xyz_ref={self.xyz_ref}", + ] + ) + + "\n)" + ) def open_interactive(self) -> None: """ @@ -176,9 +180,12 @@ def open_interactive(self) -> None: ### Open up AVL import sys, os + if sys.platform == "win32": # Run AVL - print("Running AVL interactively in a new window, quit it to continue...") + print( + "Running AVL interactively in a new window, quit it to continue..." + ) command = f'cmd /k "{self.avl_command} {airplane_file}"' @@ -194,9 +201,10 @@ def open_interactive(self) -> None: "Ability to auto-launch interactive AVL sessions isn't yet implemented for non-Windows OSes." ) - def run(self, - run_command: str = None, - ) -> Dict[str, float]: + def run( + self, + run_command: str = None, + ) -> Dict[str, float]: """ Private function to run AVL. @@ -232,12 +240,12 @@ def run(self, "o", "", "", - "quit" + "quit", ] keystrokes = "\n".join(keystroke_file_contents) - command = f'{self.avl_command} {airplane_file}' + command = f"{self.avl_command} {airplane_file}" ### Execute try: @@ -252,10 +260,7 @@ def run(self, # timeout=self.timeout, # check=True ) - outs, errs = proc.communicate( - input=keystrokes, - timeout=self.timeout - ) + outs, errs = proc.communicate(input=keystrokes, timeout=self.timeout) return_code = proc.poll() except subprocess.TimeoutExpired: @@ -266,7 +271,7 @@ def run(self, "AVL run timed out!\n" "If this was not expected, try increasing the `timeout` parameter\n" "when you create this AeroSandbox AVL instance.", - stacklevel=2 + stacklevel=2, ) ##### Parse the output file @@ -285,7 +290,9 @@ def run(self, "\t - On Windows, use `avl.open_interactive()` to run AVL interactively in a new window.\n" ) - res = self.parse_unformatted_data_output(output_data, data_identifier=" =", overwrite=False) + res = self.parse_unformatted_data_output( + output_data, data_identifier=" =", overwrite=False + ) ##### Clean up results for key_to_lowerize in ["Alpha", "Beta", "Mach"]: @@ -311,20 +318,26 @@ def run(self, res["m_b"] = q * S * c * res["Cm"] res["n_b"] = q * S * b * res["Cn"] try: - res["Clb Cnr / Clr Cnb"] = res["Clb"] * res["Cnr"] / (res["Clr"] * res["Cnb"]) + res["Clb Cnr / Clr Cnb"] = ( + res["Clb"] * res["Cnr"] / (res["Clr"] * res["Cnb"]) + ) except ZeroDivisionError: res["Clb Cnr / Clr Cnb"] = np.nan - res["F_w"] = [ - -res["D"], res["Y"], -res["L"] - ] - res["F_b"] = self.op_point.convert_axes(*res["F_w"], from_axes="wind", to_axes="body") - res["F_g"] = self.op_point.convert_axes(*res["F_b"], from_axes="body", to_axes="geometry") - res["M_b"] = [ - res["l_b"], res["m_b"], res["n_b"] - ] - res["M_g"] = self.op_point.convert_axes(*res["M_b"], from_axes="body", to_axes="geometry") - res["M_w"] = self.op_point.convert_axes(*res["M_b"], from_axes="body", to_axes="wind") + res["F_w"] = [-res["D"], res["Y"], -res["L"]] + res["F_b"] = self.op_point.convert_axes( + *res["F_w"], from_axes="wind", to_axes="body" + ) + res["F_g"] = self.op_point.convert_axes( + *res["F_b"], from_axes="body", to_axes="geometry" + ) + res["M_b"] = [res["l_b"], res["m_b"], res["n_b"]] + res["M_g"] = self.op_point.convert_axes( + *res["M_b"], from_axes="body", to_axes="geometry" + ) + res["M_w"] = self.op_point.convert_axes( + *res["M_b"], from_axes="body", to_axes="wind" + ) return res @@ -358,7 +371,7 @@ def _default_keystroke_file_contents(self) -> List[str]: f"v {float(self.op_point.velocity)}", f"d {float(self.op_point.atmosphere.density())}", "g 9.81", - "" + "", ] # Set analysis state @@ -371,19 +384,18 @@ def _default_keystroke_file_contents(self) -> List[str]: f"b b {float(self.op_point.beta)}", f"r r {float(p_bar)}", f"p p {float(q_bar)}", - f"y y {float(r_bar)}" + f"y y {float(r_bar)}", ] # Set control surface deflections - run_file_contents += [ - f"d1 d1 1" - ] + run_file_contents += [f"d1 d1 1"] return run_file_contents - def write_avl(self, - filepath: Union[Path, str] = None, - ) -> None: + def write_avl( + self, + filepath: Union[Path, str] = None, + ) -> None: """ Writes a .avl file corresponding to this airplane to a filepath. @@ -410,7 +422,8 @@ def clean(s): airplane_options = self.get_options(airplane) - avl_file += clean(f"""\ + avl_file += clean( + f"""\ {airplane.name} #Mach 0 ! AeroSandbox note: This is overwritten later to match the current OperatingPoint Mach during the AVL run. @@ -422,7 +435,8 @@ def clean(s): {self.xyz_ref[0]} {self.xyz_ref[1]} {self.xyz_ref[2]} # CDp {airplane_options["profile_drag_coefficient"]} - """) + """ + ) control_surface_counter = 0 airfoil_counter = 0 @@ -435,70 +449,85 @@ def clean(s): if wing_options["wing_level_spanwise_spacing"]: spacing_line += f" {wing_options['spanwise_resolution']} {self.AVL_spacing_parameters[wing_options['spanwise_spacing']]}" - avl_file += clean(f"""\ + avl_file += clean( + f"""\ #{"=" * 79} SURFACE {wing.name} #Nchordwise Cspace [Nspanwise Sspace] {spacing_line} - """) + """ + ) if wing_options["component"] is not None: - avl_file += clean(f"""\ + avl_file += clean( + f"""\ COMPONENT {wing_options['component']} - """) + """ + ) if wing.symmetric: - avl_file += clean(f"""\ + avl_file += clean( + f"""\ YDUPLICATE 0 - """) + """ + ) if wing_options["no_wake"]: - avl_file += clean(f"""\ + avl_file += clean( + f"""\ NOWAKE - """) + """ + ) if wing_options["no_alpha_beta"]: - avl_file += clean(f"""\ + avl_file += clean( + f"""\ NOALBE - """) + """ + ) if wing_options["no_load"]: - avl_file += clean(f"""\ + avl_file += clean( + f"""\ NOLOAD - """) + """ + ) polar = wing_options["drag_polar"] - avl_file += clean(f"""\ + avl_file += clean( + f"""\ CDCL #CL1 CD1 CL2 CD2 CL3 CD3 {polar["CL1"]} {polar["CD1"]} {polar["CL2"]} {polar["CD2"]} {polar["CL3"]} {polar["CD3"]} - """) + """ + ) ### Build up a buffer of the control surface strings to write to each section - control_surface_commands: List[List[str]] = [ - [] - for _ in wing.xsecs - ] + control_surface_commands: List[List[str]] = [[] for _ in wing.xsecs] for i, xsec in enumerate(wing.xsecs[:-1]): for surf in xsec.control_surfaces: - xhinge = surf.hinge_point if surf.trailing_edge else -surf.hinge_point + xhinge = ( + surf.hinge_point if surf.trailing_edge else -surf.hinge_point + ) sign_dup = 1 if surf.symmetric else -1 - command = clean(f"""\ + command = clean( + f"""\ CONTROL #name, gain, Xhinge, XYZhvec, SgnDup {surf.name} 1 {xhinge:.8g} 0 0 0 {sign_dup} - """) + """ + ) control_surface_commands[i].append(command) control_surface_commands[i + 1].append(command) @@ -512,8 +541,6 @@ def clean(s): if not wing_options["wing_level_spanwise_spacing"]: xsec_def_line += f" {xsec_options['spanwise_resolution']} {self.AVL_spacing_parameters[xsec_options['spanwise_spacing']]}" - - if xsec_options["cl_alpha_factor"] is None: claf_line = f"{1 + 0.77 * xsec.airfoil.max_thickness()} # Computed using rule from avl_doc.txt" else: @@ -521,9 +548,12 @@ def clean(s): af_filepath = Path(str(filepath) + f".af{airfoil_counter}") airfoil_counter += 1 - xsec.airfoil.repanel(50).write_dat(filepath=af_filepath, include_name=True) + xsec.airfoil.repanel(50).write_dat( + filepath=af_filepath, include_name=True + ) - avl_file += clean(f"""\ + avl_file += clean( + f"""\ #{"-" * 50} SECTION #Xle Yle Zle Chord Ainc [Nspanwise Sspace] @@ -535,15 +565,18 @@ def clean(s): CLAF {claf_line} - """) + """ + ) polar = xsec_options["drag_polar"] - avl_file += clean(f"""\ + avl_file += clean( + f"""\ CDCL #CL1 CD1 CL2 CD2 CL3 CD3 {polar["CL1"]} {polar["CD1"]} {polar["CL2"]} {polar["CD2"]} {polar["CL3"]} {polar["CD3"]} - """) + """ + ) for control_surface_command in control_surface_commands[i]: avl_file += control_surface_command @@ -551,13 +584,11 @@ def clean(s): filepath = Path(filepath) for i, fuse in enumerate(airplane.fuselages): fuse_filepath = Path(str(filepath) + f".fuse{i}") - self.write_avl_bfile( - fuselage=fuse, - filepath=fuse_filepath - ) + self.write_avl_bfile(fuselage=fuse, filepath=fuse_filepath) fuse_options = self.get_options(fuse) - avl_file += clean(f"""\ + avl_file += clean( + f"""\ #{"=" * 50} BODY {fuse.name} @@ -569,17 +600,19 @@ def clean(s): TRANSLATE 0 {np.mean([x.xyz_c[1] for x in fuse.xsecs]):.8g} 0 - """) + """ + ) if filepath is not None: with open(filepath, "w+") as f: f.write(avl_file) @staticmethod - def write_avl_bfile(fuselage, - filepath: Union[Path, str] = None, - include_name: bool = True, - ) -> str: + def write_avl_bfile( + fuselage, + filepath: Union[Path, str] = None, + include_name: bool = True, + ) -> str: """ Writes an AVL-compatible BFILE corresponding to this fuselage to a filepath. @@ -603,18 +636,22 @@ def write_avl_bfile(fuselage, contents += [fuselage.name] contents += [ - f"{xyz_c[0]:.8g} {xyz_c[2] + r:.8g}" - for xyz_c, r in zip( + f"{xyz_c[0]:.8g} {xyz_c[2] + r:.8g}" + for xyz_c, r in zip( [xsec.xyz_c for xsec in fuselage.xsecs][::-1], - [xsec.equivalent_radius(preserve="area") for xsec in fuselage.xsecs][::-1] + [xsec.equivalent_radius(preserve="area") for xsec in fuselage.xsecs][ + ::-1 + ], ) - ] + [ - f"{xyz_c[0]:.8g} {xyz_c[2] - r:.8g}" - for xyz_c, r in zip( + ] + [ + f"{xyz_c[0]:.8g} {xyz_c[2] - r:.8g}" + for xyz_c, r in zip( [xsec.xyz_c for xsec in fuselage.xsecs][1:], - [xsec.equivalent_radius(preserve="area") for xsec in fuselage.xsecs][1:] + [xsec.equivalent_radius(preserve="area") for xsec in fuselage.xsecs][ + 1: + ], ) - ] + ] string = "\n".join(contents) @@ -626,10 +663,10 @@ def write_avl_bfile(fuselage, @staticmethod def parse_unformatted_data_output( - s: str, - data_identifier: str = " = ", - cast_outputs_to_float: bool = True, - overwrite: bool = None + s: str, + data_identifier: str = " = ", + cast_outputs_to_float: bool = True, + overwrite: bool = None, ) -> Dict[str, float]: """ Parses a (multiline) string of unformatted data into a nice and tidy dictionary. @@ -721,7 +758,9 @@ def parse_unformatted_data_output( value = "" # start with a blank value, which we will build up as we read - i = index + len(data_identifier) # Starting from the right of the identifier + i = index + len( + data_identifier + ) # Starting from the right of the identifier while s[i] == " " and i <= len(s): # First, skip any blanks i += 1 @@ -737,43 +776,51 @@ def parse_unformatted_data_output( value = np.nan if key in items.keys(): # If you already have this key - if overwrite is None: # If the `overwrite` parameter wasn't explicitly defined True/False, raise an error + if ( + overwrite is None + ): # If the `overwrite` parameter wasn't explicitly defined True/False, raise an error raise ValueError( - f"Key \"{key}\" is being overwritten, and no behavior has been specified here (Default behavior is to error).\n" + f'Key "{key}" is being overwritten, and no behavior has been specified here (Default behavior is to error).\n' f"Check that the output file doesn't have a duplicate here.\n" f"Alternatively, set the `overwrite` parameter of this function to True or False (rather than the default None).", ) else: if overwrite: - items[key] = value # Assign (and overwrite) the key-value pair to the output we're writing + items[key] = ( + value # Assign (and overwrite) the key-value pair to the output we're writing + ) else: pass else: - items[key] = value # Assign the key-value pair to the output we're writing + items[key] = ( + value # Assign the key-value pair to the output we're writing + ) - s = s[index + len(data_identifier):] # Trim the string by starting to read from the next point. + s = s[ + index + len(data_identifier) : + ] # Trim the string by starting to read from the next point. index = s.find(data_identifier) return items -if __name__ == '__main__': +if __name__ == "__main__": ### Import Vanilla Airplane import aerosandbox as asb - from aerosandbox.aerodynamics.aero_3D.test_aero_3D.geometries.vanilla import airplane as vanilla - - vanilla.analysis_specific_options[AVL] = dict( - profile_drag_coefficient=0.1 + from aerosandbox.aerodynamics.aero_3D.test_aero_3D.geometries.vanilla import ( + airplane as vanilla, ) + + vanilla.analysis_specific_options[AVL] = dict(profile_drag_coefficient=0.1) vanilla.wings[0].xsecs[0].control_surfaces.append( ControlSurface( name="Flap", trailing_edge=True, hinge_point=0.75, symmetric=True, - deflection=10 + deflection=10, ) ) ### Do the AVL run @@ -789,7 +836,7 @@ def parse_unformatted_data_output( r=0, ), working_directory=str(Path.home() / "Downloads" / "test"), - verbose=True + verbose=True, ) res = avl.run() diff --git a/aerosandbox/aerodynamics/aero_3D/lifting_line.py b/aerosandbox/aerodynamics/aero_3D/lifting_line.py index fe58249dc..5ee09d3ad 100644 --- a/aerosandbox/aerodynamics/aero_3D/lifting_line.py +++ b/aerosandbox/aerodynamics/aero_3D/lifting_line.py @@ -1,9 +1,12 @@ from aerosandbox import ExplicitAnalysis from aerosandbox.geometry import * from aerosandbox.performance import OperatingPoint -from aerosandbox.aerodynamics.aero_3D.singularities.uniform_strength_horseshoe_singularities import \ - calculate_induced_velocity_horseshoe -from aerosandbox.aerodynamics.aero_3D.singularities.point_source import calculate_induced_velocity_point_source +from aerosandbox.aerodynamics.aero_3D.singularities.uniform_strength_horseshoe_singularities import ( + calculate_induced_velocity_horseshoe, +) +from aerosandbox.aerodynamics.aero_3D.singularities.point_source import ( + calculate_induced_velocity_point_source, +) from typing import Dict, Any, List, Callable, Union from aerosandbox.aerodynamics.aero_3D.aero_buildup import AeroBuildup from dataclasses import dataclass @@ -41,18 +44,21 @@ class LiftingLine(ExplicitAnalysis): >>> outputs = analysis.run() """ - def __init__(self, - airplane: Airplane, - op_point: OperatingPoint, - xyz_ref: List[float] = None, - model_size: str = "medium", - run_symmetric_if_possible: bool = False, - verbose: bool = False, - spanwise_resolution: int = 4, - spanwise_spacing_function: Callable[[float, float, float], np.ndarray] = np.cosspace, - vortex_core_radius: float = 1e-8, - align_trailing_vortices_with_wind: bool = False, - ): + def __init__( + self, + airplane: Airplane, + op_point: OperatingPoint, + xyz_ref: List[float] = None, + model_size: str = "medium", + run_symmetric_if_possible: bool = False, + verbose: bool = False, + spanwise_resolution: int = 4, + spanwise_spacing_function: Callable[ + [float, float, float], np.ndarray + ] = np.cosspace, + vortex_core_radius: float = 1e-8, + align_trailing_vortices_with_wind: bool = False, + ): """ Initializes and conducts a LiftingLine analysis. @@ -93,7 +99,9 @@ def __init__(self, ### Determine whether you should run the problem as symmetric self.run_symmetric = False if run_symmetric_if_possible: - raise NotImplementedError("LiftingLine with symmetry detection not yet implemented!") + raise NotImplementedError( + "LiftingLine with symmetry detection not yet implemented!" + ) # try: # self.run_symmetric = ( # Satisfies assumptions # self.op_point.beta == 0 and @@ -105,11 +113,18 @@ def __init__(self, # pass def __repr__(self): - return self.__class__.__name__ + "(\n\t" + "\n\t".join([ - f"airplane={self.airplane}", - f"op_point={self.op_point}", - f"xyz_ref={self.xyz_ref}", - ]) + "\n)" + return ( + self.__class__.__name__ + + "(\n\t" + + "\n\t".join( + [ + f"airplane={self.airplane}", + f"op_point={self.op_point}", + f"xyz_ref={self.xyz_ref}", + ] + ) + + "\n)" + ) @dataclass class AeroComponentResults: @@ -117,48 +132,67 @@ class AeroComponentResults: c_ref: float # Reference chord [m] b_ref: float # Reference span [m] op_point: OperatingPoint - F_g: List[Union[float, np.ndarray]] # An [x, y, z] list of forces in geometry axes [N] - M_g: List[Union[float, np.ndarray]] # An [x, y, z] list of moments about geometry axes [Nm] + F_g: List[ + Union[float, np.ndarray] + ] # An [x, y, z] list of forces in geometry axes [N] + M_g: List[ + Union[float, np.ndarray] + ] # An [x, y, z] list of moments about geometry axes [Nm] def __repr__(self): F_w = self.F_w M_b = self.M_b - return self.__class__.__name__ + "(\n\t" + "\n\t".join([ - f"L={-F_w[2]},", - f"Y={F_w[1]},", - f"D={-F_w[0]},", - f"l_b={M_b[0]},", - f"m_b={M_b[1]},", - f"n_b={M_b[2]},", - ]) + "\n)" + return ( + self.__class__.__name__ + + "(\n\t" + + "\n\t".join( + [ + f"L={-F_w[2]},", + f"Y={F_w[1]},", + f"D={-F_w[0]},", + f"l_b={M_b[0]},", + f"m_b={M_b[1]},", + f"n_b={M_b[2]},", + ] + ) + + "\n)" + ) @property def F_b(self) -> List[Union[float, np.ndarray]]: """ An [x, y, z] list of forces in body axes [N] """ - return self.op_point.convert_axes(*self.F_g, from_axes="geometry", to_axes="body") + return self.op_point.convert_axes( + *self.F_g, from_axes="geometry", to_axes="body" + ) @property def F_w(self) -> List[Union[float, np.ndarray]]: """ An [x, y, z] list of forces in wind axes [N] """ - return self.op_point.convert_axes(*self.F_g, from_axes="geometry", to_axes="wind") + return self.op_point.convert_axes( + *self.F_g, from_axes="geometry", to_axes="wind" + ) @property def M_b(self) -> List[Union[float, np.ndarray]]: """ An [x, y, z] list of moments about body axes [Nm] """ - return self.op_point.convert_axes(*self.M_g, from_axes="geometry", to_axes="body") + return self.op_point.convert_axes( + *self.M_g, from_axes="geometry", to_axes="body" + ) @property def M_w(self) -> List[Union[float, np.ndarray]]: """ An [x, y, z] list of moments about wind axes [Nm] """ - return self.op_point.convert_axes(*self.M_g, from_axes="geometry", to_axes="wind") + return self.op_point.convert_axes( + *self.M_g, from_axes="geometry", to_axes="wind" + ) @property def L(self) -> Union[float, np.ndarray]: @@ -260,14 +294,8 @@ def run(self) -> Dict: aero_components = [wing_aero] + fuselage_aero_components ### Sum up the forces - F_g_total = [ - sum([comp.F_g[i] for comp in aero_components]) - for i in range(3) - ] - M_g_total = [ - sum([comp.M_g[i] for comp in aero_components]) - for i in range(3) - ] + F_g_total = [sum([comp.F_g[i] for comp in aero_components]) for i in range(3)] + M_g_total = [sum([comp.M_g[i] for comp in aero_components]) for i in range(3)] ##### Start to assemble the output output = { @@ -277,24 +305,16 @@ def run(self) -> Dict: ##### Add in other metrics output["F_b"] = self.op_point.convert_axes( - *F_g_total, - from_axes="geometry", - to_axes="body" + *F_g_total, from_axes="geometry", to_axes="body" ) output["F_w"] = self.op_point.convert_axes( - *F_g_total, - from_axes="geometry", - to_axes="wind" + *F_g_total, from_axes="geometry", to_axes="wind" ) output["M_b"] = self.op_point.convert_axes( - *M_g_total, - from_axes="geometry", - to_axes="body" + *M_g_total, from_axes="geometry", to_axes="body" ) output["M_w"] = self.op_point.convert_axes( - *M_g_total, - from_axes="geometry", - to_axes="wind" + *M_g_total, from_axes="geometry", to_axes="wind" ) output["L"] = -output["F_w"][2] @@ -329,13 +349,14 @@ def run(self) -> Dict: return output - def run_with_stability_derivatives(self, - alpha=True, - beta=True, - p=True, - q=True, - r=True, - ) -> Dict[str, Union[Union[float, np.ndarray], List[Union[float, np.ndarray]]]]: + def run_with_stability_derivatives( + self, + alpha=True, + beta=True, + p=True, + q=True, + r=True, + ) -> Dict[str, Union[Union[float, np.ndarray], List[Union[float, np.ndarray]]]]: """ Computes the aerodynamic forces and moments on the airplane, and the stability derivatives. @@ -399,32 +420,32 @@ def run_with_stability_derivatives(self, """ do_analysis: Dict[str, bool] = { "alpha": alpha, - "beta" : beta, - "p" : p, - "q" : q, - "r" : r, + "beta": beta, + "p": p, + "q": q, + "r": r, } abbreviations: Dict[str, str] = { "alpha": "a", - "beta" : "b", - "p" : "p", - "q" : "q", - "r" : "r", + "beta": "b", + "p": "p", + "q": "q", + "r": "r", } finite_difference_amounts: Dict[str, float] = { "alpha": 0.001, - "beta" : 0.001, - "p" : 0.001 * (2 * self.op_point.velocity) / self.airplane.b_ref, - "q" : 0.001 * (2 * self.op_point.velocity) / self.airplane.c_ref, - "r" : 0.001 * (2 * self.op_point.velocity) / self.airplane.b_ref, + "beta": 0.001, + "p": 0.001 * (2 * self.op_point.velocity) / self.airplane.b_ref, + "q": 0.001 * (2 * self.op_point.velocity) / self.airplane.c_ref, + "r": 0.001 * (2 * self.op_point.velocity) / self.airplane.b_ref, } scaling_factors: Dict[str, float] = { "alpha": np.degrees(1), - "beta" : np.degrees(1), - "p" : (2 * self.op_point.velocity) / self.airplane.b_ref, - "q" : (2 * self.op_point.velocity) / self.airplane.c_ref, - "r" : (2 * self.op_point.velocity) / self.airplane.b_ref, + "beta": np.degrees(1), + "p": (2 * self.op_point.velocity) / self.airplane.b_ref, + "q": (2 * self.op_point.velocity) / self.airplane.c_ref, + "r": (2 * self.op_point.velocity) / self.airplane.b_ref, } original_op_point = self.op_point @@ -440,7 +461,9 @@ def run_with_stability_derivatives(self, # of integration".) for d in do_analysis.keys(): - if not do_analysis[d]: # Basically, if the parameter from the function input is not True, + if not do_analysis[ + d + ]: # Basically, if the parameter from the function input is not True, continue # Skip this run. # This way, you can (optionally) speed up this routine if you only need static derivatives, # or longitudinal derivatives, etc. @@ -475,10 +498,12 @@ def run_with_stability_derivatives(self, ]: derivative_name = derivative_numerator + abbreviations[d] # Gives "CLa" run_base[derivative_name] = ( - ( # Finite-difference out the derivatives - run_incremented[derivative_numerator] - run_base[derivative_numerator] - ) / finite_difference_amounts[d] - * scaling_factors[d] + ( # Finite-difference out the derivatives + run_incremented[derivative_numerator] + - run_base[derivative_numerator] + ) + / finite_difference_amounts[d] + * scaling_factors[d] ) ### Try to compute and append neutral point, if possible @@ -500,7 +525,9 @@ def run_with_stability_derivatives(self, run_base["CYb"], ) - run_base["x_np_lateral"] = self.xyz_ref[0] - (Cnb / CYb * self.airplane.b_ref) + run_base["x_np_lateral"] = self.xyz_ref[0] - ( + Cnb / CYb * self.airplane.b_ref + ) return run_base @@ -521,7 +548,7 @@ def wing_aerodynamics(self) -> AeroComponentResults: if self.spanwise_resolution > 1: wing = wing.subdivide_sections( ratio=self.spanwise_resolution, - spacing_function=self.spanwise_spacing_function + spacing_function=self.spanwise_spacing_function, ) points, faces = wing.mesh_thin_surface( @@ -538,7 +565,9 @@ def wing_aerodynamics(self) -> AeroComponentResults: wing_airfoils = [] wing_control_surfaces = [] - for xsec_a, xsec_b in zip(wing.xsecs[:-1], wing.xsecs[1:]): # Do the right side + for xsec_a, xsec_b in zip( + wing.xsecs[:-1], wing.xsecs[1:] + ): # Do the right side wing_airfoils.append( xsec_a.airfoil.blend_with_another_airfoil( airfoil=xsec_b.airfoil, @@ -587,10 +616,9 @@ def mirror_control_surface(surf: ControlSurface) -> ControlSurface: vortex_centers = (left_vortex_vertices + right_vortex_vertices) / 2 vortex_bound_leg = right_vortex_vertices - left_vortex_vertices vortex_bound_leg_norm = np.linalg.norm(vortex_bound_leg, axis=1) - chord_vectors = ( - (back_left_vertices + back_right_vertices) / 2 - - (front_left_vertices + front_right_vertices) / 2 - ) + chord_vectors = (back_left_vertices + back_right_vertices) / 2 - ( + front_left_vertices + front_right_vertices + ) / 2 chords = np.linalg.norm(chord_vectors, axis=1) wing_directions = vortex_bound_leg / tall(vortex_bound_leg_norm) local_forward_direction = np.cross(normal_directions, wing_directions) @@ -616,23 +644,27 @@ def mirror_control_surface(surf: ControlSurface) -> ControlSurface: ##### Setup Operating Point if self.verbose: print("Calculating the freestream influence...") - steady_freestream_velocity = self.op_point.compute_freestream_velocity_geometry_axes() # Direction the wind is GOING TO, in geometry axes coordinates - steady_freestream_direction = steady_freestream_velocity / np.linalg.norm(steady_freestream_velocity) + steady_freestream_velocity = ( + self.op_point.compute_freestream_velocity_geometry_axes() + ) # Direction the wind is GOING TO, in geometry axes coordinates + steady_freestream_direction = steady_freestream_velocity / np.linalg.norm( + steady_freestream_velocity + ) steady_freestream_velocities = np.tile( - wide(steady_freestream_velocity), - reps=(self.n_panels, 1) + wide(steady_freestream_velocity), reps=(self.n_panels, 1) ) steady_freestream_directions = np.tile( - wide(steady_freestream_direction), - reps=(self.n_panels, 1) + wide(steady_freestream_direction), reps=(self.n_panels, 1) ) - rotation_freestream_velocities = self.op_point.compute_rotation_velocity_geometry_axes( - points=vortex_centers + rotation_freestream_velocities = ( + self.op_point.compute_rotation_velocity_geometry_axes(points=vortex_centers) ) - freestream_velocities = steady_freestream_velocities + rotation_freestream_velocities + freestream_velocities = ( + steady_freestream_velocities + rotation_freestream_velocities + ) # Nx3, represents the freestream velocity at each panel collocation point (c) # freestream_influences = np.sum(freestream_velocities * normal_directions, axis=1) @@ -644,24 +676,20 @@ def mirror_control_surface(surf: ControlSurface) -> ControlSurface: ##### Compute the linearization quantities (CL0 and CLa) of each airfoil section alpha_geometrics = 90 - np.arccosd( - np.sum( - steady_freestream_directions * normal_directions, - axis=1 - ) + np.sum(steady_freestream_directions * normal_directions, axis=1) ) cos_sweeps = np.sum( - steady_freestream_directions * -local_forward_direction, - axis=1 + steady_freestream_directions * -local_forward_direction, axis=1 ) machs = self.op_point.mach() * cos_sweeps Res = ( - self.op_point.velocity * - chords / - self.op_point.atmosphere.kinematic_viscosity() - ) * cos_sweeps + self.op_point.velocity + * chords + / self.op_point.atmosphere.kinematic_viscosity() + ) * cos_sweeps # ### Do a central finite-difference in alpha to obtain CL0 and CLa quantities # finite_difference_alpha_amount = 1 # degree @@ -710,29 +738,31 @@ def mirror_control_surface(surf: ControlSurface) -> ControlSurface: if self.verbose: print("Calculating the collocation influence matrix...") - u_centers_unit, v_centers_unit, w_centers_unit = calculate_induced_velocity_horseshoe( - x_field=tall(vortex_centers[:, 0]), - y_field=tall(vortex_centers[:, 1]), - z_field=tall(vortex_centers[:, 2]), - x_left=wide(left_vortex_vertices[:, 0]), - y_left=wide(left_vortex_vertices[:, 1]), - z_left=wide(left_vortex_vertices[:, 2]), - x_right=wide(right_vortex_vertices[:, 0]), - y_right=wide(right_vortex_vertices[:, 1]), - z_right=wide(right_vortex_vertices[:, 2]), - trailing_vortex_direction=( - steady_freestream_direction - if self.align_trailing_vortices_with_wind else - np.array([1, 0, 0]) - ), - gamma=1., - vortex_core_radius=self.vortex_core_radius + u_centers_unit, v_centers_unit, w_centers_unit = ( + calculate_induced_velocity_horseshoe( + x_field=tall(vortex_centers[:, 0]), + y_field=tall(vortex_centers[:, 1]), + z_field=tall(vortex_centers[:, 2]), + x_left=wide(left_vortex_vertices[:, 0]), + y_left=wide(left_vortex_vertices[:, 1]), + z_left=wide(left_vortex_vertices[:, 2]), + x_right=wide(right_vortex_vertices[:, 0]), + y_right=wide(right_vortex_vertices[:, 1]), + z_right=wide(right_vortex_vertices[:, 2]), + trailing_vortex_direction=( + steady_freestream_direction + if self.align_trailing_vortices_with_wind + else np.array([1, 0, 0]) + ), + gamma=1.0, + vortex_core_radius=self.vortex_core_radius, + ) ) AIC = ( - u_centers_unit * tall(normal_directions[:, 0]) + - v_centers_unit * tall(normal_directions[:, 1]) + - w_centers_unit * tall(normal_directions[:, 2]) + u_centers_unit * tall(normal_directions[:, 0]) + + v_centers_unit * tall(normal_directions[:, 1]) + + w_centers_unit * tall(normal_directions[:, 2]) ) # Influence of panel j's vortex strength [m^2/s] on panel i's normal-flow-velocity [m/s] @@ -741,15 +771,22 @@ def mirror_control_surface(surf: ControlSurface) -> ControlSurface: ##### Calculate Vortex Strengths if self.verbose: - print("Calculating vortex center strengths (assembling and solving linear system)...") + print( + "Calculating vortex center strengths (assembling and solving linear system)..." + ) - V_freestream_cross_li = np.cross(steady_freestream_velocities, self.vortex_bound_leg, axis=1) + V_freestream_cross_li = np.cross( + steady_freestream_velocities, self.vortex_bound_leg, axis=1 + ) V_freestream_cross_li_magnitudes = np.linalg.norm(V_freestream_cross_li, axis=1) velocity_magnitude_perpendiculars = self.op_point.velocity * cos_sweeps A = alpha_influence_matrix * np.tile(wide(CLas), (self.n_panels, 1)) - np.diag( - 2 * V_freestream_cross_li_magnitudes / velocity_magnitude_perpendiculars ** 2 / areas + 2 + * V_freestream_cross_li_magnitudes + / velocity_magnitude_perpendiculars**2 + / areas ) b = -1 * np.array(CLs_at_alpha_geometric) @@ -787,8 +824,7 @@ def mirror_control_surface(surf: ControlSurface) -> ControlSurface: # Calculate the induced velocity at the center of each bound leg velocities = self.get_velocity_at_points( - points=vortex_centers, - vortex_strengths=vortex_strengths + points=vortex_centers, vortex_strengths=vortex_strengths ) # fuse added here velocity_magnitudes = np.linalg.norm(velocities, axis=1) @@ -797,10 +833,14 @@ def mirror_control_surface(surf: ControlSurface) -> ControlSurface: # not WIND AXES or BODY AXES. Vi_cross_li = np.cross(velocities, vortex_bound_leg, axis=1) - forces_inviscid_geometry = self.op_point.atmosphere.density() * Vi_cross_li * tall(self.vortex_strengths) + forces_inviscid_geometry = ( + self.op_point.atmosphere.density() + * Vi_cross_li + * tall(self.vortex_strengths) + ) moments_inviscid_geometry = np.cross( np.add(vortex_centers, -wide(np.array(self.xyz_ref))), - forces_inviscid_geometry + forces_inviscid_geometry, ) # Calculate total forces and moments @@ -810,13 +850,17 @@ def mirror_control_surface(surf: ControlSurface) -> ControlSurface: if self.verbose: print("Calculating profile forces and moments...") forces_profile_geometry = ( - 0.5 * self.op_point.atmosphere.density() * velocities * tall(velocity_magnitudes) - * tall(CDs) * tall(areas) + 0.5 + * self.op_point.atmosphere.density() + * velocities + * tall(velocity_magnitudes) + * tall(CDs) + * tall(areas) ) moments_profile_geometry = np.cross( np.add(vortex_centers, -wide(np.array(self.xyz_ref))), - forces_profile_geometry + forces_profile_geometry, ) force_profile_geometry = np.sum(forces_profile_geometry, axis=0) moment_profile_geometry = np.sum(moments_profile_geometry, axis=0) @@ -825,15 +869,22 @@ def mirror_control_surface(surf: ControlSurface) -> ControlSurface: bound_leg_YZ = vortex_bound_leg bound_leg_YZ[:, 0] = 0 - moments_pitching_geometry = (0.5 * self.op_point.atmosphere.density() * tall(velocity_magnitudes ** 2)) \ - * tall(CMs) * tall(chords ** 2) * bound_leg_YZ + moments_pitching_geometry = ( + (0.5 * self.op_point.atmosphere.density() * tall(velocity_magnitudes**2)) + * tall(CMs) + * tall(chords**2) + * bound_leg_YZ + ) moment_pitching_geometry = np.sum(moments_pitching_geometry, axis=0) if self.verbose: print("Calculating total forces and moments...") force_total_geometry = np.add(force_inviscid_geometry, force_profile_geometry) - moment_total_geometry = np.add(moment_inviscid_geometry, moment_profile_geometry) + moment_pitching_geometry + moment_total_geometry = ( + np.add(moment_inviscid_geometry, moment_profile_geometry) + + moment_pitching_geometry + ) return self.AeroComponentResults( s_ref=self.airplane.s_ref, @@ -844,10 +895,9 @@ def mirror_control_surface(surf: ControlSurface) -> ControlSurface: M_g=moment_total_geometry, ) - def get_induced_velocity_at_points(self, - points: np.ndarray, - vortex_strengths: np.ndarray = None - ) -> np.ndarray: + def get_induced_velocity_at_points( + self, points: np.ndarray, vortex_strengths: np.ndarray = None + ) -> np.ndarray: """ Computes the induced velocity at a set of points in the flowfield. @@ -862,7 +912,8 @@ def get_induced_velocity_at_points(self, vortex_strengths = self.vortex_strengths except AttributeError: raise ValueError( - "`LiftingLine.vortex_strengths` doesn't exist, so you need to pass in the `vortex_strengths` parameter.") + "`LiftingLine.vortex_strengths` doesn't exist, so you need to pass in the `vortex_strengths` parameter." + ) u_induced, v_induced, w_induced = calculate_induced_velocity_horseshoe( x_field=tall(points[:, 0]), @@ -876,26 +927,25 @@ def get_induced_velocity_at_points(self, z_right=wide(self.right_vortex_vertices[:, 2]), trailing_vortex_direction=( self.steady_freestream_direction - if self.align_trailing_vortices_with_wind else - np.array([1, 0, 0]) + if self.align_trailing_vortices_with_wind + else np.array([1, 0, 0]) ), gamma=wide(vortex_strengths), - vortex_core_radius=self.vortex_core_radius + vortex_core_radius=self.vortex_core_radius, ) u_induced = np.sum(u_induced, axis=1) v_induced = np.sum(v_induced, axis=1) w_induced = np.sum(w_induced, axis=1) - V_induced = np.stack([ - u_induced, v_induced, w_induced - ], axis=1) + V_induced = np.stack([u_induced, v_induced, w_induced], axis=1) return V_induced - def get_velocity_at_points(self, - points: np.ndarray, - vortex_strengths: np.ndarray = None, - ) -> np.ndarray: + def get_velocity_at_points( + self, + points: np.ndarray, + vortex_strengths: np.ndarray = None, + ) -> np.ndarray: """ Computes the velocity at a set of points in the flowfield. @@ -910,11 +960,13 @@ def get_velocity_at_points(self, vortex_strengths=vortex_strengths, ) - rotation_freestream_velocities = np.array(self.op_point.compute_rotation_velocity_geometry_axes( - points - )) + rotation_freestream_velocities = np.array( + self.op_point.compute_rotation_velocity_geometry_axes(points) + ) - freestream_velocities = np.add(wide(self.steady_freestream_velocity), rotation_freestream_velocities) + freestream_velocities = np.add( + wide(self.steady_freestream_velocity), rotation_freestream_velocities + ) V = V_induced + freestream_velocities @@ -932,33 +984,36 @@ def calculate_fuselage_influences(self, points: np.ndarray) -> np.ndarray: this_fuse_radii = [] for fuse in self.airplane.fuselages: # iterating through the airplane fuselages - for xsec_num in range(len(fuse.xsecs)): # iterating through the current fuselage sections + for xsec_num in range( + len(fuse.xsecs) + ): # iterating through the current fuselage sections this_fuse_xsec = fuse.xsecs[xsec_num] this_fuse_centerline_points.append(this_fuse_xsec.xyz_c) this_fuse_radii.append(this_fuse_xsec.width / 2) - this_fuse_centerline_points = np.stack( - this_fuse_centerline_points, - axis=0 - ) - this_fuse_centerline_points = (this_fuse_centerline_points[1:, :] + - this_fuse_centerline_points[:-1, :]) / 2 + this_fuse_centerline_points = np.stack(this_fuse_centerline_points, axis=0) + this_fuse_centerline_points = ( + this_fuse_centerline_points[1:, :] + this_fuse_centerline_points[:-1, :] + ) / 2 this_fuse_radii = np.array(this_fuse_radii) - areas = np.pi * this_fuse_radii ** 2 - freestream_x_component = self.op_point.compute_freestream_velocity_geometry_axes()[ - 0] # TODO add in rotation corrections, add in doublets for alpha + areas = np.pi * this_fuse_radii**2 + freestream_x_component = ( + self.op_point.compute_freestream_velocity_geometry_axes()[0] + ) # TODO add in rotation corrections, add in doublets for alpha sigmas = freestream_x_component * np.diff(areas) - u_induced_fuse, v_induced_fuse, w_induced_fuse = calculate_induced_velocity_point_source( - x_field=tall(points[:, 0]), - y_field=tall(points[:, 1]), - z_field=tall(points[:, 2]), - x_source=wide(this_fuse_centerline_points[:, 0]), - y_source=wide(this_fuse_centerline_points[:, 1]), - z_source=wide(this_fuse_centerline_points[:, 2]), - sigma=wide(sigmas), - viscous_radius=0.0001, + u_induced_fuse, v_induced_fuse, w_induced_fuse = ( + calculate_induced_velocity_point_source( + x_field=tall(points[:, 0]), + y_field=tall(points[:, 1]), + z_field=tall(points[:, 2]), + x_source=wide(this_fuse_centerline_points[:, 0]), + y_source=wide(this_fuse_centerline_points[:, 1]), + z_source=wide(this_fuse_centerline_points[:, 2]), + sigma=wide(sigmas), + viscous_radius=0.0001, + ) ) # # Compressibility @@ -974,17 +1029,16 @@ def calculate_fuselage_influences(self, points: np.ndarray) -> np.ndarray: fuselage_influences_y = np.sum(v_induced_fuse, axis=1) fuselage_influences_z = np.sum(w_induced_fuse, axis=1) - fuselage_influences = np.stack([ - fuselage_influences_x, fuselage_influences_y, fuselage_influences_z - ], axis=1) + fuselage_influences = np.stack( + [fuselage_influences_x, fuselage_influences_y, fuselage_influences_z], + axis=1, + ) return fuselage_influences - def calculate_streamlines(self, - seed_points: np.ndarray = None, - n_steps: int = 300, - length: float = None - ) -> np.ndarray: + def calculate_streamlines( + self, seed_points: np.ndarray = None, n_steps: int = 300, length: float = None + ) -> np.ndarray: """ Computes streamlines, starting at specific seed points. @@ -1019,24 +1073,29 @@ def calculate_streamlines(self, left_TE_vertices = self.back_left_vertices right_TE_vertices = self.back_right_vertices N_streamlines_target = 200 - seed_points_per_panel = np.maximum(1, N_streamlines_target // len(left_TE_vertices)) + seed_points_per_panel = np.maximum( + 1, N_streamlines_target // len(left_TE_vertices) + ) nondim_node_locations = np.linspace(0, 1, seed_points_per_panel + 1) - nondim_seed_locations = (nondim_node_locations[1:] + nondim_node_locations[:-1]) / 2 - - seed_points = np.concatenate([ - x * left_TE_vertices + (1 - x) * right_TE_vertices - for x in nondim_seed_locations - ]) + nondim_seed_locations = ( + nondim_node_locations[1:] + nondim_node_locations[:-1] + ) / 2 + + seed_points = np.concatenate( + [ + x * left_TE_vertices + (1 - x) * right_TE_vertices + for x in nondim_seed_locations + ] + ) streamlines = np.empty((len(seed_points), 3, n_steps)) streamlines[:, :, 0] = seed_points for i in range(1, n_steps): V = self.get_velocity_at_points(streamlines[:, :, i - 1]) - streamlines[:, :, i] = ( - streamlines[:, :, i - 1] + - length / n_steps * V / tall(np.linalg.norm(V, axis=1)) - ) + streamlines[:, :, i] = streamlines[ + :, :, i - 1 + ] + length / n_steps * V / tall(np.linalg.norm(V, axis=1)) self.streamlines = streamlines @@ -1045,16 +1104,17 @@ def calculate_streamlines(self, return streamlines - def draw(self, - c: np.ndarray = None, - cmap: str = None, - colorbar_label: str = None, - show: bool = True, - show_kwargs: Dict = None, - draw_streamlines=True, - recalculate_streamlines=False, - backend: str = "pyvista" - ): + def draw( + self, + c: np.ndarray = None, + cmap: str = None, + colorbar_label: str = None, + show: bool = True, + show_kwargs: Dict = None, + draw_streamlines=True, + recalculate_streamlines=False, + backend: str = "pyvista", + ): """ Draws the solution. Note: Must be called on a SOLVED AeroProblem object. To solve an AeroProblem, use opti.solve(). To substitute a solved solution, use ap = sol(ap). @@ -1068,11 +1128,12 @@ def draw(self, colorbar_label = "Vortex Strengths" if draw_streamlines: - if (not hasattr(self, 'streamlines')) or recalculate_streamlines: + if (not hasattr(self, "streamlines")) or recalculate_streamlines: self.calculate_streamlines() if backend == "plotly": from aerosandbox.visualization.plotly_Figure3D import Figure3D + fig = Figure3D() for i in range(len(self.front_left_vertices)): @@ -1093,24 +1154,26 @@ def draw(self, return fig.draw( show=show, - colorbar_title=colorbar_label - ** show_kwargs, + colorbar_title=colorbar_label**show_kwargs, ) elif backend == "pyvista": import pyvista as pv + plotter = pv.Plotter() plotter.title = "ASB LiftingLine" plotter.add_axes() - plotter.show_grid(color='gray') + plotter.show_grid(color="gray") ### Draw the airplane mesh - points = np.concatenate([ - self.front_left_vertices, - self.back_left_vertices, - self.back_right_vertices, - self.front_right_vertices - ]) + points = np.concatenate( + [ + self.front_left_vertices, + self.back_left_vertices, + self.back_right_vertices, + self.front_right_vertices, + ] + ) N = len(self.front_left_vertices) range_N = np.arange(N) faces = tall(range_N) + wide(np.array([0, 1, 2, 3]) * N) @@ -1133,12 +1196,13 @@ def draw(self, ### Draw the streamlines if draw_streamlines: import aerosandbox.tools.pretty_plots as p + for i in range(self.streamlines.shape[0]): plotter.add_mesh( pv.Spline(self.streamlines[i, :, :].T), color=p.adjust_lightness("#7700FF", 1.5), opacity=0.7, - line_width=1 + line_width=1, ) if show: @@ -1202,10 +1266,12 @@ def draw(self, # ) -if __name__ == '__main__': +if __name__ == "__main__": import aerosandbox as asb import aerosandbox.numpy as np - from aerosandbox.aerodynamics.aero_3D.test_aero_3D.geometries.conventional import airplane + from aerosandbox.aerodynamics.aero_3D.test_aero_3D.geometries.conventional import ( + airplane, + ) import matplotlib.pyplot as plt import aerosandbox.tools.pretty_plots as p @@ -1215,16 +1281,12 @@ def draw(self, symmetric=False, xsecs=[ asb.WingXSec( - xyz_le=[0, 0, 0], - chord=1, - airfoil=asb.Airfoil("naca0012") + xyz_le=[0, 0, 0], chord=1, airfoil=asb.Airfoil("naca0012") ), asb.WingXSec( - xyz_le=[0, 5, 0], - chord=1, - airfoil=asb.Airfoil("naca0012") + xyz_le=[0, 5, 0], chord=1, airfoil=asb.Airfoil("naca0012") ), - ] + ], ) ] ) @@ -1245,7 +1307,6 @@ def draw(self, print(aero["CL"]) print(aero["CL"] / aero["CD"]) - @np.vectorize def get_aero(resolution): return LiftingLine( @@ -1266,25 +1327,16 @@ def get_aero(resolution): import aerosandbox.tools.pretty_plots as p fig, ax = plt.subplots(3, 1) - ax[0].semilogx( - resolutions, - [aero["CL"] for aero in aeros] - ) + ax[0].semilogx(resolutions, [aero["CL"] for aero in aeros]) ax[0].plot( resolutions, np.array([aero["CL"] for aero in aeros]) * (resolutions) / (resolutions + 0.3), # np.array([aero["CL"] for aero in aeros]) * (resolutions) / (resolutions + 0.3), ) - ax[1].plot( - resolutions, - [aero["CD"] for aero in aeros] - ) + ax[1].plot(resolutions, [aero["CD"] for aero in aeros]) ax[1].plot( resolutions, np.array([aero["CD"] for aero in aeros]) * (resolutions + 0.2) / (resolutions), ) - ax[2].plot( - resolutions, - [aero["CL"] / aero["CD"] for aero in aeros] - ) + ax[2].plot(resolutions, [aero["CL"] / aero["CD"] for aero in aeros]) p.show_plot() diff --git a/aerosandbox/aerodynamics/aero_3D/linear_potential_flow.py b/aerosandbox/aerodynamics/aero_3D/linear_potential_flow.py index 9d0fe8ac1..203f0b918 100644 --- a/aerosandbox/aerodynamics/aero_3D/linear_potential_flow.py +++ b/aerosandbox/aerodynamics/aero_3D/linear_potential_flow.py @@ -2,8 +2,9 @@ from aerosandbox import ExplicitAnalysis, AeroSandboxObject from aerosandbox.geometry import * from aerosandbox.performance import OperatingPoint -from aerosandbox.aerodynamics.aero_3D.singularities.uniform_strength_horseshoe_singularities import \ - calculate_induced_velocity_horseshoe +from aerosandbox.aerodynamics.aero_3D.singularities.uniform_strength_horseshoe_singularities import ( + calculate_induced_velocity_horseshoe, +) from typing import Dict, Any, List, Callable, Optional, Union, Tuple import copy from functools import cached_property, lru_cache, partial @@ -27,19 +28,24 @@ def wide(array): class LinearPotentialFlow(ExplicitAnalysis): - def __init__(self, - airplane: Airplane, - op_point: OperatingPoint, - xyz_ref: List[float] = None, - run_symmetric_if_possible: bool = False, - verbose: bool = False, - wing_model: Union[str, Dict[Wing, str]] = "vortex_lattice_all_horseshoe", - fuselage_model: Union[str, Dict[Fuselage, str]] = "none", - wing_options: Union[Dict[str, Any], Dict[Wing, Dict[str, Any]]] = None, - fuselage_options: Union[Dict[str, Any], Dict[Fuselage, Dict[str, Any]]] = None, - ): + def __init__( + self, + airplane: Airplane, + op_point: OperatingPoint, + xyz_ref: List[float] = None, + run_symmetric_if_possible: bool = False, + verbose: bool = False, + wing_model: Union[str, Dict[Wing, str]] = "vortex_lattice_all_horseshoe", + fuselage_model: Union[str, Dict[Fuselage, str]] = "none", + wing_options: Union[Dict[str, Any], Dict[Wing, Dict[str, Any]]] = None, + fuselage_options: Union[Dict[str, Any], Dict[Fuselage, Dict[str, Any]]] = None, + ): import warnings - warnings.warn("LinearPotentialFlow is under active development and is not yet ready for use.", UserWarning) + + warnings.warn( + "LinearPotentialFlow is under active development and is not yet ready for use.", + UserWarning, + ) super().__init__() @@ -61,7 +67,9 @@ def __init__(self, if isinstance(wing_model, str): wing_model = {wing: wing_model for wing in self.airplane.wings} if isinstance(fuselage_model, str): - fuselage_model = {fuselage: fuselage_model for fuselage in self.airplane.fuselages} + fuselage_model = { + fuselage: fuselage_model for fuselage in self.airplane.fuselages + } self.wing_model: Dict[Wing, str] = wing_model self.fuselage_model: Dict[Fuselage, str] = fuselage_model @@ -69,27 +77,31 @@ def __init__(self, ##### Set up the modeling options ### Check the format of the wing options if not ( - all([isinstance(k, str) for k in wing_options.keys()]) or - all([issubclass(k, Wing) for k in wing_options.keys()]) + all([isinstance(k, str) for k in wing_options.keys()]) + or all([issubclass(k, Wing) for k in wing_options.keys()]) ): - raise ValueError("`wing_options` must be either:\n" - " - A dictionary of the form `{str: value}`, which is applied to all Wings\n" - " - A nested dictionary of the form `{Wing: {str: value}}`, which is applied to the corresponding Wings\n" - ) + raise ValueError( + "`wing_options` must be either:\n" + " - A dictionary of the form `{str: value}`, which is applied to all Wings\n" + " - A nested dictionary of the form `{Wing: {str: value}}`, which is applied to the corresponding Wings\n" + ) elif all([isinstance(k, str) for k in wing_options.keys()]): wing_options = {wing: wing_options for wing in self.airplane.wings} ### Check the format of the fuselage options if not ( - all([isinstance(k, str) for k in fuselage_options.keys()]) or - all([issubclass(k, Fuselage) for k in fuselage_options.keys()]) + all([isinstance(k, str) for k in fuselage_options.keys()]) + or all([issubclass(k, Fuselage) for k in fuselage_options.keys()]) ): - raise ValueError("`fuselage_options` must be either:\n" - " - A dictionary of the form `{str: value}`, which is applied to all Fuselages\n" - " - A nested dictionary of the form `{Fuselage: {str: value}}`, which is applied to the corresponding Fuselages\n" - ) + raise ValueError( + "`fuselage_options` must be either:\n" + " - A dictionary of the form `{str: value}`, which is applied to all Fuselages\n" + " - A nested dictionary of the form `{Fuselage: {str: value}}`, which is applied to the corresponding Fuselages\n" + ) elif all([isinstance(k, str) for k in fuselage_options.keys()]): - fuselage_options = {fuselage: fuselage_options for fuselage in self.airplane.fuselages} + fuselage_options = { + fuselage: fuselage_options for fuselage in self.airplane.fuselages + } ### Set user-specified values self.wing_options: Dict[Wing, Dict[str, Any]] = wing_options @@ -97,24 +109,24 @@ def __init__(self, ### Set default values wing_model_default_options = { - "none" : {}, + "none": {}, "vortex_lattice_all_horseshoe": { - "spanwise_resolution" : 10, - "spanwise_spacing_function" : np.cosspace, - "chordwise_resolution" : 10, - "chordwise_spacing_function" : np.cosspace, - "vortex_core_radius" : 1e-8, + "spanwise_resolution": 10, + "spanwise_spacing_function": np.cosspace, + "chordwise_resolution": 10, + "chordwise_spacing_function": np.cosspace, + "vortex_core_radius": 1e-8, "align_trailing_vortices_with_wind": False, }, - "vortex_lattice_ring" : { - "spanwise_resolution" : 10, - "spanwise_spacing_function" : np.cosspace, - "chordwise_resolution" : 10, - "chordwise_spacing_function" : np.cosspace, - "vortex_core_radius" : 1e-8, + "vortex_lattice_ring": { + "spanwise_resolution": 10, + "spanwise_spacing_function": np.cosspace, + "chordwise_resolution": 10, + "chordwise_spacing_function": np.cosspace, + "vortex_core_radius": 1e-8, "align_trailing_vortices_with_wind": False, }, - "lifting_line" : { + "lifting_line": { "sectional_data_source": "neuralfoil", }, } @@ -126,13 +138,15 @@ def __init__(self, **self.wing_options[wing], } else: - raise ValueError(f"Invalid wing model specified: \"{self.wing_model[wing]}\"\n" - f"Must be one of: {list(wing_model_default_options.keys())}") + raise ValueError( + f'Invalid wing model specified: "{self.wing_model[wing]}"\n' + f"Must be one of: {list(wing_model_default_options.keys())}" + ) fuselage_model_default_options = { - "none" : {}, + "none": {}, "prescribed_source_line": { - "lengthwise_resolution" : 1, + "lengthwise_resolution": 1, "lengthwise_spacing_function": np.cosspace, }, } @@ -144,13 +158,17 @@ def __init__(self, **self.fuselage_options[fuselage], } else: - raise ValueError(f"Invalid fuselage model specified: \"{self.fuselage_model[fuselage]}\"\n" - f"Must be one of: {list(fuselage_model_default_options.keys())}") + raise ValueError( + f'Invalid fuselage model specified: "{self.fuselage_model[fuselage]}"\n' + f"Must be one of: {list(fuselage_model_default_options.keys())}" + ) ### Determine whether you should run the problem as symmetric self.run_symmetric = False if run_symmetric_if_possible: - raise NotImplementedError("LinearPotentialFlow with symmetry detection not yet implemented!") + raise NotImplementedError( + "LinearPotentialFlow with symmetry detection not yet implemented!" + ) # try: # self.run_symmetric = ( # Satisfies assumptions # self.op_point.beta == 0 and @@ -162,11 +180,18 @@ def __init__(self, # pass def __repr__(self): - return self.__class__.__name__ + "(\n" + "\n".join([ - f"\tairplane={self.airplane}", - f"\top_point={self.op_point}", - f"\txyz_ref={self.xyz_ref}", - ]) + "\n)" + return ( + self.__class__.__name__ + + "(\n" + + "\n".join( + [ + f"\tairplane={self.airplane}", + f"\top_point={self.op_point}", + f"\txyz_ref={self.xyz_ref}", + ] + ) + + "\n)" + ) @immutable_dataclass class Elements(ABC): @@ -175,12 +200,19 @@ class Elements(ABC): end_index: int def __repr__(self): - return self.__class__.__name__ + "(\n" + "\n".join([ - f"\tparent_component={self.parent_component}", - f"\tstart_index={self.start_index}", - f"\tend_index={self.end_index}", - f"\tlength={len(self)}", - ]) + "\n)" + return ( + self.__class__.__name__ + + "(\n" + + "\n".join( + [ + f"\tparent_component={self.parent_component}", + f"\tstart_index={self.start_index}", + f"\tend_index={self.end_index}", + f"\tlength={len(self)}", + ] + ) + + "\n)" + ) @abstractmethod def __len__(self): @@ -243,16 +275,18 @@ def vortex_bound_legs(self): @cached_property def collocation_points(self): - return ( - 0.5 * (0.25 * self.front_left_vertices + 0.75 * self.back_left_vertices) + - 0.5 * (0.25 * self.front_right_vertices + 0.75 * self.back_right_vertices) + return 0.5 * ( + 0.25 * self.front_left_vertices + 0.75 * self.back_left_vertices + ) + 0.5 * ( + 0.25 * self.front_right_vertices + 0.75 * self.back_right_vertices ) - def get_induced_velocity_at_points(self, - points: np.ndarray, - vortex_strengths: np.ndarray, - sum_across_elements: bool = True - ) -> Tuple[np.ndarray]: + def get_induced_velocity_at_points( + self, + points: np.ndarray, + vortex_strengths: np.ndarray, + sum_across_elements: bool = True, + ) -> Tuple[np.ndarray]: u_induced, v_induced, w_induced = calculate_induced_velocity_horseshoe( x_field=tall(points[:, 0]), y_field=tall(points[:, 1]), @@ -265,7 +299,7 @@ def get_induced_velocity_at_points(self, z_right=wide(self.right_vortex_vertices[:, 2]), trailing_vortex_direction=self.trailing_vortex_direction, gamma=wide(vortex_strengths), - vortex_core_radius=self.vortex_core_radius + vortex_core_radius=self.vortex_core_radius, ) if sum_across_elements: @@ -331,11 +365,13 @@ def discretization(self): method="quad", chordwise_resolution=options["chordwise_resolution"], chordwise_spacing_function=options["chordwise_spacing_function"], - add_camber=True + add_camber=True, ) if options["align_trailing_vortices_with_wind"]: - raise NotImplementedError("align_trailing_vortices_with_wind not yet implemented!") + raise NotImplementedError( + "align_trailing_vortices_with_wind not yet implemented!" + ) else: trailing_vortex_direction = np.array([1, 0, 0]) @@ -359,7 +395,7 @@ def discretization(self): raise NotImplementedError("lifting_line not yet implemented!") else: - raise ValueError(f"Invalid wing model specified: \"{element_type}\"") + raise ValueError(f'Invalid wing model specified: "{element_type}"') ### Fuselages for fuselage in self.airplane.fuselages: @@ -373,13 +409,15 @@ def discretization(self): raise NotImplementedError("prescribed_source_line not yet implemented!") else: - raise ValueError(f"Invalid fuselage model specified: \"{element_type}\"") + raise ValueError(f'Invalid fuselage model specified: "{element_type}"') return discretization @cached_property def N_elements(self): - return sum([len(element_collection) for element_collection in self.discretization]) + return sum( + [len(element_collection) for element_collection in self.discretization] + ) @cached_property def AIC(self): @@ -388,10 +426,14 @@ def AIC(self): for element_collection in self.discretization: if isinstance(element_collection, self.WingHorseshoeVortexElements): - raise NotImplementedError("AIC not yet implemented for horseshoe vortices.") + raise NotImplementedError( + "AIC not yet implemented for horseshoe vortices." + ) elif isinstance(element_collection, self.WingLiftingLineElements): raise NotImplementedError("AIC not yet implemented for lifting lines.") - elif isinstance(element_collection, self.FuselagePrescribedSourceLineElements): + elif isinstance( + element_collection, self.FuselagePrescribedSourceLineElements + ): raise NotImplementedError("AIC not yet implemented for fuselages.") else: raise ValueError(f"Invalid element type: {type(element_collection)}") @@ -428,40 +470,38 @@ def run(self) -> Dict[str, Any]: raise NotImplementedError - def get_induced_velocity_at_points(self, - points: np.ndarray - ) -> np.ndarray: + def get_induced_velocity_at_points(self, points: np.ndarray) -> np.ndarray: raise NotImplementedError - def get_velocity_at_points(self, - points: np.ndarray - ) -> np.ndarray: + def get_velocity_at_points(self, points: np.ndarray) -> np.ndarray: raise NotImplementedError - def get_streamlines(self, - seed_points: np.ndarray = None, - n_steps: int = 300, - length: float = None, - ): + def get_streamlines( + self, + seed_points: np.ndarray = None, + n_steps: int = 300, + length: float = None, + ): raise NotImplementedError - def draw(self, - c: np.ndarray = None, - cmap: str = None, - colorbar_label: str = None, - show: bool = True, - show_kwargs: Dict = None, - draw_streamlines=True, - recalculate_streamlines=False, - backend: str = "pyvista" - ): + def draw( + self, + c: np.ndarray = None, + cmap: str = None, + colorbar_label: str = None, + show: bool = True, + show_kwargs: Dict = None, + draw_streamlines=True, + recalculate_streamlines=False, + backend: str = "pyvista", + ): raise NotImplementedError def draw_three_view(self): raise NotImplementedError -if __name__ == '__main__': +if __name__ == "__main__": ### Import Vanilla Airplane import aerosandbox as asb diff --git a/aerosandbox/aerodynamics/aero_3D/nonlinear_lifting_line.py b/aerosandbox/aerodynamics/aero_3D/nonlinear_lifting_line.py index 3f8ad7d0e..7b19bd110 100644 --- a/aerosandbox/aerodynamics/aero_3D/nonlinear_lifting_line.py +++ b/aerosandbox/aerodynamics/aero_3D/nonlinear_lifting_line.py @@ -1,10 +1,12 @@ from aerosandbox import ImplicitAnalysis from aerosandbox.geometry import * from aerosandbox.performance import OperatingPoint -from aerosandbox.aerodynamics.aero_3D.singularities.uniform_strength_horseshoe_singularities import \ - calculate_induced_velocity_horseshoe -from aerosandbox.aerodynamics.aero_3D.singularities.point_source import \ - calculate_induced_velocity_point_source +from aerosandbox.aerodynamics.aero_3D.singularities.uniform_strength_horseshoe_singularities import ( + calculate_induced_velocity_horseshoe, +) +from aerosandbox.aerodynamics.aero_3D.singularities.point_source import ( + calculate_induced_velocity_point_source, +) import aerosandbox.numpy as np from typing import Dict, Any, Callable, List import copy @@ -43,17 +45,20 @@ class NonlinearLiftingLine(ImplicitAnalysis): """ @ImplicitAnalysis.initialize - def __init__(self, - airplane: Airplane, - op_point: OperatingPoint, - xyz_ref: List[float] = None, - run_symmetric_if_possible: bool = False, - verbose: bool = False, - spanwise_resolution=8, # TODO document - spanwise_spacing_function: Callable[[float, float, float], np.ndarray] = np.cosspace, - vortex_core_radius: float = 1e-8, - align_trailing_vortices_with_wind: bool = False, - ): + def __init__( + self, + airplane: Airplane, + op_point: OperatingPoint, + xyz_ref: List[float] = None, + run_symmetric_if_possible: bool = False, + verbose: bool = False, + spanwise_resolution=8, # TODO document + spanwise_spacing_function: Callable[ + [float, float, float], np.ndarray + ] = np.cosspace, + vortex_core_radius: float = 1e-8, + align_trailing_vortices_with_wind: bool = False, + ): """ Initializes and conducts a NonlinearLiftingLine analysis. @@ -105,11 +110,18 @@ def __init__(self, # pass def __repr__(self): - return self.__class__.__name__ + "(\n\t" + "\n\t".join([ - f"airplane={self.airplane}", - f"op_point={self.op_point}", - f"xyz_ref={self.xyz_ref}", - ]) + "\n)" + return ( + self.__class__.__name__ + + "(\n\t" + + "\n\t".join( + [ + f"airplane={self.airplane}", + f"op_point={self.op_point}", + f"xyz_ref={self.xyz_ref}", + ] + ) + + "\n)" + ) def run(self, solve: bool = True) -> Dict[str, Any]: """ @@ -159,7 +171,7 @@ def run(self, solve: bool = True) -> Dict[str, Any]: if self.spanwise_resolution > 1: wing = wing.subdivide_sections( ratio=self.spanwise_resolution, - spacing_function=self.spanwise_spacing_function + spacing_function=self.spanwise_spacing_function, ) points, faces = wing.mesh_thin_surface( @@ -176,7 +188,9 @@ def run(self, solve: bool = True) -> Dict[str, Any]: wing_airfoils = [] wing_control_surfaces = [] - for xsec_a, xsec_b in zip(wing.xsecs[:-1], wing.xsecs[1:]): # Do the right side + for xsec_a, xsec_b in zip( + wing.xsecs[:-1], wing.xsecs[1:] + ): # Do the right side wing_airfoils.append( xsec_a.airfoil.blend_with_another_airfoil( airfoil=xsec_b.airfoil, @@ -225,10 +239,9 @@ def mirror_control_surface(surf: ControlSurface) -> ControlSurface: vortex_centers = (left_vortex_vertices + right_vortex_vertices) / 2 vortex_bound_leg = right_vortex_vertices - left_vortex_vertices vortex_bound_leg_norm = np.linalg.norm(vortex_bound_leg, axis=1) - chord_vectors = ( - (back_left_vertices + back_right_vertices) / 2 - - (front_left_vertices + front_right_vertices) / 2 - ) + chord_vectors = (back_left_vertices + back_right_vertices) / 2 - ( + front_left_vertices + front_right_vertices + ) / 2 chords = np.linalg.norm(chord_vectors, axis=1) wing_directions = vortex_bound_leg / tall(vortex_bound_leg_norm) local_forward_direction = np.cross(normal_directions, wing_directions) @@ -253,15 +266,24 @@ def mirror_control_surface(surf: ControlSurface) -> ControlSurface: ##### Setup Operating Point if self.verbose: print("Calculating the freestream influence...") - steady_freestream_velocity = self.op_point.compute_freestream_velocity_geometry_axes() # Direction the wind is GOING TO, in geometry axes coordinates - steady_freestream_direction = steady_freestream_velocity / np.linalg.norm(steady_freestream_velocity) - rotation_freestream_velocities = self.op_point.compute_rotation_velocity_geometry_axes( - vortex_centers) + steady_freestream_velocity = ( + self.op_point.compute_freestream_velocity_geometry_axes() + ) # Direction the wind is GOING TO, in geometry axes coordinates + steady_freestream_direction = steady_freestream_velocity / np.linalg.norm( + steady_freestream_velocity + ) + rotation_freestream_velocities = ( + self.op_point.compute_rotation_velocity_geometry_axes(vortex_centers) + ) - freestream_velocities = np.add(wide(steady_freestream_velocity), rotation_freestream_velocities) + freestream_velocities = np.add( + wide(steady_freestream_velocity), rotation_freestream_velocities + ) # Nx3, represents the freestream velocity at each panel collocation point (c) - freestream_influences = np.sum(freestream_velocities * normal_directions, axis=1) + freestream_influences = np.sum( + freestream_velocities * normal_directions, axis=1 + ) ### Save things to the instance for later access self.steady_freestream_velocity = steady_freestream_velocity @@ -281,8 +303,7 @@ def mirror_control_surface(surf: ControlSurface) -> ControlSurface: # Find velocities velocities = self.get_velocity_at_points( - points=self.vortex_centers, - vortex_strengths=vortex_strengths + points=self.vortex_centers, vortex_strengths=vortex_strengths ) # TODO just a reminder, fuse added here velocity_magnitudes = np.linalg.norm(velocities, axis=1) @@ -296,12 +317,14 @@ def mirror_control_surface(surf: ControlSurface) -> ControlSurface: cos_sweeps = np.sum(velocity_directions * -local_forward_direction, axis=1) Res = ( - velocity_magnitudes * - self.chords / - self.op_point.atmosphere.kinematic_viscosity() - ) * cos_sweeps + velocity_magnitudes + * self.chords + / self.op_point.atmosphere.kinematic_viscosity() + ) * cos_sweeps - machs = velocity_magnitudes / self.op_point.atmosphere.speed_of_sound() * cos_sweeps + machs = ( + velocity_magnitudes / self.op_point.atmosphere.speed_of_sound() * cos_sweeps + ) aeros = [ af.get_aero_from_neuralfoil( @@ -331,13 +354,16 @@ def mirror_control_surface(surf: ControlSurface) -> ControlSurface: velocity_magnitude_perpendiculars = velocity_magnitudes * cos_sweeps residuals = ( - vortex_strengths * Vi_cross_li_magnitudes * 2 / velocity_magnitude_perpendiculars ** 2 / areas - CLs + vortex_strengths + * Vi_cross_li_magnitudes + * 2 + / velocity_magnitude_perpendiculars**2 + / areas + - CLs ) if self.solve: - self.opti.subject_to([ - residuals == 0 - ]) + self.opti.subject_to([residuals == 0]) self.sol = self.opti.solve(verbose=False) self.vortex_strengths = self.sol(vortex_strengths) @@ -353,8 +379,7 @@ def mirror_control_surface(surf: ControlSurface) -> ControlSurface: # Calculate the induced velocity at the center of each bound leg velocities = self.get_velocity_at_points( - points=self.vortex_centers, - vortex_strengths=self.vortex_strengths + points=self.vortex_centers, vortex_strengths=self.vortex_strengths ) # fuse added here velocity_magnitudes = np.linalg.norm(velocities, axis=1) @@ -363,10 +388,14 @@ def mirror_control_surface(surf: ControlSurface) -> ControlSurface: # not WIND AXES or BODY AXES. Vi_cross_li = np.cross(velocities, vortex_bound_leg, axis=1) - forces_inviscid_geometry = self.op_point.atmosphere.density() * Vi_cross_li * tall(self.vortex_strengths) + forces_inviscid_geometry = ( + self.op_point.atmosphere.density() + * Vi_cross_li + * tall(self.vortex_strengths) + ) moments_inviscid_geometry = np.cross( np.add(vortex_centers, -wide(np.array(self.xyz_ref))), - forces_inviscid_geometry + forces_inviscid_geometry, ) # Calculate total forces and moments @@ -381,76 +410,119 @@ def mirror_control_surface(surf: ControlSurface) -> ControlSurface: if self.verbose: print("Calculating profile forces and moments...") forces_profile_geometry = ( - 0.5 * self.op_point.atmosphere.density() * velocities * tall(velocity_magnitudes) - * tall(CDs) * tall(areas) + 0.5 + * self.op_point.atmosphere.density() + * velocities + * tall(velocity_magnitudes) + * tall(CDs) + * tall(areas) ) moments_profile_geometry = np.cross( np.add(vortex_centers, -wide(np.array(self.xyz_ref))), - forces_profile_geometry + forces_profile_geometry, ) force_profile_geometry = np.sum(forces_profile_geometry, axis=0) moment_profile_geometry = np.sum(moments_profile_geometry, axis=0) # # Inviscid force from geometry to body and wind axes - force_inviscid_body = np.array(self.op_point.convert_axes( - force_inviscid_geometry[0], force_inviscid_geometry[1], force_inviscid_geometry[2], - from_axes="geometry", - to_axes="body" - )) - force_inviscid_wind = np.array(self.op_point.convert_axes( - force_inviscid_body[0], force_inviscid_body[1], force_inviscid_body[2], - from_axes="body", - to_axes="wind" - )) + force_inviscid_body = np.array( + self.op_point.convert_axes( + force_inviscid_geometry[0], + force_inviscid_geometry[1], + force_inviscid_geometry[2], + from_axes="geometry", + to_axes="body", + ) + ) + force_inviscid_wind = np.array( + self.op_point.convert_axes( + force_inviscid_body[0], + force_inviscid_body[1], + force_inviscid_body[2], + from_axes="body", + to_axes="wind", + ) + ) # # Profile force from geometry to body and wind axes - force_profile_body = np.array(self.op_point.convert_axes( - force_profile_geometry[0], force_profile_geometry[1], force_profile_geometry[2], - from_axes="geometry", - to_axes="body" - )) - force_profile_wind = np.array(self.op_point.convert_axes( - force_profile_body[0], force_profile_body[1], force_profile_body[2], - from_axes="body", - to_axes="wind" - )) + force_profile_body = np.array( + self.op_point.convert_axes( + force_profile_geometry[0], + force_profile_geometry[1], + force_profile_geometry[2], + from_axes="geometry", + to_axes="body", + ) + ) + force_profile_wind = np.array( + self.op_point.convert_axes( + force_profile_body[0], + force_profile_body[1], + force_profile_body[2], + from_axes="body", + to_axes="wind", + ) + ) # Compute pitching moment bound_leg_YZ = vortex_bound_leg bound_leg_YZ[:, 0] = 0 - moments_pitching_geometry = (0.5 * self.op_point.atmosphere.density() * tall(velocity_magnitudes ** 2)) \ - * tall(CMs) * tall(chords ** 2) * bound_leg_YZ + moments_pitching_geometry = ( + (0.5 * self.op_point.atmosphere.density() * tall(velocity_magnitudes**2)) + * tall(CMs) + * tall(chords**2) + * bound_leg_YZ + ) moment_pitching_geometry = np.sum(moments_pitching_geometry, axis=0) if self.verbose: print("Calculating total forces and moments...") force_total_geometry = np.add(force_inviscid_geometry, force_profile_geometry) - force_total_body = np.array(self.op_point.convert_axes( - force_total_geometry[0], force_total_geometry[1], force_total_geometry[2], - from_axes="geometry", - to_axes="body" - )) - force_total_wind = np.array(self.op_point.convert_axes( - force_total_body[0], force_total_body[1], force_total_body[2], - from_axes="body", - to_axes="wind" - )) - - moment_total_geometry = np.add(moment_inviscid_geometry, moment_profile_geometry) + moment_pitching_geometry - - moment_total_body = np.array(self.op_point.convert_axes( - moment_total_geometry[0], moment_total_geometry[1], moment_total_geometry[2], - from_axes="geometry", - to_axes="body" - )) - moment_total_wind = np.array(self.op_point.convert_axes( - moment_total_body[0], moment_total_body[1], moment_total_body[2], - from_axes="body", - to_axes="wind" - )) + force_total_body = np.array( + self.op_point.convert_axes( + force_total_geometry[0], + force_total_geometry[1], + force_total_geometry[2], + from_axes="geometry", + to_axes="body", + ) + ) + force_total_wind = np.array( + self.op_point.convert_axes( + force_total_body[0], + force_total_body[1], + force_total_body[2], + from_axes="body", + to_axes="wind", + ) + ) + + moment_total_geometry = ( + np.add(moment_inviscid_geometry, moment_profile_geometry) + + moment_pitching_geometry + ) + + moment_total_body = np.array( + self.op_point.convert_axes( + moment_total_geometry[0], + moment_total_geometry[1], + moment_total_geometry[2], + from_axes="geometry", + to_axes="body", + ) + ) + moment_total_wind = np.array( + self.op_point.convert_axes( + moment_total_body[0], + moment_total_body[1], + moment_total_body[2], + from_axes="body", + to_axes="wind", + ) + ) ### Save things to the instance for later access L = -force_total_wind[2] @@ -479,34 +551,33 @@ def mirror_control_surface(surf: ControlSurface) -> ControlSurface: self.CL_over_CD = np.where(self.CD == 0, 0, np.array(self.CL / self.CD)) return { - "residuals" : residuals, - "F_g" : force_total_geometry, - "F_b" : force_total_body, - "F_w" : force_total_wind, - "M_g" : moment_total_geometry, - "M_b" : moment_total_body, - "M_w" : moment_total_wind, - "L" : L, - "D" : D, - "Y" : Y, - "l_b" : l_b, - "m_b" : m_b, - "n_b" : n_b, - "CL" : self.CL, - "CD" : self.CD, - "CDi" : CDi, - "CDp" : CDp, - "CY" : self.CY, + "residuals": residuals, + "F_g": force_total_geometry, + "F_b": force_total_body, + "F_w": force_total_wind, + "M_g": moment_total_geometry, + "M_b": moment_total_body, + "M_w": moment_total_wind, + "L": L, + "D": D, + "Y": Y, + "l_b": l_b, + "m_b": m_b, + "n_b": n_b, + "CL": self.CL, + "CD": self.CD, + "CDi": CDi, + "CDp": CDp, + "CY": self.CY, "CL_over_CD": self.CL_over_CD, - "Cl" : self.Cl, - "Cm" : self.Cm, - "Cn" : self.Cn + "Cl": self.Cl, + "Cm": self.Cm, + "Cn": self.Cn, } - def get_induced_velocity_at_points(self, - points: np.ndarray, - vortex_strengths: np.ndarray = None - ) -> np.ndarray: + def get_induced_velocity_at_points( + self, points: np.ndarray, vortex_strengths: np.ndarray = None + ) -> np.ndarray: """ Computes the induced velocity at a set of points in the flowfield. @@ -521,7 +592,8 @@ def get_induced_velocity_at_points(self, vortex_strengths = self.vortex_strengths except AttributeError: raise ValueError( - "`NonlinearLiftingLine.vortex_strengths` doesn't exist, so you need to pass in the `vortex_strengths` parameter.") + "`NonlinearLiftingLine.vortex_strengths` doesn't exist, so you need to pass in the `vortex_strengths` parameter." + ) u_induced, v_induced, w_induced = calculate_induced_velocity_horseshoe( x_field=tall(points[:, 0]), @@ -535,26 +607,25 @@ def get_induced_velocity_at_points(self, z_right=wide(self.right_vortex_vertices[:, 2]), trailing_vortex_direction=( self.steady_freestream_direction - if self.align_trailing_vortices_with_wind else - np.array([1, 0, 0]) + if self.align_trailing_vortices_with_wind + else np.array([1, 0, 0]) ), gamma=wide(vortex_strengths), - vortex_core_radius=self.vortex_core_radius + vortex_core_radius=self.vortex_core_radius, ) u_induced = np.sum(u_induced, axis=1) v_induced = np.sum(v_induced, axis=1) w_induced = np.sum(w_induced, axis=1) - V_induced = np.stack([ - u_induced, v_induced, w_induced - ], axis=1) + V_induced = np.stack([u_induced, v_induced, w_induced], axis=1) return V_induced - def get_velocity_at_points(self, - points: np.ndarray, - vortex_strengths: np.ndarray = None, - ) -> np.ndarray: + def get_velocity_at_points( + self, + points: np.ndarray, + vortex_strengths: np.ndarray = None, + ) -> np.ndarray: """ Computes the velocity at a set of points in the flowfield. @@ -569,11 +640,13 @@ def get_velocity_at_points(self, vortex_strengths=vortex_strengths, ) - rotation_freestream_velocities = np.array(self.op_point.compute_rotation_velocity_geometry_axes( - points - )) + rotation_freestream_velocities = np.array( + self.op_point.compute_rotation_velocity_geometry_axes(points) + ) - freestream_velocities = np.add(wide(self.steady_freestream_velocity), rotation_freestream_velocities) + freestream_velocities = np.add( + wide(self.steady_freestream_velocity), rotation_freestream_velocities + ) V = V_induced + freestream_velocities @@ -591,33 +664,36 @@ def calculate_fuselage_influences(self, points: np.ndarray) -> np.ndarray: this_fuse_radii = [] for fuse in self.airplane.fuselages: # iterating through the airplane fuselages - for xsec_num in range(len(fuse.xsecs)): # iterating through the current fuselage sections + for xsec_num in range( + len(fuse.xsecs) + ): # iterating through the current fuselage sections this_fuse_xsec = fuse.xsecs[xsec_num] this_fuse_centerline_points.append(this_fuse_xsec.xyz_c) this_fuse_radii.append(this_fuse_xsec.width / 2) - this_fuse_centerline_points = np.stack( - this_fuse_centerline_points, - axis=0 - ) - this_fuse_centerline_points = (this_fuse_centerline_points[1:, :] + - this_fuse_centerline_points[:-1, :]) / 2 + this_fuse_centerline_points = np.stack(this_fuse_centerline_points, axis=0) + this_fuse_centerline_points = ( + this_fuse_centerline_points[1:, :] + this_fuse_centerline_points[:-1, :] + ) / 2 this_fuse_radii = np.array(this_fuse_radii) - areas = np.pi * this_fuse_radii ** 2 - freestream_x_component = self.op_point.compute_freestream_velocity_geometry_axes()[ - 0] # TODO add in rotation corrections, add in doublets for alpha + areas = np.pi * this_fuse_radii**2 + freestream_x_component = ( + self.op_point.compute_freestream_velocity_geometry_axes()[0] + ) # TODO add in rotation corrections, add in doublets for alpha sigmas = freestream_x_component * np.diff(areas) - u_induced_fuse, v_induced_fuse, w_induced_fuse = calculate_induced_velocity_point_source( - x_field=tall(points[:, 0]), - y_field=tall(points[:, 1]), - z_field=tall(points[:, 2]), - x_source=wide(this_fuse_centerline_points[:, 0]), - y_source=wide(this_fuse_centerline_points[:, 1]), - z_source=wide(this_fuse_centerline_points[:, 2]), - sigma=wide(sigmas), - viscous_radius=0.0001, + u_induced_fuse, v_induced_fuse, w_induced_fuse = ( + calculate_induced_velocity_point_source( + x_field=tall(points[:, 0]), + y_field=tall(points[:, 1]), + z_field=tall(points[:, 2]), + x_source=wide(this_fuse_centerline_points[:, 0]), + y_source=wide(this_fuse_centerline_points[:, 1]), + z_source=wide(this_fuse_centerline_points[:, 2]), + sigma=wide(sigmas), + viscous_radius=0.0001, + ) ) # # Compressibility @@ -633,17 +709,16 @@ def calculate_fuselage_influences(self, points: np.ndarray) -> np.ndarray: fuselage_influences_y = np.sum(v_induced_fuse, axis=1) fuselage_influences_z = np.sum(w_induced_fuse, axis=1) - fuselage_influences = np.stack([ - fuselage_influences_x, fuselage_influences_y, fuselage_influences_z - ], axis=1) + fuselage_influences = np.stack( + [fuselage_influences_x, fuselage_influences_y, fuselage_influences_z], + axis=1, + ) return fuselage_influences - def calculate_streamlines(self, - seed_points: np.ndarray = None, - n_steps: int = 300, - length: float = None - ) -> np.ndarray: + def calculate_streamlines( + self, seed_points: np.ndarray = None, n_steps: int = 300, length: float = None + ) -> np.ndarray: """ Computes streamlines, starting at specific seed points. @@ -678,24 +753,29 @@ def calculate_streamlines(self, left_TE_vertices = self.back_left_vertices right_TE_vertices = self.back_right_vertices N_streamlines_target = 200 - seed_points_per_panel = np.maximum(1, N_streamlines_target // len(left_TE_vertices)) + seed_points_per_panel = np.maximum( + 1, N_streamlines_target // len(left_TE_vertices) + ) nondim_node_locations = np.linspace(0, 1, seed_points_per_panel + 1) - nondim_seed_locations = (nondim_node_locations[1:] + nondim_node_locations[:-1]) / 2 - - seed_points = np.concatenate([ - x * left_TE_vertices + (1 - x) * right_TE_vertices - for x in nondim_seed_locations - ]) + nondim_seed_locations = ( + nondim_node_locations[1:] + nondim_node_locations[:-1] + ) / 2 + + seed_points = np.concatenate( + [ + x * left_TE_vertices + (1 - x) * right_TE_vertices + for x in nondim_seed_locations + ] + ) streamlines = np.empty((len(seed_points), 3, n_steps)) streamlines[:, :, 0] = seed_points for i in range(1, n_steps): V = self.get_velocity_at_points(streamlines[:, :, i - 1]) - streamlines[:, :, i] = ( - streamlines[:, :, i - 1] + - length / n_steps * V / tall(np.linalg.norm(V, axis=1)) - ) + streamlines[:, :, i] = streamlines[ + :, :, i - 1 + ] + length / n_steps * V / tall(np.linalg.norm(V, axis=1)) self.streamlines = streamlines @@ -704,16 +784,17 @@ def calculate_streamlines(self, return streamlines - def draw(self, - c: np.ndarray = None, - cmap: str = None, - colorbar_label: str = None, - show: bool = True, - show_kwargs: Dict = None, - draw_streamlines=True, - recalculate_streamlines=False, - backend: str = "pyvista" - ): + def draw( + self, + c: np.ndarray = None, + cmap: str = None, + colorbar_label: str = None, + show: bool = True, + show_kwargs: Dict = None, + draw_streamlines=True, + recalculate_streamlines=False, + backend: str = "pyvista", + ): """ Draws the solution. Note: Must be called on a SOLVED AeroProblem object. To solve an AeroProblem, use opti.solve(). To substitute a solved solution, use ap = sol(ap). @@ -727,11 +808,12 @@ def draw(self, colorbar_label = "Vortex Strengths" if draw_streamlines: - if (not hasattr(self, 'streamlines')) or recalculate_streamlines: + if (not hasattr(self, "streamlines")) or recalculate_streamlines: self.calculate_streamlines() if backend == "plotly": from aerosandbox.visualization.plotly_Figure3D import Figure3D + fig = Figure3D() for i in range(len(self.front_left_vertices)): @@ -752,24 +834,26 @@ def draw(self, return fig.draw( show=show, - colorbar_title=colorbar_label - ** show_kwargs, + colorbar_title=colorbar_label**show_kwargs, ) elif backend == "pyvista": import pyvista as pv + plotter = pv.Plotter() plotter.title = "ASB NonlinearLiftingLine" plotter.add_axes() - plotter.show_grid(color='gray') + plotter.show_grid(color="gray") ### Draw the airplane mesh - points = np.concatenate([ - self.front_left_vertices, - self.back_left_vertices, - self.back_right_vertices, - self.front_right_vertices - ]) + points = np.concatenate( + [ + self.front_left_vertices, + self.back_left_vertices, + self.back_right_vertices, + self.front_right_vertices, + ] + ) N = len(self.front_left_vertices) range_N = np.arange(N) faces = tall(range_N) + wide(np.array([0, 1, 2, 3]) * N) @@ -792,12 +876,13 @@ def draw(self, ### Draw the streamlines if draw_streamlines: import aerosandbox.tools.pretty_plots as p + for i in range(self.streamlines.shape[0]): plotter.add_mesh( pv.Spline(self.streamlines[i, :, :].T), color=p.adjust_lightness("#7700FF", 1.5), opacity=0.7, - line_width=1 + line_width=1, ) if show: @@ -861,7 +946,7 @@ def draw(self, # ) -if __name__ == '__main__': +if __name__ == "__main__": ### Import Vanilla Airplane import aerosandbox as asb @@ -879,9 +964,8 @@ def draw(self, LL_aeros = NonlinearLiftingLine( airplane=vanilla, op_point=asb.OperatingPoint( - atmosphere=asb.Atmosphere(altitude=0), - velocity=10, # m/s - alpha=5), + atmosphere=asb.Atmosphere(altitude=0), velocity=10, alpha=5 # m/s + ), verbose=True, spanwise_resolution=5, ) diff --git a/aerosandbox/aerodynamics/aero_3D/singularities/point_source.py b/aerosandbox/aerodynamics/aero_3D/singularities/point_source.py index 510ca1f3b..738763f11 100644 --- a/aerosandbox/aerodynamics/aero_3D/singularities/point_source.py +++ b/aerosandbox/aerodynamics/aero_3D/singularities/point_source.py @@ -3,14 +3,14 @@ def calculate_induced_velocity_point_source( - x_field: Union[float, np.ndarray], - y_field: Union[float, np.ndarray], - z_field: Union[float, np.ndarray], - x_source: Union[float, np.ndarray], - y_source: Union[float, np.ndarray], - z_source: Union[float, np.ndarray], - sigma: Union[float, np.ndarray] = 1, - viscous_radius=0, + x_field: Union[float, np.ndarray], + y_field: Union[float, np.ndarray], + z_field: Union[float, np.ndarray], + x_source: Union[float, np.ndarray], + y_source: Union[float, np.ndarray], + z_source: Union[float, np.ndarray], + sigma: Union[float, np.ndarray] = 1, + viscous_radius=0, ) -> [Union[float, np.ndarray], Union[float, np.ndarray], Union[float, np.ndarray]]: """ Calculates the induced velocity at a point: @@ -58,20 +58,16 @@ def calculate_induced_velocity_point_source( dy = np.add(y_field, -y_source) dz = np.add(z_field, -z_source) - r_squared = ( - dx ** 2 + - dy ** 2 + - dz ** 2 - ) + r_squared = dx**2 + dy**2 + dz**2 def smoothed_x_15_inv(x): """ Approximates x^(-1.5) with a function that sharply goes to 0 in the x -> 0 limit. """ if not np.all(viscous_radius == 0): - return x / (x ** 2.5 + viscous_radius ** 2.5) + return x / (x**2.5 + viscous_radius**2.5) else: - return x ** -1.5 + return x**-1.5 grad_phi_multiplier = np.multiply(sigma, smoothed_x_15_inv(r_squared)) / (4 * np.pi) @@ -82,7 +78,7 @@ def smoothed_x_15_inv(x): return u, v, w -if __name__ == '__main__': +if __name__ == "__main__": args = (-2, 2, 30) x = np.linspace(*args) y = np.linspace(*args) @@ -93,15 +89,12 @@ def smoothed_x_15_inv(x): Yf = Y.flatten() Zf = Z.flatten() - def wide(array): return np.reshape(array, (1, -1)) - def tall(array): return np.reshape(array, (-1, 1)) - Uf, Vf, Wf = calculate_induced_velocity_point_source( x_field=Xf, y_field=Yf, @@ -116,16 +109,12 @@ def tall(array): dir_norm = np.reshape(np.linalg.norm(dir, axis=1), (-1, 1)) - dir = dir / dir_norm * dir_norm ** 0.2 + dir = dir / dir_norm * dir_norm**0.2 import pyvista as pv - pv.set_plot_theme('dark') + pv.set_plot_theme("dark") plotter = pv.Plotter() - plotter.add_arrows( - cent=pos, - direction=dir, - mag=0.15 - ) + plotter.add_arrows(cent=pos, direction=dir, mag=0.15) plotter.show_grid() plotter.show() diff --git a/aerosandbox/aerodynamics/aero_3D/singularities/uniform_strength_horseshoe_singularities.py b/aerosandbox/aerodynamics/aero_3D/singularities/uniform_strength_horseshoe_singularities.py index 761cdaad0..7f4d15fe9 100644 --- a/aerosandbox/aerodynamics/aero_3D/singularities/uniform_strength_horseshoe_singularities.py +++ b/aerosandbox/aerodynamics/aero_3D/singularities/uniform_strength_horseshoe_singularities.py @@ -3,18 +3,18 @@ def calculate_induced_velocity_horseshoe( - x_field: Union[float, np.ndarray], - y_field: Union[float, np.ndarray], - z_field: Union[float, np.ndarray], - x_left: Union[float, np.ndarray], - y_left: Union[float, np.ndarray], - z_left: Union[float, np.ndarray], - x_right: Union[float, np.ndarray], - y_right: Union[float, np.ndarray], - z_right: Union[float, np.ndarray], - gamma: Union[float, np.ndarray] = 1, - trailing_vortex_direction: np.ndarray = None, - vortex_core_radius: float = 0, + x_field: Union[float, np.ndarray], + y_field: Union[float, np.ndarray], + z_field: Union[float, np.ndarray], + x_left: Union[float, np.ndarray], + y_left: Union[float, np.ndarray], + z_left: Union[float, np.ndarray], + x_right: Union[float, np.ndarray], + y_right: Union[float, np.ndarray], + z_right: Union[float, np.ndarray], + gamma: Union[float, np.ndarray] = 1, + trailing_vortex_direction: np.ndarray = None, + vortex_core_radius: float = 0, ) -> [Union[float, np.ndarray], Union[float, np.ndarray], Union[float, np.ndarray]]: """ Calculates the induced velocity at a point: @@ -61,19 +61,23 @@ def calculate_induced_velocity_horseshoe( if trailing_vortex_direction is None: trailing_vortex_direction = np.array([1, 0, 0]) - np.assert_equal_shape({ - "x_field": x_field, - "y_field": y_field, - "z_field": z_field, - }) - np.assert_equal_shape({ - "x_left" : x_left, - "y_left" : y_left, - "z_left" : z_left, - "x_right": x_right, - "y_right": y_right, - "z_right": z_right, - }) + np.assert_equal_shape( + { + "x_field": x_field, + "y_field": y_field, + "z_field": z_field, + } + ) + np.assert_equal_shape( + { + "x_left": x_left, + "y_left": y_left, + "z_left": z_left, + "x_right": x_right, + "y_right": y_right, + "z_right": z_right, + } + ) a_x = np.add(x_field, -x_left) a_y = np.add(y_field, -y_left) @@ -91,7 +95,7 @@ def calculate_induced_velocity_horseshoe( def smoothed_inv(x): "Approximates 1/x with a function that sharply goes to 0 in the x -> 0 limit." if not np.all(vortex_core_radius == 0): - return x / (x ** 2 + vortex_core_radius ** 2) + return x / (x**2 + vortex_core_radius**2) else: return 1 / x @@ -112,8 +116,8 @@ def smoothed_inv(x): b_cross_u_z = b_x * u_y - b_y * u_x b_dot_u = b_x * u_x + b_y * u_y + b_z * u_z - norm_a = (a_x ** 2 + a_y ** 2 + a_z ** 2) ** 0.5 - norm_b = (b_x ** 2 + b_y ** 2 + b_z ** 2) ** 0.5 + norm_a = (a_x**2 + a_y**2 + a_z**2) ** 0.5 + norm_b = (b_x**2 + b_y**2 + b_z**2) ** 0.5 norm_a_inv = smoothed_inv(norm_a) norm_b_inv = smoothed_inv(norm_b) @@ -126,34 +130,19 @@ def smoothed_inv(x): constant = gamma / (4 * np.pi) u = np.multiply( - constant, - ( - a_cross_b_x * term1 + - a_cross_u_x * term2 - - b_cross_u_x * term3 - ) + constant, (a_cross_b_x * term1 + a_cross_u_x * term2 - b_cross_u_x * term3) ) v = np.multiply( - constant, - ( - a_cross_b_y * term1 + - a_cross_u_y * term2 - - b_cross_u_y * term3 - ) + constant, (a_cross_b_y * term1 + a_cross_u_y * term2 - b_cross_u_y * term3) ) w = np.multiply( - constant, - ( - a_cross_b_z * term1 + - a_cross_u_z * term2 - - b_cross_u_z * term3 - ) + constant, (a_cross_b_z * term1 + a_cross_u_z * term2 - b_cross_u_z * term3) ) return u, v, w -if __name__ == '__main__': +if __name__ == "__main__": ##### Check single vortex u, v, w = calculate_induced_velocity_horseshoe( x_field=0, @@ -201,24 +190,17 @@ def smoothed_inv(x): dir_norm = np.reshape(np.linalg.norm(dir, axis=1), (-1, 1)) - dir = dir / dir_norm * dir_norm ** 0.2 + dir = dir / dir_norm * dir_norm**0.2 import pyvista as pv - pv.set_plot_theme('dark') + pv.set_plot_theme("dark") plotter = pv.Plotter() - plotter.add_arrows( - cent=pos, - direction=dir, - mag=0.15 - ) + plotter.add_arrows(cent=pos, direction=dir, mag=0.15) plotter.add_lines( - lines=np.array([ - [Xf.max(), left[1], left[2]], - left, - right, - [Xf.max(), right[1], right[2]] - ]) + lines=np.array( + [[Xf.max(), left[1], left[2]], left, right, [Xf.max(), right[1], right[2]]] + ) ) plotter.show_grid() plotter.show() @@ -242,15 +224,12 @@ def smoothed_inv(x): rights = np.array([center, right]) strengths = np.array([2, 1]) - def wide(array): return np.reshape(array, (1, -1)) - def tall(array): return np.reshape(array, (-1, 1)) - Uf_each, Vf_each, Wf_each = calculate_induced_velocity_horseshoe( x_field=wide(Xf), y_field=wide(Yf), @@ -273,27 +252,25 @@ def tall(array): dir_norm = np.reshape(np.linalg.norm(dir, axis=1), (-1, 1)) - dir = dir / dir_norm * dir_norm ** 0.2 + dir = dir / dir_norm * dir_norm**0.2 import pyvista as pv - pv.set_plot_theme('dark') + pv.set_plot_theme("dark") plotter = pv.Plotter() - plotter.add_arrows( - cent=pos, - direction=dir, - mag=0.15 - ) + plotter.add_arrows(cent=pos, direction=dir, mag=0.15) plotter.add_lines( - lines=np.array([ - [Xf.max(), left[1], left[2]], - left, - center, - [Xf.max(), center[1], center[2]], - center, - right, - [Xf.max(), right[1], right[2]] - ]) + lines=np.array( + [ + [Xf.max(), left[1], left[2]], + left, + center, + [Xf.max(), center[1], center[2]], + center, + right, + [Xf.max(), right[1], right[2]], + ] + ) ) plotter.show_grid() plotter.show() diff --git a/aerosandbox/aerodynamics/aero_3D/test_aero_3D/geometries/conventional.py b/aerosandbox/aerodynamics/aero_3D/test_aero_3D/geometries/conventional.py index 7dc96109a..03fa9a86b 100644 --- a/aerosandbox/aerodynamics/aero_3D/test_aero_3D/geometries/conventional.py +++ b/aerosandbox/aerodynamics/aero_3D/test_aero_3D/geometries/conventional.py @@ -15,7 +15,11 @@ symmetric=True, # Should this wing be mirrored across the XZ plane? xsecs=[ # The wing's cross ("X") sections asb.WingXSec( # Root - xyz_le=[0, 0, 0], # Coordinates of the XSec's leading edge, relative to the wing's leading edge. + xyz_le=[ + 0, + 0, + 0, + ], # Coordinates of the XSec's leading edge, relative to the wing's leading edge. chord=0.18, twist=2, # degrees airfoil=wing_airfoil, # Airfoils are blended between a given XSec and the next one. @@ -32,7 +36,7 @@ twist=-2, airfoil=wing_airfoil, ), - ] + ], ), asb.Wing( name="Horizontal Stabilizer", @@ -45,12 +49,9 @@ airfoil=tail_airfoil, ), asb.WingXSec( # tip - xyz_le=[0.02, 0.17, 0], - chord=0.08, - twist=-10, - airfoil=tail_airfoil - ) - ] + xyz_le=[0.02, 0.17, 0], chord=0.08, twist=-10, airfoil=tail_airfoil + ), + ], ).translate([0.6, 0, 0.06]), asb.Wing( name="Vertical Stabilizer", @@ -63,13 +64,10 @@ airfoil=tail_airfoil, ), asb.WingXSec( - xyz_le=[0.04, 0, 0.15], - chord=0.06, - twist=0, - airfoil=tail_airfoil - ) - ] - ).translate([0.6, 0, 0.07]) + xyz_le=[0.04, 0, 0.15], chord=0.06, twist=0, airfoil=tail_airfoil + ), + ], + ).translate([0.6, 0, 0.07]), ], fuselages=[ asb.Fuselage( @@ -77,13 +75,13 @@ xsecs=[ asb.FuselageXSec( xyz_c=[0.8 * xi - 0.1, 0, 0.1 * xi - 0.03], - radius=0.6 * asb.Airfoil("dae51").local_thickness(x_over_c=xi) + radius=0.6 * asb.Airfoil("dae51").local_thickness(x_over_c=xi), ) for xi in np.cosspace(0, 1, 30) - ] + ], ) - ] + ], ) -if __name__ == '__main__': +if __name__ == "__main__": airplane.draw() diff --git a/aerosandbox/aerodynamics/aero_3D/test_aero_3D/geometries/flat_plate.py b/aerosandbox/aerodynamics/aero_3D/test_aero_3D/geometries/flat_plate.py index 184f20dff..3779caa08 100644 --- a/aerosandbox/aerodynamics/aero_3D/test_aero_3D/geometries/flat_plate.py +++ b/aerosandbox/aerodynamics/aero_3D/test_aero_3D/geometries/flat_plate.py @@ -23,10 +23,10 @@ twist=0, airfoil=airfoil, ), - ] + ], ) - ] + ], ) -if __name__ == '__main__': +if __name__ == "__main__": airplane.draw() diff --git a/aerosandbox/aerodynamics/aero_3D/test_aero_3D/geometries/flat_plate_mirrored.py b/aerosandbox/aerodynamics/aero_3D/test_aero_3D/geometries/flat_plate_mirrored.py index c4f2392f6..964546ceb 100644 --- a/aerosandbox/aerodynamics/aero_3D/test_aero_3D/geometries/flat_plate_mirrored.py +++ b/aerosandbox/aerodynamics/aero_3D/test_aero_3D/geometries/flat_plate_mirrored.py @@ -23,10 +23,10 @@ twist=0, airfoil=airfoil, ), - ] + ], ) - ] + ], ) -if __name__ == '__main__': +if __name__ == "__main__": airplane.draw() diff --git a/aerosandbox/aerodynamics/aero_3D/test_aero_3D/geometries/vanilla.py b/aerosandbox/aerodynamics/aero_3D/test_aero_3D/geometries/vanilla.py index 91fc64c50..9be7e3d7a 100644 --- a/aerosandbox/aerodynamics/aero_3D/test_aero_3D/geometries/vanilla.py +++ b/aerosandbox/aerodynamics/aero_3D/test_aero_3D/geometries/vanilla.py @@ -25,40 +25,32 @@ chord=0.6, twist=2, airfoil=sd7037, - ) - ] + ), + ], ), asb.Wing( name="H-stab", symmetric=True, xsecs=[ asb.WingXSec( - xyz_le=[0, 0, 0], - chord=0.7, - airfoil=asb.Airfoil("naca0012") + xyz_le=[0, 0, 0], chord=0.7, airfoil=asb.Airfoil("naca0012") ), asb.WingXSec( - xyz_le=[0.14, 1.25, 0], - chord=0.42, - airfoil=asb.Airfoil("naca0012") + xyz_le=[0.14, 1.25, 0], chord=0.42, airfoil=asb.Airfoil("naca0012") ), - ] + ], ).translate([4, 0, 0]), asb.Wing( name="V-stab", xsecs=[ asb.WingXSec( - xyz_le=[0, 0, 0], - chord=0.7, - airfoil=asb.Airfoil("naca0012") + xyz_le=[0, 0, 0], chord=0.7, airfoil=asb.Airfoil("naca0012") ), asb.WingXSec( - xyz_le=[0.14, 0, 1], - chord=0.42, - airfoil=asb.Airfoil("naca0012") - ) - ] - ).translate([4, 0, 0]) + xyz_le=[0.14, 0, 1], chord=0.42, airfoil=asb.Airfoil("naca0012") + ), + ], + ).translate([4, 0, 0]), ], fuselages=[ asb.Fuselage( @@ -66,13 +58,13 @@ xsecs=[ asb.FuselageXSec( xyz_c=[xi * 5 - 0.5, 0, 0], - radius=asb.Airfoil("naca0024").local_thickness(x_over_c=xi) + radius=asb.Airfoil("naca0024").local_thickness(x_over_c=xi), ) for xi in np.cosspace(0, 1, 30) - ] + ], ) - ] + ], ) -if __name__ == '__main__': +if __name__ == "__main__": airplane.draw() diff --git a/aerosandbox/aerodynamics/aero_3D/test_aero_3D/test_aero_buildup/avl_validation.py b/aerosandbox/aerodynamics/aero_3D/test_aero_3D/test_aero_buildup/avl_validation.py index b4f1d1110..093d5e071 100644 --- a/aerosandbox/aerodynamics/aero_3D/test_aero_3D/test_aero_buildup/avl_validation.py +++ b/aerosandbox/aerodynamics/aero_3D/test_aero_3D/test_aero_buildup/avl_validation.py @@ -19,7 +19,11 @@ symmetric=True, # Should this wing be mirrored across the XZ plane? xsecs=[ # The wing's cross ("X") sections asb.WingXSec( # Root - xyz_le=[0, 0, 0], # Coordinates of the XSec's leading edge, relative to the wing's leading edge. + xyz_le=[ + 0, + 0, + 0, + ], # Coordinates of the XSec's leading edge, relative to the wing's leading edge. chord=0.18, twist=0, # degrees airfoil=wing_airfoil, # Airfoils are blended between a given XSec and the next one. @@ -36,7 +40,7 @@ twist=-0, airfoil=wing_airfoil, ), - ] + ], ), asb.Wing( name="Horizontal Stabilizer", @@ -49,12 +53,9 @@ airfoil=tail_airfoil, ), asb.WingXSec( # tip - xyz_le=[0.02, 0.17, 0], - chord=0.08, - twist=-10, - airfoil=tail_airfoil - ) - ] + xyz_le=[0.02, 0.17, 0], chord=0.08, twist=-10, airfoil=tail_airfoil + ), + ], ).translate([0.6, 0, 0.06]), asb.Wing( name="Vertical Stabilizer", @@ -67,13 +68,10 @@ airfoil=tail_airfoil, ), asb.WingXSec( - xyz_le=[0.04, 0, 0.15], - chord=0.06, - twist=0, - airfoil=tail_airfoil - ) - ] - ).translate([0.6, 0, 0.07]) + xyz_le=[0.04, 0, 0.15], chord=0.06, twist=0, airfoil=tail_airfoil + ), + ], + ).translate([0.6, 0, 0.07]), ], fuselages=[ # asb.Fuselage( @@ -86,7 +84,7 @@ # for xi in np.cosspace(0, 1, 30) # ] # ) - ] + ], ) op_point = asb.OperatingPoint( @@ -96,21 +94,13 @@ ) ab = asb.AeroBuildup( - airplane, - op_point, - xyz_ref=airplane.xyz_ref + airplane, op_point, xyz_ref=airplane.xyz_ref ).run_with_stability_derivatives() -av = asb.AVL( - airplane, - op_point, - xyz_ref=airplane.xyz_ref -).run() +av = asb.AVL(airplane, op_point, xyz_ref=airplane.xyz_ref).run() vl = asb.VortexLatticeMethod( - airplane, - op_point, - xyz_ref=airplane.xyz_ref + airplane, op_point, xyz_ref=airplane.xyz_ref ).run_with_stability_derivatives() keys = set() @@ -120,20 +110,26 @@ keys.sort() titles = [ - 'Output', - 'AeroBuildup', - 'AVL ', - 'VLM ', - 'AB & AVL Significantly Different?' + "Output", + "AeroBuildup", + "AVL ", + "VLM ", + "AB & AVL Significantly Different?", ] def println(*data): print( - " | ".join([ - d.ljust(len(t)) if isinstance(d, str) else f"{{0:{len(t)}.3g}}".format(d) - for d, t in zip(data, titles) - ]) + " | ".join( + [ + ( + d.ljust(len(t)) + if isinstance(d, str) + else f"{{0:{len(t)}.3g}}".format(d) + ) + for d, t in zip(data, titles) + ] + ) ) @@ -144,21 +140,21 @@ def println(*data): rel = 0.20 abs = 0.01 - if 'l' in k or 'm' in k or 'n' in k: + if "l" in k or "m" in k or "n" in k: rel = 0.5 abs = 0.05 differences = ab[k] != pytest.approx(av[k], rel=rel, abs=abs) - differences_text = '*' if differences else '' - if 'D' in k: - differences_text = 'Expected' + differences_text = "*" if differences else "" + if "D" in k: + differences_text = "Expected" println( k, float(ab[k]), float(av[k]), - float(vl[k]) if k in vl.keys() else ' ' * 5 + '-', - differences_text + float(vl[k]) if k in vl.keys() else " " * 5 + "-", + differences_text, ) except (KeyError, TypeError): diff --git a/aerosandbox/aerodynamics/aero_3D/test_aero_3D/test_aero_buildup/draw_fuselage_aero.py b/aerosandbox/aerodynamics/aero_3D/test_aero_3D/test_aero_buildup/draw_fuselage_aero.py index c8ee1afd7..7654fa096 100644 --- a/aerosandbox/aerodynamics/aero_3D/test_aero_3D/test_aero_buildup/draw_fuselage_aero.py +++ b/aerosandbox/aerodynamics/aero_3D/test_aero_3D/test_aero_buildup/draw_fuselage_aero.py @@ -7,41 +7,35 @@ fuselage = asb.Fuselage( xsecs=[ asb.FuselageXSec( - xyz_c=[xi, 0, 0], - radius=asb.Airfoil("naca0010").local_thickness(xi) + xyz_c=[xi, 0, 0], radius=asb.Airfoil("naca0010").local_thickness(xi) ) for xi in np.cosspace(0, 1, 20) ], ) fig, ax = plt.subplots(figsize=(7, 6)) -Beta, Alpha = np.meshgrid( - np.linspace(-90, 90, 500), - np.linspace(-90, 90, 500) -) +Beta, Alpha = np.meshgrid(np.linspace(-90, 90, 500), np.linspace(-90, 90, 500)) aero = asb.AeroBuildup( - airplane=asb.Airplane( - fuselages=[fuselage] - ), + airplane=asb.Airplane(fuselages=[fuselage]), op_point=asb.OperatingPoint( velocity=10, alpha=Alpha, beta=Beta, - ) + ), ).run() from aerosandbox.tools.string_formatting import eng_string p.contour( - Beta, Alpha, aero["L"], colorbar_label="Lift $L$ [N]", + Beta, + Alpha, + aero["L"], + colorbar_label="Lift $L$ [N]", # levels=100, linelabels_format=lambda s: f"{s:.2g} N", - cmap=p.mpl.colormaps.get_cmap("coolwarm") + cmap=p.mpl.colormaps.get_cmap("coolwarm"), ) p.equal() plt.xlabel(r"$\beta$ [deg]") plt.ylabel(r"$\alpha$ [deg]") -p.show_plot( - "3D Fuselage Lift", - rotate_axis_labels=False -) +p.show_plot("3D Fuselage Lift", rotate_axis_labels=False) diff --git a/aerosandbox/aerodynamics/aero_3D/test_aero_3D/test_aero_buildup/fuselage_moment_calculation.py b/aerosandbox/aerodynamics/aero_3D/test_aero_3D/test_aero_buildup/fuselage_moment_calculation.py index 62116a5ea..b6d1b9350 100644 --- a/aerosandbox/aerodynamics/aero_3D/test_aero_3D/test_aero_buildup/fuselage_moment_calculation.py +++ b/aerosandbox/aerodynamics/aero_3D/test_aero_3D/test_aero_buildup/fuselage_moment_calculation.py @@ -7,8 +7,7 @@ fuselage = asb.Fuselage( xsecs=[ asb.FuselageXSec( - xyz_c=[xi, 0, 0], - radius=asb.Airfoil("naca0020").local_thickness(xi) / 2 + xyz_c=[xi, 0, 0], radius=asb.Airfoil("naca0020").local_thickness(xi) / 2 ) for xi in np.cosspace(0, 1, 20) ], @@ -20,39 +19,21 @@ def get_aero(xyz_ref): return asb.AeroBuildup( - airplane=asb.Airplane( - fuselages=[fuselage], - xyz_ref=xyz_ref - ), - op_point=asb.OperatingPoint( - velocity=10, - alpha=5, - beta=5 - ) + airplane=asb.Airplane(fuselages=[fuselage], xyz_ref=xyz_ref), + op_point=asb.OperatingPoint(velocity=10, alpha=5, beta=5), ).run() x_cgs = np.linspace(0, 1, 11) -aeros = np.array([ - get_aero(xyz_ref=[x, 0, 0]) - for x in x_cgs -], dtype="O") +aeros = np.array([get_aero(xyz_ref=[x, 0, 0]) for x in x_cgs], dtype="O") import matplotlib.pyplot as plt import aerosandbox.tools.pretty_plots as p fig, ax = plt.subplots(figsize=(4, 4)) -plt.plot( - x_cgs, - np.array([a['m_b'] for a in aeros]), - ".-" -) -p.show_plot( - "Fuselage Pitching Moment", - r"$x_{cg} / l$", - "Pitching Moment [Nm]" -) +plt.plot(x_cgs, np.array([a["m_b"] for a in aeros]), ".-") +p.show_plot("Fuselage Pitching Moment", r"$x_{cg} / l$", "Pitching Moment [Nm]") """ Expected result: diff --git a/aerosandbox/aerodynamics/aero_3D/test_aero_3D/test_aero_buildup/fuselage_transonics.py b/aerosandbox/aerodynamics/aero_3D/test_aero_3D/test_aero_buildup/fuselage_transonics.py index a1cb1f126..c6dbef968 100644 --- a/aerosandbox/aerodynamics/aero_3D/test_aero_3D/test_aero_buildup/fuselage_transonics.py +++ b/aerosandbox/aerodynamics/aero_3D/test_aero_3D/test_aero_buildup/fuselage_transonics.py @@ -7,8 +7,7 @@ fuselage = asb.Fuselage( xsecs=[ asb.FuselageXSec( - xyz_c=[xi, 0, 0], - radius=asb.Airfoil("naca0020").local_thickness(xi) / 2 + xyz_c=[xi, 0, 0], radius=asb.Airfoil("naca0020").local_thickness(xi) / 2 ) for xi in np.cosspace(0, 1, 20) ], @@ -21,36 +20,19 @@ ) aero = asb.AeroBuildup( - airplane=asb.Airplane( - fuselages=[fuselage] - ), + airplane=asb.Airplane(fuselages=[fuselage]), op_point=op_point, ).run() -plt.plot( - op_point.mach(), - aero["CD"], - label="Full Model" -) +plt.plot(op_point.mach(), aero["CD"], label="Full Model") aero = asb.AeroBuildup( - airplane=asb.Airplane( - fuselages=[fuselage] - ), + airplane=asb.Airplane(fuselages=[fuselage]), op_point=op_point, - include_wave_drag=False + include_wave_drag=False, ).run() -plt.plot( - op_point.mach(), - aero["CD"], - zorder=1.9, - label="Model without Wave Drag" -) -p.show_plot( - "Transonic Fuselage Drag", - "Mach [-]", - "Drag Area $C_D \cdot A$ [m$^2$]" -) +plt.plot(op_point.mach(), aero["CD"], zorder=1.9, label="Model without Wave Drag") +p.show_plot("Transonic Fuselage Drag", "Mach [-]", "Drag Area $C_D \cdot A$ [m$^2$]") print("%.4g" % aero["CD"][-1]) diff --git a/aerosandbox/aerodynamics/aero_3D/test_aero_3D/test_aero_buildup/test_basic.py b/aerosandbox/aerodynamics/aero_3D/test_aero_3D/test_aero_buildup/test_basic.py index 5d1f9c4be..6e6bb1f02 100644 --- a/aerosandbox/aerodynamics/aero_3D/test_aero_3D/test_aero_buildup/test_basic.py +++ b/aerosandbox/aerodynamics/aero_3D/test_aero_3D/test_aero_buildup/test_basic.py @@ -1,6 +1,8 @@ import aerosandbox as asb import pytest -from aerosandbox.aerodynamics.aero_3D.test_aero_3D.geometries.conventional import airplane +from aerosandbox.aerodynamics.aero_3D.test_aero_3D.geometries.conventional import ( + airplane, +) def test_aero_buildup(): @@ -11,6 +13,6 @@ def test_aero_buildup(): return analysis.run() -if __name__ == '__main__': +if __name__ == "__main__": aero = test_aero_buildup() # pytest.main() diff --git a/aerosandbox/aerodynamics/aero_3D/test_aero_3D/test_aero_buildup/test_fuselage_aerodynamics_optimization.py b/aerosandbox/aerodynamics/aero_3D/test_aero_3D/test_aero_buildup/test_fuselage_aerodynamics_optimization.py index 7980c355b..1f2cc950d 100644 --- a/aerosandbox/aerodynamics/aero_3D/test_aero_3D/test_aero_buildup/test_fuselage_aerodynamics_optimization.py +++ b/aerosandbox/aerodynamics/aero_3D/test_aero_3D/test_aero_buildup/test_fuselage_aerodynamics_optimization.py @@ -13,21 +13,15 @@ def test_fuselage_aerodynamics_optimization(): xsecs=[ asb.FuselageXSec( xyz_c=[xi, 0, 0], - radius=asb.Airfoil("naca0010").local_thickness(0.8 * xi) + radius=asb.Airfoil("naca0010").local_thickness(0.8 * xi), ) for xi in np.cosspace(0, 1, 20) ], ) aero = asb.AeroBuildup( - airplane=asb.Airplane( - fuselages=[fuselage] - ), - op_point=asb.OperatingPoint( - velocity=10, - alpha=alpha, - beta=beta - ) + airplane=asb.Airplane(fuselages=[fuselage]), + op_point=asb.OperatingPoint(velocity=10, alpha=alpha, beta=beta), ).run() opti.minimize(-aero["L"] / aero["D"]) @@ -42,6 +36,6 @@ def test_fuselage_aerodynamics_optimization(): assert sol(beta) == pytest.approx(0, abs=1e-2) -if __name__ == '__main__': +if __name__ == "__main__": test_fuselage_aerodynamics_optimization() pytest.main() diff --git a/aerosandbox/aerodynamics/aero_3D/test_aero_3D/test_aero_buildup/test_fuselage_cylinder_crossflow_limit.py b/aerosandbox/aerodynamics/aero_3D/test_aero_3D/test_aero_buildup/test_fuselage_cylinder_crossflow_limit.py index baff6b963..19a2ffa21 100644 --- a/aerosandbox/aerodynamics/aero_3D/test_aero_3D/test_aero_buildup/test_fuselage_cylinder_crossflow_limit.py +++ b/aerosandbox/aerodynamics/aero_3D/test_aero_3D/test_aero_buildup/test_fuselage_cylinder_crossflow_limit.py @@ -4,12 +4,13 @@ rtol = 0.10 + def make_fuselage( - diameter = 0.1, - length = 10, - alpha_geometric=0., - beta_geometric=0., - N=101, + diameter=0.1, + length=10, + alpha_geometric=0.0, + beta_geometric=0.0, + N=101, ): return asb.Fuselage( xsecs=[ @@ -17,23 +18,20 @@ def make_fuselage( xyz_c=[ xi * np.cosd(alpha_geometric) * np.cosd(beta_geometric), xi * np.sind(beta_geometric), - xi * np.sind(-alpha_geometric) + xi * np.sind(-alpha_geometric), ], - radius=diameter / 2 + radius=diameter / 2, ) for xi in [0, length] ] ).subdivide_sections(N) + def test_90_deg_crossflow(): diameter = 0.1 length = 10 - fuselage = make_fuselage( - diameter=diameter, - length=length, - alpha_geometric=0 - ) + fuselage = make_fuselage(diameter=diameter, length=length, alpha_geometric=0) airplane = asb.Airplane( fuselages=[fuselage], @@ -43,33 +41,26 @@ def test_90_deg_crossflow(): ) op_point = asb.OperatingPoint( - velocity=50, - alpha=90, - beta=0, - ) + velocity=50, + alpha=90, + beta=0, + ) aero = asb.AeroBuildup( - airplane=airplane, - op_point=op_point, - xyz_ref=np.array([length / 2, 0, 0]) + airplane=airplane, op_point=op_point, xyz_ref=np.array([length / 2, 0, 0]) ).run() print(aero) from aerosandbox.library.aerodynamics.viscous import Cd_cylinder - CD_expected = Cd_cylinder( - Re_D=op_point.reynolds(diameter), - mach=op_point.mach() - ) + CD_expected = Cd_cylinder(Re_D=op_point.reynolds(diameter), mach=op_point.mach()) print(CD_expected) - assert aero["CD"] == pytest.approx( - CD_expected, rel=0.1 - ) + assert aero["CD"] == pytest.approx(CD_expected, rel=0.1) -if __name__ == '__main__': +if __name__ == "__main__": pass # make_fuselage().draw_three_view() - test_90_deg_crossflow() \ No newline at end of file + test_90_deg_crossflow() diff --git a/aerosandbox/aerodynamics/aero_3D/test_aero_3D/test_aero_buildup/test_low_ar_wings_validation/low_ar_wings_validation.py b/aerosandbox/aerodynamics/aero_3D/test_aero_3D/test_aero_buildup/test_low_ar_wings_validation/low_ar_wings_validation.py index 897646989..848f1e6c5 100644 --- a/aerosandbox/aerodynamics/aero_3D/test_aero_3D/test_aero_buildup/test_low_ar_wings_validation/low_ar_wings_validation.py +++ b/aerosandbox/aerodynamics/aero_3D/test_aero_3D/test_aero_buildup/test_low_ar_wings_validation/low_ar_wings_validation.py @@ -10,68 +10,47 @@ symmetric=True, xsecs=[ asb.WingXSec( - xyz_le=[ - -sweep_position_x_over_c * taper ** -0.5, - 0, - 0 - ], - chord=taper ** -0.5, - airfoil=asb.Airfoil("naca0010") + xyz_le=[-sweep_position_x_over_c * taper**-0.5, 0, 0], + chord=taper**-0.5, + airfoil=asb.Airfoil("naca0010"), ), asb.WingXSec( - xyz_le=[ - -sweep_position_x_over_c * taper ** 0.5, - AR, - 0 - ], - chord=taper ** 0.5, - airfoil=asb.Airfoil("naca0010") - ) - ] + xyz_le=[-sweep_position_x_over_c * taper**0.5, AR, 0], + chord=taper**0.5, + airfoil=asb.Airfoil("naca0010"), + ), + ], ).subdivide_sections(5) -airplane = asb.Airplane( - wings=[wing] -) +airplane = asb.Airplane(wings=[wing]) alphas = np.linspace(0, 90, 500) op_point = asb.OperatingPoint( - atmosphere=asb.Atmosphere(altitude=0), - velocity=10, - alpha=alphas + atmosphere=asb.Atmosphere(altitude=0), velocity=10, alpha=alphas ) -aero = asb.AeroBuildup( - airplane=airplane, - op_point=op_point -).run() +aero = asb.AeroBuildup(airplane=airplane, op_point=op_point).run() -if __name__ == '__main__': +if __name__ == "__main__": import matplotlib.pyplot as plt import aerosandbox.tools.pretty_plots as p fig, ax = plt.subplots() - plt.plot( - alphas, - aero["CL"], - label="AeroBuildup" - ) + plt.plot(alphas, aero["CL"], label="AeroBuildup") from aerosandbox.tools.webplotdigitizer_reader import read_webplotdigitizer_csv - polhamus_data = read_webplotdigitizer_csv( - filename="data/wpd_datasets.csv" - ) + polhamus_data = read_webplotdigitizer_csv(filename="data/wpd_datasets.csv") - polhamus_data_AR = polhamus_data[f'{AR:.1f}'] + polhamus_data_AR = polhamus_data[f"{AR:.1f}"] plt.plot( polhamus_data_AR[:, 0], polhamus_data_AR[:, 1], ".", - label="Experiment (Polhamus)" + label="Experiment (Polhamus)", ) plt.xlim(alphas.min(), alphas.max()) plt.ylim(0, 1.5) @@ -81,5 +60,5 @@ p.show_plot( f"AeroBuildup for Low-Aspect-Ratio, Swept Wings\n$AR={AR:.1f}$", r"Angle of Attack $\alpha$ [deg]", - r"Lift Coefficient $C_L$ [-]" + r"Lift Coefficient $C_L$ [-]", ) diff --git a/aerosandbox/aerodynamics/aero_3D/test_aero_3D/test_aero_buildup/test_moment_validation/test_fuselage_moment_and_derivatives.py b/aerosandbox/aerodynamics/aero_3D/test_aero_3D/test_aero_buildup/test_moment_validation/test_fuselage_moment_and_derivatives.py index 7dddb6e1c..888d70958 100644 --- a/aerosandbox/aerodynamics/aero_3D/test_aero_3D/test_aero_buildup/test_moment_validation/test_fuselage_moment_and_derivatives.py +++ b/aerosandbox/aerodynamics/aero_3D/test_aero_3D/test_aero_buildup/test_moment_validation/test_fuselage_moment_and_derivatives.py @@ -6,10 +6,7 @@ rtol = 0.10 -def make_fuselage( - alpha_geometric=0., - beta_geometric=0. -) -> asb.Fuselage: +def make_fuselage(alpha_geometric=0.0, beta_geometric=0.0) -> asb.Fuselage: x = [0, 0.2, 0.8, 1] return asb.Fuselage( @@ -18,17 +15,13 @@ def make_fuselage( xyz_c=[ xi * np.cosd(alpha_geometric) * np.cosd(beta_geometric), xi * np.sind(beta_geometric), - xi * np.sind(-alpha_geometric) + xi * np.sind(-alpha_geometric), ], radius=np.where( xi < 0.2, 0.25 * xi / 0.2, - np.where( - xi < 0.8, - 0.25, - 0.25 * (1 - xi) / 0.2 - ) - ) + np.where(xi < 0.8, 0.25, 0.25 * (1 - xi) / 0.2), + ), ) for xi in x ] @@ -36,13 +29,10 @@ def make_fuselage( def test_derivatives_at_zero_geometric_angles( - AeroAnalysis: Type = asb.AeroBuildup, + AeroAnalysis: Type = asb.AeroBuildup, ): - fuselage = make_fuselage( - alpha_geometric=0., - beta_geometric=0. - ) + fuselage = make_fuselage(alpha_geometric=0.0, beta_geometric=0.0) airplane = asb.Airplane( fuselages=[fuselage], @@ -74,37 +64,32 @@ def test_derivatives_at_zero_geometric_angles( assert aero["Cn"] == pytest.approx(0, abs=1e-3) assert aero["Cma"] == pytest.approx( - 2 * fuselage.volume() / airplane.s_ref / airplane.c_ref, - rel=rtol + 2 * fuselage.volume() / airplane.s_ref / airplane.c_ref, rel=rtol ) assert aero["Cnb"] == pytest.approx( - -2 * fuselage.volume() / airplane.s_ref / airplane.b_ref, - rel=rtol + -2 * fuselage.volume() / airplane.s_ref / airplane.b_ref, rel=rtol ) def test_derivatives_at_constant_offset( - AeroAnalysis: Type = asb.AeroBuildup, + AeroAnalysis: Type = asb.AeroBuildup, ): - fuselage = make_fuselage( - alpha_geometric=5., - beta_geometric=10. - ) + fuselage = make_fuselage(alpha_geometric=5.0, beta_geometric=10.0) airplane = asb.Airplane( fuselages=[fuselage], s_ref=np.pi, - c_ref=1., + c_ref=1.0, b_ref=0.5, ) analysis = AeroAnalysis( airplane=airplane, op_point=asb.OperatingPoint( - velocity=100., - alpha=-5., - beta=-10., + velocity=100.0, + alpha=-5.0, + beta=-10.0, ), xyz_ref=np.array([0, 0, 0]), ) @@ -122,37 +107,32 @@ def test_derivatives_at_constant_offset( # assert aero["Cn"] == pytest.approx(0, abs=1e-3) assert aero["Cma"] == pytest.approx( - 2 * fuselage.volume() / airplane.s_ref / airplane.c_ref, - rel=rtol + 2 * fuselage.volume() / airplane.s_ref / airplane.c_ref, rel=rtol ) assert aero["Cnb"] == pytest.approx( - -2 * fuselage.volume() / airplane.s_ref / airplane.b_ref, - rel=rtol + -2 * fuselage.volume() / airplane.s_ref / airplane.b_ref, rel=rtol ) def test_derivatives_at_incidence( - AeroAnalysis: Type = asb.AeroBuildup, + AeroAnalysis: Type = asb.AeroBuildup, ): - fuselage = make_fuselage( - alpha_geometric=0., - beta_geometric=0. - ) + fuselage = make_fuselage(alpha_geometric=0.0, beta_geometric=0.0) airplane = asb.Airplane( fuselages=[fuselage], s_ref=np.pi, - c_ref=1., + c_ref=1.0, b_ref=0.5, ) analysis = AeroAnalysis( airplane=airplane, op_point=asb.OperatingPoint( - velocity=100., - alpha=5., - beta=5., + velocity=100.0, + alpha=5.0, + beta=5.0, ), xyz_ref=np.array([0, 0, 0]), ) @@ -168,35 +148,28 @@ def test_derivatives_at_incidence( assert aero["Cm"] == pytest.approx( 2 * fuselage.volume() / airplane.s_ref / airplane.c_ref * np.radians(5), - rel=rtol + rel=rtol, ) assert aero["Cn"] == pytest.approx( -2 * fuselage.volume() / airplane.s_ref / airplane.b_ref * np.radians(5), - rel=rtol + rel=rtol, ) assert aero["Cma"] == pytest.approx( - 2 * fuselage.volume() / airplane.s_ref / airplane.c_ref, - rel=rtol + 2 * fuselage.volume() / airplane.s_ref / airplane.c_ref, rel=rtol ) assert aero["Cnb"] == pytest.approx( - -2 * fuselage.volume() / airplane.s_ref / airplane.b_ref, - rel=rtol + -2 * fuselage.volume() / airplane.s_ref / airplane.b_ref, rel=rtol ) def test_cambered_fuselage( - AeroAnalysis: Type = asb.AeroBuildup, + AeroAnalysis: Type = asb.AeroBuildup, ): fuselage = asb.Fuselage( xsecs=[ asb.FuselageXSec( - xyz_c=[ - xi, - 0, - xi * (1 - xi) * 4 * 0.15 - ], - radius=xi ** 0.5 * (1 - xi) * 0.2 + xyz_c=[xi, 0, xi * (1 - xi) * 4 * 0.15], radius=xi**0.5 * (1 - xi) * 0.2 ) for xi in np.sinspace(0, 1, 20) ] @@ -234,39 +207,34 @@ def test_cambered_fuselage( assert aero["Cma"] == pytest.approx( 2 * fuselage.volume() / airplane.s_ref / airplane.c_ref, - rel=0.5 # this is not-strictly-speaking exact, hence the large tolerance + rel=0.5, # this is not-strictly-speaking exact, hence the large tolerance ) assert aero["Cnb"] == pytest.approx( -2 * fuselage.volume() / airplane.s_ref / airplane.b_ref, - rel=0.5 # this is not-strictly-speaking exact, hence the large tolerance + rel=0.5, # this is not-strictly-speaking exact, hence the large tolerance ) def test_fuselage_with_base_drag( - AeroAnalysis: Type = asb.AeroBuildup, + AeroAnalysis: Type = asb.AeroBuildup, ): - def r(x): # radius as a function of distance along the fuselage, x - return 0.1 * x ** 0.5 * (2 - x) + def r(x): # radius as a function of distance along the fuselage, x + return 0.1 * x**0.5 * (2 - x) - def z(x): # vertical displacement (in geometry axes) of the centerline, as a function of distance along fuselage, x + def z( + x, + ): # vertical displacement (in geometry axes) of the centerline, as a function of distance along fuselage, x return 0.15 * x * (2 - x) - def dzdx(x): # derivative of z(x) with respect to x + def dzdx(x): # derivative of z(x) with respect to x return 0.15 * ((2 - x) - x) - def A(x): # cross-sectional area as a function of distance along fuselage, x + def A(x): # cross-sectional area as a function of distance along fuselage, x return np.pi * r(x) ** 2 fuselage = asb.Fuselage( xsecs=[ - asb.FuselageXSec( - xyz_c=[ - xi, - 0, - z(xi) - ], - radius=r(xi) - ) + asb.FuselageXSec(xyz_c=[xi, 0, z(xi)], radius=r(xi)) for xi in np.sinspace(0, 1, 100) ] ) @@ -301,7 +269,7 @@ def A(x): # cross-sectional area as a function of distance along fuselage, x # TODO add assertions ##### Slender body theory - l = 1 # length of fuselage + l = 1 # length of fuselage alpha_rad = np.radians(analysis.op_point.alpha) from scipy import integrate @@ -309,28 +277,28 @@ def A(x): # cross-sectional area as a function of distance along fuselage, x def alpha_tilde(x): return alpha_rad - dzdx(x) - CL = ( - 2 / airplane.s_ref * A(l) * alpha_tilde(l) - ) + CL = 2 / airplane.s_ref * A(l) * alpha_tilde(l) print(f"CL_slb: {CL}") assert aero["CL"] == pytest.approx(CL, abs=0.01) Cm = ( - 2 / airplane.s_ref / airplane.c_ref * - ( - (fuselage.volume() - l * A(l)) * alpha_rad + l * A(l) * dzdx(l) + 2 + / airplane.s_ref + / airplane.c_ref + * ( + (fuselage.volume() - l * A(l)) * alpha_rad + + l * A(l) * dzdx(l) - integrate.quad(lambda xi: A(xi) * dzdx(xi), 0, l)[0] ) ) print(f"Cm_slb: {Cm}") - assert aero["Cm"] == pytest.approx(Cm, rel=0.5) # this is not-strictly-speaking exact, hence the large tolerance + assert aero["Cm"] == pytest.approx( + Cm, rel=0.5 + ) # this is not-strictly-speaking exact, hence the large tolerance assert aero["Cn"] == pytest.approx(0, abs=1e-3) - Cma = ( - 2 / airplane.s_ref / airplane.c_ref * - (fuselage.volume() - l * A(l)) - ) + Cma = 2 / airplane.s_ref / airplane.c_ref * (fuselage.volume() - l * A(l)) # print(f"Cma_slb: {Cma}") # assert aero["Cma"] == pytest.approx( @@ -343,7 +311,7 @@ def alpha_tilde(x): # ) -if __name__ == '__main__': +if __name__ == "__main__": pass # asb.Airplane(fuselages=[make_fuselage()]).draw_three_view() # # test_derivatives_at_zero_geometric_angles(asb.AVL) diff --git a/aerosandbox/aerodynamics/aero_3D/test_aero_3D/test_aero_buildup/test_moment_validation/test_wing_moment_and_derivatives.py b/aerosandbox/aerodynamics/aero_3D/test_aero_3D/test_aero_buildup/test_moment_validation/test_wing_moment_and_derivatives.py index ea975c06b..25d893f81 100644 --- a/aerosandbox/aerodynamics/aero_3D/test_aero_3D/test_aero_buildup/test_moment_validation/test_wing_moment_and_derivatives.py +++ b/aerosandbox/aerodynamics/aero_3D/test_aero_3D/test_aero_buildup/test_moment_validation/test_wing_moment_and_derivatives.py @@ -3,6 +3,7 @@ import pytest from typing import Type + def test_wing_aero_3D_matches_2D_in_high_AR_limit(): airfoil = asb.Airfoil("naca2412") @@ -17,7 +18,7 @@ def test_wing_aero_3D_matches_2D_in_high_AR_limit(): xyz_le=np.array([0, 0.5e12, 0]), chord=1, airfoil=airfoil, - ) + ), ] ) @@ -29,7 +30,7 @@ def test_wing_aero_3D_matches_2D_in_high_AR_limit(): op_point = asb.OperatingPoint( velocity=(2 * Q / 1.225) ** 0.5, - alpha=5., + alpha=5.0, ) xyz_ref = np.array([-1, 0, 0]) @@ -39,23 +40,17 @@ def test_wing_aero_3D_matches_2D_in_high_AR_limit(): alpha=op_point.alpha, Re=op_point.reynolds(reference_length=1), mach=op_point.mach(), - model_size="large" + model_size="large", ) expected_aero = { "CL": airfoil_only_aero["CL"], "CD": airfoil_only_aero["CD"], - "Cm": ( - airfoil_only_aero["CM"] - - (0.25 - xyz_ref[0]) * airfoil_only_aero["CL"] - ), + "Cm": (airfoil_only_aero["CM"] - (0.25 - xyz_ref[0]) * airfoil_only_aero["CL"]), } ### Do 3D predictions aero = asb.AeroBuildup( - airplane=airplane, - op_point=op_point, - xyz_ref=xyz_ref, - model_size="large" + airplane=airplane, op_point=op_point, xyz_ref=xyz_ref, model_size="large" ).run() ### Compare @@ -105,16 +100,16 @@ def test_wing_aero_3D_matches_2D_in_high_AR_limit(): symmetric=True, xsecs=[ asb.WingXSec( - xyz_le=np.array([0., 0, 0]), - chord=1., + xyz_le=np.array([0.0, 0, 0]), + chord=1.0, airfoil=asb.Airfoil("naca2412"), ), asb.WingXSec( - xyz_le=np.array([0., 5, 0]), - chord=1., + xyz_le=np.array([0.0, 5, 0]), + chord=1.0, airfoil=asb.Airfoil("naca2412"), ), - ] + ], ) airplane = asb.Airplane( @@ -123,20 +118,16 @@ def test_wing_aero_3D_matches_2D_in_high_AR_limit(): def test_simple_wing_stability_derivatives( - AeroAnalysis: Type = asb.AeroBuildup, + AeroAnalysis: Type = asb.AeroBuildup, ): analysis = AeroAnalysis( airplane=airplane, op_point=asb.OperatingPoint( - velocity=100., - alpha=0., - beta=5., + velocity=100.0, + alpha=0.0, + beta=5.0, ), - xyz_ref=np.array([ - 0., - 0., - 0. - ]), + xyz_ref=np.array([0.0, 0.0, 0.0]), ) try: @@ -149,6 +140,6 @@ def test_simple_wing_stability_derivatives( print(f"{key.rjust(10)}: {float(aero[key]):20.4f}") -if __name__ == '__main__': +if __name__ == "__main__": test_wing_aero_3D_matches_2D_in_high_AR_limit() test_simple_wing_stability_derivatives() diff --git a/aerosandbox/aerodynamics/aero_3D/test_aero_3D/test_aero_buildup/test_trefftz_plane_wake_continuity.py b/aerosandbox/aerodynamics/aero_3D/test_aero_3D/test_aero_buildup/test_trefftz_plane_wake_continuity.py index e066fad2f..77c5605db 100644 --- a/aerosandbox/aerodynamics/aero_3D/test_aero_3D/test_aero_buildup/test_trefftz_plane_wake_continuity.py +++ b/aerosandbox/aerodynamics/aero_3D/test_aero_3D/test_aero_buildup/test_trefftz_plane_wake_continuity.py @@ -13,7 +13,7 @@ solvers = [ # asb.AVL, asb.VortexLatticeMethod, - asb.AeroBuildup + asb.AeroBuildup, ] @@ -24,40 +24,32 @@ def test_horizontal(): symmetric=True, color="blue", xsecs=[ - asb.WingXSec( - xyz_le=[0, 0, 0], - chord=1, - airfoil=af - ), - asb.WingXSec( - xyz_le=[0, 1, 0], - chord=1, - airfoil=af - ) - ] + asb.WingXSec(xyz_le=[0, 0, 0], chord=1, airfoil=af), + asb.WingXSec(xyz_le=[0, 1, 0], chord=1, airfoil=af), + ], ) ] ) # airplane_horizontal.draw_three_view() - op_point_horiz = asb.OperatingPoint( - velocity=100, - alpha=5 + op_point_horiz = asb.OperatingPoint(velocity=100, alpha=5) + + horiz = ( + pd.DataFrame( + { + solver.__name__: solver(airplane_horizontal, op_point_horiz).run() + for solver in solvers + } + ) + .dropna() + .loc[["L", "CL", "CD"], :] ) - horiz = pd.DataFrame( - { - solver.__name__: solver(airplane_horizontal, op_point_horiz).run() - for solver in solvers - } - ).dropna().loc[["L", "CL", "CD"], :] - print("\nHorizontal\n" + "-" * 80) print(horiz) - assert horiz["VortexLatticeMethod"]['L'] == pytest.approx( - horiz["AeroBuildup"]['L'], - rel=0.1 + assert horiz["VortexLatticeMethod"]["L"] == pytest.approx( + horiz["AeroBuildup"]["L"], rel=0.1 ) @@ -68,36 +60,28 @@ def test_v_tail(): symmetric=True, color="purple", xsecs=[ - asb.WingXSec( - xyz_le=[0, 0, 0], - chord=1, - airfoil=af - ), - asb.WingXSec( - xyz_le=[0, 1, 1], - chord=1, - airfoil=af - ) - ] + asb.WingXSec(xyz_le=[0, 0, 0], chord=1, airfoil=af), + asb.WingXSec(xyz_le=[0, 1, 1], chord=1, airfoil=af), + ], ) ] ) # airplane_v_tail.draw_three_view() - op_point_v_tail = asb.OperatingPoint( - velocity=100, - alpha=5, - beta=5 + op_point_v_tail = asb.OperatingPoint(velocity=100, alpha=5, beta=5) + + v_tail = ( + pd.DataFrame( + { + solver.__name__: solver(airplane_v_tail, op_point_v_tail).run() + for solver in solvers + } + ) + .dropna() + .loc[["L", "CL", "Y", "CY", "CD"], :] ) - v_tail = pd.DataFrame( - { - solver.__name__: solver(airplane_v_tail, op_point_v_tail).run() - for solver in solvers - } - ).dropna().loc[["L", "CL", "Y", "CY", "CD"], :] - print("\nV-Tail\n" + "-" * 80) print(v_tail) @@ -111,45 +95,37 @@ def test_vertical(): symmetric=True, color="red", xsecs=[ - asb.WingXSec( - xyz_le=[0, 1, 0], - chord=1, - airfoil=af - ), - asb.WingXSec( - xyz_le=[0, 1, 1], - chord=1, - airfoil=af - ) - ] + asb.WingXSec(xyz_le=[0, 1, 0], chord=1, airfoil=af), + asb.WingXSec(xyz_le=[0, 1, 1], chord=1, airfoil=af), + ], ) ] ) # airplane_vertical.draw_three_view() - op_point_vert = asb.OperatingPoint( - velocity=100, - beta=5 + op_point_vert = asb.OperatingPoint(velocity=100, beta=5) + + vert = ( + pd.DataFrame( + { + solver.__name__: solver(airplane_vertical, op_point_vert).run() + for solver in solvers + } + ) + .dropna() + .loc[["Y", "CY", "CD"], :] ) - vert = pd.DataFrame( - { - solver.__name__: solver(airplane_vertical, op_point_vert).run() - for solver in solvers - } - ).dropna().loc[["Y", "CY", "CD"], :] - print("\nVertical\n" + "-" * 80) print(vert) - assert vert["VortexLatticeMethod"]['Y'] == pytest.approx( - vert["AeroBuildup"]['Y'], - rel=0.1 + assert vert["VortexLatticeMethod"]["Y"] == pytest.approx( + vert["AeroBuildup"]["Y"], rel=0.1 ) -if __name__ == '__main__': +if __name__ == "__main__": test_horizontal() test_v_tail() test_vertical() diff --git a/aerosandbox/aerodynamics/aero_3D/test_aero_3D/test_avl.py b/aerosandbox/aerodynamics/aero_3D/test_aero_3D/test_avl.py index acd6b2b8f..011ff3db5 100644 --- a/aerosandbox/aerodynamics/aero_3D/test_aero_3D/test_avl.py +++ b/aerosandbox/aerodynamics/aero_3D/test_aero_3D/test_avl.py @@ -11,13 +11,16 @@ def is_tool(name): return which(name) is not None -avl_present = is_tool('avl') +avl_present = is_tool("avl") def test_conventional(): if not avl_present: return - from aerosandbox.aerodynamics.aero_3D.test_aero_3D.geometries.conventional import airplane + from aerosandbox.aerodynamics.aero_3D.test_aero_3D.geometries.conventional import ( + airplane, + ) + analysis = asb.AVL( airplane=airplane, op_point=asb.OperatingPoint(alpha=10), @@ -28,7 +31,10 @@ def test_conventional(): def test_vanilla(): if not avl_present: return - from aerosandbox.aerodynamics.aero_3D.test_aero_3D.geometries.vanilla import airplane + from aerosandbox.aerodynamics.aero_3D.test_aero_3D.geometries.vanilla import ( + airplane, + ) + analysis = asb.AVL( airplane=airplane, op_point=asb.OperatingPoint(alpha=10), @@ -39,7 +45,10 @@ def test_vanilla(): def test_flat_plate(): if not avl_present: return - from aerosandbox.aerodynamics.aero_3D.test_aero_3D.geometries.flat_plate import airplane + from aerosandbox.aerodynamics.aero_3D.test_aero_3D.geometries.flat_plate import ( + airplane, + ) + analysis = asb.AVL( airplane=airplane, op_point=asb.OperatingPoint(alpha=10), @@ -50,7 +59,10 @@ def test_flat_plate(): def test_flat_plate_mirrored(): if not avl_present: return - from aerosandbox.aerodynamics.aero_3D.test_aero_3D.geometries.flat_plate_mirrored import airplane + from aerosandbox.aerodynamics.aero_3D.test_aero_3D.geometries.flat_plate_mirrored import ( + airplane, + ) + analysis = asb.AVL( airplane=airplane, op_point=asb.OperatingPoint(alpha=10), @@ -58,9 +70,9 @@ def test_flat_plate_mirrored(): return analysis.run() -if __name__ == '__main__': +if __name__ == "__main__": # test_conventional() # test_vanilla() - print(test_flat_plate()['CL']) + print(test_flat_plate()["CL"]) # pytest.main() diff --git a/aerosandbox/aerodynamics/aero_3D/test_aero_3D/test_lifting_line.py b/aerosandbox/aerodynamics/aero_3D/test_aero_3D/test_lifting_line.py index 16f479c0b..dbad30eb7 100644 --- a/aerosandbox/aerodynamics/aero_3D/test_aero_3D/test_lifting_line.py +++ b/aerosandbox/aerodynamics/aero_3D/test_aero_3D/test_lifting_line.py @@ -1,6 +1,8 @@ import aerosandbox as asb import pytest -from aerosandbox.aerodynamics.aero_3D.test_aero_3D.geometries.conventional import airplane +from aerosandbox.aerodynamics.aero_3D.test_aero_3D.geometries.conventional import ( + airplane, +) # def test_lifting_line(): # analysis = asb.LiftingLine( @@ -9,6 +11,6 @@ # ) # return analysis.run() -if __name__ == '__main__': +if __name__ == "__main__": test_lifting_line() # pytest.main() diff --git a/aerosandbox/aerodynamics/aero_3D/test_aero_3D/test_vlm/avl_validation.py b/aerosandbox/aerodynamics/aero_3D/test_aero_3D/test_vlm/avl_validation.py index 706c31731..0abba26fc 100644 --- a/aerosandbox/aerodynamics/aero_3D/test_aero_3D/test_vlm/avl_validation.py +++ b/aerosandbox/aerodynamics/aero_3D/test_aero_3D/test_vlm/avl_validation.py @@ -1,12 +1,11 @@ -from aerosandbox.aerodynamics.aero_3D.test_aero_3D.geometries.conventional import airplane +from aerosandbox.aerodynamics.aero_3D.test_aero_3D.geometries.conventional import ( + airplane, +) import aerosandbox as asb import aerosandbox.numpy as np -op_point = asb.OperatingPoint( - velocity=25, - alpha=3 -) +op_point = asb.OperatingPoint(velocity=25, alpha=3) vlm = asb.VortexLatticeMethod( airplane, @@ -17,10 +16,7 @@ ) vlm_aero = vlm.run() -ab = asb.AeroBuildup( - airplane, - op_point -) +ab = asb.AeroBuildup(airplane, op_point) ab_aero = ab.run() avl = asb.AVL( @@ -29,11 +25,7 @@ ) avl_aero = avl.run() -for k, v in { - "VLM": vlm_aero, - "AVL": avl_aero, - "AB" : ab_aero -}.items(): +for k, v in {"VLM": vlm_aero, "AVL": avl_aero, "AB": ab_aero}.items(): print(f"{k}:") for f in ["CL", "CD", "Cm"]: print(f"\t{f} : {v[f]}") diff --git a/aerosandbox/aerodynamics/aero_3D/test_aero_3D/test_vlm/test_airplane_optimization.py b/aerosandbox/aerodynamics/aero_3D/test_aero_3D/test_vlm/test_airplane_optimization.py index 4b296c600..c37654612 100644 --- a/aerosandbox/aerodynamics/aero_3D/test_aero_3D/test_vlm/test_airplane_optimization.py +++ b/aerosandbox/aerodynamics/aero_3D/test_aero_3D/test_vlm/test_airplane_optimization.py @@ -17,8 +17,8 @@ def get_aero(alpha, taper_ratio): asb.WingXSec( xyz_le=[-0.25 * taper_ratio, 1, 0], chord=taper_ratio, - ) - ] + ), + ], ) ] ) @@ -55,6 +55,6 @@ def get_aero(alpha, taper_ratio): assert sol(alpha) == pytest.approx(5.5, abs=1) -if __name__ == '__main__': +if __name__ == "__main__": pytest.main() diff --git a/aerosandbox/aerodynamics/aero_3D/test_aero_3D/test_vlm/test_operating_point_optimization.py b/aerosandbox/aerodynamics/aero_3D/test_aero_3D/test_vlm/test_operating_point_optimization.py index fb9987b73..85138aa44 100644 --- a/aerosandbox/aerodynamics/aero_3D/test_aero_3D/test_vlm/test_operating_point_optimization.py +++ b/aerosandbox/aerodynamics/aero_3D/test_aero_3D/test_vlm/test_operating_point_optimization.py @@ -7,15 +7,11 @@ asb.Wing( xsecs=[ asb.WingXSec( - xyz_le=[0, 0, 0], - chord=1, - airfoil=asb.Airfoil("naca0001") + xyz_le=[0, 0, 0], chord=1, airfoil=asb.Airfoil("naca0001") ), asb.WingXSec( - xyz_le=[0, 1, 0], - chord=1, - airfoil=asb.Airfoil("naca0001") - ) + xyz_le=[0, 1, 0], chord=1, airfoil=asb.Airfoil("naca0001") + ), ] ) ] @@ -29,9 +25,7 @@ def LD_from_alpha(alpha): ) vlm = asb.VortexLatticeMethod( - airplane, - op_point, - align_trailing_vortices_with_wind=True + airplane, op_point, align_trailing_vortices_with_wind=True ) aero = vlm.run() @@ -54,7 +48,7 @@ def test_vlm_optimization_operating_point(): assert sol(alpha) == pytest.approx(5.85, abs=0.1) -if __name__ == '__main__': +if __name__ == "__main__": LD_from_alpha(6) test_vlm_optimization_operating_point() # pytest.main() diff --git a/aerosandbox/aerodynamics/aero_3D/test_aero_3D/test_vortex_lattice_method.py b/aerosandbox/aerodynamics/aero_3D/test_aero_3D/test_vortex_lattice_method.py index 27df6dfda..083664b22 100644 --- a/aerosandbox/aerodynamics/aero_3D/test_aero_3D/test_vortex_lattice_method.py +++ b/aerosandbox/aerodynamics/aero_3D/test_aero_3D/test_vortex_lattice_method.py @@ -3,7 +3,10 @@ def test_conventional(): - from aerosandbox.aerodynamics.aero_3D.test_aero_3D.geometries.conventional import airplane + from aerosandbox.aerodynamics.aero_3D.test_aero_3D.geometries.conventional import ( + airplane, + ) + analysis = asb.VortexLatticeMethod( airplane=airplane, op_point=asb.OperatingPoint(alpha=10), @@ -12,7 +15,10 @@ def test_conventional(): def test_vanilla(): - from aerosandbox.aerodynamics.aero_3D.test_aero_3D.geometries.vanilla import airplane + from aerosandbox.aerodynamics.aero_3D.test_aero_3D.geometries.vanilla import ( + airplane, + ) + analysis = asb.VortexLatticeMethod( airplane=airplane, op_point=asb.OperatingPoint(alpha=10), @@ -21,7 +27,10 @@ def test_vanilla(): def test_flat_plate(): - from aerosandbox.aerodynamics.aero_3D.test_aero_3D.geometries.flat_plate import airplane + from aerosandbox.aerodynamics.aero_3D.test_aero_3D.geometries.flat_plate import ( + airplane, + ) + analysis = asb.VortexLatticeMethod( airplane=airplane, op_point=asb.OperatingPoint(alpha=10), @@ -30,7 +39,10 @@ def test_flat_plate(): def test_flat_plate_mirrored(): - from aerosandbox.aerodynamics.aero_3D.test_aero_3D.geometries.flat_plate_mirrored import airplane + from aerosandbox.aerodynamics.aero_3D.test_aero_3D.geometries.flat_plate_mirrored import ( + airplane, + ) + analysis = asb.VortexLatticeMethod( airplane=airplane, op_point=asb.OperatingPoint(alpha=10), @@ -40,13 +52,15 @@ def test_flat_plate_mirrored(): return analysis.run() -if __name__ == '__main__': +if __name__ == "__main__": # test_conventional() # test_vanilla() # test_flat_plate()['CL'] # test_flat_plate_mirrored() # pytest.main() - from aerosandbox.aerodynamics.aero_3D.test_aero_3D.geometries.conventional import airplane + from aerosandbox.aerodynamics.aero_3D.test_aero_3D.geometries.conventional import ( + airplane, + ) analysis = asb.VortexLatticeMethod( airplane=airplane, diff --git a/aerosandbox/aerodynamics/aero_3D/vortex_lattice_method.py b/aerosandbox/aerodynamics/aero_3D/vortex_lattice_method.py index 4b8e1ec03..046e2f91f 100644 --- a/aerosandbox/aerodynamics/aero_3D/vortex_lattice_method.py +++ b/aerosandbox/aerodynamics/aero_3D/vortex_lattice_method.py @@ -2,8 +2,9 @@ from aerosandbox import ExplicitAnalysis from aerosandbox.geometry import * from aerosandbox.performance import OperatingPoint -from aerosandbox.aerodynamics.aero_3D.singularities.uniform_strength_horseshoe_singularities import \ - calculate_induced_velocity_horseshoe +from aerosandbox.aerodynamics.aero_3D.singularities.uniform_strength_horseshoe_singularities import ( + calculate_induced_velocity_horseshoe, +) from typing import Dict, Any, List, Callable import copy @@ -38,19 +39,24 @@ class VortexLatticeMethod(ExplicitAnalysis): >>> analysis.draw() """ - def __init__(self, - airplane: Airplane, - op_point: OperatingPoint, - xyz_ref: List[float] = None, - run_symmetric_if_possible: bool = False, - verbose: bool = False, - spanwise_resolution: int = 10, - spanwise_spacing_function: Callable[[float, float, float], np.ndarray] = np.cosspace, - chordwise_resolution: int = 10, - chordwise_spacing_function: Callable[[float, float, float], np.ndarray] = np.cosspace, - vortex_core_radius: float = 1e-8, - align_trailing_vortices_with_wind: bool = False, - ): + def __init__( + self, + airplane: Airplane, + op_point: OperatingPoint, + xyz_ref: List[float] = None, + run_symmetric_if_possible: bool = False, + verbose: bool = False, + spanwise_resolution: int = 10, + spanwise_spacing_function: Callable[ + [float, float, float], np.ndarray + ] = np.cosspace, + chordwise_resolution: int = 10, + chordwise_spacing_function: Callable[ + [float, float, float], np.ndarray + ] = np.cosspace, + vortex_core_radius: float = 1e-8, + align_trailing_vortices_with_wind: bool = False, + ): super().__init__() ### Set defaults @@ -72,7 +78,9 @@ def __init__(self, ### Determine whether you should run the problem as symmetric self.run_symmetric = False if run_symmetric_if_possible: - raise NotImplementedError("VLM with symmetry detection not yet implemented!") + raise NotImplementedError( + "VLM with symmetry detection not yet implemented!" + ) # try: # self.run_symmetric = ( # Satisfies assumptions # self.op_point.beta == 0 and @@ -84,11 +92,18 @@ def __init__(self, # pass def __repr__(self): - return self.__class__.__name__ + "(\n\t" + "\n\t".join([ - f"airplane={self.airplane}", - f"op_point={self.op_point}", - f"xyz_ref={self.xyz_ref}", - ]) + "\n)" + return ( + self.__class__.__name__ + + "(\n\t" + + "\n\t".join( + [ + f"airplane={self.airplane}", + f"op_point={self.op_point}", + f"xyz_ref={self.xyz_ref}", + ] + ) + + "\n)" + ) def run(self) -> Dict[str, Any]: """ @@ -132,14 +147,14 @@ def run(self) -> Dict[str, Any]: if self.spanwise_resolution > 1: wing = wing.subdivide_sections( ratio=self.spanwise_resolution, - spacing_function=self.spanwise_spacing_function + spacing_function=self.spanwise_spacing_function, ) points, faces = wing.mesh_thin_surface( method="quad", chordwise_resolution=self.chordwise_resolution, chordwise_spacing_function=self.chordwise_spacing_function, - add_camber=True + add_camber=True, ) front_left_vertices.append(points[faces[:, 0], :]) back_left_vertices.append(points[faces[:, 1], :]) @@ -168,10 +183,9 @@ def run(self) -> Dict[str, Any]: right_vortex_vertices = 0.75 * front_right_vertices + 0.25 * back_right_vertices vortex_centers = (left_vortex_vertices + right_vortex_vertices) / 2 vortex_bound_leg = right_vortex_vertices - left_vortex_vertices - collocation_points = ( - 0.5 * (0.25 * front_left_vertices + 0.75 * back_left_vertices) + - 0.5 * (0.25 * front_right_vertices + 0.75 * back_right_vertices) - ) + collocation_points = 0.5 * ( + 0.25 * front_left_vertices + 0.75 * back_left_vertices + ) + 0.5 * (0.25 * front_right_vertices + 0.75 * back_right_vertices) ### Save things to the instance for later access self.front_left_vertices = front_left_vertices @@ -190,15 +204,24 @@ def run(self) -> Dict[str, Any]: ##### Setup Operating Point if self.verbose: print("Calculating the freestream influence...") - steady_freestream_velocity = self.op_point.compute_freestream_velocity_geometry_axes() # Direction the wind is GOING TO, in geometry axes coordinates - steady_freestream_direction = steady_freestream_velocity / np.linalg.norm(steady_freestream_velocity) - rotation_freestream_velocities = self.op_point.compute_rotation_velocity_geometry_axes( - collocation_points) + steady_freestream_velocity = ( + self.op_point.compute_freestream_velocity_geometry_axes() + ) # Direction the wind is GOING TO, in geometry axes coordinates + steady_freestream_direction = steady_freestream_velocity / np.linalg.norm( + steady_freestream_velocity + ) + rotation_freestream_velocities = ( + self.op_point.compute_rotation_velocity_geometry_axes(collocation_points) + ) - freestream_velocities = np.add(wide(steady_freestream_velocity), rotation_freestream_velocities) + freestream_velocities = np.add( + wide(steady_freestream_velocity), rotation_freestream_velocities + ) # Nx3, represents the freestream velocity at each panel collocation point (c) - freestream_influences = np.sum(freestream_velocities * normal_directions, axis=1) + freestream_influences = np.sum( + freestream_velocities * normal_directions, axis=1 + ) ### Save things to the instance for later access self.steady_freestream_velocity = steady_freestream_velocity @@ -210,29 +233,31 @@ def run(self) -> Dict[str, Any]: if self.verbose: print("Calculating the collocation influence matrix...") - u_collocations_unit, v_collocations_unit, w_collocations_unit = calculate_induced_velocity_horseshoe( - x_field=tall(collocation_points[:, 0]), - y_field=tall(collocation_points[:, 1]), - z_field=tall(collocation_points[:, 2]), - x_left=wide(left_vortex_vertices[:, 0]), - y_left=wide(left_vortex_vertices[:, 1]), - z_left=wide(left_vortex_vertices[:, 2]), - x_right=wide(right_vortex_vertices[:, 0]), - y_right=wide(right_vortex_vertices[:, 1]), - z_right=wide(right_vortex_vertices[:, 2]), - trailing_vortex_direction=( - steady_freestream_direction - if self.align_trailing_vortices_with_wind else - np.array([1, 0, 0]) - ), - gamma=1., - vortex_core_radius=self.vortex_core_radius + u_collocations_unit, v_collocations_unit, w_collocations_unit = ( + calculate_induced_velocity_horseshoe( + x_field=tall(collocation_points[:, 0]), + y_field=tall(collocation_points[:, 1]), + z_field=tall(collocation_points[:, 2]), + x_left=wide(left_vortex_vertices[:, 0]), + y_left=wide(left_vortex_vertices[:, 1]), + z_left=wide(left_vortex_vertices[:, 2]), + x_right=wide(right_vortex_vertices[:, 0]), + y_right=wide(right_vortex_vertices[:, 1]), + z_right=wide(right_vortex_vertices[:, 2]), + trailing_vortex_direction=( + steady_freestream_direction + if self.align_trailing_vortices_with_wind + else np.array([1, 0, 0]) + ), + gamma=1.0, + vortex_core_radius=self.vortex_core_radius, + ) ) AIC = ( - u_collocations_unit * tall(normal_directions[:, 0]) + - v_collocations_unit * tall(normal_directions[:, 1]) + - w_collocations_unit * tall(normal_directions[:, 2]) + u_collocations_unit * tall(normal_directions[:, 0]) + + v_collocations_unit * tall(normal_directions[:, 1]) + + w_collocations_unit * tall(normal_directions[:, 2]) ) ##### Calculate Vortex Strengths @@ -256,10 +281,13 @@ def run(self) -> Dict[str, Any]: # not WIND AXES or BODY AXES. Vi_cross_li = np.cross(V_centers, vortex_bound_leg, axis=1) - forces_geometry = self.op_point.atmosphere.density() * Vi_cross_li * tall(self.vortex_strengths) + forces_geometry = ( + self.op_point.atmosphere.density() + * Vi_cross_li + * tall(self.vortex_strengths) + ) moments_geometry = np.cross( - np.add(vortex_centers, -wide(np.array(self.xyz_ref))), - forces_geometry + np.add(vortex_centers, -wide(np.array(self.xyz_ref))), forces_geometry ) # Calculate total forces and moments @@ -267,24 +295,32 @@ def run(self) -> Dict[str, Any]: moment_geometry = np.sum(moments_geometry, axis=0) force_body = self.op_point.convert_axes( - force_geometry[0], force_geometry[1], force_geometry[2], + force_geometry[0], + force_geometry[1], + force_geometry[2], from_axes="geometry", - to_axes="body" + to_axes="body", ) force_wind = self.op_point.convert_axes( - force_body[0], force_body[1], force_body[2], + force_body[0], + force_body[1], + force_body[2], from_axes="body", - to_axes="wind" + to_axes="wind", ) moment_body = self.op_point.convert_axes( - moment_geometry[0], moment_geometry[1], moment_geometry[2], + moment_geometry[0], + moment_geometry[1], + moment_geometry[2], from_axes="geometry", - to_axes="body" + to_axes="body", ) moment_wind = self.op_point.convert_axes( - moment_body[0], moment_body[1], moment_body[2], + moment_body[0], + moment_body[1], + moment_body[2], from_axes="body", - to_axes="wind" + to_axes="wind", ) ### Save things to the instance for later access @@ -324,109 +360,110 @@ def run(self) -> Dict[str, Any]: "M_g": moment_geometry, "M_b": moment_body, "M_w": moment_wind, - "L" : L, - "D" : D, - "Y" : Y, + "L": L, + "D": D, + "Y": Y, "l_b": l_b, "m_b": m_b, "n_b": n_b, - "CL" : CL, - "CD" : CD, - "CY" : CY, - "Cl" : Cl, - "Cm" : Cm, - "Cn" : Cn, + "CL": CL, + "CD": CD, + "CY": CY, + "Cl": Cl, + "Cm": Cm, + "Cn": Cn, } - def run_with_stability_derivatives(self, - alpha=True, - beta=True, - p=True, - q=True, - r=True, - ): + def run_with_stability_derivatives( + self, + alpha=True, + beta=True, + p=True, + q=True, + r=True, + ): + """ + Computes the aerodynamic forces and moments on the airplane, and the stability derivatives. + + Arguments essentially determine which stability derivatives are computed. If a stability derivative is not + needed, leaving it False will speed up the computation. + + Args: + + - alpha (bool): If True, compute the stability derivatives with respect to the angle of attack (alpha). + - beta (bool): If True, compute the stability derivatives with respect to the sideslip angle (beta). + - p (bool): If True, compute the stability derivatives with respect to the body-axis roll rate (p). + - q (bool): If True, compute the stability derivatives with respect to the body-axis pitch rate (q). + - r (bool): If True, compute the stability derivatives with respect to the body-axis yaw rate (r). + + Returns: a dictionary with keys: + + - 'F_g' : an [x, y, z] list of forces in geometry axes [N] + - 'F_b' : an [x, y, z] list of forces in body axes [N] + - 'F_w' : an [x, y, z] list of forces in wind axes [N] + - 'M_g' : an [x, y, z] list of moments about geometry axes [Nm] + - 'M_b' : an [x, y, z] list of moments about body axes [Nm] + - 'M_w' : an [x, y, z] list of moments about wind axes [Nm] + - 'L' : the lift force [N]. Definitionally, this is in wind axes. + - 'Y' : the side force [N]. This is in wind axes. + - 'D' : the drag force [N]. Definitionally, this is in wind axes. + - 'l_b', the rolling moment, in body axes [Nm]. Positive is roll-right. + - 'm_b', the pitching moment, in body axes [Nm]. Positive is pitch-up. + - 'n_b', the yawing moment, in body axes [Nm]. Positive is nose-right. + - 'CL', the lift coefficient [-]. Definitionally, this is in wind axes. + - 'CY', the sideforce coefficient [-]. This is in wind axes. + - 'CD', the drag coefficient [-]. Definitionally, this is in wind axes. + - 'Cl', the rolling coefficient [-], in body axes + - 'Cm', the pitching coefficient [-], in body axes + - 'Cn', the yawing coefficient [-], in body axes + + Along with additional keys, depending on the value of the `alpha`, `beta`, `p`, `q`, and `r` arguments. For + example, if `alpha=True`, then the following additional keys will be present: + + - 'CLa', the lift coefficient derivative with respect to alpha [1/rad] + - 'CDa', the drag coefficient derivative with respect to alpha [1/rad] + - 'CYa', the sideforce coefficient derivative with respect to alpha [1/rad] + - 'Cla', the rolling moment coefficient derivative with respect to alpha [1/rad] + - 'Cma', the pitching moment coefficient derivative with respect to alpha [1/rad] + - 'Cna', the yawing moment coefficient derivative with respect to alpha [1/rad] + - 'x_np', the neutral point location in the x direction [m] + + Nondimensional values are nondimensionalized using reference values in the + VortexLatticeMethod.airplane object. + + Data types: + - The "L", "Y", "D", "l_b", "m_b", "n_b", "CL", "CY", "CD", "Cl", "Cm", and "Cn" keys are: + + - floats if the OperatingPoint object is not vectorized (i.e., if all attributes of OperatingPoint + are floats, not arrays). + + - arrays if the OperatingPoint object is vectorized (i.e., if any attribute of OperatingPoint is an + array). + + - The "F_g", "F_b", "F_w", "M_g", "M_b", and "M_w" keys are always lists, which will contain either + floats or arrays, again depending on whether the OperatingPoint object is vectorized or not. + """ - Computes the aerodynamic forces and moments on the airplane, and the stability derivatives. - - Arguments essentially determine which stability derivatives are computed. If a stability derivative is not - needed, leaving it False will speed up the computation. - - Args: - - - alpha (bool): If True, compute the stability derivatives with respect to the angle of attack (alpha). - - beta (bool): If True, compute the stability derivatives with respect to the sideslip angle (beta). - - p (bool): If True, compute the stability derivatives with respect to the body-axis roll rate (p). - - q (bool): If True, compute the stability derivatives with respect to the body-axis pitch rate (q). - - r (bool): If True, compute the stability derivatives with respect to the body-axis yaw rate (r). - - Returns: a dictionary with keys: - - - 'F_g' : an [x, y, z] list of forces in geometry axes [N] - - 'F_b' : an [x, y, z] list of forces in body axes [N] - - 'F_w' : an [x, y, z] list of forces in wind axes [N] - - 'M_g' : an [x, y, z] list of moments about geometry axes [Nm] - - 'M_b' : an [x, y, z] list of moments about body axes [Nm] - - 'M_w' : an [x, y, z] list of moments about wind axes [Nm] - - 'L' : the lift force [N]. Definitionally, this is in wind axes. - - 'Y' : the side force [N]. This is in wind axes. - - 'D' : the drag force [N]. Definitionally, this is in wind axes. - - 'l_b', the rolling moment, in body axes [Nm]. Positive is roll-right. - - 'm_b', the pitching moment, in body axes [Nm]. Positive is pitch-up. - - 'n_b', the yawing moment, in body axes [Nm]. Positive is nose-right. - - 'CL', the lift coefficient [-]. Definitionally, this is in wind axes. - - 'CY', the sideforce coefficient [-]. This is in wind axes. - - 'CD', the drag coefficient [-]. Definitionally, this is in wind axes. - - 'Cl', the rolling coefficient [-], in body axes - - 'Cm', the pitching coefficient [-], in body axes - - 'Cn', the yawing coefficient [-], in body axes - - Along with additional keys, depending on the value of the `alpha`, `beta`, `p`, `q`, and `r` arguments. For - example, if `alpha=True`, then the following additional keys will be present: - - - 'CLa', the lift coefficient derivative with respect to alpha [1/rad] - - 'CDa', the drag coefficient derivative with respect to alpha [1/rad] - - 'CYa', the sideforce coefficient derivative with respect to alpha [1/rad] - - 'Cla', the rolling moment coefficient derivative with respect to alpha [1/rad] - - 'Cma', the pitching moment coefficient derivative with respect to alpha [1/rad] - - 'Cna', the yawing moment coefficient derivative with respect to alpha [1/rad] - - 'x_np', the neutral point location in the x direction [m] - - Nondimensional values are nondimensionalized using reference values in the - VortexLatticeMethod.airplane object. - - Data types: - - The "L", "Y", "D", "l_b", "m_b", "n_b", "CL", "CY", "CD", "Cl", "Cm", and "Cn" keys are: - - - floats if the OperatingPoint object is not vectorized (i.e., if all attributes of OperatingPoint - are floats, not arrays). - - - arrays if the OperatingPoint object is vectorized (i.e., if any attribute of OperatingPoint is an - array). - - - The "F_g", "F_b", "F_w", "M_g", "M_b", and "M_w" keys are always lists, which will contain either - floats or arrays, again depending on whether the OperatingPoint object is vectorized or not. - - """ abbreviations = { "alpha": "a", - "beta" : "b", - "p" : "p", - "q" : "q", - "r" : "r", + "beta": "b", + "p": "p", + "q": "q", + "r": "r", } finite_difference_amounts = { "alpha": 0.001, - "beta" : 0.001, - "p" : 0.001 * (2 * self.op_point.velocity) / self.airplane.b_ref, - "q" : 0.001 * (2 * self.op_point.velocity) / self.airplane.c_ref, - "r" : 0.001 * (2 * self.op_point.velocity) / self.airplane.b_ref, + "beta": 0.001, + "p": 0.001 * (2 * self.op_point.velocity) / self.airplane.b_ref, + "q": 0.001 * (2 * self.op_point.velocity) / self.airplane.c_ref, + "r": 0.001 * (2 * self.op_point.velocity) / self.airplane.b_ref, } scaling_factors = { "alpha": np.degrees(1), - "beta" : np.degrees(1), - "p" : (2 * self.op_point.velocity) / self.airplane.b_ref, - "q" : (2 * self.op_point.velocity) / self.airplane.c_ref, - "r" : (2 * self.op_point.velocity) / self.airplane.b_ref, + "beta": np.degrees(1), + "p": (2 * self.op_point.velocity) / self.airplane.b_ref, + "q": (2 * self.op_point.velocity) / self.airplane.c_ref, + "r": (2 * self.op_point.velocity) / self.airplane.b_ref, } original_op_point = self.op_point @@ -442,7 +479,9 @@ def run_with_stability_derivatives(self, # of integration".) for derivative_denominator in abbreviations.keys(): - if not locals()[derivative_denominator]: # Basically, if the parameter from the function input is not True, + if not locals()[ + derivative_denominator + ]: # Basically, if the parameter from the function input is not True, continue # Skip this run. # This way, you can (optionally) speed up this routine if you only need static derivatives, # or longitudinal derivatives, etc. @@ -452,8 +491,8 @@ def run_with_stability_derivatives(self, incremented_op_point = copy.copy(original_op_point) incremented_op_point.__setattr__( derivative_denominator, - original_op_point.__getattribute__(derivative_denominator) + finite_difference_amounts[ - derivative_denominator] + original_op_point.__getattribute__(derivative_denominator) + + finite_difference_amounts[derivative_denominator], ) vlm_incremented = copy.copy(self) @@ -468,30 +507,34 @@ def run_with_stability_derivatives(self, "Cm", "Cn", ]: - derivative_name = derivative_numerator + abbreviations[derivative_denominator] # Gives "CLa" + derivative_name = ( + derivative_numerator + abbreviations[derivative_denominator] + ) # Gives "CLa" run_base[derivative_name] = ( - ( # Finite-difference out the derivatives - run_incremented[derivative_numerator] - run_base[ - derivative_numerator] - ) / finite_difference_amounts[derivative_denominator] - * scaling_factors[derivative_denominator] + ( # Finite-difference out the derivatives + run_incremented[derivative_numerator] + - run_base[derivative_numerator] + ) + / finite_difference_amounts[derivative_denominator] + * scaling_factors[derivative_denominator] ) ### Try to compute and append neutral point, if possible if derivative_denominator == "alpha": run_base["x_np"] = self.xyz_ref[0] - ( - run_base["Cma"] * (self.airplane.c_ref / run_base["CLa"]) + run_base["Cma"] * (self.airplane.c_ref / run_base["CLa"]) ) if derivative_denominator == "beta": run_base["x_np_lateral"] = self.xyz_ref[0] - ( - run_base["Cnb"] * (self.airplane.b_ref / run_base["CYb"]) + run_base["Cnb"] * (self.airplane.b_ref / run_base["CYb"]) ) return run_base - def get_induced_velocity_at_points(self, - points: np.ndarray, - ) -> np.ndarray: + def get_induced_velocity_at_points( + self, + points: np.ndarray, + ) -> np.ndarray: """ Computes the induced velocity at a set of points in the flowfield. @@ -511,24 +554,23 @@ def get_induced_velocity_at_points(self, x_right=wide(self.right_vortex_vertices[:, 0]), y_right=wide(self.right_vortex_vertices[:, 1]), z_right=wide(self.right_vortex_vertices[:, 2]), - trailing_vortex_direction=self.steady_freestream_direction if self.align_trailing_vortices_with_wind else np.array( - [1, 0, 0]), + trailing_vortex_direction=( + self.steady_freestream_direction + if self.align_trailing_vortices_with_wind + else np.array([1, 0, 0]) + ), gamma=wide(self.vortex_strengths), - vortex_core_radius=self.vortex_core_radius + vortex_core_radius=self.vortex_core_radius, ) u_induced = np.sum(u_induced, axis=1) v_induced = np.sum(v_induced, axis=1) w_induced = np.sum(w_induced, axis=1) - V_induced = np.stack([ - u_induced, v_induced, w_induced - ], axis=1) + V_induced = np.stack([u_induced, v_induced, w_induced], axis=1) return V_induced - def get_velocity_at_points(self, - points: np.ndarray - ) -> np.ndarray: + def get_velocity_at_points(self, points: np.ndarray) -> np.ndarray: """ Computes the velocity at a set of points in the flowfield. @@ -540,20 +582,23 @@ def get_velocity_at_points(self, """ V_induced = self.get_induced_velocity_at_points(points) - rotation_freestream_velocities = self.op_point.compute_rotation_velocity_geometry_axes( - points + rotation_freestream_velocities = ( + self.op_point.compute_rotation_velocity_geometry_axes(points) ) - freestream_velocities = np.add(wide(self.steady_freestream_velocity), rotation_freestream_velocities) + freestream_velocities = np.add( + wide(self.steady_freestream_velocity), rotation_freestream_velocities + ) V = V_induced + freestream_velocities return V - def calculate_streamlines(self, - seed_points: np.ndarray = None, - n_steps: int = 300, - length: float = None, - ) -> np.ndarray: + def calculate_streamlines( + self, + seed_points: np.ndarray = None, + n_steps: int = 300, + length: float = None, + ) -> np.ndarray: """ Computes streamlines, starting at specific seed points. @@ -585,27 +630,36 @@ def calculate_streamlines(self, if length is None: length = self.airplane.c_ref * 5 if seed_points is None: - left_TE_vertices = self.back_left_vertices[self.is_trailing_edge.astype(bool)] - right_TE_vertices = self.back_right_vertices[self.is_trailing_edge.astype(bool)] + left_TE_vertices = self.back_left_vertices[ + self.is_trailing_edge.astype(bool) + ] + right_TE_vertices = self.back_right_vertices[ + self.is_trailing_edge.astype(bool) + ] N_streamlines_target = 200 - seed_points_per_panel = np.maximum(1, N_streamlines_target // len(left_TE_vertices)) + seed_points_per_panel = np.maximum( + 1, N_streamlines_target // len(left_TE_vertices) + ) nondim_node_locations = np.linspace(0, 1, seed_points_per_panel + 1) - nondim_seed_locations = (nondim_node_locations[1:] + nondim_node_locations[:-1]) / 2 - - seed_points = np.concatenate([ - x * left_TE_vertices + (1 - x) * right_TE_vertices - for x in nondim_seed_locations - ]) + nondim_seed_locations = ( + nondim_node_locations[1:] + nondim_node_locations[:-1] + ) / 2 + + seed_points = np.concatenate( + [ + x * left_TE_vertices + (1 - x) * right_TE_vertices + for x in nondim_seed_locations + ] + ) streamlines = np.empty((len(seed_points), 3, n_steps)) streamlines[:, :, 0] = seed_points for i in range(1, n_steps): V = self.get_velocity_at_points(streamlines[:, :, i - 1]) - streamlines[:, :, i] = ( - streamlines[:, :, i - 1] + - length / n_steps * V / tall(np.linalg.norm(V, axis=1)) - ) + streamlines[:, :, i] = streamlines[ + :, :, i - 1 + ] + length / n_steps * V / tall(np.linalg.norm(V, axis=1)) self.streamlines = streamlines @@ -614,16 +668,17 @@ def calculate_streamlines(self, return streamlines - def draw(self, - c: np.ndarray = None, - cmap: str = None, - colorbar_label: str = None, - show: bool = True, - show_kwargs: Dict = None, - draw_streamlines=True, - recalculate_streamlines=False, - backend: str = "pyvista" - ): + def draw( + self, + c: np.ndarray = None, + cmap: str = None, + colorbar_label: str = None, + show: bool = True, + show_kwargs: Dict = None, + draw_streamlines=True, + recalculate_streamlines=False, + backend: str = "pyvista", + ): """ Draws the solution. Note: Must be called on a SOLVED AeroProblem object. To solve an AeroProblem, use opti.solve(). To substitute a solved solution, use ap = sol(ap). @@ -637,11 +692,12 @@ def draw(self, colorbar_label = "Vortex Strengths" if draw_streamlines: - if (not hasattr(self, 'streamlines')) or recalculate_streamlines: + if (not hasattr(self, "streamlines")) or recalculate_streamlines: self.calculate_streamlines() if backend == "plotly": from aerosandbox.visualization.plotly_Figure3D import Figure3D + fig = Figure3D() for i in range(len(self.front_left_vertices)): @@ -668,18 +724,21 @@ def draw(self, elif backend == "pyvista": import pyvista as pv + plotter = pv.Plotter() plotter.title = "ASB VortexLatticeMethod" plotter.add_axes() - plotter.show_grid(color='gray') + plotter.show_grid(color="gray") ### Draw the airplane mesh - points = np.concatenate([ - self.front_left_vertices, - self.back_left_vertices, - self.back_right_vertices, - self.front_right_vertices - ]) + points = np.concatenate( + [ + self.front_left_vertices, + self.back_left_vertices, + self.back_right_vertices, + self.front_right_vertices, + ] + ) N = len(self.front_left_vertices) range_N = np.arange(N) faces = tall(range_N) + wide(np.array([0, 1, 2, 3]) * N) @@ -702,12 +761,13 @@ def draw(self, ### Draw the streamlines if draw_streamlines: import aerosandbox.tools.pretty_plots as p + for i in range(self.streamlines.shape[0]): plotter.add_mesh( pv.Spline(self.streamlines[i, :, :].T), color=p.adjust_lightness("#7700FF", 1.5), opacity=0.7, - line_width=1 + line_width=1, ) if show: @@ -718,7 +778,7 @@ def draw(self, raise ValueError("Bad value of `backend`!") -if __name__ == '__main__': +if __name__ == "__main__": ### Import Vanilla Airplane import aerosandbox as asb diff --git a/aerosandbox/atmosphere/_diff_atmo_functions.py b/aerosandbox/atmosphere/_diff_atmo_functions.py index 742fe09e5..c0e7880a0 100644 --- a/aerosandbox/atmosphere/_diff_atmo_functions.py +++ b/aerosandbox/atmosphere/_diff_atmo_functions.py @@ -1,7 +1,11 @@ import aerosandbox.numpy as np from pathlib import Path from aerosandbox.modeling.interpolation import InterpolatedModel -from aerosandbox.atmosphere._isa_atmo_functions import pressure_isa, temperature_isa, isa_base_altitude +from aerosandbox.atmosphere._isa_atmo_functions import ( + pressure_isa, + temperature_isa, + isa_base_altitude, +) # Define the altitudes of knot points @@ -13,15 +17,19 @@ 13e3, 18e3, 22e3, - 30e3, 34e3, - 45e3, 49e3, + 30e3, + 34e3, + 45e3, + 49e3, 53e3, - 69e3, 73e3, + 69e3, + 73e3, 77e3, - 83e3, 87e3, - ] + - list(87e3 + np.geomspace(5e3, 2000e3, 11)) + - list(0 - np.geomspace(5e3, 5000e3, 11)) + 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)) diff --git a/aerosandbox/atmosphere/_isa_atmo_functions.py b/aerosandbox/atmosphere/_isa_atmo_functions.py index 33cb9dd27..28af4d582 100644 --- a/aerosandbox/atmosphere/_isa_atmo_functions.py +++ b/aerosandbox/atmosphere/_isa_atmo_functions.py @@ -5,7 +5,9 @@ ### Define constants gas_constant_universal = 8.31432 # J/(mol*K); universal gas constant molecular_mass_air = 28.9644e-3 # kg/mol; molecular mass of air -gas_constant_air = gas_constant_universal / molecular_mass_air # J/(kg*K); gas constant of air +gas_constant_air = ( + gas_constant_universal / molecular_mass_air +) # J/(kg*K); gas constant of air g = 9.81 # m/s^2, gravitational acceleration on earth ### Read ISA table data @@ -17,11 +19,11 @@ ### Calculate pressure at each ISA level programmatically using the barometric pressure equation with linear temperature. def barometric_formula( - P_b, - T_b, - L_b, - h, - h_b, + P_b, + T_b, + L_b, + h, + h_b, ): """ The barometric pressure equation, from here: https://en.wikipedia.org/wiki/Barometric_formula @@ -41,15 +43,11 @@ def barometric_formula( return P_b * (T / T_b) ** (-g / (gas_constant_air * L_b)) else: return P_b * np.exp( - np.clip( - -g * (h - h_b) / (gas_constant_air * T_b), - -500, - 500 - ) + np.clip(-g * (h - h_b) / (gas_constant_air * T_b), -500, 500) ) -isa_pressure = [101325.] # Pascals +isa_pressure = [101325.0] # Pascals for i in range(len(isa_table) - 1): isa_pressure.append( barometric_formula( @@ -57,7 +55,7 @@ def barometric_formula( T_b=isa_base_temperature[i], L_b=isa_lapse_rate[i], h=isa_base_altitude[i + 1], - h_b=isa_base_altitude[i] + h_b=isa_base_altitude[i], ) ) @@ -84,9 +82,9 @@ def pressure_isa(altitude): T_b=isa_base_temperature[i], L_b=isa_lapse_rate[i], h=altitude, - h_b=isa_base_altitude[i] + h_b=isa_base_altitude[i], ), - pressure + pressure, ) ### Add lower bound case @@ -97,9 +95,9 @@ def pressure_isa(altitude): T_b=isa_base_temperature[0], L_b=isa_lapse_rate[0], h=altitude, - h_b=isa_base_altitude[0] + h_b=isa_base_altitude[0], ), - pressure + pressure, ) return pressure @@ -120,19 +118,20 @@ def temperature_isa(altitude): for i in range(len(isa_table)): temp = np.where( altitude > isa_base_altitude[i], - (altitude - isa_base_altitude[i]) * isa_lapse_rate[i] + isa_base_temperature[i], - temp + (altitude - isa_base_altitude[i]) * isa_lapse_rate[i] + + isa_base_temperature[i], + temp, ) ### Add lower bound case temp = np.where( altitude <= isa_base_altitude[0], (altitude - isa_base_altitude[0]) * isa_lapse_rate[0] + isa_base_temperature[0], - temp + temp, ) return temp -if __name__ == '__main__': +if __name__ == "__main__": pressure_isa(-50e3) diff --git a/aerosandbox/atmosphere/atmosphere.py b/aerosandbox/atmosphere/atmosphere.py index ffc449c3a..32c8d94d5 100644 --- a/aerosandbox/atmosphere/atmosphere.py +++ b/aerosandbox/atmosphere/atmosphere.py @@ -1,14 +1,21 @@ from aerosandbox.common import AeroSandboxObject import aerosandbox.numpy as np from aerosandbox.atmosphere._isa_atmo_functions import pressure_isa, temperature_isa -from aerosandbox.atmosphere._diff_atmo_functions import pressure_differentiable, temperature_differentiable +from aerosandbox.atmosphere._diff_atmo_functions import ( + pressure_differentiable, + temperature_differentiable, +) import aerosandbox.tools.units as u ### Define constants gas_constant_universal = 8.31432 # J/(mol*K); universal gas constant molecular_mass_air = 28.9644e-3 # kg/mol; molecular mass of air -gas_constant_air = gas_constant_universal / molecular_mass_air # J/(kg*K); gas constant of air -effective_collision_diameter = 0.365e-9 # m, effective collision diameter of an air molecule +gas_constant_air = ( + gas_constant_universal / molecular_mass_air +) # J/(kg*K); gas constant of air +effective_collision_diameter = ( + 0.365e-9 # m, effective collision diameter of an air molecule +) ### Define the Atmosphere class @@ -19,18 +26,19 @@ class Atmosphere(AeroSandboxObject): """ - def __init__(self, - altitude: float = 0., # meters - method: str = "differentiable", - temperature_deviation: float = 0. # Kelvin - ): + def __init__( + self, + altitude: float = 0.0, # meters + method: str = "differentiable", + temperature_deviation: float = 0.0, # Kelvin + ): """ Initialize a new Atmosphere. - + Args: - + altitude: Flight altitude, in meters. This is assumed to be a geopotential altitude above MSL. - + method: Method of atmosphere modeling to use. Either: * "differentiable" - a C1-continuous fit to the International Standard Atmosphere; useful for optimization. Mean absolute error of pressure relative to the ISA is 0.02% over 0-100 km altitude range. @@ -38,7 +46,7 @@ def __init__(self, temperature_deviation: A deviation from the temperature model, in Kelvin (or equivalently, Celsius). This is useful for modeling the impact of temperature on density altitude, for example. - + """ self.altitude = altitude self.method = method @@ -47,7 +55,9 @@ def __init__(self, def __repr__(self) -> str: try: - altitude_string = f"altitude: {self.altitude:.0f} m ({self.altitude / u.foot:.0f} ft)" + altitude_string = ( + f"altitude: {self.altitude:.0f} m ({self.altitude / u.foot:.0f} ft)" + ) except (ValueError, TypeError): altitude_string = f"altitude: {self.altitude} m" @@ -77,21 +87,20 @@ def get_item_of_attribute(a): try: return a[index] except IndexError as e: - raise IndexError(f"A state variable could not be indexed; it has length {len(a)} while the" - f"parent has length {l}.") + raise IndexError( + f"A state variable could not be indexed; it has length {len(a)} while the" + f"parent has length {l}." + ) else: return a inputs = { - "altitude" : self.altitude, + "altitude": self.altitude, "temperature_deviation": self.temperature_deviation, } return self.__class__( - **{ - k: get_item_of_attribute(v) - for k, v in inputs.items() - }, + **{k: get_item_of_attribute(v) for k, v in inputs.items()}, method=self.method, ) @@ -112,7 +121,9 @@ def __len__(self): elif length == np.length(v): pass else: - raise ValueError("State variables are appear vectorized, but of different lengths!") + raise ValueError( + "State variables are appear vectorized, but of different lengths!" + ) return length def __array__(self, dtype="O"): @@ -141,7 +152,9 @@ def temperature(self): if self.method.lower() == "isa": return temperature_isa(self.altitude) + self.temperature_deviation elif self.method.lower() == "differentiable": - return temperature_differentiable(self.altitude) + self.temperature_deviation + return ( + temperature_differentiable(self.altitude) + self.temperature_deviation + ) else: raise ValueError("Bad value of 'type'!") @@ -155,10 +168,7 @@ def density(self): return rho - def density_altitude( - self, - method: str = "approximate" - ): + def density_altitude(self, method: str = "approximate"): """ Returns the density altitude, in meters. @@ -173,15 +183,15 @@ def density_altitude( lapse_rate = 0.0065 # K/m, ISA temperature lapse rate in troposphere - return ( - (temperature_sea_level / lapse_rate) * - ( - 1 - (pressure_ratio / temperature_ratio) ** ( - (9.80665 / (gas_constant_air * lapse_rate) - 1) ** -1) - ) + return (temperature_sea_level / lapse_rate) * ( + 1 + - (pressure_ratio / temperature_ratio) + ** ((9.80665 / (gas_constant_air * lapse_rate) - 1) ** -1) ) elif method.lower() == "exact": - raise NotImplementedError("Exact density altitude calculation not yet implemented.") + raise NotImplementedError( + "Exact density altitude calculation not yet implemented." + ) else: raise ValueError("Bad value of 'method'!") @@ -211,7 +221,7 @@ def dynamic_viscosity(self): # Sutherland equation temperature = self.temperature() - mu = C1 * temperature ** 1.5 / (temperature + S) + mu = C1 * temperature**1.5 / (temperature + S) return mu @@ -242,8 +252,15 @@ def mean_free_path(self): From Vincenti, W. G. and Kruger, C. H. (1965). Introduction to physical gas dynamics. Krieger Publishing Company. p. 414. """ - return self.dynamic_viscosity() / self.pressure() * np.sqrt( - np.pi * gas_constant_universal * self.temperature() / (2 * molecular_mass_air) + return ( + self.dynamic_viscosity() + / self.pressure() + * np.sqrt( + np.pi + * gas_constant_universal + * self.temperature() + / (2 * molecular_mass_air) + ) ) def knudsen(self, length): @@ -301,17 +318,11 @@ def knudsen(self, length): a.set_ylim(altitude.min() / u.foot, altitude.max() / u.foot) ax[0].set_ylabel("Altitude [ft]") plt.legend(title="Method") - p.show_plot( - f"Atmosphere", - rotate_axis_labels=False, - legend=False - ) + p.show_plot(f"Atmosphere", rotate_axis_labels=False, legend=False) fig, ax = plt.subplots(1, 2, sharey=True) ax[0].plot( - ( - (atmo_diff.pressure() - atmo_isa.pressure()) / atmo_isa.pressure() - ) * 100, + ((atmo_diff.pressure() - atmo_isa.pressure()) / atmo_isa.pressure()) * 100, altitude / 1e3, ) ax[0].set_xlabel("Pressure, Relative Error [%]") diff --git a/aerosandbox/atmosphere/test_atmosphere/test_atmosphere.py b/aerosandbox/atmosphere/test_atmosphere/test_atmosphere.py index b5b20efb8..2dca8f705 100644 --- a/aerosandbox/atmosphere/test_atmosphere/test_atmosphere.py +++ b/aerosandbox/atmosphere/test_atmosphere/test_atmosphere.py @@ -10,9 +10,7 @@ Some deviation is allowed, as the ISA model is not C1-continuous, but we want our to be C1-continuous for optimization. """ -isa_data = pd.read_csv(str( - Path(__file__).parent / "../isa_data/isa_sample_values.csv" -)) +isa_data = pd.read_csv(str(Path(__file__).parent / "../isa_data/isa_sample_values.csv")) altitudes = isa_data["Altitude [m]"].values pressures = isa_data["Pressure [Pa]"].values temperatures = isa_data["Temperature [K]"].values @@ -22,42 +20,48 @@ def test_isa_atmosphere(): for altitude, pressure, temperature, density, speed_of_sound in zip( - altitudes, - pressures, - temperatures, - densities, - speeds_of_sound + altitudes, pressures, temperatures, densities, speeds_of_sound ): - atmo = Atmosphere(altitude=altitude, method='isa') + atmo = Atmosphere(altitude=altitude, method="isa") - if altitude >= atmo._valid_altitude_range[0] and altitude <= atmo._valid_altitude_range[1]: + if ( + altitude >= atmo._valid_altitude_range[0] + and altitude <= atmo._valid_altitude_range[1] + ): fail_message = f"FAILED @ {altitude} m" assert atmo.pressure() == pytest.approx(pressure, abs=100), fail_message assert atmo.temperature() == pytest.approx(temperature, abs=1), fail_message assert atmo.density() == pytest.approx(density, abs=0.01), fail_message - assert atmo.speed_of_sound() == pytest.approx(speed_of_sound, abs=1), fail_message + assert atmo.speed_of_sound() == pytest.approx( + speed_of_sound, abs=1 + ), fail_message def test_diff_atmosphere(): altitudes = np.linspace(-50e2, 150e3, 1000) - atmo_isa = Atmosphere(altitude=altitudes, method='isa') + atmo_isa = Atmosphere(altitude=altitudes, method="isa") atmo_diff = Atmosphere(altitude=altitudes) temp_isa = atmo_isa.temperature() pressure_isa = atmo_isa.pressure() temp_diff = atmo_diff.temperature() pressure_diff = atmo_diff.pressure() - assert max(abs((temp_isa - temp_diff) / temp_isa)) < 0.025, "temperature failed for differentiable model" - assert max(abs((pressure_isa - pressure_diff) / pressure_isa)) < 0.01, "pressure failed for differentiable model" + assert ( + max(abs((temp_isa - temp_diff) / temp_isa)) < 0.025 + ), "temperature failed for differentiable model" + assert ( + max(abs((pressure_isa - pressure_diff) / pressure_isa)) < 0.01 + ), "pressure failed for differentiable model" def plot_isa_residuals(): - atmo = Atmosphere(altitude=altitudes, method='isa') + atmo = Atmosphere(altitude=altitudes, method="isa") import matplotlib.pyplot as plt import seaborn as sns + sns.set(palette=sns.color_palette("husl")) fig, ax = plt.subplots(1, 1, figsize=(6.4, 4.8), dpi=200) @@ -79,6 +83,6 @@ def plot_isa_residuals(): plt.show() -if __name__ == '__main__': +if __name__ == "__main__": # plot_isa_residuals() pytest.main() diff --git a/aerosandbox/atmosphere/thermodynamics/choked_flow.py b/aerosandbox/atmosphere/thermodynamics/choked_flow.py index 55ce76b9d..c83bb0411 100644 --- a/aerosandbox/atmosphere/thermodynamics/choked_flow.py +++ b/aerosandbox/atmosphere/thermodynamics/choked_flow.py @@ -3,15 +3,17 @@ def mass_flow_rate( - mach, - area, - total_pressure, - total_temperature, - molecular_mass=28.9644e-3, - gamma=1.4, + mach, + area, + total_pressure, + total_temperature, + molecular_mass=28.9644e-3, + gamma=1.4, ): specific_gas_constant = universal_gas_constant / molecular_mass return ( - (area * total_pressure) * (gamma / specific_gas_constant / total_temperature) ** 0.5 - * mach * (1 + (gamma - 1) / 2 * mach ** 2) ** (- (gamma + 1) / (2 * (gamma - 1))) + (area * total_pressure) + * (gamma / specific_gas_constant / total_temperature) ** 0.5 + * mach + * (1 + (gamma - 1) / 2 * mach**2) ** (-(gamma + 1) / (2 * (gamma - 1))) ) diff --git a/aerosandbox/atmosphere/thermodynamics/gas.py b/aerosandbox/atmosphere/thermodynamics/gas.py index b565c6a94..3b2919a5e 100644 --- a/aerosandbox/atmosphere/thermodynamics/gas.py +++ b/aerosandbox/atmosphere/thermodynamics/gas.py @@ -18,14 +18,15 @@ class PerfectGas: * Has internal energy and enthalpy purely as functions of temperature """ - def __init__(self, - pressure: Union[float, np.ndarray] = 101325, - temperature: Union[float, np.ndarray] = 273.15 + 15, - specific_heat_constant_pressure: float = 1006, - specific_heat_constant_volume: float = 717, - molecular_mass: float = 28.9644e-3, - effective_collision_diameter: float = 0.365e-9, - ): + def __init__( + self, + pressure: Union[float, np.ndarray] = 101325, + temperature: Union[float, np.ndarray] = 273.15 + 15, + specific_heat_constant_pressure: float = 1006, + specific_heat_constant_volume: float = 717, + molecular_mass: float = 28.9644e-3, + effective_collision_diameter: float = 0.365e-9, + ): """ Args: @@ -61,7 +62,9 @@ def density(self): @property def speed_of_sound(self): - return (self.ratio_of_specific_heats * self.specific_gas_constant * self.temperature) ** 0.5 + return ( + self.ratio_of_specific_heats * self.specific_gas_constant * self.temperature + ) ** 0.5 @property def specific_gas_constant(self): @@ -83,7 +86,9 @@ def specific_enthalpy_change(self, start_temperature, end_temperature): Returns: The change in specific enthalpy, in J/kg. """ - return self.specific_heat_constant_pressure * (end_temperature - start_temperature) + return self.specific_heat_constant_pressure * ( + end_temperature - start_temperature + ) def specific_internal_energy_change(self, start_temperature, end_temperature): """ @@ -97,7 +102,9 @@ def specific_internal_energy_change(self, start_temperature, end_temperature): Returns: The change in specific internal energy, in J/kg. """ - return self.specific_heat_constant_volume * (end_temperature - start_temperature) + return self.specific_heat_constant_volume * ( + end_temperature - start_temperature + ) @property def specific_volume(self): @@ -116,7 +123,9 @@ def specific_enthalpy(self): Enthalpy here is in units of J/kg. """ - return self.specific_enthalpy_change(start_temperature=0, end_temperature=self.temperature) + return self.specific_enthalpy_change( + start_temperature=0, end_temperature=self.temperature + ) @property def specific_internal_energy(self): @@ -125,18 +134,21 @@ def specific_internal_energy(self): Internal energy here is in units of J/kg. """ - return self.specific_internal_energy_change(start_temperature=0, end_temperature=self.temperature) - - def process(self, - process: str = "isentropic", - new_pressure: float = None, - new_temperature: float = None, - new_density: float = None, - enthalpy_addition_at_constant_pressure: float = None, - enthalpy_addition_at_constant_volume: float = None, - polytropic_n: float = None, - inplace=False - ) -> "PerfectGas": + return self.specific_internal_energy_change( + start_temperature=0, end_temperature=self.temperature + ) + + def process( + self, + process: str = "isentropic", + new_pressure: float = None, + new_temperature: float = None, + new_density: float = None, + enthalpy_addition_at_constant_pressure: float = None, + enthalpy_addition_at_constant_volume: float = None, + polytropic_n: float = None, + inplace=False, + ) -> "PerfectGas": """ Puts this gas under a thermodynamic process. @@ -178,30 +190,45 @@ def process(self, pressure_specified = new_pressure is not None temperature_specified = new_temperature is not None density_specified = new_density is not None - enthalpy_at_pressure_specified = enthalpy_addition_at_constant_pressure is not None + enthalpy_at_pressure_specified = ( + enthalpy_addition_at_constant_pressure is not None + ) enthalpy_at_volume_specified = enthalpy_addition_at_constant_volume is not None number_of_conditions_specified = ( - pressure_specified + - temperature_specified + - density_specified + - enthalpy_at_pressure_specified + - enthalpy_at_volume_specified + pressure_specified + + temperature_specified + + density_specified + + enthalpy_at_pressure_specified + + enthalpy_at_volume_specified ) if number_of_conditions_specified != 1: - raise ValueError("You must specify exactly one of the following arguments:\n" + "\n".join([ - "\t* `new_pressure`", - "\t* `new_temperature`", - "\t* `new_density`", - "\t* `enthalpy_addition_at_constant_pressure`", - "\t* `enthalpy_addition_at_constant_volume`", - ])) + raise ValueError( + "You must specify exactly one of the following arguments:\n" + + "\n".join( + [ + "\t* `new_pressure`", + "\t* `new_temperature`", + "\t* `new_density`", + "\t* `enthalpy_addition_at_constant_pressure`", + "\t* `enthalpy_addition_at_constant_volume`", + ] + ) + ) if enthalpy_at_pressure_specified: - new_temperature = self.temperature + enthalpy_addition_at_constant_pressure / self.specific_heat_constant_pressure + new_temperature = ( + self.temperature + + enthalpy_addition_at_constant_pressure + / self.specific_heat_constant_pressure + ) elif enthalpy_at_volume_specified: - new_temperature = self.temperature + enthalpy_addition_at_constant_volume / self.specific_heat_constant_volume + new_temperature = ( + self.temperature + + enthalpy_addition_at_constant_volume + / self.specific_heat_constant_volume + ) if pressure_specified: P_ratio = new_pressure / self.pressure @@ -215,7 +242,9 @@ def process(self, new_pressure = self.pressure if pressure_specified: - raise ValueError("Can't specify pressure change for an isobaric process!") + raise ValueError( + "Can't specify pressure change for an isobaric process!" + ) elif density_specified: new_temperature = self.temperature * V_ratio @@ -229,7 +258,9 @@ def process(self, new_temperature = self.temperature * P_ratio elif density_specified: - raise ValueError("Can't specify density change for an isochoric process!") + raise ValueError( + "Can't specify density change for an isochoric process!" + ) elif temperature_specified: new_pressure = self.pressure * T_ratio @@ -245,7 +276,9 @@ def process(self, new_pressure = self.pressure / V_ratio elif temperature_specified: - raise ValueError("Can't specify temperature change for an isothermal process!") + raise ValueError( + "Can't specify temperature change for an isothermal process!" + ) elif process == "isentropic": @@ -255,7 +288,7 @@ def process(self, new_temperature = self.temperature * P_ratio ** ((gam - 1) / gam) elif density_specified: - new_pressure = self.pressure * V_ratio ** -gam + new_pressure = self.pressure * V_ratio**-gam new_temperature = self.temperature * V_ratio ** (1 - gam) elif temperature_specified: @@ -264,7 +297,9 @@ def process(self, elif process == "polytropic": if polytropic_n is None: - raise ValueError("If the process is polytropic, then the polytropic index `n` must be specified.") + raise ValueError( + "If the process is polytropic, then the polytropic index `n` must be specified." + ) n = polytropic_n @@ -272,7 +307,7 @@ def process(self, new_temperature = self.temperature * P_ratio ** ((n - 1) / n) elif density_specified: - new_pressure = self.pressure * V_ratio ** -n + new_pressure = self.pressure * V_ratio**-n new_temperature = self.temperature * V_ratio ** (1 - n) elif temperature_specified: @@ -295,11 +330,11 @@ def process(self, specific_heat_constant_pressure=self.specific_heat_constant_pressure, specific_heat_constant_volume=self.specific_heat_constant_volume, molecular_mass=self.molecular_mass, - effective_collision_diameter=self.effective_collision_diameter + effective_collision_diameter=self.effective_collision_diameter, ) -if __name__ == '__main__': +if __name__ == "__main__": ### Carnot g = [] diff --git a/aerosandbox/atmosphere/thermodynamics/isentropic_flow.py b/aerosandbox/atmosphere/thermodynamics/isentropic_flow.py index 2881c82ad..1a07ef5a3 100644 --- a/aerosandbox/atmosphere/thermodynamics/isentropic_flow.py +++ b/aerosandbox/atmosphere/thermodynamics/isentropic_flow.py @@ -1,10 +1,7 @@ import aerosandbox.numpy as np -def temperature_over_total_temperature( - mach, - gamma=1.4 -): +def temperature_over_total_temperature(mach, gamma=1.4): """ Gives T/T_t, the ratio of static temperature to total temperature. @@ -12,13 +9,10 @@ def temperature_over_total_temperature( mach: Mach number [-] gamma: The ratio of specific heats. 1.4 for air across most temperature ranges of interest. """ - return (1 + (gamma - 1) / 2 * mach ** 2) ** -1 + return (1 + (gamma - 1) / 2 * mach**2) ** -1 -def pressure_over_total_pressure( - mach, - gamma=1.4 -): +def pressure_over_total_pressure(mach, gamma=1.4): """ Gives P/P_t, the ratio of static pressure to total pressure. @@ -26,13 +20,12 @@ def pressure_over_total_pressure( mach: Mach number [-] gamma: The ratio of specific heats. 1.4 for air across most temperature ranges of interest. """ - return temperature_over_total_temperature(mach=mach, gamma=gamma) ** (gamma / (gamma - 1)) + return temperature_over_total_temperature(mach=mach, gamma=gamma) ** ( + gamma / (gamma - 1) + ) -def density_over_total_density( - mach, - gamma=1.4 -): +def density_over_total_density(mach, gamma=1.4): """ Gives rho/rho_t, the ratio of density to density after isentropic compression. @@ -40,13 +33,12 @@ def density_over_total_density( mach: Mach number [-] gamma: The ratio of specific heats. 1.4 for air across most temperature ranges of interest. """ - return temperature_over_total_temperature(mach=mach, gamma=gamma) ** (1 / (gamma - 1)) + return temperature_over_total_temperature(mach=mach, gamma=gamma) ** ( + 1 / (gamma - 1) + ) -def area_over_choked_area( - mach, - gamma=1.4 -): +def area_over_choked_area(mach, gamma=1.4): """ Gives A/A^* (where A^* is "A-star"), the ratio of cross-sectional flow area to the cross-sectional flow area that would result in choked (M=1) flow. @@ -60,12 +52,13 @@ def area_over_choked_area( gm1 = gamma - 1 return ( - (gp1 / 2) ** (-gp1 / (2 * gm1)) * - (1 + gm1 / 2 * mach ** 2) ** (gp1 / (2 * gm1)) / mach + (gp1 / 2) ** (-gp1 / (2 * gm1)) + * (1 + gm1 / 2 * mach**2) ** (gp1 / (2 * gm1)) + / mach ) -if __name__ == '__main__': +if __name__ == "__main__": import matplotlib.pyplot as plt import aerosandbox.tools.pretty_plots as p @@ -75,9 +68,9 @@ def area_over_choked_area( fig, ax = plt.subplots() for name, data in { - "$T/T_t$" : temperature_over_total_temperature(mach), - "$P/P_t$" : pressure_over_total_pressure(mach), - "$A/A^*$" : area_over_choked_area(mach), + "$T/T_t$": temperature_over_total_temperature(mach), + "$P/P_t$": pressure_over_total_pressure(mach), + "$A/A^*$": area_over_choked_area(mach), r"$\rho/\rho_t$": density_over_total_density(mach), }.items(): plt.plot(mach, data, label=name) diff --git a/aerosandbox/common.py b/aerosandbox/common.py index 2a98161b0..423fd6172 100644 --- a/aerosandbox/common.py +++ b/aerosandbox/common.py @@ -38,11 +38,14 @@ def __eq__(self, other): if self is other: # If they point to the same object in memory, they're equal return True - if type(self) != type(other): # If they are of different types, they cannot be equal + if type(self) != type( + other + ): # If they are of different types, they cannot be equal return False if set(self.__dict__.keys()) != set( - other.__dict__.keys()): # If they have differing dict keys, don't bother checking values + other.__dict__.keys() + ): # If they have differing dict keys, don't bother checking values return False for key in self.__dict__.keys(): # Check equality of all values @@ -53,11 +56,12 @@ def __eq__(self, other): return True - def save(self, - filename: Union[str, Path] = None, - verbose: bool = True, - automatically_add_extension: bool = True, - ) -> None: + def save( + self, + filename: Union[str, Path] = None, + verbose: bool = True, + automatically_add_extension: bool = True, + ) -> None: """ Saves the object to a binary file, using the `dill` library. @@ -95,12 +99,14 @@ def save(self, import aerosandbox as asb self._asb_metadata = { - "python_version": ".".join([ - str(sys.version_info.major), - str(sys.version_info.minor), - str(sys.version_info.micro), - ]), - "asb_version" : asb.__version__ + "python_version": ".".join( + [ + str(sys.version_info.major), + str(sys.version_info.minor), + str(sys.version_info.micro), + ] + ), + "asb_version": asb.__version__, } with open(filename, "wb") as f: dill.dump( @@ -120,10 +126,11 @@ def deepcopy(self): """ return copy.deepcopy(self) - def substitute_solution(self, - sol: cas.OptiSol, - inplace: bool = None, - ): + def substitute_solution( + self, + sol: cas.OptiSol, + inplace: bool = None, + ): """ Substitutes a solution from CasADi's solver recursively as an in-place operation. @@ -132,10 +139,11 @@ def substitute_solution(self, :return: """ import warnings + warnings.warn( "This function is deprecated and will break at some future point.\n" "Use `sol(x)`, which now works recursively on complex data structures.", - DeprecationWarning + DeprecationWarning, ) # Set defaults @@ -167,36 +175,32 @@ def convert(item): if issubclass(t, set) or issubclass(t, frozenset): return {convert(i) for i in item} if issubclass(t, dict): - return { - convert(k): convert(v) - for k, v in item.items() - } + return {convert(k): convert(v) for k, v in item.items()} # Skip certain Python types for type_to_skip in ( - bool, str, - int, float, complex, - range, - type(None), - bytes, bytearray, memoryview + bool, + str, + int, + float, + complex, + range, + type(None), + bytes, + bytearray, + memoryview, ): if issubclass(t, type_to_skip): return item # Skip certain CasADi types - for type_to_skip in ( - cas.Opti, - cas.OptiSol - ): + for type_to_skip in (cas.Opti, cas.OptiSol): if issubclass(t, type_to_skip): return item # If it's any other type, try converting its attribute dictionary: try: - newdict = { - k: convert(v) - for k, v in item.__dict__.items() - } + newdict = {k: convert(v) for k, v in item.__dict__.items()} if inplace: for k, v in newdict.items(): @@ -222,8 +226,12 @@ def convert(item): # At this point, we're not really sure what type the object is. Raise a warning and return the item, then hope for the best. import warnings - warnings.warn(f"In solution substitution, could not convert an object of type {t}.\n" - f"Returning it and hoping for the best.", UserWarning) + + warnings.warn( + f"In solution substitution, could not convert an object of type {t}.\n" + f"Returning it and hoping for the best.", + UserWarning, + ) return item @@ -235,8 +243,8 @@ def convert(item): def load( - filename: Union[str, Path], - verbose: bool = True, + filename: Union[str, Path], + verbose: bool = True, ) -> AeroSandboxObject: """ Loads an AeroSandboxObject from a file. @@ -265,26 +273,30 @@ def load( except AttributeError: warnings.warn( "This object was saved without metadata. This may cause compatibility issues.", - stacklevel=2 + stacklevel=2, ) return obj # Check if the Python version is different try: saved_python_version = metadata["python_version"] - current_python_version = ".".join([ - str(sys.version_info.major), - str(sys.version_info.minor), - str(sys.version_info.micro), - ]) + current_python_version = ".".join( + [ + str(sys.version_info.major), + str(sys.version_info.minor), + str(sys.version_info.micro), + ] + ) saved_python_version_split = saved_python_version.split(".") current_python_version_split = current_python_version.split(".") - if any([ - saved_python_version_split[0] != current_python_version_split[0], - saved_python_version_split[1] != current_python_version_split[1], - ]): + if any( + [ + saved_python_version_split[0] != current_python_version_split[0], + saved_python_version_split[1] != current_python_version_split[1], + ] + ): warnings.warn( f"This object was saved with Python {saved_python_version}, but you are currently using Python {current_python_version}.\n" f"This may cause compatibility issues.", @@ -294,7 +306,7 @@ def load( except KeyError: warnings.warn( "This object was saved without Python version info metadata. This may cause compatibility issues.", - stacklevel=2 + stacklevel=2, ) # Check if the AeroSandbox version is different @@ -313,7 +325,7 @@ def load( except KeyError: warnings.warn( "This object was saved without AeroSandbox version info metadata. This may cause compatibility issues.", - stacklevel=2 + stacklevel=2, ) if verbose: @@ -363,9 +375,10 @@ class ExplicitAnalysis(AeroSandboxObject): """ - def get_options(self, - geometry_object: AeroSandboxObject, - ) -> Dict[str, Any]: + def get_options( + self, + geometry_object: AeroSandboxObject, + ) -> Dict[str, Any]: """ Retrieves the analysis-specific options that correspond to both: @@ -407,13 +420,17 @@ def get_options(self, ### Determine whether this analysis and the geometry object have options that specifically reference each other or not. try: - analysis_options_for_this_geometry = self.default_analysis_specific_options[geometry_type] + analysis_options_for_this_geometry = self.default_analysis_specific_options[ + geometry_type + ] assert hasattr(analysis_options_for_this_geometry, "items") except (AttributeError, KeyError, AssertionError): analysis_options_for_this_geometry = None try: - geometry_options_for_this_analysis = geometry_object.analysis_specific_options[analysis_type] + geometry_options_for_this_analysis = ( + geometry_object.analysis_specific_options[analysis_type] + ) assert hasattr(geometry_options_for_this_analysis, "items") except (AttributeError, KeyError, AssertionError): geometry_options_for_this_analysis = None @@ -428,11 +445,15 @@ def get_options(self, else: import warnings - allowable_keys = [f'"{k}"' for k in analysis_options_for_this_geometry.keys()] + + allowable_keys = [ + f'"{k}"' for k in analysis_options_for_this_geometry.keys() + ] warnings.warn( f"\nAn object of type '{geometry_type.__name__}' declared the analysis-specific option '{k}' for use with analysis '{analysis_type.__name__}'.\n" f"This was unexpected! Allowable analysis-specific options for '{geometry_type.__name__}' with '{analysis_type.__name__}' are:\n" - "\t" + "\n\t".join(allowable_keys) + "\n" "Did you make a typo?", + "\t" + "\n\t".join(allowable_keys) + "\n" + "Did you make a typo?", stacklevel=2, ) @@ -509,13 +530,14 @@ def init_wrapped(self, *args, opti=None, **kwargs): return init_wrapped class ImplicitAnalysisInitError(Exception): - def __init__(self, - message=""" + def __init__( + self, + message=""" Your ImplicitAnalysis object doesn't have an `opti` property! This is almost certainly because you didn't decorate your object's __init__ method with `@ImplicitAnalysis.initialize`, which you should go do. - """ - ): + """, + ): self.message = message super().__init__(self.message) diff --git a/aerosandbox/dynamics/flight_dynamics/airplane.py b/aerosandbox/dynamics/flight_dynamics/airplane.py index 96b12cf31..ad22526b1 100644 --- a/aerosandbox/dynamics/flight_dynamics/airplane.py +++ b/aerosandbox/dynamics/flight_dynamics/airplane.py @@ -5,11 +5,11 @@ def get_modes( - airplane: Airplane, - op_point: OperatingPoint, - mass_props: MassProperties, - aero, - g=9.81, + airplane: Airplane, + op_point: OperatingPoint, + mass_props: MassProperties, + aero, + g=9.81, ): Q = op_point.dynamic_pressure() S = airplane.s_ref @@ -41,12 +41,13 @@ def get_modes( modes = {} def get_mode_info( - sigma, - omega_squared, + sigma, + omega_squared, ): is_oscillatory = omega_squared > 0 return { - "eigenvalue_real": sigma + np.where( + "eigenvalue_real": sigma + + np.where( is_oscillatory, 0, np.abs(omega_squared + 1e-100) ** 0.5, @@ -61,72 +62,70 @@ def get_mode_info( ##### Longitudinal modes ### Phugoid - modes['phugoid'] = { # FVA, Eq. 9.55-9.57 - **get_mode_info( - sigma=X_u / 2, - omega_squared=-(X_u ** 2) / 4 - g * Z_u / u0 - ), - "eigenvalue_imag_approx": 2 ** 0.5 * g / u0, - "damping_ratio_approx" : 2 ** -0.5 * aero["CD"] / aero["CL"], + modes["phugoid"] = { # FVA, Eq. 9.55-9.57 + **get_mode_info(sigma=X_u / 2, omega_squared=-(X_u**2) / 4 - g * Z_u / u0), + "eigenvalue_imag_approx": 2**0.5 * g / u0, + "damping_ratio_approx": 2**-0.5 * aero["CD"] / aero["CL"], } # # ### Short-period - modes['short_period'] = get_mode_info( - sigma=0.5 * M_q, - omega_squared=-(M_q ** 2) / 4 - u0 * M_w + modes["short_period"] = get_mode_info( + sigma=0.5 * M_q, omega_squared=-(M_q**2) / 4 - u0 * M_w ) ##### Lateral modes ### Roll subsidence - modes['roll_subsidence'] = { # FVA, Eq. 9.63 - "eigenvalue_real": ( - QS * b ** 2 / (2 * Ixx * u0) * aero["Clp"] - ), + modes["roll_subsidence"] = { # FVA, Eq. 9.63 + "eigenvalue_real": (QS * b**2 / (2 * Ixx * u0) * aero["Clp"]), "eigenvalue_imag": 0, - "damping_ratio" : 1, + "damping_ratio": 1, } ### Dutch roll - modes['dutch_roll'] = get_mode_info( # FVA, Eq. 9.68 + modes["dutch_roll"] = get_mode_info( # FVA, Eq. 9.68 sigma=( - QS * b ** 2 / - (2 * Izz * u0) * - (aero["Cnr"] + Izz / (m * b ** 2) * aero["CYb"]) + QS * b**2 / (2 * Izz * u0) * (aero["Cnr"] + Izz / (m * b**2) * aero["CYb"]) ), omega_squared=( - QS * b / Izz * - ( - aero["Cnb"] + ( - op_point.atmosphere.density() * S * b / (4 * m) * - (aero["CYb"] * aero["Cnr"] - aero["Cnb"] * aero["CYr"]) - ) + QS + * b + / Izz + * ( + aero["Cnb"] + + ( + op_point.atmosphere.density() + * S + * b + / (4 * m) + * (aero["CYb"] * aero["Cnr"] - aero["Cnb"] * aero["CYr"]) ) - ) + ) + ), ) ### Spiral - spiral_parameter = (aero["Cnr"] - aero["Cnb"] * aero["Clr"] / aero["Clb"]) # FVA, Eq. 9.66 - modes['spiral'] = { # FVA, Eq. 9.66 - "eigenvalue_real": ( - QS * b ** 2 / (2 * Izz * u0) * - spiral_parameter - ), + spiral_parameter = ( + aero["Cnr"] - aero["Cnb"] * aero["Clr"] / aero["Clb"] + ) # FVA, Eq. 9.66 + modes["spiral"] = { # FVA, Eq. 9.66 + "eigenvalue_real": (QS * b**2 / (2 * Izz * u0) * spiral_parameter), "eigenvalue_imag": 0, } ### Compute damping ratios of all modes for mode_name, mode_data in modes.items(): - modes[mode_name]['damping_ratio'] = ( - -mode_data['eigenvalue_real'] / - (mode_data['eigenvalue_real'] ** 2 + mode_data['eigenvalue_imag'] ** 2) ** 0.5 + modes[mode_name]["damping_ratio"] = ( + -mode_data["eigenvalue_real"] + / (mode_data["eigenvalue_real"] ** 2 + mode_data["eigenvalue_imag"] ** 2) + ** 0.5 ) return modes -if __name__ == '__main__': +if __name__ == "__main__": import aerosandbox as asb import aerosandbox.numpy as np from aerosandbox.tools import units as u @@ -137,8 +136,8 @@ def get_mode_info( # Caughey, David A., "Introduction to Aircraft Stability and Control, Course Notes for M&AE 5070", 2011 # https://courses.cit.cornell.edu/mae5070/Caughey_2011_04.pdf airplane = asb.Airplane( - name='Boeing 737-800', - s_ref=1260 * u.foot ** 2, + name="Boeing 737-800", + s_ref=1260 * u.foot**2, c_ref=11 * u.foot, b_ref=113 * u.foot, ) @@ -159,7 +158,7 @@ def get_mode_info( Cmq=-74.997742, CYr=0.796001, Clr=0.364638, - Cnr=-0.434410 + Cnr=-0.434410, ) mass_props_TOGW = asb.MassProperties( @@ -178,7 +177,7 @@ def get_mode_info( op_point = asb.OperatingPoint( atmosphere=asb.Atmosphere( altitude=2438.399975619396, - method='differentiable', + method="differentiable", ), velocity=85.64176936131635, ) @@ -186,22 +185,19 @@ def get_mode_info( assert np.allclose( aero["CL"], mass_props_TOGW.mass * 9.81 / op_point.dynamic_pressure() / airplane.s_ref, - rtol=0.001 + rtol=0.001, ) eigenvalues_from_AVL = { - 'phugoid' : -0.0171382 + 0.145072j, # Real is wrong (2x) - 'short_period' : -0.439841 + 0.842195j, # Pretty close - 'roll_subsidence': -1.35132, # get_modes says -1.81 - 'dutch_roll' : -0.385418 + 1.52695j, # Imag is wrong (1.5x) - 'spiral' : -0.0573017, # Too small, get_modes says -0.17 + "phugoid": -0.0171382 + 0.145072j, # Real is wrong (2x) + "short_period": -0.439841 + 0.842195j, # Pretty close + "roll_subsidence": -1.35132, # get_modes says -1.81 + "dutch_roll": -0.385418 + 1.52695j, # Imag is wrong (1.5x) + "spiral": -0.0573017, # Too small, get_modes says -0.17 } pprint( get_modes( - airplane=airplane, - op_point=op_point, - mass_props=mass_props_TOGW, - aero=aero + airplane=airplane, op_point=op_point, mass_props=mass_props_TOGW, aero=aero ) ) diff --git a/aerosandbox/dynamics/point_mass/common_point_mass.py b/aerosandbox/dynamics/point_mass/common_point_mass.py index e747385ab..8d7707ee9 100644 --- a/aerosandbox/dynamics/point_mass/common_point_mass.py +++ b/aerosandbox/dynamics/point_mass/common_point_mass.py @@ -2,7 +2,14 @@ from aerosandbox.common import AeroSandboxObject from abc import ABC, abstractmethod, abstractproperty from typing import Union, Dict, Tuple, List -from aerosandbox import MassProperties, Opti, OperatingPoint, Atmosphere, Airplane, _asb_root +from aerosandbox import ( + MassProperties, + Opti, + OperatingPoint, + Atmosphere, + Airplane, + _asb_root, +) from aerosandbox.tools.string_formatting import trim_string import inspect import copy @@ -10,10 +17,11 @@ class _DynamicsPointMassBaseClass(AeroSandboxObject, ABC): @abstractmethod - def __init__(self, - mass_props: MassProperties = None, - **state_variables_and_indirect_control_variables, - ): + def __init__( + self, + mass_props: MassProperties = None, + **state_variables_and_indirect_control_variables, + ): self.mass_props = MassProperties() if mass_props is None else mass_props """ For each state variable, self.state_var = state_var @@ -42,12 +50,12 @@ def state(self) -> Dict[str, Union[float, np.ndarray]]: """ pass - def get_new_instance_with_state(self, - new_state: Union[ - Dict[str, Union[float, np.ndarray]], - List, Tuple, np.ndarray - ] = None - ): + def get_new_instance_with_state( + self, + new_state: Union[ + Dict[str, Union[float, np.ndarray]], List, Tuple, np.ndarray + ] = None, + ): """ Creates a new instance of this same Dynamics class from the given state. @@ -65,10 +73,7 @@ def get_new_instance_with_state(self, init_args = list(init_signature.parameters.keys())[1:] # Ignore 'self' ### Create a new instance, and give the constructor all the inputs it wants to see (based on values in this instance) - new_dyn: __class__ = self.__class__(**{ - k: getattr(self, k) - for k in init_args - }) + new_dyn: __class__ = self.__class__(**{k: getattr(self, k) for k in init_args}) ### Overwrite the state variables in the new instance with those from the input new_dyn._set_state(new_state=new_state) @@ -76,12 +81,12 @@ def get_new_instance_with_state(self, ### Return the new instance return new_dyn - def _set_state(self, - new_state: Union[ - Dict[str, Union[float, np.ndarray]], - List, Tuple, np.ndarray - ] = None - ): + def _set_state( + self, + new_state: Union[ + Dict[str, Union[float, np.ndarray]], List, Tuple, np.ndarray + ] = None, + ): """ Force-overwrites all state variables with a new set (either partial or complete) of state variables. @@ -98,16 +103,19 @@ def _set_state(self, new_state = {} try: # Assume `value` is a dict-like, with keys - for key in new_state.keys(): # Overwrite each of the specified state variables + for ( + key + ) in new_state.keys(): # Overwrite each of the specified state variables setattr(self, key, new_state[key]) except AttributeError: # Assume it's an iterable that has been sorted. self._set_state( - self.pack_state(new_state)) # Pack the iterable into a dict-like, then do the same thing as above. + self.pack_state(new_state) + ) # Pack the iterable into a dict-like, then do the same thing as above. - def unpack_state(self, - dict_like_state: Dict[str, Union[float, np.ndarray]] = None - ) -> Tuple[Union[float, np.ndarray]]: + def unpack_state( + self, dict_like_state: Dict[str, Union[float, np.ndarray]] = None + ) -> Tuple[Union[float, np.ndarray]]: """ 'Unpacks' a Dict-like state into an array-like that represents the state of the dynamical system. @@ -121,9 +129,9 @@ def unpack_state(self, dict_like_state = self.state return tuple(dict_like_state.values()) - def pack_state(self, - array_like_state: Union[List, Tuple, np.ndarray] = None - ) -> Dict[str, Union[float, np.ndarray]]: + def pack_state( + self, array_like_state: Union[List, Tuple, np.ndarray] = None + ) -> Dict[str, Union[float, np.ndarray]]: """ 'Packs' an array into a Dict that represents the state of the dynamical system. @@ -137,14 +145,9 @@ def pack_state(self, return self.state if not len(self.state.keys()) == len(array_like_state): raise ValueError( - "There are a differing number of elements in the `state` variable and the `array_like` you're trying to pack!") - return { - k: v - for k, v in zip( - self.state.keys(), - array_like_state + "There are a differing number of elements in the `state` variable and the `array_like` you're trying to pack!" ) - } + return {k: v for k, v in zip(self.state.keys(), array_like_state)} @property @abstractmethod @@ -165,25 +168,25 @@ def makeline(k, v): state_variables_title = "\tState variables:" - state_variables = "\n".join([ - "\t\t" + makeline(k, v) - for k, v in self.state.items() - ]) + state_variables = "\n".join( + ["\t\t" + makeline(k, v) for k, v in self.state.items()] + ) control_variables_title = "\tControl variables:" - control_variables = "\n".join([ - "\t\t" + makeline(k, v) - for k, v in self.control_variables.items() - ]) + control_variables = "\n".join( + ["\t\t" + makeline(k, v) for k, v in self.control_variables.items()] + ) - return "\n".join([ - title, - state_variables_title, - state_variables, - control_variables_title, - control_variables - ]) + return "\n".join( + [ + title, + state_variables_title, + state_variables, + control_variables_title, + control_variables, + ] + ) def __getitem__(self, index: Union[int, slice]): """ @@ -209,8 +212,10 @@ def get_item_of_attribute(a): try: return a[index] except IndexError as e: - raise IndexError(f"A state variable could not be indexed; it has length {len(a)} while the" - f"parent has length {l}.") + raise IndexError( + f"A state variable could not be indexed; it has length {len(a)} while the" + f"parent has length {l}." + ) else: return a @@ -231,7 +236,9 @@ def __len__(self): elif length == lv: pass else: - raise ValueError("State variables are appear vectorized, but of different lengths!") + raise ValueError( + "State variables are appear vectorized, but of different lengths!" + ) return length def __array__(self, dtype="O"): @@ -249,13 +256,14 @@ def state_derivatives(self) -> Dict[str, Union[float, np.ndarray]]: """ pass - def constrain_derivatives(self, - opti: Opti, - time: np.ndarray, - method: str = "trapezoidal", - which: Union[str, List[str], Tuple[str]] = "all", - _stacklevel=1, - ): + def constrain_derivatives( + self, + opti: Opti, + time: np.ndarray, + method: str = "trapezoidal", + which: Union[str, List[str], Tuple[str]] = "all", + _stacklevel=1, + ): """ Applies the relevant state derivative constraints to a given Opti instance. @@ -303,21 +311,26 @@ def constrain_derivatives(self, variable=self.state[state_var_name], with_respect_to=time, method=method, - _stacklevel=_stacklevel + 1 + _stacklevel=_stacklevel + 1, ) except KeyError: - raise ValueError(f"This dynamics instance does not have a state named '{state_var_name}'!") + raise ValueError( + f"This dynamics instance does not have a state named '{state_var_name}'!" + ) except Exception as e: - raise ValueError(f"Error while constraining state variable '{state_var_name}': \n{e}") + raise ValueError( + f"Error while constraining state variable '{state_var_name}': \n{e}" + ) @abstractmethod - def convert_axes(self, - x_from: float, - y_from: float, - z_from: float, - from_axes: str, - to_axes: str, - ) -> Tuple[float, float, float]: + def convert_axes( + self, + x_from: float, + y_from: float, + z_from: float, + from_axes: str, + to_axes: str, + ) -> Tuple[float, float, float]: """ Converts a vector [x_from, y_from, z_from], as given in the `from_axes` frame, to an equivalent vector [x_to, y_to, z_to], as given in the `to_axes` frame. @@ -350,12 +363,13 @@ def convert_axes(self, pass @abstractmethod - def add_force(self, - Fx: Union[float, np.ndarray] = 0, - Fy: Union[float, np.ndarray] = 0, - Fz: Union[float, np.ndarray] = 0, - axes: str = "wind", - ) -> None: + def add_force( + self, + Fx: Union[float, np.ndarray] = 0, + Fy: Union[float, np.ndarray] = 0, + Fz: Union[float, np.ndarray] = 0, + axes: str = "wind", + ) -> None: """ Adds a force (in whichever axis system you choose) to this Dynamics instance. @@ -378,9 +392,7 @@ def add_force(self, """ pass - def add_gravity_force(self, - g=9.81 - ) -> None: + def add_gravity_force(self, g=9.81) -> None: """ In-place modifies the forces associated with this Dynamics instance: adds a force in the -z direction, equal to the weight of the aircraft. @@ -413,48 +425,56 @@ def op_point(self): r=0, ) - def draw(self, - vehicle_model: Union[Airplane, "PolyData"] = None, - backend: str = "pyvista", - plotter=None, - draw_axes: bool = True, - draw_global_axes: bool = True, - draw_global_grid: bool = True, - scale_vehicle_model: Union[float, None] = None, - n_vehicles_to_draw: int = 10, - cg_axes: str = "geometry", - draw_trajectory_line: bool = True, - trajectory_line_color=None, - draw_altitude_drape: bool = True, - draw_ground_plane: bool = True, - draw_wingtip_ribbon: bool = True, - set_sky_background: bool = True, - vehicle_color=None, - vehicle_opacity: float = 0.95, - show: bool = True, - ): + def draw( + self, + vehicle_model: Union[Airplane, "PolyData"] = None, + backend: str = "pyvista", + plotter=None, + draw_axes: bool = True, + draw_global_axes: bool = True, + draw_global_grid: bool = True, + scale_vehicle_model: Union[float, None] = None, + n_vehicles_to_draw: int = 10, + cg_axes: str = "geometry", + draw_trajectory_line: bool = True, + trajectory_line_color=None, + draw_altitude_drape: bool = True, + draw_ground_plane: bool = True, + draw_wingtip_ribbon: bool = True, + set_sky_background: bool = True, + vehicle_color=None, + vehicle_opacity: float = 0.95, + 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/talon.stl" + default_vehicle_stl = ( + _asb_root / "dynamics/visualization/default_assets/talon.stl" + ) vehicle_model = pv.read(str(default_vehicle_stl)) elif isinstance(vehicle_model, pv.PolyData): pass elif isinstance(vehicle_model, Airplane): vehicle_model: pv.PolyData = vehicle_model.draw( - backend="pyvista", - show=False + backend="pyvista", show=False ) - vehicle_model.rotate_y(180, inplace=True) # Rotate from geometry axes to body axes. - elif isinstance(vehicle_model, str): # Interpret the string as a filepath to a .stl or similar + vehicle_model.rotate_y( + 180, inplace=True + ) # 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 Exception: raise ValueError("Could not parse `vehicle_model`!") else: - raise TypeError("`vehicle_model` should be an Airplane or PolyData object.") + 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) @@ -466,11 +486,13 @@ def draw(self, if np.length(z_e) == 1: z_e = z_e * np.ones(len(self)) - trajectory_bounds = np.array([ - [x_e.min(), x_e.max()], - [y_e.min(), y_e.max()], - [z_e.min(), z_e.max()], - ]) + trajectory_bounds = np.array( + [ + [x_e.min(), x_e.max()], + [y_e.min(), y_e.max()], + [z_e.min(), z_e.max()], + ] + ) vehicle_bounds = np.array(vehicle_model.bounds).reshape((3, 2)) @@ -482,10 +504,13 @@ def draw(self, scale_vehicle_model = 1 else: path_length = np.sum( - (np.diff(x_e) ** 2 + np.diff(y_e) ** 2 + np.diff(z_e) ** 2) ** 0.5 + (np.diff(x_e) ** 2 + np.diff(y_e) ** 2 + np.diff(z_e) ** 2) + ** 0.5 ) vehicle_length = np.diff(vehicle_bounds[0, :]) - scale_vehicle_model = float(0.5 * path_length / vehicle_length / n_vehicles_to_draw) + scale_vehicle_model = float( + 0.5 * path_length / vehicle_length / n_vehicles_to_draw + ) ### Initialize the plotter if plotter is None: @@ -511,10 +536,11 @@ def draw(self, if draw_global_axes: plotter.add_axes() if draw_global_grid: - plotter.show_grid(color='gray') + plotter.show_grid(color="gray") ### Set up interpolators for dynamics instances from scipy import interpolate + state_interpolators = { k: interpolate.InterpolatedUnivariateSpline( x=np.arange(len(self)), @@ -536,10 +562,9 @@ def draw(self, ### Draw the vehicle for i in np.linspace(0, len(self) - 1, n_vehicles_to_draw): - dyn = self.get_new_instance_with_state({ - k: float(v(i)) - for k, v in state_interpolators.items() - }) + dyn = self.get_new_instance_with_state( + {k: float(v(i)) for k, v in state_interpolators.items()} + ) for k, v in control_interpolators.items(): setattr(dyn, k, float(v(i))) @@ -561,24 +586,30 @@ def draw(self, dyn.mass_props.y_cg, dyn.mass_props.z_cg, from_axes=cg_axes, - to_axes="body" + to_axes="body", ) this_vehicle = copy.deepcopy(vehicle_model) - this_vehicle.translate([ - -np.mean(x_cg_b), - -np.mean(y_cg_b), - -np.mean(z_cg_b), - ], inplace=True) + this_vehicle.translate( + [ + -np.mean(x_cg_b), + -np.mean(y_cg_b), + -np.mean(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) + this_vehicle.translate( + [ + dyn.x_e, + dyn.y_e, + dyn.z_e, + ], + inplace=True, + ) plotter.add_mesh( this_vehicle, color=( @@ -593,33 +624,34 @@ def draw(self, 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 - ) + np.diff(np.array(this_vehicle.bounds).reshape((3, -1)), axis=1) + ) + origin = np.array( + [ + dyn.x_e, + dyn.y_e, + dyn.z_e, + ] ) - 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 - ])), + pv.Spline( + np.array([origin, origin + rot[:, i] * axes_scale]) + ), color=c, line_width=2.5, opacity=0.5, ) ### Draw the trajectory line - path = np.stack([ - x_e, - y_e, - z_e, - ], axis=1) + path = np.stack( + [ + x_e, + y_e, + z_e, + ], + axis=1, + ) if len(self) > 1: if draw_trajectory_line: @@ -636,30 +668,47 @@ def draw(self, if draw_wingtip_ribbon: - left_wingtip_points = np.array(self.convert_axes( - 0, scale_vehicle_model * vehicle_bounds[1, 0], 0, - from_axes="body", - to_axes="earth" - )).T + path + left_wingtip_points = ( + np.array( + self.convert_axes( + 0, + scale_vehicle_model * vehicle_bounds[1, 0], + 0, + from_axes="body", + to_axes="earth", + ) + ).T + + path + ) plotter.add_mesh( pv.Spline(left_wingtip_points), color="pink", ) - right_wingtip_points = np.array(self.convert_axes( - 0, scale_vehicle_model * vehicle_bounds[1, 1], 0, - from_axes="body", - to_axes="earth" - )).T + path + right_wingtip_points = ( + np.array( + self.convert_axes( + 0, + scale_vehicle_model * vehicle_bounds[1, 1], + 0, + from_axes="body", + to_axes="earth", + ) + ).T + + path + ) plotter.add_mesh( pv.Spline(right_wingtip_points), color="pink", ) grid = pv.StructuredGrid() - grid.points = np.concatenate([ - left_wingtip_points, - right_wingtip_points, - ], axis=0) + grid.points = np.concatenate( + [ + left_wingtip_points, + right_wingtip_points, + ], + axis=0, + ) grid.dimensions = len(left_wingtip_points), 2, 1 plotter.add_mesh( @@ -671,10 +720,9 @@ def draw(self, if draw_altitude_drape: ### Drape grid = pv.StructuredGrid() - grid.points = np.concatenate([ - path, - path * np.array([[1, 1, 0]]) - ], axis=0) + grid.points = np.concatenate( + [path, path * np.array([[1, 1, 0]])], axis=0 + ) grid.dimensions = len(path), 2, 1 plotter.add_mesh( @@ -689,18 +737,16 @@ def draw(self, xlim = (x_e.min(), x_e.max()) ylim = (y_e.min(), y_e.max()) - grid.points = np.array([ - [xlim[0], ylim[0], 0], - [xlim[1], ylim[0], 0], - [xlim[0], ylim[1], 0], - [xlim[1], ylim[1], 0] - ]) - grid.dimensions = 2, 2, 1 - plotter.add_mesh( - grid, - color="darkkhaki", - opacity=0.5 + grid.points = np.array( + [ + [xlim[0], ylim[0], 0], + [xlim[1], ylim[0], 0], + [xlim[0], ylim[1], 0], + [xlim[1], ylim[1], 0], + ] ) + grid.dimensions = 2, 2, 1 + plotter.add_mesh(grid, color="darkkhaki", opacity=0.5) ### Finalize the plotter plotter.camera.up = (0, 0, -1) @@ -727,7 +773,7 @@ def translational_kinetic_energy(self) -> float: Returns: Kinetic energy [J] """ - return 0.5 * self.mass_props.mass * self.speed ** 2 + return 0.5 * self.mass_props.mass * self.speed**2 @property def rotational_kinetic_energy(self) -> float: @@ -740,9 +786,9 @@ def rotational_kinetic_energy(self) -> float: Kinetic energy [J] """ return 0.5 * ( - self.mass_props.Ixx * self.p ** 2 + - self.mass_props.Iyy * self.q ** 2 + - self.mass_props.Izz * self.r ** 2 + self.mass_props.Ixx * self.p**2 + + self.mass_props.Iyy * self.q**2 + + self.mass_props.Izz * self.r**2 ) @property @@ -758,9 +804,7 @@ def kinetic_energy(self): return self.translational_kinetic_energy + self.rotational_kinetic_energy @property - def potential_energy(self, - g: float = 9.81 - ): + def potential_energy(self, g: float = 9.81): """ Computes the potential energy [J] from gravity. diff --git a/aerosandbox/dynamics/point_mass/point_1D/horizontal.py b/aerosandbox/dynamics/point_mass/point_1D/horizontal.py index 35bc4e55d..52e8a21cd 100644 --- a/aerosandbox/dynamics/point_mass/point_1D/horizontal.py +++ b/aerosandbox/dynamics/point_mass/point_1D/horizontal.py @@ -1,4 +1,6 @@ -from aerosandbox.dynamics.point_mass.point_3D.cartesian import DynamicsPointMass3DCartesian +from aerosandbox.dynamics.point_mass.point_3D.cartesian import ( + DynamicsPointMass3DCartesian, +) from aerosandbox.weights.mass_properties import MassProperties import aerosandbox.numpy as np from typing import Union, Dict, Tuple @@ -19,11 +21,12 @@ class DynamicsPointMass1DHorizontal(DynamicsPointMass3DCartesian): """ - def __init__(self, - mass_props: MassProperties = None, - x_e: Union[float, np.ndarray] = 0, - u_e: Union[float, np.ndarray] = 0, - ): + def __init__( + self, + mass_props: MassProperties = None, + x_e: Union[float, np.ndarray] = 0, + u_e: Union[float, np.ndarray] = 0, + ): # Initialize state variables self.mass_props = MassProperties() if mass_props is None else mass_props self.x_e = x_e @@ -58,10 +61,8 @@ def control_variables(self) -> Dict[str, Union[float, np.ndarray]]: def state_derivatives(self) -> Dict[str, Union[float, np.ndarray]]: derivatives = super().state_derivatives() - return { - k: derivatives[k] for k in self.state.keys() - } + return {k: derivatives[k] for k in self.state.keys()} -if __name__ == '__main__': +if __name__ == "__main__": dyn = DynamicsPointMass1DHorizontal() diff --git a/aerosandbox/dynamics/point_mass/point_1D/test/test_block_move.py b/aerosandbox/dynamics/point_mass/point_1D/test/test_block_move.py index 88a2eacfd..8b3f44559 100644 --- a/aerosandbox/dynamics/point_mass/point_1D/test/test_block_move.py +++ b/aerosandbox/dynamics/point_mass/point_1D/test/test_block_move.py @@ -18,29 +18,25 @@ def test_block_move_fixed_time(): u = opti.variable(init_guess=np.linspace(1, -1, n_timesteps)) - dyn.add_force( - Fx=u - ) + dyn.add_force(Fx=u) - dyn.constrain_derivatives( - opti=opti, - time=time - ) + 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.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) + np.diff(time) / 3 * (u[:-1] ** 2 + u[:-1] * u[1:] + u[1:] ** 2) ) opti.minimize(effort) @@ -75,28 +71,25 @@ def test_block_move_minimum_time(): 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 + u = opti.variable( + init_guess=np.linspace(1, -1, n_timesteps), lower_bound=-1, upper_bound=1 ) - dyn.constrain_derivatives( - opti=opti, - time=time - ) + dyn.add_force(Fx=u) - opti.subject_to([ - dyn.x_e[0] == 0, - dyn.x_e[-1] == 1, - dyn.u_e[0] == 0, - dyn.u_e[-1] == 0, - ]) + dyn.constrain_derivatives(opti=opti, time=time) - opti.minimize( - time[-1] + 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 = sol(dyn) @@ -111,5 +104,5 @@ def test_block_move_minimum_time(): assert np.mean(np.abs(sol(u))) == pytest.approx(1, abs=0.01) -if __name__ == '__main__': +if __name__ == "__main__": pytest.main() diff --git a/aerosandbox/dynamics/point_mass/point_1D/test/test_rocket.py b/aerosandbox/dynamics/point_mass/point_1D/test/test_rocket.py index cf1210d8e..46b9590ed 100644 --- a/aerosandbox/dynamics/point_mass/point_1D/test/test_rocket.py +++ b/aerosandbox/dynamics/point_mass/point_1D/test/test_rocket.py @@ -16,11 +16,17 @@ def test_rocket(): mass_initial = 500e3 # Initial mass, 500 metric tons z_e_final = -100e3 # Final altitude, 100 km g = 9.81 # Gravity, m/s^2 - alpha = 1 / (300 * g) # kg/(N*s), Inverse of specific impulse, basically - don't worry about this + alpha = 1 / ( + 300 * g + ) # kg/(N*s), Inverse of specific impulse, basically - don't worry about this dyn = asb.DynamicsPointMass1DVertical( - mass_props=asb.MassProperties(mass=opti.variable(init_guess=mass_initial, n_vars=N)), - z_e=opti.variable(init_guess=np.linspace(0, z_e_final, N)), # Altitude (negative due to Earth-axes convention) + mass_props=asb.MassProperties( + mass=opti.variable(init_guess=mass_initial, n_vars=N) + ), + z_e=opti.variable( + init_guess=np.linspace(0, z_e_final, N) + ), # Altitude (negative due to Earth-axes convention) w_e=opti.variable(init_guess=-z_e_final / time_final, n_vars=N), # Velocity ) @@ -42,21 +48,22 @@ def test_rocket(): ) ### Boundary conditions - opti.subject_to([ - dyn.z_e[0] == 0, - dyn.w_e[0] == 0, - dyn.mass_props.mass[0] == mass_initial, - dyn.z_e[-1] == z_e_final, - ]) + opti.subject_to( + [ + dyn.z_e[0] == 0, + dyn.w_e[0] == 0, + dyn.mass_props.mass[0] == mass_initial, + dyn.z_e[-1] == z_e_final, + ] + ) ### Path constraints - opti.subject_to([ - dyn.mass_props.mass >= 0, - thrust >= 0 - ]) + opti.subject_to([dyn.mass_props.mass >= 0, thrust >= 0]) ### Objective - opti.minimize(-dyn.mass_props.mass[-1]) # Maximize the final mass == minimize fuel expenditure + opti.minimize( + -dyn.mass_props.mass[-1] + ) # Maximize the final mass == minimize fuel expenditure ### Solve sol = opti.solve(verbose=False) @@ -67,6 +74,6 @@ def test_rocket(): assert np.abs(dyn.w_e).max() == pytest.approx(1448, rel=0.05) -if __name__ == '__main__': +if __name__ == "__main__": test_rocket() pytest.main() diff --git a/aerosandbox/dynamics/point_mass/point_1D/vertical.py b/aerosandbox/dynamics/point_mass/point_1D/vertical.py index 797a5b59a..0ac997775 100644 --- a/aerosandbox/dynamics/point_mass/point_1D/vertical.py +++ b/aerosandbox/dynamics/point_mass/point_1D/vertical.py @@ -1,4 +1,6 @@ -from aerosandbox.dynamics.point_mass.point_3D.cartesian import DynamicsPointMass3DCartesian +from aerosandbox.dynamics.point_mass.point_3D.cartesian import ( + DynamicsPointMass3DCartesian, +) from aerosandbox.weights.mass_properties import MassProperties import aerosandbox.numpy as np from typing import Union, Dict, Tuple @@ -19,11 +21,12 @@ class DynamicsPointMass1DVertical(DynamicsPointMass3DCartesian): """ - def __init__(self, - mass_props: MassProperties = None, - z_e: Union[float, np.ndarray] = 0, - w_e: Union[float, np.ndarray] = 0, - ): + def __init__( + self, + mass_props: MassProperties = None, + z_e: Union[float, np.ndarray] = 0, + w_e: Union[float, np.ndarray] = 0, + ): # Initialize state variables self.mass_props = MassProperties() if mass_props is None else mass_props self.x_e = 0 @@ -58,10 +61,8 @@ def control_variables(self) -> Dict[str, Union[float, np.ndarray]]: def state_derivatives(self) -> Dict[str, Union[float, np.ndarray]]: derivatives = super().state_derivatives() - return { - k: derivatives[k] for k in self.state.keys() - } + return {k: derivatives[k] for k in self.state.keys()} -if __name__ == '__main__': +if __name__ == "__main__": dyn = DynamicsPointMass1DVertical() diff --git a/aerosandbox/dynamics/point_mass/point_2D/cartesian.py b/aerosandbox/dynamics/point_mass/point_2D/cartesian.py index 1d9ffdcd0..9260f0520 100644 --- a/aerosandbox/dynamics/point_mass/point_2D/cartesian.py +++ b/aerosandbox/dynamics/point_mass/point_2D/cartesian.py @@ -1,4 +1,6 @@ -from aerosandbox.dynamics.point_mass.point_3D.cartesian import DynamicsPointMass3DCartesian +from aerosandbox.dynamics.point_mass.point_3D.cartesian import ( + DynamicsPointMass3DCartesian, +) from aerosandbox.weights.mass_properties import MassProperties import aerosandbox.numpy as np from typing import Union, Dict, Tuple @@ -26,14 +28,15 @@ class DynamicsPointMass2DCartesian(DynamicsPointMass3DCartesian): """ - def __init__(self, - mass_props: MassProperties = None, - x_e: Union[float, np.ndarray] = 0, - z_e: Union[float, np.ndarray] = 0, - u_e: Union[float, np.ndarray] = 0, - w_e: Union[float, np.ndarray] = 0, - alpha: Union[float, np.ndarray] = 0, - ): + def __init__( + self, + mass_props: MassProperties = None, + x_e: Union[float, np.ndarray] = 0, + z_e: Union[float, np.ndarray] = 0, + u_e: Union[float, np.ndarray] = 0, + w_e: Union[float, np.ndarray] = 0, + alpha: Union[float, np.ndarray] = 0, + ): # Initialize state variables self.mass_props = MassProperties() if mass_props is None else mass_props self.x_e = x_e @@ -66,16 +69,14 @@ def state(self) -> Dict[str, Union[float, np.ndarray]]: def control_variables(self) -> Dict[str, Union[float, np.ndarray]]: return { "alpha": self.alpha, - "Fx_e" : self.Fx_e, - "Fz_e" : self.Fz_e, + "Fx_e": self.Fx_e, + "Fz_e": self.Fz_e, } def state_derivatives(self) -> Dict[str, Union[float, np.ndarray]]: derivatives = super().state_derivatives() - return { - k: derivatives[k] for k in self.state.keys() - } + return {k: derivatives[k] for k in self.state.keys()} -if __name__ == '__main__': +if __name__ == "__main__": dyn = DynamicsPointMass2DCartesian() diff --git a/aerosandbox/dynamics/point_mass/point_2D/speed_gamma.py b/aerosandbox/dynamics/point_mass/point_2D/speed_gamma.py index 2fbea0c00..33b55dad6 100644 --- a/aerosandbox/dynamics/point_mass/point_2D/speed_gamma.py +++ b/aerosandbox/dynamics/point_mass/point_2D/speed_gamma.py @@ -1,4 +1,6 @@ -from aerosandbox.dynamics.point_mass.point_3D.speed_gamma_track import DynamicsPointMass3DSpeedGammaTrack +from aerosandbox.dynamics.point_mass.point_3D.speed_gamma_track import ( + DynamicsPointMass3DSpeedGammaTrack, +) from aerosandbox.weights.mass_properties import MassProperties import aerosandbox.numpy as np from typing import Union, Dict, Tuple @@ -26,14 +28,15 @@ class DynamicsPointMass2DSpeedGamma(DynamicsPointMass3DSpeedGammaTrack): """ - def __init__(self, - mass_props: MassProperties = None, - x_e: Union[float, np.ndarray] = 0, - z_e: Union[float, np.ndarray] = 0, - speed: Union[float, np.ndarray] = 0, - gamma: Union[float, np.ndarray] = 0, - alpha: Union[float, np.ndarray] = 0, - ): + def __init__( + self, + mass_props: MassProperties = None, + x_e: Union[float, np.ndarray] = 0, + z_e: Union[float, np.ndarray] = 0, + speed: Union[float, np.ndarray] = 0, + gamma: Union[float, np.ndarray] = 0, + alpha: Union[float, np.ndarray] = 0, + ): # Initialize state variables self.mass_props = MassProperties() if mass_props is None else mass_props self.x_e = x_e @@ -57,8 +60,8 @@ def __init__(self, @property def state(self) -> Dict[str, Union[float, np.ndarray]]: return { - "x_e" : self.x_e, - "z_e" : self.z_e, + "x_e": self.x_e, + "z_e": self.z_e, "speed": self.speed, "gamma": self.gamma, } @@ -67,16 +70,14 @@ def state(self) -> Dict[str, Union[float, np.ndarray]]: def control_variables(self) -> Dict[str, Union[float, np.ndarray]]: return { "alpha": self.alpha, - "Fx_w" : self.Fx_w, - "Fz_w" : self.Fz_w, + "Fx_w": self.Fx_w, + "Fz_w": self.Fz_w, } def state_derivatives(self) -> Dict[str, Union[float, np.ndarray]]: derivatives = super().state_derivatives() - return { - k: derivatives[k] for k in self.state.keys() - } + return {k: derivatives[k] for k in self.state.keys()} -if __name__ == '__main__': +if __name__ == "__main__": dyn = DynamicsPointMass2DSpeedGamma() diff --git a/aerosandbox/dynamics/point_mass/point_2D/test/test_cannonball_2D.py b/aerosandbox/dynamics/point_mass/point_2D/test/test_cannonball_2D.py index 7e2aa61dc..ecb975b3c 100644 --- a/aerosandbox/dynamics/point_mass/point_2D/test/test_cannonball_2D.py +++ b/aerosandbox/dynamics/point_mass/point_2D/test/test_cannonball_2D.py @@ -5,17 +5,17 @@ u_e_0 = 100 w_e_0 = -100 -speed_0 = (u_e_0 ** 2 + w_e_0 ** 2) ** 0.5 +speed_0 = (u_e_0**2 + w_e_0**2) ** 0.5 gamma_0 = np.arctan2(-w_e_0, u_e_0) time = np.linspace(0, 10, 501) def get_trajectory( - parameterization: type = asb.DynamicsPointMass2DCartesian, - gravity=True, - drag=True, - plot=False + parameterization: type = asb.DynamicsPointMass2DCartesian, + gravity=True, + drag=True, + plot=False, ): if parameterization is asb.DynamicsPointMass2DCartesian: dyn = parameterization( @@ -40,12 +40,9 @@ def derivatives(t, y): this_dyn = dyn.get_new_instance_with_state(y) if gravity: this_dyn.add_gravity_force() - q = 0.5 * 1.225 * this_dyn.speed ** 2 + q = 0.5 * 1.225 * this_dyn.speed**2 if drag: - this_dyn.add_force( - Fx=-1 * (0.1) ** 2 * q, - axes="wind" - ) + this_dyn.add_force(Fx=-1 * (0.1) ** 2 * q, axes="wind") return this_dyn.unpack_state(this_dyn.state_derivatives()) @@ -75,50 +72,32 @@ def derivatives(t, y): def test_final_position_Cartesian_with_drag(): - dyn = get_trajectory( - parameterization=asb.DynamicsPointMass2DCartesian, - drag=True - ) + dyn = get_trajectory(parameterization=asb.DynamicsPointMass2DCartesian, drag=True) assert dyn[-1].x_e == pytest.approx(277.5774436945314, abs=1e-2) assert dyn[-1].z_e == pytest.approx(3.1722727033631886, abs=1e-2) def test_final_position_Cartesian_no_drag(): - dyn = get_trajectory( - parameterization=asb.DynamicsPointMass2DCartesian, - drag=False - ) + dyn = get_trajectory(parameterization=asb.DynamicsPointMass2DCartesian, drag=False) assert dyn[-1].x_e == pytest.approx(1000, abs=1e-2) assert dyn[-1].z_e == pytest.approx(-509.5, abs=1e-2) def test_final_position_SpeedGamma_with_drag(): - dyn = get_trajectory( - parameterization=asb.DynamicsPointMass2DSpeedGamma, - drag=True - ) + dyn = get_trajectory(parameterization=asb.DynamicsPointMass2DSpeedGamma, drag=True) assert dyn[-1].x_e == pytest.approx(277.5774436945314, abs=1e-2) assert dyn[-1].z_e == pytest.approx(3.1722727033631886, abs=1e-2) def test_final_position_SpeedGamma_no_drag(): - dyn = get_trajectory( - parameterization=asb.DynamicsPointMass2DSpeedGamma, - drag=False - ) + dyn = get_trajectory(parameterization=asb.DynamicsPointMass2DSpeedGamma, drag=False) assert dyn[-1].x_e == pytest.approx(1000, abs=1e-2) assert dyn[-1].z_e == pytest.approx(-509.5, abs=1e-2) def test_cross_compare_with_drag(): - dyn1 = get_trajectory( - parameterization=asb.DynamicsPointMass2DCartesian, - drag=True - ) - dyn2 = get_trajectory( - parameterization=asb.DynamicsPointMass2DSpeedGamma, - drag=True - ) + dyn1 = get_trajectory(parameterization=asb.DynamicsPointMass2DCartesian, drag=True) + dyn2 = get_trajectory(parameterization=asb.DynamicsPointMass2DSpeedGamma, drag=True) assert dyn1[-1].x_e == pytest.approx(dyn2[-1].x_e, abs=1e-6, rel=1e-6) assert dyn1[-1].z_e == pytest.approx(dyn2[-1].z_e, abs=1e-6, rel=1e-6) assert dyn1[-1].u_e == pytest.approx(dyn2[-1].u_e, abs=1e-6, rel=1e-6) @@ -126,13 +105,9 @@ def test_cross_compare_with_drag(): def test_cross_compare_no_drag(): - dyn1 = get_trajectory( - parameterization=asb.DynamicsPointMass2DCartesian, - drag=False - ) + dyn1 = get_trajectory(parameterization=asb.DynamicsPointMass2DCartesian, drag=False) dyn2 = get_trajectory( - parameterization=asb.DynamicsPointMass2DSpeedGamma, - drag=False + parameterization=asb.DynamicsPointMass2DSpeedGamma, drag=False ) assert dyn1[-1].x_e == pytest.approx(dyn2[-1].x_e, abs=1e-6, rel=1e-6) assert dyn1[-1].z_e == pytest.approx(dyn2[-1].z_e, abs=1e-6, rel=1e-6) @@ -141,7 +116,7 @@ def test_cross_compare_no_drag(): # -if __name__ == '__main__': +if __name__ == "__main__": dyn = get_trajectory( asb.DynamicsPointMass2DSpeedGamma, # plot=True diff --git a/aerosandbox/dynamics/point_mass/point_2D/test/test_cannonball_2D_opti.py b/aerosandbox/dynamics/point_mass/point_2D/test/test_cannonball_2D_opti.py index 9389212c6..4746ed06f 100644 --- a/aerosandbox/dynamics/point_mass/point_2D/test/test_cannonball_2D_opti.py +++ b/aerosandbox/dynamics/point_mass/point_2D/test/test_cannonball_2D_opti.py @@ -4,7 +4,7 @@ u_e_0 = 100 w_e_0 = -100 -speed_0 = (u_e_0 ** 2 + w_e_0 ** 2) ** 0.5 +speed_0 = (u_e_0**2 + w_e_0**2) ** 0.5 gamma_0 = np.arctan2(-w_e_0, u_e_0) time = np.linspace(0, 10, 501) @@ -12,11 +12,11 @@ def get_trajectory( - parameterization: type = asb.DynamicsPointMass2DCartesian, - gravity=True, - drag=True, - plot=False, - verbose=False, + parameterization: type = asb.DynamicsPointMass2DCartesian, + gravity=True, + drag=True, + plot=False, + verbose=False, ): opti = asb.Opti() @@ -41,24 +41,23 @@ def get_trajectory( if gravity: dyn.add_gravity_force() - q = 0.5 * 1.225 * dyn.speed ** 2 + q = 0.5 * 1.225 * dyn.speed**2 if drag: - dyn.add_force( - Fx=-1 * (0.1) ** 2 * q, - axes="wind" - ) + dyn.add_force(Fx=-1 * (0.1) ** 2 * q, axes="wind") dyn.constrain_derivatives( opti=opti, time=time, ) - opti.subject_to([ - dyn.x_e[0] == 0, - dyn.z_e[0] == 0, - dyn.u_e[0] == 100, - dyn.w_e[0] == -100, - ]) + opti.subject_to( + [ + dyn.x_e[0] == 0, + dyn.z_e[0] == 0, + dyn.u_e[0] == 100, + dyn.w_e[0] == -100, + ] + ) sol = opti.solve(verbose=verbose) @@ -77,50 +76,32 @@ def get_trajectory( def test_final_position_Cartesian_with_drag(): - dyn = get_trajectory( - parameterization=asb.DynamicsPointMass2DCartesian, - drag=True - ) + dyn = get_trajectory(parameterization=asb.DynamicsPointMass2DCartesian, drag=True) assert dyn[-1].x_e == pytest.approx(277.5774436945314, abs=1e-2) assert dyn[-1].z_e == pytest.approx(3.1722727033631886, abs=1e-2) def test_final_position_Cartesian_no_drag(): - dyn = get_trajectory( - parameterization=asb.DynamicsPointMass2DCartesian, - drag=False - ) + dyn = get_trajectory(parameterization=asb.DynamicsPointMass2DCartesian, drag=False) assert dyn[-1].x_e == pytest.approx(1000, abs=1e-2) assert dyn[-1].z_e == pytest.approx(-509.5, abs=1e-2) def test_final_position_SpeedGamma_with_drag(): - dyn = get_trajectory( - parameterization=asb.DynamicsPointMass2DSpeedGamma, - drag=True - ) + dyn = get_trajectory(parameterization=asb.DynamicsPointMass2DSpeedGamma, drag=True) assert dyn[-1].x_e == pytest.approx(277.5774436945314, abs=1e-2) assert dyn[-1].z_e == pytest.approx(3.1722727033631886, abs=1e-2) def test_final_position_SpeedGamma_no_drag(): - dyn = get_trajectory( - parameterization=asb.DynamicsPointMass2DSpeedGamma, - drag=False - ) + dyn = get_trajectory(parameterization=asb.DynamicsPointMass2DSpeedGamma, drag=False) assert dyn[-1].x_e == pytest.approx(1000, abs=1e-2) assert dyn[-1].z_e == pytest.approx(-509.5, abs=1e-2) def test_cross_compare_with_drag(): - dyn1 = get_trajectory( - parameterization=asb.DynamicsPointMass2DCartesian, - drag=True - ) - dyn2 = get_trajectory( - parameterization=asb.DynamicsPointMass2DSpeedGamma, - drag=True - ) + dyn1 = get_trajectory(parameterization=asb.DynamicsPointMass2DCartesian, drag=True) + dyn2 = get_trajectory(parameterization=asb.DynamicsPointMass2DSpeedGamma, drag=True) assert dyn1[-1].x_e == pytest.approx(dyn2[-1].x_e, abs=1e-2, rel=1e-2) assert dyn1[-1].z_e == pytest.approx(dyn2[-1].z_e, abs=1e-2, rel=1e-2) assert dyn1[-1].u_e == pytest.approx(dyn2[-1].u_e, abs=1e-2, rel=1e-2) @@ -128,13 +109,9 @@ def test_cross_compare_with_drag(): def test_cross_compare_no_drag(): - dyn1 = get_trajectory( - parameterization=asb.DynamicsPointMass2DCartesian, - drag=False - ) + dyn1 = get_trajectory(parameterization=asb.DynamicsPointMass2DCartesian, drag=False) dyn2 = get_trajectory( - parameterization=asb.DynamicsPointMass2DSpeedGamma, - drag=False + parameterization=asb.DynamicsPointMass2DSpeedGamma, drag=False ) assert dyn1[-1].x_e == pytest.approx(dyn2[-1].x_e, abs=1e-2, rel=1e-2) assert dyn1[-1].z_e == pytest.approx(dyn2[-1].z_e, abs=1e-2, rel=1e-2) @@ -143,9 +120,6 @@ def test_cross_compare_no_drag(): # -if __name__ == '__main__': - dyn = get_trajectory( - asb.DynamicsPointMass2DSpeedGamma, - plot=True - ) +if __name__ == "__main__": + dyn = get_trajectory(asb.DynamicsPointMass2DSpeedGamma, plot=True) pytest.main() diff --git a/aerosandbox/dynamics/point_mass/point_3D/cartesian.py b/aerosandbox/dynamics/point_mass/point_3D/cartesian.py index 6e3699df3..03933bd2f 100644 --- a/aerosandbox/dynamics/point_mass/point_3D/cartesian.py +++ b/aerosandbox/dynamics/point_mass/point_3D/cartesian.py @@ -1,4 +1,6 @@ -from aerosandbox.dynamics.point_mass.common_point_mass import _DynamicsPointMassBaseClass +from aerosandbox.dynamics.point_mass.common_point_mass import ( + _DynamicsPointMassBaseClass, +) from aerosandbox.weights.mass_properties import MassProperties import aerosandbox.numpy as np from typing import Union, Dict, Tuple @@ -31,18 +33,19 @@ class DynamicsPointMass3DCartesian(_DynamicsPointMassBaseClass): """ - def __init__(self, - mass_props: MassProperties = None, - x_e: Union[float, np.ndarray] = 0, - y_e: Union[float, np.ndarray] = 0, - z_e: Union[float, np.ndarray] = 0, - u_e: Union[float, np.ndarray] = 0, - v_e: Union[float, np.ndarray] = 0, - w_e: Union[float, np.ndarray] = 0, - alpha: Union[float, np.ndarray] = 0, - beta: Union[float, np.ndarray] = 0, - bank: Union[float, np.ndarray] = 0, - ): + def __init__( + self, + mass_props: MassProperties = None, + x_e: Union[float, np.ndarray] = 0, + y_e: Union[float, np.ndarray] = 0, + z_e: Union[float, np.ndarray] = 0, + u_e: Union[float, np.ndarray] = 0, + v_e: Union[float, np.ndarray] = 0, + w_e: Union[float, np.ndarray] = 0, + alpha: Union[float, np.ndarray] = 0, + beta: Union[float, np.ndarray] = 0, + bank: Union[float, np.ndarray] = 0, + ): # Initialize state variables self.mass_props = MassProperties() if mass_props is None else mass_props self.x_e = x_e @@ -77,11 +80,11 @@ def state(self) -> Dict[str, Union[float, np.ndarray]]: def control_variables(self) -> Dict[str, Union[float, np.ndarray]]: return { "alpha": self.alpha, - "beta" : self.beta, - "bank" : self.bank, - "Fx_e" : self.Fx_e, - "Fy_e" : self.Fy_e, - "Fz_e" : self.Fz_e, + "beta": self.beta, + "bank": self.bank, + "Fx_e": self.Fx_e, + "Fy_e": self.Fy_e, + "Fz_e": self.Fz_e, } def state_derivatives(self) -> Dict[str, Union[float, np.ndarray]]: @@ -97,10 +100,10 @@ def state_derivatives(self) -> Dict[str, Union[float, np.ndarray]]: @property def speed(self) -> float: return ( - self.u_e ** 2 + - self.v_e ** 2 + - self.w_e ** 2 + - 1e-200 # To avoid gradient singularities + self.u_e**2 + + self.v_e**2 + + self.w_e**2 + + 1e-200 # To avoid gradient singularities ) ** 0.5 @property @@ -113,11 +116,8 @@ def gamma(self): """ return np.arctan2( -self.w_e, - ( - self.u_e ** 2 + - self.v_e ** 2 + - 1e-200 # To avoid gradient singularities - ) ** 0.5 + (self.u_e**2 + self.v_e**2 + 1e-200) # To avoid gradient singularities + ** 0.5, ) @property @@ -130,17 +130,17 @@ def track(self): """ return np.arctan2( - self.v_e, - self.u_e + 1e-200 # To avoid gradient singularities, + self.v_e, self.u_e + 1e-200 # To avoid gradient singularities, ) - def convert_axes(self, - x_from: float, - y_from: float, - z_from: float, - from_axes: str, - to_axes: str, - ) -> Tuple[float, float, float]: + def convert_axes( + self, + x_from: float, + y_from: float, + z_from: float, + from_axes: str, + to_axes: str, + ) -> Tuple[float, float, float]: if from_axes == to_axes: return x_from, y_from, z_from @@ -149,7 +149,7 @@ def convert_axes(self, roll_angle=self.bank, pitch_angle=self.gamma, yaw_angle=self.track, - as_array=False + as_array=False, ) if from_axes == "earth": @@ -157,54 +157,79 @@ def convert_axes(self, y_e = y_from z_e = z_from elif from_axes == "wind": - x_e = rot_w_to_e[0][0] * x_from + rot_w_to_e[0][1] * y_from + rot_w_to_e[0][2] * z_from - y_e = rot_w_to_e[1][0] * x_from + rot_w_to_e[1][1] * y_from + rot_w_to_e[1][2] * z_from - z_e = rot_w_to_e[2][0] * x_from + rot_w_to_e[2][1] * y_from + rot_w_to_e[2][2] * z_from + x_e = ( + rot_w_to_e[0][0] * x_from + + rot_w_to_e[0][1] * y_from + + rot_w_to_e[0][2] * z_from + ) + y_e = ( + rot_w_to_e[1][0] * x_from + + rot_w_to_e[1][1] * y_from + + rot_w_to_e[1][2] * z_from + ) + z_e = ( + rot_w_to_e[2][0] * x_from + + rot_w_to_e[2][1] * y_from + + rot_w_to_e[2][2] * z_from + ) else: x_w, y_w, z_w = self.op_point.convert_axes( - x_from, y_from, z_from, - from_axes=from_axes, to_axes="wind" + x_from, y_from, z_from, from_axes=from_axes, to_axes="wind" + ) + x_e = ( + rot_w_to_e[0][0] * x_w + rot_w_to_e[0][1] * y_w + rot_w_to_e[0][2] * z_w + ) + y_e = ( + rot_w_to_e[1][0] * x_w + rot_w_to_e[1][1] * y_w + rot_w_to_e[1][2] * z_w + ) + z_e = ( + rot_w_to_e[2][0] * x_w + rot_w_to_e[2][1] * y_w + rot_w_to_e[2][2] * z_w ) - x_e = rot_w_to_e[0][0] * x_w + rot_w_to_e[0][1] * y_w + rot_w_to_e[0][2] * z_w - y_e = rot_w_to_e[1][0] * x_w + rot_w_to_e[1][1] * y_w + rot_w_to_e[1][2] * z_w - z_e = rot_w_to_e[2][0] * x_w + rot_w_to_e[2][1] * y_w + rot_w_to_e[2][2] * z_w if to_axes == "earth": x_to = x_e y_to = y_e z_to = z_e elif to_axes == "wind": - x_to = rot_w_to_e[0][0] * x_e + rot_w_to_e[1][0] * y_e + rot_w_to_e[2][0] * z_e - y_to = rot_w_to_e[0][1] * x_e + rot_w_to_e[1][1] * y_e + rot_w_to_e[2][1] * z_e - z_to = rot_w_to_e[0][2] * x_e + rot_w_to_e[1][2] * y_e + rot_w_to_e[2][2] * z_e + x_to = ( + rot_w_to_e[0][0] * x_e + rot_w_to_e[1][0] * y_e + rot_w_to_e[2][0] * z_e + ) + y_to = ( + rot_w_to_e[0][1] * x_e + rot_w_to_e[1][1] * y_e + rot_w_to_e[2][1] * z_e + ) + z_to = ( + rot_w_to_e[0][2] * x_e + rot_w_to_e[1][2] * y_e + rot_w_to_e[2][2] * z_e + ) else: - x_w = rot_w_to_e[0][0] * x_e + rot_w_to_e[1][0] * y_e + rot_w_to_e[2][0] * z_e - y_w = rot_w_to_e[0][1] * x_e + rot_w_to_e[1][1] * y_e + rot_w_to_e[2][1] * z_e - z_w = rot_w_to_e[0][2] * x_e + rot_w_to_e[1][2] * y_e + rot_w_to_e[2][2] * z_e + x_w = ( + rot_w_to_e[0][0] * x_e + rot_w_to_e[1][0] * y_e + rot_w_to_e[2][0] * z_e + ) + y_w = ( + rot_w_to_e[0][1] * x_e + rot_w_to_e[1][1] * y_e + rot_w_to_e[2][1] * z_e + ) + z_w = ( + rot_w_to_e[0][2] * x_e + rot_w_to_e[1][2] * y_e + rot_w_to_e[2][2] * z_e + ) x_to, y_to, z_to = self.op_point.convert_axes( - x_w, y_w, z_w, - from_axes="wind", to_axes=to_axes + x_w, y_w, z_w, from_axes="wind", to_axes=to_axes ) return x_to, y_to, z_to - def add_force(self, - Fx: Union[float, np.ndarray] = 0, - Fy: Union[float, np.ndarray] = 0, - Fz: Union[float, np.ndarray] = 0, - axes="earth", - ) -> None: + def add_force( + self, + Fx: Union[float, np.ndarray] = 0, + Fy: Union[float, np.ndarray] = 0, + Fz: Union[float, np.ndarray] = 0, + axes="earth", + ) -> None: Fx_e, Fy_e, Fz_e = self.convert_axes( - x_from=Fx, - y_from=Fy, - z_from=Fz, - from_axes=axes, - to_axes="earth" + x_from=Fx, y_from=Fy, z_from=Fz, from_axes=axes, to_axes="earth" ) self.Fx_e = self.Fx_e + Fx_e self.Fy_e = self.Fy_e + Fy_e self.Fz_e = self.Fz_e + Fz_e -if __name__ == '__main__': +if __name__ == "__main__": dyn = DynamicsPointMass3DCartesian() diff --git a/aerosandbox/dynamics/point_mass/point_3D/speed_gamma_track.py b/aerosandbox/dynamics/point_mass/point_3D/speed_gamma_track.py index f1663383e..b64e0e321 100644 --- a/aerosandbox/dynamics/point_mass/point_3D/speed_gamma_track.py +++ b/aerosandbox/dynamics/point_mass/point_3D/speed_gamma_track.py @@ -1,4 +1,6 @@ -from aerosandbox.dynamics.point_mass.common_point_mass import _DynamicsPointMassBaseClass +from aerosandbox.dynamics.point_mass.common_point_mass import ( + _DynamicsPointMassBaseClass, +) from aerosandbox.weights.mass_properties import MassProperties import aerosandbox.numpy as np from typing import Union, Dict, Tuple @@ -33,18 +35,19 @@ class DynamicsPointMass3DSpeedGammaTrack(_DynamicsPointMassBaseClass): """ - def __init__(self, - mass_props: MassProperties = None, - x_e: Union[float, np.ndarray] = 0, - y_e: Union[float, np.ndarray] = 0, - z_e: Union[float, np.ndarray] = 0, - speed: Union[float, np.ndarray] = 0, - gamma: Union[float, np.ndarray] = 0, - track: Union[float, np.ndarray] = 0, - alpha: Union[float, np.ndarray] = 0, - beta: Union[float, np.ndarray] = 0, - bank: Union[float, np.ndarray] = 0, - ): + def __init__( + self, + mass_props: MassProperties = None, + x_e: Union[float, np.ndarray] = 0, + y_e: Union[float, np.ndarray] = 0, + z_e: Union[float, np.ndarray] = 0, + speed: Union[float, np.ndarray] = 0, + gamma: Union[float, np.ndarray] = 0, + track: Union[float, np.ndarray] = 0, + alpha: Union[float, np.ndarray] = 0, + beta: Union[float, np.ndarray] = 0, + bank: Union[float, np.ndarray] = 0, + ): # Initialize state variables self.mass_props = MassProperties() if mass_props is None else mass_props self.x_e = x_e @@ -67,9 +70,9 @@ def __init__(self, @property def state(self) -> Dict[str, Union[float, np.ndarray]]: return { - "x_e" : self.x_e, - "y_e" : self.y_e, - "z_e" : self.z_e, + "x_e": self.x_e, + "y_e": self.y_e, + "z_e": self.z_e, "speed": self.speed, "gamma": self.gamma, "track": self.track, @@ -79,11 +82,11 @@ def state(self) -> Dict[str, Union[float, np.ndarray]]: def control_variables(self) -> Dict[str, Union[float, np.ndarray]]: return { "alpha": self.alpha, - "beta" : self.beta, - "bank" : self.bank, - "Fx_w" : self.Fx_w, - "Fy_w" : self.Fy_w, - "Fz_w" : self.Fz_w, + "beta": self.beta, + "bank": self.bank, + "Fx_w": self.Fx_w, + "Fy_w": self.Fy_w, + "Fz_w": self.Fz_w, } def state_derivatives(self) -> Dict[str, Union[float, np.ndarray]]: @@ -93,16 +96,25 @@ def state_derivatives(self) -> Dict[str, Union[float, np.ndarray]]: sb = np.sin(self.bank) cb = np.cos(self.bank) - force_gamma_direction = -cb * self.Fz_w - sb * self.Fy_w # Force in the direction that acts to increase gamma - force_track_direction = -sb * self.Fz_w + cb * self.Fy_w # Force in the direction that acts to increase track + force_gamma_direction = ( + -cb * self.Fz_w - sb * self.Fy_w + ) # Force in the direction that acts to increase gamma + force_track_direction = ( + -sb * self.Fz_w + cb * self.Fy_w + ) # Force in the direction that acts to increase track d_gamma = force_gamma_direction / self.mass_props.mass / self.speed - d_track = force_track_direction / self.mass_props.mass / self.speed / np.cos(self.gamma) + d_track = ( + force_track_direction + / self.mass_props.mass + / self.speed + / np.cos(self.gamma) + ) return { - "x_e" : self.u_e, - "y_e" : self.v_e, - "z_e" : self.w_e, + "x_e": self.u_e, + "y_e": self.v_e, + "z_e": self.w_e, "speed": d_speed, "gamma": d_gamma, "track": d_track, @@ -120,22 +132,23 @@ def v_e(self): def w_e(self): return -self.speed * np.sin(self.gamma) - def convert_axes(self, - x_from: float, - y_from: float, - z_from: float, - from_axes: str, - to_axes: str, - ) -> Tuple[float, float, float]: + def convert_axes( + self, + x_from: float, + y_from: float, + z_from: float, + from_axes: str, + to_axes: str, + ) -> Tuple[float, float, float]: if from_axes == to_axes: return x_from, y_from, z_from - if (from_axes == "earth" or to_axes == "earth"): + if from_axes == "earth" or to_axes == "earth": rot_w_to_e = np.rotation_matrix_from_euler_angles( roll_angle=self.bank, pitch_angle=self.gamma, yaw_angle=self.track, - as_array=False + as_array=False, ) if from_axes == "wind": @@ -143,13 +156,24 @@ def convert_axes(self, y_w = y_from z_w = z_from elif from_axes == "earth": - x_w = rot_w_to_e[0][0] * x_from + rot_w_to_e[1][0] * y_from + rot_w_to_e[2][0] * z_from - y_w = rot_w_to_e[0][1] * x_from + rot_w_to_e[1][1] * y_from + rot_w_to_e[2][1] * z_from - z_w = rot_w_to_e[0][2] * x_from + rot_w_to_e[1][2] * y_from + rot_w_to_e[2][2] * z_from + x_w = ( + rot_w_to_e[0][0] * x_from + + rot_w_to_e[1][0] * y_from + + rot_w_to_e[2][0] * z_from + ) + y_w = ( + rot_w_to_e[0][1] * x_from + + rot_w_to_e[1][1] * y_from + + rot_w_to_e[2][1] * z_from + ) + z_w = ( + rot_w_to_e[0][2] * x_from + + rot_w_to_e[1][2] * y_from + + rot_w_to_e[2][2] * z_from + ) else: x_w, y_w, z_w = self.op_point.convert_axes( - x_from, y_from, z_from, - from_axes=from_axes, to_axes="wind" + x_from, y_from, z_from, from_axes=from_axes, to_axes="wind" ) if to_axes == "wind": @@ -157,34 +181,36 @@ def convert_axes(self, y_to = y_w z_to = z_w elif to_axes == "earth": - x_to = rot_w_to_e[0][0] * x_w + rot_w_to_e[0][1] * y_w + rot_w_to_e[0][2] * z_w - y_to = rot_w_to_e[1][0] * x_w + rot_w_to_e[1][1] * y_w + rot_w_to_e[1][2] * z_w - z_to = rot_w_to_e[2][0] * x_w + rot_w_to_e[2][1] * y_w + rot_w_to_e[2][2] * z_w + x_to = ( + rot_w_to_e[0][0] * x_w + rot_w_to_e[0][1] * y_w + rot_w_to_e[0][2] * z_w + ) + y_to = ( + rot_w_to_e[1][0] * x_w + rot_w_to_e[1][1] * y_w + rot_w_to_e[1][2] * z_w + ) + z_to = ( + rot_w_to_e[2][0] * x_w + rot_w_to_e[2][1] * y_w + rot_w_to_e[2][2] * z_w + ) else: x_to, y_to, z_to = self.op_point.convert_axes( - x_w, y_w, z_w, - from_axes="wind", to_axes=to_axes + x_w, y_w, z_w, from_axes="wind", to_axes=to_axes ) return x_to, y_to, z_to - def add_force(self, - Fx: Union[float, np.ndarray] = 0, - Fy: Union[float, np.ndarray] = 0, - Fz: Union[float, np.ndarray] = 0, - axes="wind", - ) -> None: + def add_force( + self, + Fx: Union[float, np.ndarray] = 0, + Fy: Union[float, np.ndarray] = 0, + Fz: Union[float, np.ndarray] = 0, + axes="wind", + ) -> None: Fx_w, Fy_w, Fz_w = self.convert_axes( - x_from=Fx, - y_from=Fy, - z_from=Fz, - from_axes=axes, - to_axes="wind" + x_from=Fx, y_from=Fy, z_from=Fz, from_axes=axes, to_axes="wind" ) self.Fx_w = self.Fx_w + Fx_w self.Fy_w = self.Fy_w + Fy_w self.Fz_w = self.Fz_w + Fz_w -if __name__ == '__main__': +if __name__ == "__main__": dyn = DynamicsPointMass3DSpeedGammaTrack() diff --git a/aerosandbox/dynamics/point_mass/point_3D/test/test_cannonball_3D.py b/aerosandbox/dynamics/point_mass/point_3D/test/test_cannonball_3D.py index 133980166..c0525d554 100644 --- a/aerosandbox/dynamics/point_mass/point_3D/test/test_cannonball_3D.py +++ b/aerosandbox/dynamics/point_mass/point_3D/test/test_cannonball_3D.py @@ -6,7 +6,7 @@ u_e_0 = 100 v_e_0 = 0 w_e_0 = -100 -speed_0 = (u_e_0 ** 2 + w_e_0 ** 2) ** 0.5 +speed_0 = (u_e_0**2 + w_e_0**2) ** 0.5 gamma_0 = np.arctan2(-w_e_0, u_e_0) track_0 = 0 @@ -14,11 +14,11 @@ def get_trajectory( - parameterization: type = asb.DynamicsPointMass3DCartesian, - gravity=True, - drag=True, - sideforce=True, - plot=False + parameterization: type = asb.DynamicsPointMass3DCartesian, + gravity=True, + drag=True, + sideforce=True, + plot=False, ): if parameterization is asb.DynamicsPointMass3DCartesian: dyn = parameterization( @@ -47,18 +47,14 @@ def derivatives(t, y): this_dyn = dyn.get_new_instance_with_state(y) if gravity: this_dyn.add_gravity_force() - q = 0.5 * 1.225 * this_dyn.speed ** 2 + q = 0.5 * 1.225 * this_dyn.speed**2 if drag: - this_dyn.add_force( - Fx=-1 * (0.1) ** 2 * q, - axes="wind" - ) + this_dyn.add_force(Fx=-1 * (0.1) ** 2 * q, axes="wind") if sideforce: strouhal = 0.2 frequency = 5 # strouhal * this_dyn.speed / 0.1 this_dyn.add_force( - Fy=q * 1 * (0.1) ** 2 * np.sin(2 * np.pi * frequency * t), - axes="wind" + Fy=q * 1 * (0.1) ** 2 * np.sin(2 * np.pi * frequency * t), axes="wind" ) return this_dyn.unpack_state(this_dyn.state_derivatives()) @@ -90,9 +86,7 @@ def derivatives(t, y): def test_final_position_Cartesian_with_sideforce(): dyn = get_trajectory( - parameterization=asb.DynamicsPointMass3DCartesian, - drag=True, - sideforce=True + parameterization=asb.DynamicsPointMass3DCartesian, drag=True, sideforce=True ) assert dyn[-1].x_e == pytest.approx(277.3463197415092, abs=1e-2) assert dyn[-1].y_e == pytest.approx(10.791223276048788, abs=1e-2) @@ -101,9 +95,7 @@ def test_final_position_Cartesian_with_sideforce(): def test_final_position_Cartesian_with_drag(): dyn = get_trajectory( - parameterization=asb.DynamicsPointMass3DCartesian, - drag=True, - sideforce=False + parameterization=asb.DynamicsPointMass3DCartesian, drag=True, sideforce=False ) assert dyn[-1].x_e == pytest.approx(277.5774436945314, abs=1e-2) assert dyn[-1].z_e == pytest.approx(3.1722727033631886, abs=1e-2) @@ -111,9 +103,7 @@ def test_final_position_Cartesian_with_drag(): def test_final_position_Cartesian_no_drag(): dyn = get_trajectory( - parameterization=asb.DynamicsPointMass3DCartesian, - drag=False, - sideforce=False + parameterization=asb.DynamicsPointMass3DCartesian, drag=False, sideforce=False ) assert dyn[-1].x_e == pytest.approx(1000, abs=1e-2) assert dyn[-1].z_e == pytest.approx(-509.5, abs=1e-2) @@ -123,7 +113,7 @@ def test_final_position_SpeedGammaTrack_with_sideforce(): dyn = get_trajectory( parameterization=asb.DynamicsPointMass3DSpeedGammaTrack, drag=True, - sideforce=True + sideforce=True, ) assert dyn[-1].x_e == pytest.approx(277.3463197415092, abs=1e-2) assert dyn[-1].y_e == pytest.approx(10.791223276048788, abs=1e-2) @@ -134,7 +124,7 @@ def test_final_position_SpeedGammaTrack_with_drag(): dyn = get_trajectory( parameterization=asb.DynamicsPointMass3DSpeedGammaTrack, drag=True, - sideforce=False + sideforce=False, ) assert dyn[-1].x_e == pytest.approx(277.5774436945314, abs=1e-2) assert dyn[-1].z_e == pytest.approx(3.1722727033631886, abs=1e-2) @@ -144,20 +134,16 @@ def test_final_position_SpeedGammaTrack_no_drag(): dyn = get_trajectory( parameterization=asb.DynamicsPointMass3DSpeedGammaTrack, drag=False, - sideforce=False + sideforce=False, ) assert dyn[-1].x_e == pytest.approx(1000, abs=1e-2) assert dyn[-1].z_e == pytest.approx(-509.5, abs=1e-2) def test_cross_compare_with_drag(): - dyn1 = get_trajectory( - parameterization=asb.DynamicsPointMass3DCartesian, - drag=True - ) + dyn1 = get_trajectory(parameterization=asb.DynamicsPointMass3DCartesian, drag=True) dyn2 = get_trajectory( - parameterization=asb.DynamicsPointMass3DSpeedGammaTrack, - drag=True + parameterization=asb.DynamicsPointMass3DSpeedGammaTrack, drag=True ) assert dyn1[-1].x_e == pytest.approx(dyn2[-1].x_e, abs=1e-6, rel=1e-6) assert dyn1[-1].y_e == pytest.approx(dyn2[-1].y_e, abs=1e-6, rel=1e-6) @@ -168,13 +154,9 @@ def test_cross_compare_with_drag(): def test_cross_compare_no_drag(): - dyn1 = get_trajectory( - parameterization=asb.DynamicsPointMass3DCartesian, - drag=False - ) + dyn1 = get_trajectory(parameterization=asb.DynamicsPointMass3DCartesian, drag=False) dyn2 = get_trajectory( - parameterization=asb.DynamicsPointMass3DSpeedGammaTrack, - drag=False + parameterization=asb.DynamicsPointMass3DSpeedGammaTrack, drag=False ) assert dyn1[-1].x_e == pytest.approx(dyn2[-1].x_e, abs=1e-6, rel=1e-6) assert dyn1[-1].y_e == pytest.approx(dyn2[-1].y_e, abs=1e-6, rel=1e-6) @@ -185,9 +167,6 @@ def test_cross_compare_no_drag(): # -if __name__ == '__main__': +if __name__ == "__main__": pytest.main() - dyn = get_trajectory( - asb.DynamicsPointMass3DSpeedGammaTrack, - plot=True - ) + dyn = get_trajectory(asb.DynamicsPointMass3DSpeedGammaTrack, plot=True) diff --git a/aerosandbox/dynamics/point_mass/point_3D/test/test_cannonball_3D_opti.py b/aerosandbox/dynamics/point_mass/point_3D/test/test_cannonball_3D_opti.py index d2fa6c120..c68b4b1c6 100644 --- a/aerosandbox/dynamics/point_mass/point_3D/test/test_cannonball_3D_opti.py +++ b/aerosandbox/dynamics/point_mass/point_3D/test/test_cannonball_3D_opti.py @@ -5,7 +5,7 @@ u_e_0 = 100 v_e_0 = 0 w_e_0 = -100 -speed_0 = (u_e_0 ** 2 + w_e_0 ** 2) ** 0.5 +speed_0 = (u_e_0**2 + w_e_0**2) ** 0.5 gamma_0 = np.arctan2(-w_e_0, u_e_0) track_0 = 0 @@ -14,12 +14,12 @@ def get_trajectory( - parameterization: type = asb.DynamicsPointMass3DCartesian, - gravity=True, - drag=True, - sideforce=True, - plot=False, - verbose=False, + parameterization: type = asb.DynamicsPointMass3DCartesian, + gravity=True, + drag=True, + sideforce=True, + plot=False, + verbose=False, ): opti = asb.Opti() @@ -48,18 +48,14 @@ def get_trajectory( if gravity: dyn.add_gravity_force() - q = 0.5 * 1.225 * dyn.speed ** 2 + q = 0.5 * 1.225 * dyn.speed**2 if drag: - dyn.add_force( - Fx=-1 * (0.1) ** 2 * q, - axes="wind" - ) + dyn.add_force(Fx=-1 * (0.1) ** 2 * q, axes="wind") if sideforce: strouhal = 0.2 frequency = 5 # strouhal * dyn.speed / 0.1 dyn.add_force( - Fy=q * 1 * (0.1) ** 2 * np.sin(2 * np.pi * frequency * time), - axes="wind" + Fy=q * 1 * (0.1) ** 2 * np.sin(2 * np.pi * frequency * time), axes="wind" ) dyn.constrain_derivatives( @@ -67,14 +63,16 @@ def get_trajectory( time=time, ) - opti.subject_to([ - dyn.x_e[0] == 0, - dyn.y_e[0] == 0, - dyn.z_e[0] == 0, - dyn.u_e[0] == 100, - dyn.v_e[0] == 0, - dyn.w_e[0] == -100, - ]) + opti.subject_to( + [ + dyn.x_e[0] == 0, + dyn.y_e[0] == 0, + dyn.z_e[0] == 0, + dyn.u_e[0] == 100, + dyn.v_e[0] == 0, + dyn.w_e[0] == -100, + ] + ) sol = opti.solve(verbose=verbose) @@ -94,9 +92,7 @@ def get_trajectory( def test_final_position_Cartesian_with_sideforce(): dyn = get_trajectory( - parameterization=asb.DynamicsPointMass3DCartesian, - drag=True, - sideforce=True + parameterization=asb.DynamicsPointMass3DCartesian, drag=True, sideforce=True ) assert dyn[-1].x_e == pytest.approx(277.3463197415092, abs=1e-1) assert dyn[-1].y_e == pytest.approx(10.791223276048788, abs=1) @@ -105,9 +101,7 @@ def test_final_position_Cartesian_with_sideforce(): def test_final_position_Cartesian_with_drag(): dyn = get_trajectory( - parameterization=asb.DynamicsPointMass3DCartesian, - drag=True, - sideforce=False + parameterization=asb.DynamicsPointMass3DCartesian, drag=True, sideforce=False ) assert dyn[-1].x_e == pytest.approx(277.5774436945314, abs=1e-2) assert dyn[-1].z_e == pytest.approx(3.1722727033631886, abs=1e-2) @@ -115,9 +109,7 @@ def test_final_position_Cartesian_with_drag(): def test_final_position_Cartesian_no_drag(): dyn = get_trajectory( - parameterization=asb.DynamicsPointMass3DCartesian, - drag=False, - sideforce=False + parameterization=asb.DynamicsPointMass3DCartesian, drag=False, sideforce=False ) assert dyn[-1].x_e == pytest.approx(1000, abs=1e-2) assert dyn[-1].z_e == pytest.approx(-509.5, abs=1e-2) @@ -127,7 +119,7 @@ def test_final_position_SpeedGammaTrack_with_sideforce(): dyn = get_trajectory( parameterization=asb.DynamicsPointMass3DSpeedGammaTrack, drag=True, - sideforce=True + sideforce=True, ) assert dyn[-1].x_e == pytest.approx(277.3463197415092, abs=1e-1) assert dyn[-1].y_e == pytest.approx(10.791223276048788, abs=1) @@ -138,7 +130,7 @@ def test_final_position_SpeedGammaTrack_with_drag(): dyn = get_trajectory( parameterization=asb.DynamicsPointMass3DSpeedGammaTrack, drag=True, - sideforce=False + sideforce=False, ) assert dyn[-1].x_e == pytest.approx(277.5774436945314, abs=1e-2) assert dyn[-1].z_e == pytest.approx(3.1722727033631886, abs=1e-2) @@ -148,20 +140,16 @@ def test_final_position_SpeedGammaTrack_no_drag(): dyn = get_trajectory( parameterization=asb.DynamicsPointMass3DSpeedGammaTrack, drag=False, - sideforce=False + sideforce=False, ) assert dyn[-1].x_e == pytest.approx(1000, abs=1e-2) assert dyn[-1].z_e == pytest.approx(-509.5, abs=1e-2) def test_cross_compare_with_drag(): - dyn1 = get_trajectory( - parameterization=asb.DynamicsPointMass3DCartesian, - drag=True - ) + dyn1 = get_trajectory(parameterization=asb.DynamicsPointMass3DCartesian, drag=True) dyn2 = get_trajectory( - parameterization=asb.DynamicsPointMass3DSpeedGammaTrack, - drag=True + parameterization=asb.DynamicsPointMass3DSpeedGammaTrack, drag=True ) assert dyn1[-1].x_e == pytest.approx(dyn2[-1].x_e, abs=1e-2, rel=1e-2) assert dyn1[-1].y_e == pytest.approx(dyn2[-1].y_e, abs=1e-2, rel=1e-2) @@ -172,13 +160,9 @@ def test_cross_compare_with_drag(): def test_cross_compare_no_drag(): - dyn1 = get_trajectory( - parameterization=asb.DynamicsPointMass3DCartesian, - drag=False - ) + dyn1 = get_trajectory(parameterization=asb.DynamicsPointMass3DCartesian, drag=False) dyn2 = get_trajectory( - parameterization=asb.DynamicsPointMass3DSpeedGammaTrack, - drag=False + parameterization=asb.DynamicsPointMass3DSpeedGammaTrack, drag=False ) assert dyn1[-1].x_e == pytest.approx(dyn2[-1].x_e, abs=1e-2, rel=1e-2) assert dyn1[-1].y_e == pytest.approx(dyn2[-1].y_e, abs=1e-2, rel=1e-2) @@ -189,9 +173,6 @@ def test_cross_compare_no_drag(): # -if __name__ == '__main__': - dyn = get_trajectory( - asb.DynamicsPointMass3DSpeedGammaTrack, - plot=True - ) +if __name__ == "__main__": + dyn = get_trajectory(asb.DynamicsPointMass3DSpeedGammaTrack, plot=True) pytest.main() diff --git a/aerosandbox/dynamics/point_mass/point_3D/test/test_helix_3D.py b/aerosandbox/dynamics/point_mass/point_3D/test/test_helix_3D.py index f61f6a3a3..83c9be6f3 100644 --- a/aerosandbox/dynamics/point_mass/point_3D/test/test_helix_3D.py +++ b/aerosandbox/dynamics/point_mass/point_3D/test/test_helix_3D.py @@ -6,7 +6,7 @@ u_e_0 = 8 v_e_0 = 0 w_e_0 = -6 -speed_0 = (u_e_0 ** 2 + w_e_0 ** 2) ** 0.5 +speed_0 = (u_e_0**2 + w_e_0**2) ** 0.5 gamma_0 = np.arctan2(-w_e_0, u_e_0) track_0 = 0 @@ -14,8 +14,7 @@ def get_trajectory( - parameterization: type = asb.DynamicsPointMass3DCartesian, - plot=False + parameterization: type = asb.DynamicsPointMass3DCartesian, plot=False ): if parameterization is asb.DynamicsPointMass3DCartesian: dyn = parameterization( @@ -42,10 +41,7 @@ def get_trajectory( def derivatives(t, y): this_dyn = dyn.get_new_instance_with_state(y) - this_dyn.add_force( - Fy=8, - axes="wind" - ) + this_dyn.add_force(Fy=8, axes="wind") return this_dyn.unpack_state(this_dyn.state_derivatives()) @@ -112,13 +108,7 @@ def test_cross_compare(): assert dyn1[-1].w_e == pytest.approx(dyn2[-1].w_e, abs=1e-6, rel=1e-6) -if __name__ == '__main__': +if __name__ == "__main__": # pytest.main() - dyn1 = get_trajectory( - asb.DynamicsPointMass3DCartesian, - plot=True - ) - dyn2 = get_trajectory( - asb.DynamicsPointMass3DSpeedGammaTrack, - plot=True - ) + dyn1 = get_trajectory(asb.DynamicsPointMass3DCartesian, plot=True) + dyn2 = get_trajectory(asb.DynamicsPointMass3DSpeedGammaTrack, plot=True) diff --git a/aerosandbox/dynamics/rigid_body/common_rigid_body.py b/aerosandbox/dynamics/rigid_body/common_rigid_body.py index ca14a90aa..04bfb17ff 100644 --- a/aerosandbox/dynamics/rigid_body/common_rigid_body.py +++ b/aerosandbox/dynamics/rigid_body/common_rigid_body.py @@ -1,5 +1,7 @@ import aerosandbox.numpy as np -from aerosandbox.dynamics.point_mass.common_point_mass import _DynamicsPointMassBaseClass +from aerosandbox.dynamics.point_mass.common_point_mass import ( + _DynamicsPointMassBaseClass, +) from abc import ABC, abstractmethod, abstractproperty from typing import Union, Tuple from aerosandbox import OperatingPoint, Atmosphere @@ -10,12 +12,13 @@ class _DynamicsRigidBodyBaseClass(_DynamicsPointMassBaseClass, ABC): # TODO: add method for force at offset (i.e., add moment and force) @abstractmethod - def add_moment(self, - Mx: Union[float, np.ndarray] = 0, - My: Union[float, np.ndarray] = 0, - Mz: Union[float, np.ndarray] = 0, - axes="body", - ) -> None: + def add_moment( + self, + Mx: Union[float, np.ndarray] = 0, + My: Union[float, np.ndarray] = 0, + Mz: Union[float, np.ndarray] = 0, + axes="body", + ) -> None: """ Adds a moment (in whichever axis system you choose) to this Dynamics instance. @@ -50,28 +53,19 @@ def op_point(self): @property def alpha(self): """The angle of attack, in degrees.""" - return np.arctan2d( - self.w_b, - self.u_b - ) + return np.arctan2d(self.w_b, self.u_b) @property def beta(self): """The sideslip angle, in degrees.""" - return np.arctan2d( - self.v_b, - ( - self.u_b ** 2 + - self.w_b ** 2 - ) ** 0.5 - ) + return np.arctan2d(self.v_b, (self.u_b**2 + self.w_b**2) ** 0.5) @property def rotational_kinetic_energy(self): return 0.5 * ( - self.mass_props.Ixx * self.p ** 2 + - self.mass_props.Iyy * self.q ** 2 + - self.mass_props.Izz * self.r ** 2 + self.mass_props.Ixx * self.p**2 + + self.mass_props.Iyy * self.q**2 + + self.mass_props.Izz * self.r**2 ) @property diff --git a/aerosandbox/dynamics/rigid_body/rigid_2D/body.py b/aerosandbox/dynamics/rigid_body/rigid_2D/body.py index c6a6292cd..7a8cd7084 100644 --- a/aerosandbox/dynamics/rigid_body/rigid_2D/body.py +++ b/aerosandbox/dynamics/rigid_body/rigid_2D/body.py @@ -1,4 +1,6 @@ -from aerosandbox.dynamics.rigid_body.rigid_3D.body_euler import DynamicsRigidBody3DBodyEuler +from aerosandbox.dynamics.rigid_body.rigid_3D.body_euler import ( + DynamicsRigidBody3DBodyEuler, +) from aerosandbox.weights.mass_properties import MassProperties import aerosandbox.numpy as np from typing import Union, Dict, Tuple @@ -26,15 +28,16 @@ class DynamicsRigidBody2DBody(DynamicsRigidBody3DBodyEuler): """ - def __init__(self, - mass_props: MassProperties = None, - x_e: Union[float, np.ndarray] = 0, - z_e: Union[float, np.ndarray] = 0, - u_b: Union[float, np.ndarray] = 0, - w_b: Union[float, np.ndarray] = 0, - theta: Union[float, np.ndarray] = 0, - q: Union[float, np.ndarray] = 0, - ): + def __init__( + self, + mass_props: MassProperties = None, + x_e: Union[float, np.ndarray] = 0, + z_e: Union[float, np.ndarray] = 0, + u_b: Union[float, np.ndarray] = 0, + w_b: Union[float, np.ndarray] = 0, + theta: Union[float, np.ndarray] = 0, + q: Union[float, np.ndarray] = 0, + ): # Initialize state variables self.mass_props = MassProperties() if mass_props is None else mass_props self.x_e = x_e @@ -64,12 +67,12 @@ def __init__(self, @property def state(self): return { - "x_e" : self.x_e, - "z_e" : self.z_e, - "u_b" : self.u_b, - "w_b" : self.w_b, + "x_e": self.x_e, + "z_e": self.z_e, + "u_b": self.u_b, + "w_b": self.w_b, "theta": self.theta, - "q" : self.q, + "q": self.q, } @property @@ -82,10 +85,8 @@ def control_variables(self): def state_derivatives(self) -> Dict[str, Union[float, np.ndarray]]: derivatives = super().state_derivatives() - return { - k: derivatives[k] for k in self.state.keys() - } + return {k: derivatives[k] for k in self.state.keys()} -if __name__ == '__main__': +if __name__ == "__main__": dyn = DynamicsRigidBody2DBody() diff --git a/aerosandbox/dynamics/rigid_body/rigid_2D/ignore/quadcopter - work in progress.py b/aerosandbox/dynamics/rigid_body/rigid_2D/ignore/quadcopter - work in progress.py index d1ef18c50..d318f5f38 100644 --- a/aerosandbox/dynamics/rigid_body/rigid_2D/ignore/quadcopter - work in progress.py +++ b/aerosandbox/dynamics/rigid_body/rigid_2D/ignore/quadcopter - work in progress.py @@ -10,8 +10,12 @@ def test_quadcopter_navigation(): time_final = 1 time = np.linspace(0, time_final, N) - left_thrust = opti.variable(init_guess=0.5, scale=1, n_vars=N, lower_bound=0, upper_bound=1) - right_thrust = opti.variable(init_guess=0.5, scale=1, n_vars=N, lower_bound=0, upper_bound=1) + left_thrust = opti.variable( + init_guess=0.5, scale=1, n_vars=N, lower_bound=0, upper_bound=1 + ) + right_thrust = opti.variable( + init_guess=0.5, scale=1, n_vars=N, lower_bound=0, upper_bound=1 + ) mass = 0.1 @@ -27,31 +31,38 @@ def test_quadcopter_navigation(): X=left_thrust + right_thrust, M=(right_thrust - left_thrust) * 0.1 / 2, mass=mass, - Iyy=0.5 * mass * 0.1 ** 2, + Iyy=0.5 * mass * 0.1**2, g=9.81, ) - opti.subject_to([ # Starting state - dyn.xe[0] == 0, - dyn.ze[0] == 0, - dyn.u[0] == 0, - dyn.w[0] == 0, - dyn.theta[0] == np.radians(90), - dyn.q[0] == 0, - ]) - - opti.subject_to([ # Final state - dyn.xe[-1] == 1, - dyn.ze[-1] == -1, - dyn.u[-1] == 0, - dyn.w[-1] == 0, - dyn.theta[-1] == np.radians(90), - dyn.q[-1] == 0, - ]) - - effort = np.sum( # The average "effort per second", where effort is integrated as follows: - np.trapz(left_thrust ** 2 + right_thrust ** 2) * np.diff(time) - ) / time_final + opti.subject_to( + [ # Starting state + dyn.xe[0] == 0, + dyn.ze[0] == 0, + dyn.u[0] == 0, + dyn.w[0] == 0, + dyn.theta[0] == np.radians(90), + dyn.q[0] == 0, + ] + ) + + opti.subject_to( + [ # Final state + dyn.xe[-1] == 1, + dyn.ze[-1] == -1, + dyn.u[-1] == 0, + dyn.w[-1] == 0, + dyn.theta[-1] == np.radians(90), + dyn.q[-1] == 0, + ] + ) + + effort = ( + np.sum( # The average "effort per second", where effort is integrated as follows: + np.trapz(left_thrust**2 + right_thrust**2) * np.diff(time) + ) + / time_final + ) opti.minimize(effort) @@ -70,8 +81,12 @@ def test_quadcopter_flip(): time_final = opti.variable(init_guess=1, lower_bound=0) time = np.linspace(0, time_final, N) - left_thrust = opti.variable(init_guess=0.7, scale=1, n_vars=N, lower_bound=0, upper_bound=1) - right_thrust = opti.variable(init_guess=0.6, scale=1, n_vars=N, lower_bound=0, upper_bound=1) + left_thrust = opti.variable( + init_guess=0.7, scale=1, n_vars=N, lower_bound=0, upper_bound=1 + ) + right_thrust = opti.variable( + init_guess=0.6, scale=1, n_vars=N, lower_bound=0, upper_bound=1 + ) mass = 0.1 @@ -82,32 +97,38 @@ def test_quadcopter_flip(): ze=opti.variable(init_guess=0, n_vars=N), u=opti.variable(init_guess=0, n_vars=N), w=opti.variable(init_guess=0, n_vars=N), - theta=opti.variable(init_guess=np.linspace(np.pi / 2, np.pi / 2 - 2 * np.pi, N)), + theta=opti.variable( + init_guess=np.linspace(np.pi / 2, np.pi / 2 - 2 * np.pi, N) + ), q=opti.variable(init_guess=0, n_vars=N), X=left_thrust + right_thrust, M=(right_thrust - left_thrust) * 0.1 / 2, mass=mass, - Iyy=0.5 * mass * 0.1 ** 2, + Iyy=0.5 * mass * 0.1**2, g=9.81, ) - opti.subject_to([ # Starting state - dyn.xe[0] == 0, - dyn.ze[0] == 0, - dyn.u[0] == 0, - dyn.w[0] == 0, - dyn.theta[0] == np.radians(90), - dyn.q[0] == 0, - ]) - - opti.subject_to([ # Final state - dyn.xe[-1] == 1, - dyn.ze[-1] == 0, - dyn.u[-1] == 0, - dyn.w[-1] == 0, - dyn.theta[-1] == np.radians(90 - 360), - dyn.q[-1] == 0, - ]) + opti.subject_to( + [ # Starting state + dyn.xe[0] == 0, + dyn.ze[0] == 0, + dyn.u[0] == 0, + dyn.w[0] == 0, + dyn.theta[0] == np.radians(90), + dyn.q[0] == 0, + ] + ) + + opti.subject_to( + [ # Final state + dyn.xe[-1] == 1, + dyn.ze[-1] == 0, + dyn.u[-1] == 0, + dyn.w[-1] == 0, + dyn.theta[-1] == np.radians(90 - 360), + dyn.q[-1] == 0, + ] + ) opti.minimize(time_final) @@ -117,5 +138,5 @@ def test_quadcopter_flip(): assert sol(time_final) == pytest.approx(0.824, abs=0.01) -if __name__ == '__main__': +if __name__ == "__main__": pytest.main() diff --git a/aerosandbox/dynamics/rigid_body/rigid_3D/body_euler.py b/aerosandbox/dynamics/rigid_body/rigid_3D/body_euler.py index 62aaaacb5..023bdc955 100644 --- a/aerosandbox/dynamics/rigid_body/rigid_3D/body_euler.py +++ b/aerosandbox/dynamics/rigid_body/rigid_3D/body_euler.py @@ -1,4 +1,6 @@ -from aerosandbox.dynamics.rigid_body.common_rigid_body import _DynamicsRigidBodyBaseClass +from aerosandbox.dynamics.rigid_body.common_rigid_body import ( + _DynamicsRigidBodyBaseClass, +) import aerosandbox.numpy as np from aerosandbox.weights.mass_properties import MassProperties from typing import Union @@ -11,7 +13,7 @@ class DynamicsRigidBody3DBodyEuler(_DynamicsRigidBodyBaseClass): * in 3D * with velocity parameterized in body axes * and angle parameterized in Euler angles - + State variables: x_e: x-position, in Earth axes. [meters] y_e: y-position, in Earth axes. [meters] @@ -39,21 +41,22 @@ class DynamicsRigidBody3DBodyEuler(_DynamicsRigidBodyBaseClass): """ - def __init__(self, - mass_props: MassProperties = None, - x_e: Union[float, np.ndarray] = 0, - y_e: Union[float, np.ndarray] = 0, - z_e: Union[float, np.ndarray] = 0, - u_b: Union[float, np.ndarray] = 0, - v_b: Union[float, np.ndarray] = 0, - w_b: Union[float, np.ndarray] = 0, - phi: Union[float, np.ndarray] = 0, - theta: Union[float, np.ndarray] = 0, - psi: Union[float, np.ndarray] = 0, - p: Union[float, np.ndarray] = 0, - q: Union[float, np.ndarray] = 0, - r: Union[float, np.ndarray] = 0, - ): + def __init__( + self, + mass_props: MassProperties = None, + x_e: Union[float, np.ndarray] = 0, + y_e: Union[float, np.ndarray] = 0, + z_e: Union[float, np.ndarray] = 0, + u_b: Union[float, np.ndarray] = 0, + v_b: Union[float, np.ndarray] = 0, + w_b: Union[float, np.ndarray] = 0, + phi: Union[float, np.ndarray] = 0, + theta: Union[float, np.ndarray] = 0, + psi: Union[float, np.ndarray] = 0, + p: Union[float, np.ndarray] = 0, + q: Union[float, np.ndarray] = 0, + r: Union[float, np.ndarray] = 0, + ): # Initialize state variables self.mass_props = MassProperties() if mass_props is None else mass_props self.x_e = x_e @@ -83,18 +86,18 @@ def __init__(self, @property def state(self): return { - "x_e" : self.x_e, - "y_e" : self.y_e, - "z_e" : self.z_e, - "u_b" : self.u_b, - "v_b" : self.v_b, - "w_b" : self.w_b, - "phi" : self.phi, + "x_e": self.x_e, + "y_e": self.y_e, + "z_e": self.z_e, + "u_b": self.u_b, + "v_b": self.v_b, + "w_b": self.w_b, + "phi": self.phi, "theta": self.theta, - "psi" : self.psi, - "p" : self.p, - "q" : self.q, - "r" : self.r, + "psi": self.psi, + "p": self.p, + "q": self.q, + "r": self.r, } @property @@ -196,113 +199,92 @@ def sincos(x): ### Position derivatives d_xe = ( - (cthe * cpsi) * u + - (sphi * sthe * cpsi - cphi * spsi) * v + - (cphi * sthe * cpsi + sphi * spsi) * w + (cthe * cpsi) * u + + (sphi * sthe * cpsi - cphi * spsi) * v + + (cphi * sthe * cpsi + sphi * spsi) * w ) d_ye = ( - (cthe * spsi) * u + - (sphi * sthe * spsi + cphi * cpsi) * v + - (cphi * sthe * spsi - sphi * cpsi) * w - ) - d_ze = ( - (-sthe) * u + - (sphi * cthe) * v + - (cphi * cthe) * w + (cthe * spsi) * u + + (sphi * sthe * spsi + cphi * cpsi) * v + + (cphi * sthe * spsi - sphi * cpsi) * w ) + d_ze = (-sthe) * u + (sphi * cthe) * v + (cphi * cthe) * w ### Velocity derivatives - d_u = ( - (X / mass) - - q * w + - r * v - ) - d_v = ( - (Y / mass) - - r * u + - p * w - ) - d_w = ( - (Z / mass) - - p * v + - q * u - ) + d_u = (X / mass) - q * w + r * v + d_v = (Y / mass) - r * u + p * w + d_w = (Z / mass) - p * v + q * u ### Angle derivatives if np.all(cthe == 0): d_phi = 0 else: - d_phi = ( - p + - q * sphi * sthe / cthe + - r * cphi * sthe / cthe - ) + d_phi = p + q * sphi * sthe / cthe + r * cphi * sthe / cthe - d_theta = ( - q * cphi - - r * sphi - ) + d_theta = q * cphi - r * sphi if np.all(cthe == 0): d_psi = 0 else: - d_psi = ( - q * sphi / cthe + - r * cphi / cthe - ) + d_psi = q * sphi / cthe + r * cphi / cthe ### Angular velocity derivatives RHS_L = ( - L - - (Izz - Iyy) * q * r - - Iyz * (q ** 2 - r ** 2) - - Ixz * p * q + - Ixy * p * r - - hz * q + - hy * r + L + - (Izz - Iyy) * q * r + - Iyz * (q**2 - r**2) + - Ixz * p * q + + Ixy * p * r + - hz * q + + hy * r ) RHS_M = ( - M - - (Ixx - Izz) * r * p - - Ixz * (r ** 2 - p ** 2) - - Ixy * q * r + - Iyz * q * p - - hx * r + - hz * p + M + - (Ixx - Izz) * r * p + - Ixz * (r**2 - p**2) + - Ixy * q * r + + Iyz * q * p + - hx * r + + hz * p ) RHS_N = ( - N - - (Iyy - Ixx) * p * q - - Ixy * (p ** 2 - q ** 2) - - Iyz * r * p + - Ixz * r * q - - hy * p + - hx * q + N + - (Iyy - Ixx) * p * q + - Ixy * (p**2 - q**2) + - Iyz * r * p + + Ixz * r * q + - hy * p + + hx * q + ) + i11, i22, i33, i12, i23, i13 = np.linalg.inv_symmetric_3x3( + Ixx, Iyy, Izz, Ixy, Iyz, Ixz ) - i11, i22, i33, i12, i23, i13 = np.linalg.inv_symmetric_3x3(Ixx, Iyy, Izz, Ixy, Iyz, Ixz) d_p = i11 * RHS_L + i12 * RHS_M + i13 * RHS_N d_q = i12 * RHS_L + i22 * RHS_M + i23 * RHS_N d_r = i13 * RHS_L + i23 * RHS_M + i33 * RHS_N return { - "x_e" : d_xe, - "y_e" : d_ye, - "z_e" : d_ze, - "u_b" : d_u, - "v_b" : d_v, - "w_b" : d_w, - "phi" : d_phi, + "x_e": d_xe, + "y_e": d_ye, + "z_e": d_ze, + "u_b": d_u, + "v_b": d_v, + "w_b": d_w, + "phi": d_phi, "theta": d_theta, - "psi" : d_psi, - "p" : d_p, - "q" : d_q, - "r" : d_r, + "psi": d_psi, + "p": d_p, + "q": d_q, + "r": d_r, } - def convert_axes(self, - x_from, y_from, z_from, - from_axes: str, - to_axes: str, - ): + def convert_axes( + self, + x_from, + y_from, + z_from, + from_axes: str, + to_axes: str, + ): """ Converts a vector [x_from, y_from, z_from], as given in the `from_axes` frame, to an equivalent vector [x_to, y_to, z_to], as given in the `to_axes` frame. @@ -364,80 +346,64 @@ def sincos(x): spsi, cpsi = sincos(self.psi) if from_axes == "earth": - x_b = ( - (cthe * cpsi) * x_from + - (cthe * spsi) * y_from + - (-sthe) * z_from - ) + x_b = (cthe * cpsi) * x_from + (cthe * spsi) * y_from + (-sthe) * z_from y_b = ( - (sphi * sthe * cpsi - cphi * spsi) * x_from + - (sphi * sthe * spsi + cphi * cpsi) * y_from + - (sphi * cthe) * z_from + (sphi * sthe * cpsi - cphi * spsi) * x_from + + (sphi * sthe * spsi + cphi * cpsi) * y_from + + (sphi * cthe) * z_from ) z_b = ( - (cphi * sthe * cpsi + sphi * spsi) * x_from + - (cphi * sthe * spsi - sphi * cpsi) * y_from + - (cphi * cthe) * z_from + (cphi * sthe * cpsi + sphi * spsi) * x_from + + (cphi * sthe * spsi - sphi * cpsi) * y_from + + (cphi * cthe) * z_from ) else: x_b, y_b, z_b = self.op_point.convert_axes( - x_from, y_from, z_from, - from_axes=from_axes, to_axes="body" + x_from, y_from, z_from, from_axes=from_axes, to_axes="body" ) if to_axes == "earth": x_to = ( - (cthe * cpsi) * x_b + - (sphi * sthe * cpsi - cphi * spsi) * y_b + - (cphi * sthe * cpsi + sphi * spsi) * z_b + (cthe * cpsi) * x_b + + (sphi * sthe * cpsi - cphi * spsi) * y_b + + (cphi * sthe * cpsi + sphi * spsi) * z_b ) y_to = ( - (cthe * spsi) * x_b + - (sphi * sthe * spsi + cphi * cpsi) * y_b + - (cphi * sthe * spsi - sphi * cpsi) * z_b - ) - z_to = ( - (-sthe) * x_b + - (sphi * cthe) * y_b + - (cphi * cthe) * z_b + (cthe * spsi) * x_b + + (sphi * sthe * spsi + cphi * cpsi) * y_b + + (cphi * sthe * spsi - sphi * cpsi) * z_b ) + z_to = (-sthe) * x_b + (sphi * cthe) * y_b + (cphi * cthe) * z_b else: x_to, y_to, z_to = self.op_point.convert_axes( - x_b, y_b, z_b, - from_axes="body", to_axes=to_axes + x_b, y_b, z_b, from_axes="body", to_axes=to_axes ) return x_to, y_to, z_to - def add_force(self, - Fx: Union[float, np.ndarray] = 0, - Fy: Union[float, np.ndarray] = 0, - Fz: Union[float, np.ndarray] = 0, - axes="body", - ): + def add_force( + self, + Fx: Union[float, np.ndarray] = 0, + Fy: Union[float, np.ndarray] = 0, + Fz: Union[float, np.ndarray] = 0, + axes="body", + ): Fx_b, Fy_b, Fz_b = self.convert_axes( - x_from=Fx, - y_from=Fy, - z_from=Fz, - from_axes=axes, - to_axes="body" + x_from=Fx, y_from=Fy, z_from=Fz, from_axes=axes, to_axes="body" ) self.Fx_b = self.Fx_b + Fx_b self.Fy_b = self.Fy_b + Fy_b self.Fz_b = self.Fz_b + Fz_b - def add_moment(self, - Mx: Union[float, np.ndarray] = 0, - My: Union[float, np.ndarray] = 0, - Mz: Union[float, np.ndarray] = 0, - axes="body", - ): + def add_moment( + self, + Mx: Union[float, np.ndarray] = 0, + My: Union[float, np.ndarray] = 0, + Mz: Union[float, np.ndarray] = 0, + axes="body", + ): Mx_b, My_b, Mz_b = self.convert_axes( - x_from=Mx, - y_from=My, - z_from=Mz, - from_axes=axes, - to_axes="body" + x_from=Mx, y_from=My, z_from=Mz, from_axes=axes, to_axes="body" ) self.Mx_b = self.Mx_b + Mx_b self.My_b = self.My_b + My_b @@ -446,37 +412,20 @@ def add_moment(self, @property def speed(self): """The speed of the object, expressed as a scalar.""" - return ( - self.u_b ** 2 + - self.v_b ** 2 + - self.w_b ** 2 - ) ** 0.5 + return (self.u_b**2 + self.v_b**2 + self.w_b**2) ** 0.5 @property def alpha(self): """The angle of attack, in degrees.""" - return np.arctan2d( - self.w_b, - self.u_b - ) + return np.arctan2d(self.w_b, self.u_b) @property def beta(self): """The sideslip angle, in degrees.""" - return np.arctan2d( - self.v_b, - ( - self.u_b ** 2 + - self.w_b ** 2 - ) ** 0.5 - ) + return np.arctan2d(self.v_b, (self.u_b**2 + self.w_b**2) ** 0.5) -if __name__ == '__main__': +if __name__ == "__main__": import aerosandbox as asb - dyn = DynamicsRigidBody3DBodyEuler( - mass_props=asb.MassProperties( - mass=1 - ) - ) + dyn = DynamicsRigidBody3DBodyEuler(mass_props=asb.MassProperties(mass=1)) diff --git a/aerosandbox/dynamics/rigid_body/rigid_3D/test/test_convert_axes_chain.py b/aerosandbox/dynamics/rigid_body/rigid_3D/test/test_convert_axes_chain.py index a8a90b0d8..8aff61b99 100644 --- a/aerosandbox/dynamics/rigid_body/rigid_3D/test/test_convert_axes_chain.py +++ b/aerosandbox/dynamics/rigid_body/rigid_3D/test/test_convert_axes_chain.py @@ -21,20 +21,14 @@ ) -def chain_conversion( - axes: List[str] = None -): +def chain_conversion(axes: List[str] = None): if axes is None: axes = ["geometry", "body", "geometry"] x, y, z = copy.deepcopy(vector) for from_axes, to_axes in zip(axes, axes[1:]): x, y, z = dyn.convert_axes( - x_from=x, - y_from=y, - z_from=z, - from_axes=from_axes, - to_axes=to_axes + x_from=x, y_from=y, z_from=z, from_axes=from_axes, to_axes=to_axes ) return np.array(vector) == pytest.approx(np.array([x, y, z])) @@ -60,24 +54,26 @@ def test_earth(): def test_cycle(): - assert chain_conversion([ - "body", - "geometry", - "stability", - "wind", - "body", - "wind", - "stability", - "geometry", - "body", - "geometry", - "wind", - "geometry", - "stability", - "body", - ]) - - -if __name__ == '__main__': + assert chain_conversion( + [ + "body", + "geometry", + "stability", + "wind", + "body", + "wind", + "stability", + "geometry", + "body", + "geometry", + "wind", + "geometry", + "stability", + "body", + ] + ) + + +if __name__ == "__main__": pytest.main() chain_conversion() diff --git a/aerosandbox/dynamics/rigid_body/rigid_3D/test/test_convert_axes_specific_rotations.py b/aerosandbox/dynamics/rigid_body/rigid_3D/test/test_convert_axes_specific_rotations.py index 7b9a46420..85c5cf56b 100644 --- a/aerosandbox/dynamics/rigid_body/rigid_3D/test/test_convert_axes_specific_rotations.py +++ b/aerosandbox/dynamics/rigid_body/rigid_3D/test/test_convert_axes_specific_rotations.py @@ -10,11 +10,7 @@ def test_alpha_wind(): u_b=0, w_b=1, ) - x, y, z = dyn.convert_axes( - 0, 0, 1, - "geometry", - "wind" - ) + x, y, z = dyn.convert_axes(0, 0, 1, "geometry", "wind") assert x == pytest.approx(-1) assert y == pytest.approx(0) assert z == pytest.approx(0) @@ -27,11 +23,7 @@ def test_beta_wind(): # alpha=0, # beta=90 ) - x, y, z = dyn.convert_axes( - 0, 1, 0, - "geometry", - "wind" - ) + x, y, z = dyn.convert_axes(0, 1, 0, "geometry", "wind") assert x == pytest.approx(1) assert y == pytest.approx(0) assert z == pytest.approx(0) @@ -44,11 +36,7 @@ def test_beta_wind_body(): # alpha=0, # beta=90 ) - x, y, z = dyn.convert_axes( - 0, 1, 0, - "body", - "wind" - ) + x, y, z = dyn.convert_axes(0, 1, 0, "body", "wind") assert x == pytest.approx(1) assert y == pytest.approx(0) assert z == pytest.approx(0) @@ -61,11 +49,7 @@ def test_alpha_stability_body(): # alpha=90, # beta=0 ) - x, y, z = dyn.convert_axes( - 0, 0, 1, - "body", - "stability" - ) + x, y, z = dyn.convert_axes(0, 0, 1, "body", "stability") assert x == pytest.approx(1) assert y == pytest.approx(0) assert z == pytest.approx(0) @@ -74,15 +58,11 @@ def test_alpha_stability_body(): def test_beta_stability_body(): dyn = asb.DynamicsRigidBody3DBodyEuler( u_b=0, - v_b=1 + v_b=1, # alpha=0, # beta=90 ) - x, y, z = dyn.convert_axes( - 0, 1, 0, - "body", - "stability" - ) + x, y, z = dyn.convert_axes(0, 1, 0, "body", "stability") assert x == pytest.approx(0) assert y == pytest.approx(1) assert z == pytest.approx(0) @@ -96,15 +76,11 @@ def test_order_wind_body(): # alpha=90, # beta=90, ) - x, y, z = dyn.convert_axes( - 0, 1, 0, - "body", - "wind" - ) + x, y, z = dyn.convert_axes(0, 1, 0, "body", "wind") assert x == pytest.approx(1) assert y == pytest.approx(0) assert z == pytest.approx(0) -if __name__ == '__main__': +if __name__ == "__main__": pytest.main() diff --git a/aerosandbox/dynamics/test_dynamics/test_dyn_indexing.py b/aerosandbox/dynamics/test_dynamics/test_dyn_indexing.py index 378363802..c3eb1cb40 100644 --- a/aerosandbox/dynamics/test_dynamics/test_dyn_indexing.py +++ b/aerosandbox/dynamics/test_dynamics/test_dyn_indexing.py @@ -2,13 +2,12 @@ import aerosandbox.numpy as np import pytest + def test_dyn_indexing(): # Test indexing of a simple Dynamics object dyn = asb.DynamicsPointMass1DHorizontal( - mass_props=asb.MassProperties( - mass=1 - ), + mass_props=asb.MassProperties(mass=1), x_e=np.arange(10) ** 2, u_e=2 * np.arange(10), ) @@ -30,5 +29,6 @@ def test_dyn_indexing(): assert all(dslice.x_e == [4, 9, 16]) assert all(dslice.u_e == [4, 6, 8]) -if __name__ == '__main__': - test_dyn_indexing() \ No newline at end of file + +if __name__ == "__main__": + test_dyn_indexing() diff --git a/aerosandbox/dynamics/utilities/lti_systems.py b/aerosandbox/dynamics/utilities/lti_systems.py index dbfddcfe6..48b75f0b1 100644 --- a/aerosandbox/dynamics/utilities/lti_systems.py +++ b/aerosandbox/dynamics/utilities/lti_systems.py @@ -2,9 +2,9 @@ def peak_of_harmonic_oscillation( - amplitude: float = 1, - frequency_hz: float = 1, - derivative_order: int = 0, + amplitude: float = 1, + frequency_hz: float = 1, + derivative_order: int = 0, ): """ Computes the peak value of the nth derivative of a simple harmonic oscillation (e.g., sine wave). diff --git a/aerosandbox/geometry/airfoil/airfoil.py b/aerosandbox/geometry/airfoil/airfoil.py index 7189ab4c9..a64a6ffc7 100644 --- a/aerosandbox/geometry/airfoil/airfoil.py +++ b/aerosandbox/geometry/airfoil/airfoil.py @@ -3,10 +3,13 @@ from aerosandbox.geometry.airfoil.airfoil_families import ( get_NACA_coordinates, get_UIUC_coordinates, - get_file_coordinates + get_file_coordinates, ) from aerosandbox.library.aerodynamics import transonic -from aerosandbox.modeling.splines.hermite import linear_hermite_patch, cubic_hermite_patch +from aerosandbox.modeling.splines.hermite import ( + linear_hermite_patch, + cubic_hermite_patch, +) from scipy import interpolate from typing import Callable, Union, Any, Dict, List import json @@ -19,11 +22,12 @@ class Airfoil(Polygon): An airfoil. See constructor docstring for usage details. """ - def __init__(self, - name: str = "Untitled", - coordinates: Union[None, str, Path, np.ndarray] = None, - **deprecated_keyword_arguments - ): + def __init__( + self, + name: str = "Untitled", + coordinates: Union[None, str, Path, np.ndarray] = None, + **deprecated_keyword_arguments, + ): """ Creates an Airfoil object. @@ -77,6 +81,7 @@ def __init__(self, pass except UnicodeDecodeError: import warnings + warnings.warn( f"Airfoil {self.name} was found in the UIUC airfoil database, but could not be parsed.\n" f"Check for any non-Unicode-compatible characters in the file, or specify the airfoil " @@ -100,6 +105,7 @@ def __init__(self, if self.coordinates is None: import warnings + warnings.warn( f"Airfoil {self.name} had no coordinates assigned, and could not parse the `coordinates` input!", UserWarning, @@ -109,11 +115,12 @@ def __init__(self, ### Handle deprecated keyword arguments if len(deprecated_keyword_arguments) > 0: import warnings + warnings.warn( "The `generate_polars`, `CL_function`, `CD_function`, and `CM_function` keyword arguments to the " "Airfoil constructor will be deprecated in an upcoming release. Their functionality is replaced" "by `Airfoil.get_aero_from_neuralfoil()`, which is faster and has better properties for optimization.", - DeprecationWarning + DeprecationWarning, ) generate_polars = deprecated_keyword_arguments.get("generate_polars", False) @@ -129,12 +136,17 @@ def __init__(self, from aerosandbox.library.aerodynamics.viscous import Cf_flat_plate def print_default_warning(): - warnings.warn("\n".join([ - "Warning: Using a placeholder aerodynamics model for this Airfoil!", - "It's highly recommended that you either:", - "\ta) Specify polar functions in the Airfoil constructor, or", - "\tb) Call Airfoil.generate_polars() to auto-generate these polar functions with XFoil." - ]), stacklevel=3) + warnings.warn( + "\n".join( + [ + "Warning: Using a placeholder aerodynamics model for this Airfoil!", + "It's highly recommended that you either:", + "\ta) Specify polar functions in the Airfoil constructor, or", + "\tb) Call Airfoil.generate_polars() to auto-generate these polar functions with XFoil.", + ] + ), + stacklevel=3, + ) def default_CL_function(alpha, Re, mach=0, deflection=0): """ @@ -156,11 +168,9 @@ def default_CD_function(alpha, Re, mach=0, deflection=0): ### Form factor model from Raymer, "Aircraft Design". Section 12.5, Eq. 12.30 t_over_c = 0.12 - FF = 1 + 2 * t_over_c * 100 * t_over_c ** 4 + FF = 1 + 2 * t_over_c * 100 * t_over_c**4 - Cd_inc = 2 * Cf * FF * ( - 1 + (np.sind(alpha) * 180 / np.pi / 5) ** 2 - ) + Cd_inc = 2 * Cf * FF * (1 + (np.sind(alpha) * 180 / np.pi / 5) ** 2) beta = (1 - mach) ** 2 Cd = Cd_inc * beta @@ -190,13 +200,14 @@ def default_CM_function(alpha, Re, mach=0, deflection=0): def __repr__(self) -> str: return f"Airfoil {self.name} ({self.n_points()} points)" - def to_kulfan_airfoil(self, - n_weights_per_side: int = 8, - N1: float = 0.5, - N2: float = 1.0, - normalize_coordinates: bool = True, - use_leading_edge_modification: bool = True, - ) -> "KulfanAirfoil": + def to_kulfan_airfoil( + self, + n_weights_per_side: int = 8, + N1: float = 0.5, + N2: float = 1.0, + normalize_coordinates: bool = True, + use_leading_edge_modification: bool = True, + ) -> "KulfanAirfoil": from aerosandbox.geometry.airfoil.kulfan_airfoil import KulfanAirfoil from aerosandbox.geometry.airfoil.airfoil_families import get_kulfan_parameters @@ -220,16 +231,17 @@ def to_kulfan_airfoil(self, N2=N2, ) - def generate_polars(self, - alphas=np.linspace(-13, 13, 27), - Res=np.geomspace(1e3, 1e8, 12), - cache_filename: str = None, - xfoil_kwargs: Dict[str, Any] = None, - unstructured_interpolated_model_kwargs: Dict[str, Any] = None, - include_compressibility_effects: bool = True, - transonic_buffet_lift_knockdown: float = 0.3, - make_symmetric_polars: bool = False, - ) -> None: + def generate_polars( + self, + alphas=np.linspace(-13, 13, 27), + Res=np.geomspace(1e3, 1e8, 12), + cache_filename: str = None, + xfoil_kwargs: Dict[str, Any] = None, + unstructured_interpolated_model_kwargs: Dict[str, Any] = None, + include_compressibility_effects: bool = True, + transonic_buffet_lift_knockdown: float = 0.3, + make_symmetric_polars: bool = False, + ) -> None: """ Generates airfoil polar surrogate models (CL, CD, CM functions) from XFoil data and assigns them in-place to this Airfoil's polar functions. @@ -276,7 +288,9 @@ def generate_polars(self, """ if self.coordinates is None: - raise ValueError("Cannot generate polars for an airfoil that you don't have the coordinates of!") + raise ValueError( + "Cannot generate polars for an airfoil that you don't have the coordinates of!" + ) ### Set defaults if xfoil_kwargs is None: @@ -285,22 +299,22 @@ def generate_polars(self, unstructured_interpolated_model_kwargs = {} xfoil_kwargs = { # See asb.XFoil for the documentation on these. - "verbose" : False, - "max_iter" : 20, + "verbose": False, + "max_iter": 20, "xfoil_repanel": True, - **xfoil_kwargs + **xfoil_kwargs, } unstructured_interpolated_model_kwargs = { # These were tuned heuristically as defaults! "resampling_interpolator_kwargs": { - "degree" : 0, + "degree": 0, # "kernel": "linear", - "kernel" : "multiquadric", - "epsilon" : 3, + "kernel": "multiquadric", + "epsilon": 3, "smoothing": 0.01, # "kernel": "cubic" }, - **unstructured_interpolated_model_kwargs + **unstructured_interpolated_model_kwargs, } ### Retrieve XFoil Polar Data from the cache, if it exists. @@ -308,10 +322,7 @@ def generate_polars(self, 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() - } + data = {k: np.array(v) for k, v in json.load(f).items()} except FileNotFoundError: pass @@ -323,12 +334,10 @@ def generate_polars(self, 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) + 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. @@ -342,27 +351,31 @@ def get_run_data(Re): # Get the data for an XFoil alpha sweep at one specific R ) ] data = { # Merge the dicts into one big database of all runs. - k: np.concatenate( - tuple([run_data[k] for run_data in run_datas]) - ) + k: np.concatenate(tuple([run_data[k] for run_data in run_datas])) for k in run_datas[0].keys() } - if make_symmetric_polars: # If the airfoil is known to be symmetric, duplicate all data across alpha. - keys_symmetric_across_alpha = ['CD', 'CDp', 'Re'] # Assumes the rest are antisymmetric + if ( + make_symmetric_polars + ): # If the airfoil is known to be symmetric, duplicate all data across alpha. + keys_symmetric_across_alpha = [ + "CD", + "CDp", + "Re", + ] # Assumes the rest are antisymmetric data = { - k: np.concatenate([v, v if k in keys_symmetric_across_alpha else -v]) + k: np.concatenate( + [v, v if k in keys_symmetric_across_alpha else -v] + ) for k, v in data.items() } - if cache_filename is not None: # Cache the accumulated data for later use, if it doesn't already exist. + 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 - ) + 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 @@ -370,47 +383,46 @@ def get_run_data(Re): # Get the data for an XFoil alpha sweep at one specific R ### Make the interpolators for attached aerodynamics from aerosandbox.modeling import UnstructuredInterpolatedModel - attached_alphas_to_use = ( - alphas[::2] if len(alphas) > 20 else alphas - ) + attached_alphas_to_use = alphas[::2] if len(alphas) > 20 else alphas - alpha_resample = np.concatenate([ - np.linspace(-180, attached_alphas_to_use.min(), 10)[:-1], - attached_alphas_to_use, - np.linspace(attached_alphas_to_use.max(), 180, 10)[1:], - ]) # 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([ - Res.min() / 10 ** np.arange(1, 5)[::-1], - Res, - Res.max() * 10 ** np.arange(1, 5), - ]) # This is the list of points that we're going to resample from the XFoil runs for our InterpolatedModel, using an RBF. + alpha_resample = np.concatenate( + [ + np.linspace(-180, attached_alphas_to_use.min(), 10)[:-1], + attached_alphas_to_use, + np.linspace(attached_alphas_to_use.max(), 180, 10)[1:], + ] + ) # 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( + [ + Res.min() / 10 ** np.arange(1, 5)[::-1], + Res, + Res.max() * 10 ** np.arange(1, 5), + ] + ) # 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) - } + 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 + **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 + **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 + **unstructured_interpolated_model_kwargs, ) ### Determine if separated @@ -425,72 +437,73 @@ def separation_parameter(alpha, Re=0): ~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 + 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 + 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 + 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 + 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) + x_data=alpha_resample, y_data=np.log10(CD_if_separated) ) CM_separated_interpolator = UnstructuredInterpolatedModel( - x_data=alpha_resample, - y_data=CM_if_separated + x_data=alpha_resample, y_data=CM_if_separated ) def CL_function(alpha, Re, mach=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) # Lift coefficient if separated + CL_attached = CL_attached_interpolator( + { + "alpha": alpha, + "ln_Re": np.log(Re), + } + ) + CL_separated = CL_separated_interpolator( + alpha + ) # Lift coefficient if separated CL_mach_0 = np.blend( # Lift coefficient at mach = 0 - separation_parameter(alpha, Re), - CL_separated, - CL_attached + separation_parameter(alpha, Re), CL_separated, CL_attached ) if include_compressibility_effects: - prandtl_glauert_beta_squared_ideal = 1 - mach ** 2 + prandtl_glauert_beta_squared_ideal = 1 - mach**2 - prandtl_glauert_beta = np.softmax( - prandtl_glauert_beta_squared_ideal, - -prandtl_glauert_beta_squared_ideal, - hardness=2.0 # Empirically tuned to data - ) ** 0.5 + prandtl_glauert_beta = ( + np.softmax( + prandtl_glauert_beta_squared_ideal, + -prandtl_glauert_beta_squared_ideal, + hardness=2.0, # Empirically tuned to data + ) + ** 0.5 + ) CL = CL_mach_0 / prandtl_glauert_beta mach_crit = transonic.mach_crit_Korn( - CL=CL, - t_over_c=self.max_thickness(), - sweep=0, - kappa_A=0.95 + CL=CL, t_over_c=self.max_thickness(), sweep=0, kappa_A=0.95 ) ### Accounts approximately for the lift drop due to buffet. buffet_factor = np.blend( - 40 * (mach - mach_crit - (0.1 / 80) ** (1 / 3) - 0.06) * (mach - 1.1), + 40 + * (mach - mach_crit - (0.1 / 80) ** (1 / 3) - 0.06) + * (mach - 1.1), 1, - transonic_buffet_lift_knockdown + transonic_buffet_lift_knockdown, ) ### Accounts for the fact that theoretical CL_alpha goes from 2 * pi (subsonic) to 4 (supersonic), @@ -509,10 +522,12 @@ def CL_function(alpha, Re, mach=0): def CD_function(alpha, Re, mach=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_attached = log10_CD_attached_interpolator( + { + "alpha": alpha, + "ln_Re": np.log(Re), + } + ) log10_CD_separated = log10_CD_separated_interpolator(alpha) log10_CD_mach_0 = np.blend( @@ -523,34 +538,34 @@ def CD_function(alpha, Re, mach=0): if include_compressibility_effects: - CL_attached = CL_attached_interpolator({ - "alpha": alpha, - "ln_Re": np.log(Re), - }) + CL_attached = CL_attached_interpolator( + { + "alpha": alpha, + "ln_Re": np.log(Re), + } + ) CL_separated = CL_separated_interpolator(alpha) CL_mach_0 = np.blend( - separation_parameter(alpha, Re), - CL_separated, - CL_attached + separation_parameter(alpha, Re), CL_separated, CL_attached ) - prandtl_glauert_beta_squared_ideal = 1 - mach ** 2 + prandtl_glauert_beta_squared_ideal = 1 - mach**2 - prandtl_glauert_beta = np.softmax( - prandtl_glauert_beta_squared_ideal, - -prandtl_glauert_beta_squared_ideal, - hardness=2.0 # Empirically tuned to data - ) ** 0.5 + prandtl_glauert_beta = ( + np.softmax( + prandtl_glauert_beta_squared_ideal, + -prandtl_glauert_beta_squared_ideal, + hardness=2.0, # Empirically tuned to data + ) + ** 0.5 + ) CL = CL_mach_0 / prandtl_glauert_beta t_over_c = self.max_thickness() mach_crit = transonic.mach_crit_Korn( - CL=CL, - t_over_c=t_over_c, - sweep=0, - kappa_A=0.92 + CL=CL, t_over_c=t_over_c, sweep=0, kappa_A=0.92 ) mach_dd = mach_crit + (0.1 / 80) ** (1 / 3) CD_wave = np.where( @@ -568,7 +583,7 @@ def CD_function(alpha, Re, mach=0): f_a=20 * (0.1 / 80) ** (4 / 3), f_b=0.8 * t_over_c, dfdx_a=0.1, - dfdx_b=0.8 * t_over_c * 8 + dfdx_b=0.8 * t_over_c * 8, ), np.where( mach < 1.1, @@ -585,10 +600,10 @@ def CD_function(alpha, Re, mach=0): 8 * 2 * (mach - 1.1) / (1.2 - 0.8), 0.8 * 0.8 * t_over_c, 1.2 * 0.8 * t_over_c, - ) - ) - ) - ) + ), + ), + ), + ), ) # CD_wave = transonic.approximate_CD_wave( @@ -597,34 +612,36 @@ def CD_function(alpha, Re, mach=0): # CD_wave_at_fully_supersonic=0.90 * self.max_thickness() # ) - return 10 ** log10_CD_mach_0 + CD_wave - + return 10**log10_CD_mach_0 + CD_wave else: - return 10 ** log10_CD_mach_0 + return 10**log10_CD_mach_0 def CM_function(alpha, Re, mach=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_attached = CM_attached_interpolator( + { + "alpha": alpha, + "ln_Re": np.log(Re), + } + ) CM_separated = CM_separated_interpolator(alpha) CM_mach_0 = np.blend( - separation_parameter(alpha, Re), - CM_separated, - CM_attached + separation_parameter(alpha, Re), CM_separated, CM_attached ) if include_compressibility_effects: - prandtl_glauert_beta_squared_ideal = 1 - mach ** 2 + prandtl_glauert_beta_squared_ideal = 1 - mach**2 - prandtl_glauert_beta = np.softmax( - prandtl_glauert_beta_squared_ideal, - -prandtl_glauert_beta_squared_ideal, - hardness=2.0 # Empirically tuned to data - ) ** 0.5 + prandtl_glauert_beta = ( + np.softmax( + prandtl_glauert_beta_squared_ideal, + -prandtl_glauert_beta_squared_ideal, + hardness=2.0, # Empirically tuned to data + ) + ** 0.5 + ) CM = CM_mach_0 / prandtl_glauert_beta @@ -636,31 +653,35 @@ def CM_function(alpha, Re, mach=0): self.CD_function = CD_function self.CM_function = CM_function - def get_aero_from_neuralfoil(self, - alpha: Union[float, np.ndarray], - Re: Union[float, np.ndarray], - mach: Union[float, np.ndarray] = 0., - n_crit: Union[float, np.ndarray] = 9.0, - xtr_upper: Union[float, np.ndarray] = 1.0, - xtr_lower: Union[float, np.ndarray] = 1.0, - model_size: str = "large", - control_surfaces: List["ControlSurface"] = None, - include_360_deg_effects: bool = True, - ) -> Dict[str, Union[float, np.ndarray]]: + def get_aero_from_neuralfoil( + self, + alpha: Union[float, np.ndarray], + Re: Union[float, np.ndarray], + mach: Union[float, np.ndarray] = 0.0, + n_crit: Union[float, np.ndarray] = 9.0, + xtr_upper: Union[float, np.ndarray] = 1.0, + xtr_lower: Union[float, np.ndarray] = 1.0, + model_size: str = "large", + control_surfaces: List["ControlSurface"] = None, + include_360_deg_effects: bool = True, + ) -> Dict[str, Union[float, np.ndarray]]: ### Normalize the inputs and evaluate normalization_outputs = self.normalize(return_dict=True) normalized_airfoil = normalization_outputs["airfoil"].to_kulfan_airfoil( - n_weights_per_side=8, - normalize_coordinates=False # No need to redo this + n_weights_per_side=8, normalize_coordinates=False # No need to redo this ) delta_alpha = normalization_outputs["rotation_angle"] # degrees x_translation_LE = normalization_outputs["x_translation"] y_translation_LE = normalization_outputs["y_translation"] scale = normalization_outputs["scale_factor"] - x_translation_qc = -x_translation_LE + 0.25 * (1/scale * np.cosd(delta_alpha)) - 0.25 - y_translation_qc = -y_translation_LE + 0.25 * (1/scale * np.sind(-delta_alpha)) + x_translation_qc = ( + -x_translation_LE + 0.25 * (1 / scale * np.cosd(delta_alpha)) - 0.25 + ) + y_translation_qc = -y_translation_LE + 0.25 * ( + 1 / scale * np.sind(-delta_alpha) + ) raw_aero = normalized_airfoil.get_aero_from_neuralfoil( alpha=alpha + delta_alpha, @@ -671,22 +692,25 @@ def get_aero_from_neuralfoil(self, xtr_lower=xtr_lower, model_size=model_size, control_surfaces=control_surfaces, - include_360_deg_effects=include_360_deg_effects + include_360_deg_effects=include_360_deg_effects, ) ### Correct the force vectors and lift-induced moment from translation - extra_CM = -raw_aero["CL"] * x_translation_qc + raw_aero["CD"] * y_translation_qc + extra_CM = ( + -raw_aero["CL"] * x_translation_qc + raw_aero["CD"] * y_translation_qc + ) raw_aero["CM"] = raw_aero["CM"] + extra_CM return raw_aero - def plot_polars(self, - alphas: Union[np.ndarray, List[float]] = np.linspace(-20, 20, 500), - Res: Union[np.ndarray, List[float]] = 10 ** np.arange(3, 9), - mach: float = 0., - show: bool = True, - Re_colors=None, - ) -> None: + def plot_polars( + self, + alphas: Union[np.ndarray, List[float]] = np.linspace(-20, 20, 500), + Res: Union[np.ndarray, List[float]] = 10 ** np.arange(3, 9), + mach: float = 0.0, + show: bool = True, + Re_colors=None, + ) -> None: import matplotlib.pyplot as plt import aerosandbox.tools.pretty_plots as p @@ -714,49 +738,27 @@ def plot_polars(self, p.set_ticks(5, 1, 20, 5) if Re_colors is None: - Re_colors = p.mpl.colormaps.get_cmap('rainbow')(np.linspace(0, 1, len(Res))) - Re_colors = [ - p.adjust_lightness(color, 0.7) - for color in Re_colors - ] + Re_colors = p.mpl.colormaps.get_cmap("rainbow")(np.linspace(0, 1, len(Res))) + Re_colors = [p.adjust_lightness(color, 0.7) for color in Re_colors] for i, Re in enumerate(Res): - kwargs = dict( - alpha=alphas, - Re=Re, - mach=mach - ) + kwargs = dict(alpha=alphas, Re=Re, mach=mach) plt.sca(ax[0, 0]) - plt.plot( - alphas, - self.CL_function(**kwargs), - color=Re_colors[i], - alpha=0.7 - ) + plt.plot(alphas, self.CL_function(**kwargs), color=Re_colors[i], alpha=0.7) plt.sca(ax[0, 1]) - plt.plot( - alphas, - self.CD_function(**kwargs), - color=Re_colors[i], - alpha=0.7 - ) + plt.plot(alphas, self.CD_function(**kwargs), color=Re_colors[i], alpha=0.7) plt.sca(ax[1, 0]) - plt.plot( - alphas, - self.CM_function(**kwargs), - color=Re_colors[i], - alpha=0.7 - ) + plt.plot(alphas, self.CM_function(**kwargs), color=Re_colors[i], alpha=0.7) plt.sca(ax[1, 1]) plt.plot( alphas, self.CL_function(**kwargs) / self.CD_function(**kwargs), color=Re_colors[i], - alpha=0.7 + alpha=0.7, ) from aerosandbox.tools.string_formatting import eng_string @@ -769,7 +771,7 @@ def plot_polars(self, # Note: `ncol` is old syntax; preserves backwards-compatibility with matplotlib 3.5.x. # New matplotlib versions use `ncols` instead. fontsize=8, - loc='lower right' + loc="lower right", ) if show: @@ -778,9 +780,9 @@ def plot_polars(self, legend=False, ) - def local_camber(self, - x_over_c: Union[float, np.ndarray] = np.linspace(0, 1, 101) - ) -> Union[float, np.ndarray]: + def local_camber( + self, x_over_c: Union[float, np.ndarray] = np.linspace(0, 1, 101) + ) -> Union[float, np.ndarray]: """ Returns the local camber of the airfoil at a given point or points. @@ -806,9 +808,9 @@ def local_camber(self, return (upper_interpolated + lower_interpolated) / 2 - def local_thickness(self, - x_over_c: Union[float, np.ndarray] = np.linspace(0, 1, 101) - ) -> Union[float, np.ndarray]: + def local_thickness( + self, x_over_c: Union[float, np.ndarray] = np.linspace(0, 1, 101) + ) -> Union[float, np.ndarray]: """ Returns the local thickness of the airfoil at a given point or points. @@ -834,9 +836,7 @@ def local_thickness(self, return upper_interpolated - lower_interpolated - def max_camber(self, - x_over_c_sample: np.ndarray = np.linspace(0, 1, 101) - ) -> float: + def max_camber(self, x_over_c_sample: np.ndarray = np.linspace(0, 1, 101)) -> float: """ Returns the maximum camber of the airfoil. @@ -848,9 +848,9 @@ def max_camber(self, """ return np.max(self.local_camber(x_over_c=x_over_c_sample)) - def max_thickness(self, - x_over_c_sample: np.ndarray = np.linspace(0, 1, 101) - ) -> float: + def max_thickness( + self, x_over_c_sample: np.ndarray = np.linspace(0, 1, 101) + ) -> float: """ Returns the maximum thickness of the airfoil. @@ -862,12 +862,9 @@ def max_thickness(self, """ return np.max(self.local_thickness(x_over_c=x_over_c_sample)) - def draw(self, - draw_mcl=False, - draw_markers=True, - backend="matplotlib", - show=True - ) -> None: + def draw( + self, draw_mcl=False, draw_markers=True, backend="matplotlib", show=True + ) -> None: """ Draw the airfoil object. @@ -890,11 +887,8 @@ def draw(self, import matplotlib.pyplot as plt import aerosandbox.tools.pretty_plots as p - color = '#280887' - plt.plot( - x, y, - ".-" if draw_markers else "-", - zorder=11, color=color) + color = "#280887" + plt.plot(x, y, ".-" if draw_markers else "-", 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) @@ -904,11 +898,11 @@ def draw(self, title=f"{self.name} Airfoil", xlabel=r"$x/c$", ylabel=r"$y/c$", - ) elif backend == "plotly": from aerosandbox.visualization.plotly import go + fig = go.Figure() fig.add_trace( go.Scatter( @@ -917,9 +911,7 @@ def draw(self, mode="lines+markers" if draw_markers else "lines", name="Airfoil", fill="toself", - line=dict( - color="blue" - ) + line=dict(color="blue"), ), ) if draw_mcl: @@ -929,16 +921,14 @@ def draw(self, y=y_mcl, mode="lines", name="Mean Camber Line (MCL)", - line=dict( - color="navy" - ) + 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" + title=f"{self.name} Airfoil", ) if show: fig.show() @@ -960,7 +950,7 @@ def lower_coordinates(self) -> np.ndarray: Includes the leading edge point; be careful about duplicates if using this method in conjunction with Airfoil.upper_coordinates(). """ - return self.coordinates[self.LE_index():, :] + return self.coordinates[self.LE_index() :, :] def upper_coordinates(self) -> np.ndarray: """ @@ -971,13 +961,13 @@ def upper_coordinates(self) -> np.ndarray: Includes the leading edge point; be careful about duplicates if using this method in conjunction with Airfoil.lower_coordinates(). """ - return self.coordinates[:self.LE_index() + 1, :] + return self.coordinates[: self.LE_index() + 1, :] def LE_radius(self, softness: float = 1e-6): LE_index = self.LE_index() # The three points closest to the leading edge - LE_points = self.coordinates[LE_index - 1:LE_index + 2, :] + LE_points = self.coordinates[LE_index - 1 : LE_index + 2, :] # Make these 3 points into a triangle; these are the vectors representing edges edge_vectors = LE_points - np.roll(LE_points, 1, axis=0) @@ -990,9 +980,7 @@ def LE_radius(self, softness: float = 1e-6): s = (a + b + c) / 2 - diameter = (a * b * c) / ( - 2 * np.sqrt(s * (s - a) * (s - b) * (s - c)) - ) + diameter = (a * b * c) / (2 * np.sqrt(s * (s - a) * (s - b) * (s - c))) return diameter / 2 @@ -1003,10 +991,7 @@ def TE_thickness(self) -> float: x_gap = self.coordinates[0, 0] - self.coordinates[-1, 0] y_gap = self.coordinates[0, 1] - self.coordinates[-1, 1] - return ( - x_gap ** 2 + - y_gap ** 2 - ) ** 0.5 + return (x_gap**2 + y_gap**2) ** 0.5 def TE_angle(self) -> float: """ @@ -1017,7 +1002,7 @@ def TE_angle(self) -> float: return np.arctan2d( upper_TE_vec[0] * lower_TE_vec[1] - upper_TE_vec[1] * lower_TE_vec[0], - upper_TE_vec[0] * lower_TE_vec[0] + upper_TE_vec[1] * upper_TE_vec[1] + upper_TE_vec[0] * lower_TE_vec[0] + upper_TE_vec[1] * upper_TE_vec[1], ) # def LE_radius(self) -> float: @@ -1025,10 +1010,11 @@ def TE_angle(self) -> float: # Gives the approximate leading edge radius of the airfoil, in chord-normalized units. # """ # TODO finish me - def repanel(self, - n_points_per_side: int = 100, - spacing_function_per_side=np.cosspace, - ) -> 'Airfoil': + def repanel( + self, + n_points_per_side: int = 100, + spacing_function_per_side=np.cosspace, + ) -> "Airfoil": """ Returns a repaneled copy of the airfoil with cosine-spaced coordinates on the upper and lower surfaces. @@ -1046,14 +1032,26 @@ def repanel(self, Returns: A copy of the airfoil with the new coordinates. """ - old_upper_coordinates = self.upper_coordinates() # Note: includes leading edge point, be careful about duplicates - old_lower_coordinates = self.lower_coordinates() # Note: includes leading edge point, be careful about duplicates + old_upper_coordinates = ( + self.upper_coordinates() + ) # Note: includes leading edge point, be careful about duplicates + old_lower_coordinates = ( + self.lower_coordinates() + ) # Note: includes leading edge point, be careful about duplicates # Find the streamwise distances between coordinates, assuming linear interpolation - upper_distances_between_points = np.linalg.norm(np.diff(old_upper_coordinates, axis=0), axis=1) - lower_distances_between_points = np.linalg.norm(np.diff(old_lower_coordinates, axis=0), axis=1) - upper_distances_from_TE = np.concatenate(([0], np.cumsum(upper_distances_between_points))) - lower_distances_from_LE = np.concatenate(([0], np.cumsum(lower_distances_between_points))) + upper_distances_between_points = np.linalg.norm( + np.diff(old_upper_coordinates, axis=0), axis=1 + ) + lower_distances_between_points = np.linalg.norm( + np.diff(old_lower_coordinates, axis=0), axis=1 + ) + upper_distances_from_TE = np.concatenate( + ([0], np.cumsum(upper_distances_between_points)) + ) + lower_distances_from_LE = np.concatenate( + ([0], np.cumsum(lower_distances_between_points)) + ) try: new_upper_coordinates = interpolate.CubicSpline( @@ -1063,8 +1061,12 @@ def repanel(self, bc_type=( (2, (0, 0)), (1, (0, -1)), + ), + )( + spacing_function_per_side( + 0, upper_distances_from_TE[-1], n_points_per_side ) - )(spacing_function_per_side(0, upper_distances_from_TE[-1], n_points_per_side)) + ) new_lower_coordinates = interpolate.CubicSpline( x=lower_distances_from_LE, @@ -1073,13 +1075,17 @@ def repanel(self, bc_type=( (1, (0, -1)), (2, (0, 0)), + ), + )( + spacing_function_per_side( + 0, lower_distances_from_LE[-1], n_points_per_side ) - )(spacing_function_per_side(0, lower_distances_from_LE[-1], n_points_per_side)) + ) except ValueError as e: if not ( - (np.all(np.diff(upper_distances_from_TE)) > 0) and - (np.all(np.diff(lower_distances_from_LE)) > 0) + (np.all(np.diff(upper_distances_from_TE)) > 0) + and (np.all(np.diff(lower_distances_from_LE)) > 0) ): raise ValueError( "It looks like your Airfoil has a duplicate point. Try removing the duplicate point and " @@ -1090,13 +1096,15 @@ def repanel(self, return Airfoil( name=self.name, - coordinates=np.concatenate((new_upper_coordinates, new_lower_coordinates[1:, :]), axis=0), + coordinates=np.concatenate( + (new_upper_coordinates, new_lower_coordinates[1:, :]), axis=0 + ), ) def normalize( - self, - return_dict: bool = False, - ) -> Union['Airfoil', Dict[str, Union['Airfoil', float]]]: + self, + return_dict: bool = False, + ) -> Union["Airfoil", Dict[str, Union["Airfoil", float]]]: """ Returns a copy of the Airfoil with a new set of `coordinates`, such that: - The leading edge (LE) is at (0, 0) @@ -1116,7 +1124,7 @@ def normalize( return_dict: Determines the output type of the function. - If `False` (default), returns a copy of the Airfoil with the new coordinates. - If `True`, returns a dictionary with keys: - + - "airfoil": a copy of the Airfoil with the new coordinates - "x_translation": the amount by which the airfoil's LE was translated in the x-direction @@ -1148,10 +1156,7 @@ def normalize( x_te = (self.x()[0] + self.x()[-1]) / 2 y_te = (self.y()[0] + self.y()[-1]) / 2 - distance_to_te = ( - (self.x() - x_te) ** 2 + - (self.y() - y_te) ** 2 - ) ** 0.5 + distance_to_te = ((self.x() - x_te) ** 2 + (self.y() - y_te) ** 2) ** 0.5 le_index = np.argmax(distance_to_te) @@ -1186,20 +1191,20 @@ def normalize( return newfoil else: return { - "airfoil" : newfoil, - "x_translation" : x_translation, - "y_translation" : y_translation, - "scale_factor" : scale_factor, + "airfoil": newfoil, + "x_translation": x_translation, + "y_translation": y_translation, + "scale_factor": scale_factor, "rotation_angle": np.degrees(rotation_angle), } def add_control_surface( - self, - deflection: float = 0., - hinge_point_x: float = 0.75, - modify_coordinates: bool = True, - modify_polars: bool = True, - ) -> 'Airfoil': + self, + deflection: float = 0.0, + hinge_point_x: float = 0.75, + modify_coordinates: bool = True, + modify_polars: bool = True, + ) -> "Airfoil": """ Returns a version of the airfoil with a trailing-edge control surface added at a given point. Implicitly repanels the airfoil as part of this operation. @@ -1215,22 +1220,19 @@ def add_control_surface( # Find the hinge point hinge_point_y = np.where( deflection > 0, - self.local_camber(hinge_point_x) - self.local_thickness(hinge_point_x) / 2, - self.local_camber(hinge_point_x) + self.local_thickness(hinge_point_x) / 2, + self.local_camber(hinge_point_x) + - self.local_thickness(hinge_point_x) / 2, + self.local_camber(hinge_point_x) + + self.local_thickness(hinge_point_x) / 2, ) # hinge_point_y = self.local_camber(hinge_point_x) - hinge_point = np.reshape( - np.array([hinge_point_x, hinge_point_y]), - (1, 2) - ) + hinge_point = np.reshape(np.array([hinge_point_x, hinge_point_y]), (1, 2)) def is_behind_hinge(xy: np.ndarray) -> np.ndarray: - return ( - (xy[:, 0] - hinge_point_x) * np.cosd(deflection / 2) - - (xy[:, 1] - hinge_point_y) * np.sind(deflection / 2) - > 0 - ) + return (xy[:, 0] - hinge_point_x) * np.cosd(deflection / 2) - ( + xy[:, 1] - hinge_point_y + ) * np.sind(deflection / 2) > 0 orig_u = self.upper_coordinates() orig_l = self.lower_coordinates()[1:, :] @@ -1248,40 +1250,27 @@ def T(xy): rot_u = T(rotation_matrix @ T(orig_u - hinge_point_u)) + hinge_point_u rot_l = T(rotation_matrix @ T(orig_l - hinge_point_l)) + hinge_point_l - coordinates_x = np.concatenate([ - np.where( - is_behind_hinge(rot_u), - rot_u[:, 0], - orig_u[:, 0] - ), - np.where( - is_behind_hinge(rot_l), - rot_l[:, 0], - orig_l[:, 0] - ) - ]) - coordinates_y = np.concatenate([ - np.where( - is_behind_hinge(rot_u), - rot_u[:, 1], - orig_u[:, 1] - ), - np.where( - is_behind_hinge(rot_l), - rot_l[:, 1], - orig_l[:, 1] - ) - ]) + coordinates_x = np.concatenate( + [ + np.where(is_behind_hinge(rot_u), rot_u[:, 0], orig_u[:, 0]), + np.where(is_behind_hinge(rot_l), rot_l[:, 0], orig_l[:, 0]), + ] + ) + coordinates_y = np.concatenate( + [ + np.where(is_behind_hinge(rot_u), rot_u[:, 1], orig_u[:, 1]), + np.where(is_behind_hinge(rot_l), rot_l[:, 1], orig_l[:, 1]), + ] + ) - coordinates = np.stack([ - coordinates_x, - coordinates_y - ], axis=1) + coordinates = np.stack([coordinates_x, coordinates_y], axis=1) else: coordinates = self.coordinates if modify_polars: - effectiveness = 1 - np.maximum(0, hinge_point_x + 1e-16) ** 2.751428551177291 + effectiveness = ( + 1 - np.maximum(0, hinge_point_x + 1e-16) ** 2.751428551177291 + ) dalpha = deflection * effectiveness def CL_function(alpha: float, Re: float, mach: float) -> float: @@ -1318,9 +1307,10 @@ def CM_function(alpha: float, Re: float, mach: float) -> float: CM_function=CM_function, ) - def set_TE_thickness(self, - thickness: float = 0., - ) -> 'Airfoil': + def set_TE_thickness( + self, + thickness: float = 0.0, + ) -> "Airfoil": """ Creates a modified copy of the Airfoil that has a specified trailing-edge thickness. @@ -1336,10 +1326,7 @@ def set_TE_thickness(self, x_gap = self.coordinates[0, 0] - self.coordinates[-1, 0] y_gap = self.coordinates[0, 1] - self.coordinates[-1, 1] - s_gap = ( - x_gap ** 2 + - y_gap ** 2 - ) ** 0.5 + s_gap = (x_gap**2 + y_gap**2) ** 0.5 s_adjustment = (thickness - self.TE_thickness()) / 2 @@ -1368,16 +1355,16 @@ def set_TE_thickness(self, new_u = np.stack( arrays=[ ux + x_adjustment * (ux - le_x) / (te_x - le_x), - uy + y_adjustment * (ux - le_x) / (te_x - le_x) + uy + y_adjustment * (ux - le_x) / (te_x - le_x), ], - axis=1 + axis=1, ) new_l = np.stack( arrays=[ lx - x_adjustment * (lx - le_x) / (te_x - le_x), - ly - y_adjustment * (lx - le_x) / (te_x - le_x) + ly - y_adjustment * (lx - le_x) / (te_x - le_x), ], - axis=1 + axis=1, ) ### If the desired thickness is zero, ensure that is precisely reached. @@ -1385,24 +1372,16 @@ def set_TE_thickness(self, new_l[-1] = new_u[0] ### Combine the upper and lower surface coordinates into a single array. - new_coordinates = np.concatenate( - [ - new_u, - new_l - ], - axis=0 - ) + new_coordinates = np.concatenate([new_u, new_l], axis=0) ### Return a new Airfoil with the desired coordinates. - return Airfoil( - name=self.name, - coordinates=new_coordinates - ) + return Airfoil(name=self.name, coordinates=new_coordinates) - def scale(self, - scale_x: float = 1., - scale_y: float = 1., - ) -> 'Airfoil': + def scale( + self, + scale_x: float = 1.0, + scale_y: float = 1.0, + ) -> "Airfoil": """ Scales an Airfoil about the origin. @@ -1422,14 +1401,8 @@ def scale(self, if scale_x < 0: TE_index = np.argmax(x) - x = np.concatenate([ - x[TE_index::-1], - x[-2:TE_index-1:-1] - ]) - y = np.concatenate([ - y[TE_index::-1], - y[-2:TE_index-1:-1] - ]) + x = np.concatenate([x[TE_index::-1], x[-2 : TE_index - 1 : -1]]) + y = np.concatenate([y[TE_index::-1], y[-2 : TE_index - 1 : -1]]) if scale_y < 0: x = x[::-1] @@ -1442,10 +1415,11 @@ def scale(self, coordinates=coordinates, ) - def translate(self, - translate_x: float = 0., - translate_y: float = 0., - ) -> 'Airfoil': + def translate( + self, + translate_x: float = 0.0, + translate_y: float = 0.0, + ) -> "Airfoil": """ Translates an Airfoil by a given amount. Args: @@ -1458,16 +1432,11 @@ def translate(self, x = self.x() + translate_x y = self.y() + translate_y - return Airfoil( - name=self.name, - coordinates=np.stack((x, y), axis=1) - ) + return Airfoil(name=self.name, coordinates=np.stack((x, y), axis=1)) - def rotate(self, - angle: float, - x_center: float = 0., - y_center: float = 0. - ) -> 'Airfoil': + def rotate( + self, angle: float, x_center: float = 0.0, y_center: float = 0.0 + ) -> "Airfoil": """ Rotates the airfoil clockwise by the specified amount, in radians. @@ -1499,16 +1468,14 @@ def rotate(self, ### Translate coordinates += translation - return Airfoil( - name=self.name, - coordinates=coordinates - ) + return Airfoil(name=self.name, coordinates=coordinates) - def blend_with_another_airfoil(self, - airfoil: "Airfoil", - blend_fraction: float = 0.5, - n_points_per_side: int = 100, - ) -> "Airfoil": + def blend_with_another_airfoil( + self, + airfoil: "Airfoil", + blend_fraction: float = 0.5, + n_points_per_side: int = 100, + ) -> "Airfoil": """ Blends this airfoil with another airfoil. Merges both the coordinates and the aerodynamic functions. @@ -1534,10 +1501,7 @@ def blend_with_another_airfoil(self, name = f"{a_fraction * 100:.0f}% {self.name}, {b_fraction * 100:.0f}% {airfoil.name}" - coordinates = ( - a_fraction * foil_a.coordinates + - b_fraction * foil_b.coordinates - ) + coordinates = a_fraction * foil_a.coordinates + b_fraction * foil_b.coordinates return Airfoil( name=name, @@ -1547,10 +1511,11 @@ def blend_with_another_airfoil(self, # def normalize(self): # pass # TODO finish me - def write_dat(self, - filepath: Union[str, Path] = None, - include_name: bool = True, - ) -> str: + def write_dat( + self, + filepath: Union[str, Path] = None, + include_name: bool = True, + ) -> str: """ Writes a .dat file corresponding to this airfoil to a filepath. @@ -1941,7 +1906,7 @@ def write_dat(self, # return self -if __name__ == '__main__': +if __name__ == "__main__": af = Airfoil("dae11") import matplotlib.pyplot as plt @@ -1950,7 +1915,16 @@ def write_dat(self, fig, ax = plt.subplots(4, 2, figsize=(6.4, 6.4), dpi=200) alpha = np.linspace(-90, 90, 500) - sizes = ["xxsmall", "xsmall", "small", "medium", "large", "xlarge", "xxlarge", "xxxlarge"] + sizes = [ + "xxsmall", + "xsmall", + "small", + "medium", + "large", + "xlarge", + "xxlarge", + "xxxlarge", + ] colors = plt.cm.rainbow(np.linspace(0, 1, len(sizes)))[::-1] for i, ms in enumerate(sizes): @@ -1965,10 +1939,13 @@ def write_dat(self, alpha=0.5, color=colors[i], ) - for a, key in zip(ax.T.flatten(), ["CL", "CD", "CM", "Cpmin", "mach_crit", "Top_Xtr", "Bot_Xtr", "Cpmin_0"]): + for a, key in zip( + ax.T.flatten(), + ["CL", "CD", "CM", "Cpmin", "mach_crit", "Top_Xtr", "Bot_Xtr", "Cpmin_0"], + ): a.plot(alpha, aero[key], **kwargs) if key == "CD": - a.set_yscale('log') + a.set_yscale("log") a.set_ylabel(key) p.show_plot() diff --git a/aerosandbox/geometry/airfoil/airfoil_database/utils/convert_all_files_to_ascii.py b/aerosandbox/geometry/airfoil/airfoil_database/utils/convert_all_files_to_ascii.py index 4ddec3b55..58616a54c 100644 --- a/aerosandbox/geometry/airfoil/airfoil_database/utils/convert_all_files_to_ascii.py +++ b/aerosandbox/geometry/airfoil/airfoil_database/utils/convert_all_files_to_ascii.py @@ -10,4 +10,4 @@ if s != s_fixed: with open(file, "w+") as f: - f.write(s_fixed) \ No newline at end of file + f.write(s_fixed) diff --git a/aerosandbox/geometry/airfoil/airfoil_families.py b/aerosandbox/geometry/airfoil/airfoil_families.py index c06b68691..5859e923a 100644 --- a/aerosandbox/geometry/airfoil/airfoil_families.py +++ b/aerosandbox/geometry/airfoil/airfoil_families.py @@ -9,11 +9,11 @@ def get_NACA_coordinates( - name: str = None, - n_points_per_side: int = _default_n_points_per_side, - max_camber: float = None, - camber_loc: float = None, - thickness: float = None, + name: str = None, + n_points_per_side: int = _default_n_points_per_side, + max_camber: float = None, + camber_loc: float = None, + thickness: float = None, ) -> np.ndarray: """ Returns the coordinates of a 4-series NACA airfoil. @@ -46,13 +46,14 @@ def get_NACA_coordinates( params_specified = [ (max_camber is not None), (camber_loc is not None), - (thickness is not None) + (thickness is not None), ] if name_specified: if any(params_specified): raise ValueError( - "Cannot specify both `name` and (`max_camber`, `camber_loc`, `thickness`) parameters - must be one or the other.") + "Cannot specify both `name` and (`max_camber`, `camber_loc`, `thickness`) parameters - must be one or the other." + ) name = name.lower().strip() @@ -64,7 +65,9 @@ def get_NACA_coordinates( 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!") + raise NotImplementedError( + "Only 4-digit NACA airfoils are currently supported!" + ) # Parse max_camber = int(nacanumber[0]) * 0.01 @@ -74,19 +77,24 @@ def get_NACA_coordinates( else: if not all(params_specified): raise ValueError( - "Must specify either `name` or all three (`max_camber`, `camber_loc`, `thickness`) parameters.") + "Must specify either `name` or all three (`max_camber`, `camber_loc`, `thickness`) parameters." + ) # 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 + 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 + - 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: @@ -95,15 +103,17 @@ def get_NACA_coordinates( # 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) + 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) + 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) @@ -126,14 +136,14 @@ def get_NACA_coordinates( def get_kulfan_coordinates( - lower_weights: np.ndarray = -0.2 * np.ones(8), - upper_weights: np.ndarray = 0.2 * np.ones(8), - leading_edge_weight: float = 0., - TE_thickness: float = 0., - n_points_per_side: int = _default_n_points_per_side, - N1: float = 0.5, - N2: float = 1.0, - **deprecated_kwargs + lower_weights: np.ndarray = -0.2 * np.ones(8), + upper_weights: np.ndarray = 0.2 * np.ones(8), + leading_edge_weight: float = 0.0, + TE_thickness: float = 0.0, + n_points_per_side: int = _default_n_points_per_side, + N1: float = 0.5, + N2: float = 1.0, + **deprecated_kwargs, ) -> np.ndarray: """ Given a set of Kulfan parameters, computes the coordinates of the resulting airfoil. @@ -206,10 +216,11 @@ def get_kulfan_coordinates( """ if len(deprecated_kwargs) > 0: import warnings + warnings.warn( "The following arguments are deprecated and will be removed in a future version:\n" f"{deprecated_kwargs}", - DeprecationWarning + DeprecationWarning, ) if deprecated_kwargs.get("enforce_continuous_LE_radius", False): @@ -235,8 +246,9 @@ def tall(vector): return np.tile(np.reshape(vector, (dims[0], 1)), (1, dims[1])) S_matrix = ( - tall(K) * wide(x) ** tall(np.arange(N + 1)) * - wide(1 - x) ** tall(N - np.arange(N + 1)) + tall(K) + * wide(x) ** tall(np.arange(N + 1)) + * wide(1 - x) ** tall(N - np.arange(N + 1)) ) # Bernstein polynomial coefficients * weight matrix S_x = np.sum(tall(w) * S_matrix, axis=0) @@ -263,14 +275,14 @@ def tall(vector): def get_kulfan_parameters( - coordinates: np.ndarray, - n_weights_per_side: int = 8, - N1: float = 0.5, - N2: float = 1.0, - n_points_per_side: int = _default_n_points_per_side, - normalize_coordinates: bool = True, - use_leading_edge_modification: bool = True, - method: str = "least_squares", + coordinates: np.ndarray, + n_weights_per_side: int = 8, + N1: float = 0.5, + N2: float = 1.0, + n_points_per_side: int = _default_n_points_per_side, + normalize_coordinates: bool = True, + use_leading_edge_modification: bool = True, + method: str = "least_squares", ) -> Dict[str, Union[np.ndarray, float]]: """ Given a set of airfoil coordinates, reconstructs the Kulfan parameters that would recreate that airfoil. Uses a @@ -362,11 +374,8 @@ def get_kulfan_parameters( if method == "opti": target_airfoil = Airfoil( - name="Target Airfoil", - coordinates=coordinates - ).repanel( - n_points_per_side=n_points_per_side - ) + name="Target Airfoil", coordinates=coordinates + ).repanel(n_points_per_side=n_points_per_side) if normalize_coordinates: target_airfoil = target_airfoil.normalize() @@ -396,8 +405,9 @@ def tall(vector): return np.tile(np.reshape(vector, (dims[0], 1)), (1, dims[1])) S_matrix = ( - tall(K) * wide(x) ** tall(np.arange(N + 1)) * - wide(1 - x) ** tall(N - np.arange(N + 1)) + tall(K) + * wide(x) ** tall(np.arange(N + 1)) + * wide(1 - x) ** tall(N - np.arange(N + 1)) ) # Bernstein polynomial coefficients * weight matrix S_x = np.sum(tall(w) * S_matrix, axis=0) @@ -422,53 +432,56 @@ def tall(vector): y_upper += x * TE_thickness / 2 # Add Kulfan's leading-edge-modification (LEM) - y_lower += leading_edge_weight * (x) * (1 - x) ** (np.length(lower_weights) + 0.5) - y_upper += leading_edge_weight * (x) * (1 - x) ** (np.length(upper_weights) + 0.5) + y_lower += ( + leading_edge_weight * (x) * (1 - x) ** (np.length(lower_weights) + 0.5) + ) + y_upper += ( + leading_edge_weight * (x) * (1 - x) ** (np.length(upper_weights) + 0.5) + ) opti.minimize( - np.sum((y_lower - target_y_lower) ** 2) + - np.sum((y_upper - target_y_upper) ** 2) + np.sum((y_lower - target_y_lower) ** 2) + + np.sum((y_upper - target_y_upper) ** 2) ) - sol = opti.solve( - verbose=False - ) + sol = opti.solve(verbose=False) return { - "lower_weights" : sol(lower_weights), - "upper_weights" : sol(upper_weights), - "TE_thickness" : sol(TE_thickness), + "lower_weights": sol(lower_weights), + "upper_weights": sol(upper_weights), + "TE_thickness": sol(TE_thickness), "leading_edge_weight": sol(leading_edge_weight), } elif method == "least_squares": """ - - The goal here is to set up this fitting problem as a least-squares problem (likely an overconstrained one, - but keeping it general for now. This will then be solved with np.linalg.lstsq(A, b), where A will (likely) + + The goal here is to set up this fitting problem as a least-squares problem (likely an overconstrained one, + but keeping it general for now. This will then be solved with np.linalg.lstsq(A, b), where A will (likely) not be square. - + The columns of the A matrix will correspond to our unknowns, which are going to be a 1D vector `x` packed in as: * upper_weights from 0 to n_weights_per_side - 1 * lower_weights from 0 to n_weights_per_side - 1 * leading_edge_weight * trailing_edge_thickness - + See `get_kulfan_coordinates()` for more details on the meaning of these variables. - - The rows of the A matrix will correspond to each row of the given airfoil coordinates (i.e., a single vertex + + The rows of the A matrix will correspond to each row of the given airfoil coordinates (i.e., a single vertex on the airfoil). The idea here is to express each vertex as a linear combination of the unknowns, and then solve for the unknowns that minimize the error between the given airfoil coordinates and the reconstructed airfoil coordinates. - + """ if normalize_coordinates: - coordinates = Airfoil( - name="Target Airfoil", - coordinates=coordinates - ).normalize().coordinates + coordinates = ( + Airfoil(name="Target Airfoil", coordinates=coordinates) + .normalize() + .coordinates + ) n_coordinates = np.length(coordinates) @@ -495,24 +508,24 @@ def tall(vector): return np.tile(np.reshape(vector, (dims[0], 1)), (1, dims[1])) S_matrix = ( - tall(K) * wide(x) ** tall(np.arange(N + 1)) * - wide(1 - x) ** tall(N - np.arange(N + 1)) + tall(K) + * wide(x) ** tall(np.arange(N + 1)) + * wide(1 - x) ** tall(N - np.arange(N + 1)) ) # Bernstein polynomial coefficients * weight matrix leading_edge_weight_row = x * np.maximum(1 - x, 0) ** (n_weights_per_side + 0.5) - trailing_edge_thickness_row = np.where( - is_upper, - x / 2, - -x / 2 - ) + trailing_edge_thickness_row = np.where(is_upper, x / 2, -x / 2) - A = np.concatenate([ - np.where(wide(is_upper), 0, wide(C) * S_matrix).T, - np.where(wide(is_upper), wide(C) * S_matrix, 0).T, - np.reshape(leading_edge_weight_row, (n_coordinates, 1)), - np.reshape(trailing_edge_thickness_row, (n_coordinates, 1)), - ], axis=1) + A = np.concatenate( + [ + np.where(wide(is_upper), 0, wide(C) * S_matrix).T, + np.where(wide(is_upper), wide(C) * S_matrix, 0).T, + np.reshape(leading_edge_weight_row, (n_coordinates, 1)), + np.reshape(trailing_edge_thickness_row, (n_coordinates, 1)), + ], + axis=1, + ) b = y @@ -520,7 +533,7 @@ def tall(vector): x, _, _, _ = np.linalg.lstsq(A, b, rcond=None) lower_weights = x[:n_weights_per_side] - upper_weights = x[n_weights_per_side:2 * n_weights_per_side] + upper_weights = x[n_weights_per_side : 2 * n_weights_per_side] leading_edge_weight = x[-2] trailing_edge_thickness = x[-1] @@ -530,14 +543,14 @@ def tall(vector): x, _, _, _ = np.linalg.lstsq(A[:, :-1], b, rcond=None) lower_weights = x[:n_weights_per_side] - upper_weights = x[n_weights_per_side:2 * n_weights_per_side] + upper_weights = x[n_weights_per_side : 2 * n_weights_per_side] leading_edge_weight = x[-1] trailing_edge_thickness = 0 return { - "lower_weights" : lower_weights, - "upper_weights" : upper_weights, - "TE_thickness" : trailing_edge_thickness, + "lower_weights": lower_weights, + "upper_weights": upper_weights, + "TE_thickness": trailing_edge_thickness, "leading_edge_weight": leading_edge_weight, } @@ -545,9 +558,7 @@ def tall(vector): raise ValueError(f"Invalid method '{method}'.") -def get_coordinates_from_raw_dat( - raw_text: List[str] -) -> np.ndarray: +def get_coordinates_from_raw_dat(raw_text: List[str]) -> np.ndarray: """ Returns a Nx2 ndarray of airfoil coordinates from the raw text of a airfoil *.dat file. @@ -572,7 +583,7 @@ def is_number(s: str) -> bool: def parse_line(line: str) -> Optional[List[float]]: # Given a single line of a `*.dat` file, tries to parse it into a list of two floats [x, y]. # If not possible, returns None. - line_split = re.split(r'[;|,|\s|\t]', line) + line_split = re.split(r"[;|,|\s|\t]", line) line_items = [s for s in line_split if s != ""] if len(line_items) == 2 and all([is_number(item) for item in line_items]): return line_items @@ -592,9 +603,7 @@ def parse_line(line: str) -> Optional[List[float]]: return coordinates -def get_file_coordinates( - filepath: Union[str, os.PathLike] -): +def get_file_coordinates(filepath: Union[str, os.PathLike]): possible_errors = (FileNotFoundError, UnicodeDecodeError) if isinstance(filepath, np.ndarray): @@ -618,9 +627,7 @@ def get_file_coordinates( raise ValueError("File was found, but could not read any coordinates!") -def get_UIUC_coordinates( - name: str = 'dae11' -) -> np.ndarray: +def get_UIUC_coordinates(name: str = "dae11") -> np.ndarray: """ Returns the coordinates of a specified airfoil in the UIUC airfoil database. Args: @@ -647,7 +654,7 @@ def get_UIUC_coordinates( return get_coordinates_from_raw_dat(raw_text) -if __name__ == '__main__': +if __name__ == "__main__": import aerosandbox as asb import aerosandbox.numpy as np @@ -661,9 +668,7 @@ def get_UIUC_coordinates( af_reconstructed = asb.Airfoil( name="Reconstructed Airfoil", - coordinates=get_kulfan_coordinates( - **kulfan_params - ), + coordinates=get_kulfan_coordinates(**kulfan_params), ) af_reconstructed.draw(backend="plotly") diff --git a/aerosandbox/geometry/airfoil/default_airfoil_aerodynamics.py b/aerosandbox/geometry/airfoil/default_airfoil_aerodynamics.py index 8a072cf77..a7f2436d1 100644 --- a/aerosandbox/geometry/airfoil/default_airfoil_aerodynamics.py +++ b/aerosandbox/geometry/airfoil/default_airfoil_aerodynamics.py @@ -6,7 +6,7 @@ "This file is deprecated and will be removed in the subsequent version of ASB.\n" "You can use `asb.Airfoil.get_aero_from_neuralfoil()` instead\n" "to get airfoil aerodynamics for any airfoil.", - DeprecationWarning + DeprecationWarning, ) @@ -29,11 +29,9 @@ def default_CD_function(alpha, Re, mach=0, deflection=0): ### Form factor model from Raymer, "Aircraft Design". Section 12.5, Eq. 12.30 t_over_c = 0.12 - FF = 1 + 2 * t_over_c * 100 * t_over_c ** 4 + FF = 1 + 2 * t_over_c * 100 * t_over_c**4 - Cd_inc = 2 * Cf * FF * ( - 1 + (np.sind(alpha) * 180 / np.pi / 5) ** 2 - ) + Cd_inc = 2 * Cf * FF * (1 + (np.sind(alpha) * 180 / np.pi / 5) ** 2) beta = (1 - mach) ** 2 Cd = Cd_inc * beta diff --git a/aerosandbox/geometry/airfoil/kulfan_airfoil.py b/aerosandbox/geometry/airfoil/kulfan_airfoil.py index 3b9bdbadc..a8588c0fc 100644 --- a/aerosandbox/geometry/airfoil/kulfan_airfoil.py +++ b/aerosandbox/geometry/airfoil/kulfan_airfoil.py @@ -1,28 +1,32 @@ import aerosandbox.numpy as np from aerosandbox.geometry.airfoil.airfoil import Airfoil from aerosandbox.geometry.airfoil.airfoil_families import get_kulfan_parameters -from aerosandbox.modeling.splines.hermite import linear_hermite_patch, cubic_hermite_patch, cosine_hermite_patch +from aerosandbox.modeling.splines.hermite import ( + linear_hermite_patch, + cubic_hermite_patch, + cosine_hermite_patch, +) from typing import Union, Dict, List import warnings class KulfanAirfoil(Airfoil): - def __init__(self, - name: str = "Untitled", - lower_weights: np.ndarray = None, - upper_weights: np.ndarray = None, - leading_edge_weight: float = 0., - TE_thickness: float = 0., - N1: float = 0.5, - N2: float = 1.0, - ): + def __init__( + self, + name: str = "Untitled", + lower_weights: np.ndarray = None, + upper_weights: np.ndarray = None, + leading_edge_weight: float = 0.0, + TE_thickness: float = 0.0, + N1: float = 0.5, + N2: float = 1.0, + ): ### Handle the airfoil name self.name = name ### Check to see if the airfoil is a "known" airfoil, based on its name. if ( - lower_weights is None and - upper_weights is None + lower_weights is None and upper_weights is None ): # Try to fall back on parameters from the coordinate airfoil, if it's something from the UIUC database class IgnoreUserWarnings: # A context manager to ignore UserWarnings @@ -36,9 +40,11 @@ def __exit__(self, exc_type, exc_val, exc_tb): coordinate_airfoil = Airfoil(name) if coordinate_airfoil.coordinates is None: - raise ValueError("You must either:\n" - "\t* Specify both `lower_weights` and `upper_weights`, at minimum" - "\t* Give an airfoil `name` corresponding to an airfoil in the UIUC database, or a NACA airfoil.") + raise ValueError( + "You must either:\n" + "\t* Specify both `lower_weights` and `upper_weights`, at minimum" + "\t* Give an airfoil `name` corresponding to an airfoil in the UIUC database, or a NACA airfoil." + ) else: parameters = get_kulfan_parameters( @@ -69,10 +75,10 @@ def __repr__(self) -> str: @property def kulfan_parameters(self): return { - "lower_weights" : self.lower_weights, - "upper_weights" : self.upper_weights, + "lower_weights": self.lower_weights, + "upper_weights": self.upper_weights, "leading_edge_weight": self.leading_edge_weight, - "TE_thickness" : self.TE_thickness, + "TE_thickness": self.TE_thickness, } @property @@ -81,15 +87,18 @@ def coordinates(self) -> np.ndarray: @coordinates.setter def coordinates(self, value): - raise TypeError("The coordinates of a `KulfanAirfoil` can't be modified directly, " - "as they're a function of the Kulfan parameters.\n" - "Instead, you can either modify the Kulfan parameters directly, or use the " - "more general (coordinate-parameterized) `asb.Airfoil` class.") - - def to_airfoil(self, - n_coordinates_per_side=200, - spacing_function_per_side=np.cosspace, - ) -> Airfoil: + raise TypeError( + "The coordinates of a `KulfanAirfoil` can't be modified directly, " + "as they're a function of the Kulfan parameters.\n" + "Instead, you can either modify the Kulfan parameters directly, or use the " + "more general (coordinate-parameterized) `asb.Airfoil` class." + ) + + def to_airfoil( + self, + n_coordinates_per_side=200, + spacing_function_per_side=np.cosspace, + ) -> Airfoil: x_upper = spacing_function_per_side(1, 0, n_coordinates_per_side)[:-1] upper = self.upper_coordinates(x_over_c=x_upper) @@ -97,26 +106,23 @@ def to_airfoil(self, lower = self.lower_coordinates(x_over_c=x_lower) return Airfoil( - name=self.name, - coordinates=np.concatenate([ - upper, - lower - ], axis=0) + name=self.name, coordinates=np.concatenate([upper, lower], axis=0) ) - def repanel(self, - n_points_per_side: int = 100, - spacing_function_per_side=np.cosspace, - ) -> 'Airfoil': + def repanel( + self, + n_points_per_side: int = 100, + spacing_function_per_side=np.cosspace, + ) -> "Airfoil": return self.to_airfoil( n_coordinates_per_side=n_points_per_side, - spacing_function_per_side=spacing_function_per_side + spacing_function_per_side=spacing_function_per_side, ) def normalize( - self, - return_dict: bool = False, - ) -> Union['KulfanAirfoil', Dict[str, Union['KulfanAirfoil', float]]]: + self, + return_dict: bool = False, + ) -> Union["KulfanAirfoil", Dict[str, Union["KulfanAirfoil", float]]]: """ Returns a copy of the Airfoil with a new set of `coordinates`, such that: - The leading edge (LE) is at (0, 0) @@ -170,47 +176,38 @@ def normalize( return self else: return { - "airfoil" : self, - "x_translation" : 0, - "y_translation" : 0, - "scale_factor" : 1, - "rotation_angle": 0 + "airfoil": self, + "x_translation": 0, + "y_translation": 0, + "scale_factor": 1, + "rotation_angle": 0, } - def draw(self, - *args, - draw_markers=False, - **kwargs - ): - return self.to_airfoil().draw( - *args, - draw_markers=draw_markers, - **kwargs - ) - - def get_aero_from_neuralfoil(self, - alpha: Union[float, np.ndarray], - Re: Union[float, np.ndarray], - mach: Union[float, np.ndarray] = 0., - n_crit: Union[float, np.ndarray] = 9.0, - xtr_upper: Union[float, np.ndarray] = 1.0, - xtr_lower: Union[float, np.ndarray] = 1.0, - model_size: str = "large", - control_surfaces: List["ControlSurface"] = None, - include_360_deg_effects: bool = True, - ) -> Dict[str, Union[float, np.ndarray]]: + def draw(self, *args, draw_markers=False, **kwargs): + return self.to_airfoil().draw(*args, draw_markers=draw_markers, **kwargs) + + def get_aero_from_neuralfoil( + self, + alpha: Union[float, np.ndarray], + Re: Union[float, np.ndarray], + mach: Union[float, np.ndarray] = 0.0, + n_crit: Union[float, np.ndarray] = 9.0, + xtr_upper: Union[float, np.ndarray] = 1.0, + xtr_lower: Union[float, np.ndarray] = 1.0, + model_size: str = "large", + control_surfaces: List["ControlSurface"] = None, + include_360_deg_effects: bool = True, + ) -> Dict[str, Union[float, np.ndarray]]: ### Validate inputs - if ( - (np.length(self.lower_weights) != 8) or - (np.length(self.upper_weights) != 8) - ): - raise NotImplementedError("NeuralFoil is only trained to handle exactly 8 CST coefficients per side.") + if (np.length(self.lower_weights) != 8) or (np.length(self.upper_weights) != 8): + raise NotImplementedError( + "NeuralFoil is only trained to handle exactly 8 CST coefficients per side." + ) - if ( - self.N1 != 0.5 or - self.N2 != 1.0 - ): - raise NotImplementedError("NeuralFoil is only trained to handle airfoils with N1 = 0.5 and N2 = 1.0.") + if self.N1 != 0.5 or self.N2 != 1.0: + raise NotImplementedError( + "NeuralFoil is only trained to handle airfoils with N1 = 0.5 and N2 = 1.0." + ) ### Set up inputs if control_surfaces is None: @@ -219,24 +216,29 @@ def get_aero_from_neuralfoil(self, alpha = np.mod(alpha + 180, 360) - 180 # Enforce periodicity of alpha ##### Evaluate the control surfaces of the airfoil - effective_d_alpha = 0. - effective_CD_multiplier_from_control_surfaces = 1. + effective_d_alpha = 0.0 + effective_CD_multiplier_from_control_surfaces = 1.0 for surf in control_surfaces: - effectiveness = 1 - np.maximum(0, surf.hinge_point + 1e-16) ** 2.751428551177291 + effectiveness = ( + 1 - np.maximum(0, surf.hinge_point + 1e-16) ** 2.751428551177291 + ) # From XFoil-based study at `/AeroSandbox/studies/ControlSurfaceEffectiveness/` effective_d_alpha += surf.deflection * effectiveness effective_CD_multiplier_from_control_surfaces *= ( - 2 + (surf.deflection / 11.5) ** 2 - (1 + (surf.deflection / 11.5) ** 2) ** 0.5 + 2 + + (surf.deflection / 11.5) ** 2 + - (1 + (surf.deflection / 11.5) ** 2) ** 0.5 ) # From fit to wind tunnel data from Hoerner, "Fluid Dynamic Drag", 1965. Page 13-13, Figure 32, # "Variation of section drag coefficient of a horizontal tail surface at constant C_L" ##### Use NeuralFoil to evaluate the incompressible aerodynamics of the airfoil import neuralfoil as nf + nf_aero = nf.get_aero_from_kulfan_parameters( kulfan_parameters=dict( lower_weights=self.lower_weights, @@ -249,7 +251,7 @@ def get_aero_from_neuralfoil(self, n_crit=n_crit, xtr_upper=xtr_upper, xtr_lower=xtr_lower, - model_size=model_size + model_size=model_size, ) CL = nf_aero["CL"] @@ -265,18 +267,19 @@ def get_aero_from_neuralfoil(self, 1 - nf_aero[f"lower_bl_ue/vinf_{i}"] ** 2 for i in range(len(nf.bl_x_points)) ], - softness=0.01 + softness=0.01, ) Top_Xtr = nf_aero["Top_Xtr"] Bot_Xtr = nf_aero["Bot_Xtr"] ##### Extend aerodynamic data to 360 degrees (post-stall) using wind tunnel behavior here. if include_360_deg_effects: - from aerosandbox.aerodynamics.aero_2D.airfoil_polar_functions import airfoil_coefficients_post_stall + 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 + CL_if_separated, CD_if_separated, CM_if_separated = ( + airfoil_coefficients_post_stall(airfoil=self, alpha=alpha) ) import aerosandbox.library.aerodynamics as lib_aero @@ -286,26 +289,23 @@ def get_aero_from_neuralfoil(self, # 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. - is_separated = np.softmax( - alpha - alpha_stall_positive, - alpha_stall_negative - alpha - ) / 3 - - CL = np.blend( - is_separated, - CL_if_separated, - CL + is_separated = ( + np.softmax(alpha - alpha_stall_positive, alpha_stall_negative - alpha) + / 3 ) - CD = np.exp(np.blend( - is_separated, - np.log(CD_if_separated + lib_aero.Cf_flat_plate(Re_L=Re, method="turbulent")), - np.log(CD) - )) - CM = np.blend( - is_separated, - CM_if_separated, - CM + + CL = np.blend(is_separated, CL_if_separated, CL) + CD = np.exp( + np.blend( + is_separated, + np.log( + CD_if_separated + + lib_aero.Cf_flat_plate(Re_L=Re, method="turbulent") + ), + np.log(CD), + ) ) + CM = np.blend(is_separated, CM_if_separated, CM) """ Separated Cpmin_0 model is a very rough fit to Figure 3 of: @@ -315,21 +315,13 @@ def get_aero_from_neuralfoil(self, https://www.researchgate.net/publication/342316140_Effects_of_aspect_ratio_and_inclination_angle_on_aerodynamic_loads_of_a_flat_plate """ - Cpmin_0 = np.blend( - is_separated, - -1 - 0.5 * np.sind(alpha) ** 2, - Cpmin_0 - ) + Cpmin_0 = np.blend(is_separated, -1 - 0.5 * np.sind(alpha) ** 2, Cpmin_0) Top_Xtr = np.blend( - is_separated, - 0.5 - 0.5 * np.tanh(10 * np.sind(alpha)), - Top_Xtr + is_separated, 0.5 - 0.5 * np.tanh(10 * np.sind(alpha)), Top_Xtr ) Bot_Xtr = np.blend( - is_separated, - 0.5 + 0.5 * np.tanh(10 * np.sind(alpha)), - Bot_Xtr + is_separated, 0.5 + 0.5 * np.tanh(10 * np.sind(alpha)), Bot_Xtr ) ###### Add compressibility effects @@ -347,29 +339,28 @@ def get_aero_from_neuralfoil(self, See fits at: /AeroSandbox/studies/MachFitting/CriticalMach/ """ - Cpmin_0 = np.softmin( - Cpmin_0, - 0, - softness=0.001 - ) + Cpmin_0 = np.softmin(Cpmin_0, 0, softness=0.001) mach_crit = ( - 1.011571026701678 - - Cpmin_0 - + 0.6582431351007195 * (-Cpmin_0) ** 0.6724789439840343 - ) ** -0.5504677038358711 + 1.011571026701678 + - Cpmin_0 + + 0.6582431351007195 * (-Cpmin_0) ** 0.6724789439840343 + ) ** -0.5504677038358711 mach_dd = mach_crit + (0.1 / 320) ** (1 / 3) # drag divergence Mach number # Relation taken from W.H. Mason's Korn Equation ### Step 2: adjust CL, CD, CM, Cpmin by compressibility effects gamma = 1.4 # Ratio of specific heats, 1.4 for air (mostly diatomic nitrogen and oxygen) - beta_squared_ideal = 1 - mach ** 2 - beta = np.softmax( - beta_squared_ideal, - -beta_squared_ideal, - softness=0.5 # Empirically tuned to data - ) ** 0.5 + beta_squared_ideal = 1 - mach**2 + beta = ( + np.softmax( + beta_squared_ideal, + -beta_squared_ideal, + softness=0.5, # Empirically tuned to data + ) + ** 0.5 + ) CL = CL / beta # CD = CD / beta @@ -394,11 +385,7 @@ def get_aero_from_neuralfoil(self, # Accounts approximately for the lift drop due to buffet. buffet_factor = np.blend( 50 * (mach - (mach_dd + 0.04)), # Tuned to RANS CFD data empirically - np.blend( - (mach - 1) / 0.1, - 1, - 0.5 - ), + np.blend((mach - 1) / 0.1, 1, 0.5), 1, ) @@ -435,9 +422,9 @@ def get_aero_from_neuralfoil(self, 8 * 2 * (mach - 1.1) / (1.2 - 0.8), 0.8 * 0.8 * t_over_c, 1.2 * 0.8 * t_over_c, - ) - ) - ) + ), + ), + ), ) CD = CD + CD_wave @@ -447,9 +434,7 @@ def get_aero_from_neuralfoil(self, if include_360_deg_effects: has_aerodynamic_center_shift = np.softmax( - is_separated, - has_aerodynamic_center_shift, - softness=0.1 + is_separated, has_aerodynamic_center_shift, softness=0.1 ) CM = CM + np.blend( @@ -462,26 +447,33 @@ def get_aero_from_neuralfoil(self, return { "analysis_confidence": nf_aero["analysis_confidence"], - "CL" : CL, - "CD" : CD, - "CM" : CM, - "Cpmin" : Cpmin, - "Top_Xtr" : Top_Xtr, - "Bot_Xtr" : Bot_Xtr, + "CL": CL, + "CD": CD, + "CM": CM, + "Cpmin": Cpmin, + "Top_Xtr": Top_Xtr, + "Bot_Xtr": Bot_Xtr, "mach_crit": mach_crit, - "mach_dd" : mach_dd, - "Cpmin_0" : Cpmin_0, + "mach_dd": mach_dd, + "Cpmin_0": Cpmin_0, **{f"upper_bl_theta_{i}": nf_aero[f"upper_bl_theta_{i}"] for i in range(N)}, **{f"upper_bl_H_{i}": nf_aero[f"upper_bl_H_{i}"] for i in range(N)}, - **{f"upper_bl_ue/vinf_{i}": nf_aero[f"upper_bl_ue/vinf_{i}"] for i in range(N)}, + **{ + f"upper_bl_ue/vinf_{i}": nf_aero[f"upper_bl_ue/vinf_{i}"] + for i in range(N) + }, **{f"lower_bl_theta_{i}": nf_aero[f"lower_bl_theta_{i}"] for i in range(N)}, **{f"lower_bl_H_{i}": nf_aero[f"lower_bl_H_{i}"] for i in range(N)}, - **{f"lower_bl_ue/vinf_{i}": nf_aero[f"lower_bl_ue/vinf_{i}"] for i in range(N)}, + **{ + f"lower_bl_ue/vinf_{i}": nf_aero[f"lower_bl_ue/vinf_{i}"] + for i in range(N) + }, } - def upper_coordinates(self, - x_over_c: Union[float, np.ndarray] = np.linspace(1, 0, 101), - ) -> np.ndarray: + def upper_coordinates( + self, + x_over_c: Union[float, np.ndarray] = np.linspace(1, 0, 101), + ) -> np.ndarray: x_over_c = np.array(x_over_c) # Class function @@ -504,8 +496,9 @@ def tall(vector): return np.tile(np.reshape(vector, (dims[0], 1)), (1, dims[1])) S_matrix = ( - tall(K) * wide(x_over_c) ** tall(np.arange(N + 1)) * - wide(1 - x_over_c) ** tall(N - np.arange(N + 1)) + tall(K) + * wide(x_over_c) ** tall(np.arange(N + 1)) + * wide(1 - x_over_c) ** tall(N - np.arange(N + 1)) ) # Bernstein polynomial coefficients * weight matrix S_x = np.sum(tall(w) * S_matrix, axis=0) @@ -519,16 +512,24 @@ def tall(vector): y_upper += x_over_c * self.TE_thickness / 2 # Add Kulfan's leading-edge-modification (LEM) - y_upper += self.leading_edge_weight * (x_over_c) * (1 - x_over_c) ** (np.length(self.upper_weights) + 0.5) + y_upper += ( + self.leading_edge_weight + * (x_over_c) + * (1 - x_over_c) ** (np.length(self.upper_weights) + 0.5) + ) - return np.stack(( - np.reshape(x_over_c, (-1)), - y_upper, - ), axis=1) + return np.stack( + ( + np.reshape(x_over_c, (-1)), + y_upper, + ), + axis=1, + ) - def lower_coordinates(self, - x_over_c: Union[float, np.ndarray] = np.linspace(0, 1, 101), - ) -> np.ndarray: + def lower_coordinates( + self, + x_over_c: Union[float, np.ndarray] = np.linspace(0, 1, 101), + ) -> np.ndarray: x_over_c = np.array(x_over_c) # Class function @@ -551,8 +552,9 @@ def tall(vector): return np.tile(np.reshape(vector, (dims[0], 1)), (1, dims[1])) S_matrix = ( - tall(K) * wide(x_over_c) ** tall(np.arange(N + 1)) * - wide(1 - x_over_c) ** tall(N - np.arange(N + 1)) + tall(K) + * wide(x_over_c) ** tall(np.arange(N + 1)) + * wide(1 - x_over_c) ** tall(N - np.arange(N + 1)) ) # Bernstein polynomial coefficients * weight matrix S_x = np.sum(tall(w) * S_matrix, axis=0) @@ -566,16 +568,18 @@ def tall(vector): y_lower -= x_over_c * self.TE_thickness / 2 # Add Kulfan's leading-edge-modification (LEM) - y_lower += self.leading_edge_weight * (x_over_c) * (1 - x_over_c) ** (np.length(self.lower_weights) + 0.5) + y_lower += ( + self.leading_edge_weight + * (x_over_c) + * (1 - x_over_c) ** (np.length(self.lower_weights) + 0.5) + ) - return np.stack(( - np.reshape(x_over_c, (-1)), - y_lower - ), axis=1) + return np.stack((np.reshape(x_over_c, (-1)), y_lower), axis=1) - def local_camber(self, - x_over_c: Union[float, np.ndarray] = np.linspace(0, 1, 101), - ) -> Union[float, np.ndarray]: + def local_camber( + self, + x_over_c: Union[float, np.ndarray] = np.linspace(0, 1, 101), + ) -> Union[float, np.ndarray]: upper = self.upper_coordinates(x_over_c=x_over_c) lower = self.lower_coordinates(x_over_c=x_over_c) @@ -584,16 +588,17 @@ def local_camber(self, else: return (upper[:, 1] + lower[:, 1]) / 2 - def local_thickness(self, - x_over_c: Union[float, np.ndarray] = np.linspace(0, 1, 101), - ) -> Union[float, np.ndarray]: + def local_thickness( + self, + x_over_c: Union[float, np.ndarray] = np.linspace(0, 1, 101), + ) -> Union[float, np.ndarray]: upper = self.upper_coordinates(x_over_c=x_over_c) lower = self.lower_coordinates(x_over_c=x_over_c) if np.isscalar(x_over_c): - return (upper[0, 1] - lower[0, 1]) + return upper[0, 1] - lower[0, 1] else: - return (upper[:, 1] - lower[:, 1]) + return upper[:, 1] - lower[:, 1] def LE_radius(self, relative_softness: float = 0.03): # LE_radius_upper = np.where( @@ -607,26 +612,20 @@ def LE_radius(self, relative_softness: float = 0.03): # 0 # ) / 2 - return np.softmin_scalefree( - np.where( - self.upper_weights[0] > 0, - self.upper_weights[0], - 0 - ) ** 2, - np.where( - self.lower_weights[0] < 0, - self.lower_weights[0], - 0 - ) ** 2, - relative_softness=relative_softness - ) / 2 - + return ( + np.softmin_scalefree( + np.where(self.upper_weights[0] > 0, self.upper_weights[0], 0) ** 2, + np.where(self.lower_weights[0] < 0, self.lower_weights[0], 0) ** 2, + relative_softness=relative_softness, + ) + / 2 + ) def TE_angle(self): return np.degrees( - np.arctan(self.upper_weights[-1]) - - np.arctan(self.lower_weights[-1]) + - np.arctan(self.TE_thickness) + np.arctan(self.upper_weights[-1]) + - np.arctan(self.lower_weights[-1]) + + np.arctan(self.TE_thickness) ) def area(self): @@ -636,23 +635,19 @@ def get_area_of_side(weights): N = np.length(weights) - 1 i = np.arange(N + 1) - area_of_each_mode = comb(N, i) * beta( - self.N1 + i + 1, - self.N2 + N - i + 1 - ) - return np.sum( - area_of_each_mode * weights - ) + area_of_each_mode = comb(N, i) * beta(self.N1 + i + 1, self.N2 + N - i + 1) + return np.sum(area_of_each_mode * weights) return ( - get_area_of_side(self.upper_weights) - - get_area_of_side(self.lower_weights) + - (self.TE_thickness / 2) + get_area_of_side(self.upper_weights) + - get_area_of_side(self.lower_weights) + + (self.TE_thickness / 2) ) - def set_TE_thickness(self, - thickness: float = 0., - ) -> 'KulfanAirfoil': + def set_TE_thickness( + self, + thickness: float = 0.0, + ) -> "KulfanAirfoil": """ Creates a modified copy of the KulfanAirfoil that has a specified trailing-edge thickness. @@ -674,10 +669,11 @@ def set_TE_thickness(self, N2=self.N2, ) - def scale(self, - scale_x: float = 1., - scale_y: float = 1., - ) -> "KulfanAirfoil": + def scale( + self, + scale_x: float = 1.0, + scale_y: float = 1.0, + ) -> "KulfanAirfoil": """ Scales a KulfanAirfoil about the origin. @@ -693,13 +689,17 @@ def scale(self, Returns: A copy of the KulfanAirfoil with appropriate scaling applied. """ if scale_x != 1: - raise ValueError("\n".join([ - "Scaling a KulfanAirfoil in the x-direction is not supported due to inherent limitations of the", - "Kulfan parameterization. If you need to scale in the x-direction: " - "\t- Convert to an Airfoil first (`KulfanAirfoil.to_airfoil()`)" - "\t- Scale it (`Airfoil.scale()`)" - "\t- Convert back to a KulfanAirfoil (`Airfoil.to_kulfan_airfoil()`)" - ])) + raise ValueError( + "\n".join( + [ + "Scaling a KulfanAirfoil in the x-direction is not supported due to inherent limitations of the", + "Kulfan parameterization. If you need to scale in the x-direction: " + "\t- Convert to an Airfoil first (`KulfanAirfoil.to_airfoil()`)" + "\t- Scale it (`Airfoil.scale()`)" + "\t- Convert back to a KulfanAirfoil (`Airfoil.to_kulfan_airfoil()`)", + ] + ) + ) if scale_y >= 0: return KulfanAirfoil( @@ -722,16 +722,19 @@ def scale(self, N2=self.N2, ) - def blend_with_another_airfoil(self, - airfoil: Union["KulfanAirfoil", Airfoil], - blend_fraction: float = 0.5, - ) -> "KulfanAirfoil": + def blend_with_another_airfoil( + self, + airfoil: Union["KulfanAirfoil", Airfoil], + blend_fraction: float = 0.5, + ) -> "KulfanAirfoil": if not isinstance(airfoil, KulfanAirfoil): try: airfoil = airfoil.to_kulfan_airfoil() except AttributeError: - raise TypeError("The `airfoil` argument should be either a `KulfanAirfoil` or an `Airfoil`.\n" - f"You gave an object of type \"{type(airfoil)}\".") + raise TypeError( + "The `airfoil` argument should be either a `KulfanAirfoil` or an `Airfoil`.\n" + f'You gave an object of type "{type(airfoil)}".' + ) foil_a = self foil_b = airfoil @@ -743,10 +746,14 @@ def blend_with_another_airfoil(self, return KulfanAirfoil( name=name, - lower_weights=a_fraction * foil_a.lower_weights + b_fraction * foil_b.lower_weights, - upper_weights=a_fraction * foil_a.upper_weights + b_fraction * foil_b.upper_weights, - leading_edge_weight=a_fraction * foil_a.leading_edge_weight + b_fraction * foil_b.leading_edge_weight, - TE_thickness=a_fraction * foil_a.TE_thickness + b_fraction * foil_b.TE_thickness, + lower_weights=a_fraction * foil_a.lower_weights + + b_fraction * foil_b.lower_weights, + upper_weights=a_fraction * foil_a.upper_weights + + b_fraction * foil_b.upper_weights, + leading_edge_weight=a_fraction * foil_a.leading_edge_weight + + b_fraction * foil_b.leading_edge_weight, + TE_thickness=a_fraction * foil_a.TE_thickness + + b_fraction * foil_b.TE_thickness, N1=a_fraction * foil_a.N1 + b_fraction * foil_b.N1, N2=a_fraction * foil_a.N2 + b_fraction * foil_b.N2, ) diff --git a/aerosandbox/geometry/airfoil/test_airfoil/test_airfoil_database_validity.py b/aerosandbox/geometry/airfoil/test_airfoil/test_airfoil_database_validity.py index 63f406adc..eb6e93760 100644 --- a/aerosandbox/geometry/airfoil/test_airfoil/test_airfoil_database_validity.py +++ b/aerosandbox/geometry/airfoil/test_airfoil/test_airfoil_database_validity.py @@ -29,7 +29,9 @@ def check_validity(af: asb.Airfoil) -> None: """ if af.n_points() < 4: - raise ValueError(f"Airfoil {af.name} has too few points (n_points = {af.n_points()})!") + raise ValueError( + f"Airfoil {af.name} has too few points (n_points = {af.n_points()})!" + ) if af.area() < 0: raise ValueError(f"Airfoil {af.name} has negative area (area = {af.area()})!") @@ -38,53 +40,78 @@ def check_validity(af: asb.Airfoil) -> None: raise ValueError(f"Airfoil {af.name} has zero area!") if af.area() > 0.6: - raise UserWarning(f"Airfoil {af.name} has unusually large area (area = {af.area()})!") + raise UserWarning( + f"Airfoil {af.name} has unusually large area (area = {af.area()})!" + ) if af.x().max() > 1.1: - raise UserWarning(f"Airfoil {af.name} has unusually high x_max (x_max = {af.x().max()})!") + raise UserWarning( + f"Airfoil {af.name} has unusually high x_max (x_max = {af.x().max()})!" + ) if af.x().max() < 0.9: - raise UserWarning(f"Airfoil {af.name} has unusually low x_max (x_max = {af.x().max()})!") + raise UserWarning( + f"Airfoil {af.name} has unusually low x_max (x_max = {af.x().max()})!" + ) if af.x().min() < -0.1: - raise UserWarning(f"Airfoil {af.name} has unusually low x_min (x_min = {af.x().min()})!") + raise UserWarning( + f"Airfoil {af.name} has unusually low x_min (x_min = {af.x().min()})!" + ) if af.x().min() > 0.1: - raise UserWarning(f"Airfoil {af.name} has unusually high x_min (x_min = {af.x().min()})!") + raise UserWarning( + f"Airfoil {af.name} has unusually high x_min (x_min = {af.x().min()})!" + ) if af.y().max() > 0.5: - raise UserWarning(f"Airfoil {af.name} has unusually high y_max (y_max = {af.y().max()})!") + raise UserWarning( + f"Airfoil {af.name} has unusually high y_max (y_max = {af.y().max()})!" + ) if af.y().min() < -0.5: - raise UserWarning(f"Airfoil {af.name} has unusually low y_min (y_min = {af.y().min()})!") + raise UserWarning( + f"Airfoil {af.name} has unusually low y_min (y_min = {af.y().min()})!" + ) ## Check for any duplicate points ds = np.linalg.norm(np.diff(af.coordinates, axis=0), axis=1) if np.any(ds <= 0): - raise ValueError(f"Airfoil {af.name} has duplicate points (index = {np.argmin(ds)})!") + raise ValueError( + f"Airfoil {af.name} has duplicate points (index = {np.argmin(ds)})!" + ) if ds.max() > 0.8: - raise UserWarning(f"Airfoil {af.name} has unusually large ds_max (ds_max = {ds.max()})!") + raise UserWarning( + f"Airfoil {af.name} has unusually large ds_max (ds_max = {ds.max()})!" + ) ## Check for any negative thickness regions x_thicknesses = np.linspace(af.x().min(), af.x().max(), 501) thicknesses = af.local_thickness(x_over_c=x_thicknesses) if np.any(thicknesses < 0): - raise ValueError(f"Airfoil {af.name} has negative thickness @ x = {x_thicknesses[np.argmin(thicknesses)]}!") + raise ValueError( + f"Airfoil {af.name} has negative thickness @ x = {x_thicknesses[np.argmin(thicknesses)]}!" + ) ### Make sure the TE thickness is nonnegative if af.TE_thickness() < 0: - raise ValueError(f"Airfoil {af.name} has negative trailing edge thickness {af.TE_thickness()}!") + raise ValueError( + f"Airfoil {af.name} has negative trailing edge thickness {af.TE_thickness()}!" + ) ### Make sure the TE angle is not exactly zero, or negative, if the TE thickness is zero. if af.TE_thickness() <= 0: if af.TE_angle() <= 0: - raise ValueError(f"Airfoil {af.name} has trailing edge angle {af.TE_angle()}!") + raise ValueError( + f"Airfoil {af.name} has trailing edge angle {af.TE_angle()}!" + ) ### See if Shapely has any complaints try: import shapely + if not af.as_shapely_polygon().is_valid: raise ValueError(f"Airfoil {af.name} is not a valid Shapely polygon!") @@ -108,7 +135,10 @@ def test_airfoil_database_validity(): if len(failed_airfoils_and_errors) > 0: raise ValueError( f"The following airfoils failed the validity test:\n" - "\n".join(f"{af_name}: {error}" for af_name, error in failed_airfoils_and_errors.items()) + "\n".join( + f"{af_name}: {error}" + for af_name, error in failed_airfoils_and_errors.items() + ) ) @@ -116,14 +146,12 @@ def debug_draw(af: asb.Airfoil): if isinstance(af, str): af = asb.Airfoil(af) - af.draw( - draw_mcl=False, backend='plotly', show=False - ).update_layout( + af.draw(draw_mcl=False, backend="plotly", show=False).update_layout( yaxis=dict(scaleanchor=None) ).show() -if __name__ == '__main__': +if __name__ == "__main__": afs = get_airfoil_database() for af in afs: diff --git a/aerosandbox/geometry/airfoil/test_airfoil/test_airfoil_polar_generation.py b/aerosandbox/geometry/airfoil/test_airfoil/test_airfoil_polar_generation.py index 4ca94d798..97688445d 100644 --- a/aerosandbox/geometry/airfoil/test_airfoil/test_airfoil_polar_generation.py +++ b/aerosandbox/geometry/airfoil/test_airfoil/test_airfoil_polar_generation.py @@ -1,7 +1,7 @@ import aerosandbox as asb import aerosandbox.numpy as np -if __name__ == '__main__': +if __name__ == "__main__": af = asb.Airfoil("dae11") af.generate_polars() @@ -20,7 +20,7 @@ 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') + plt.yscale("log") show_plot( f"Auto-generated Polar for {af.name} Airfoil", "Angle of Attack [deg]", @@ -30,7 +30,7 @@ fig, ax = plt.subplots() contour(Alpha, Re, CD, levels=30, colorbar_label=r"$C_D$", z_log_scale=True) plt.scatter(af.xfoil_data["alpha"], af.xfoil_data["Re"], color="k", alpha=0.2) - plt.yscale('log') + plt.yscale("log") show_plot( f"Auto-generated Polar for {af.name} Airfoil", "Angle of Attack [deg]", @@ -55,7 +55,6 @@ mach=0, ) - plt.sca(ax[0, 0]) plt.plot(ma, mCL, label="`Airfoil.generate_polars()`") plt.plot(na, nf_aero["CL"], label="NeuralFoil") @@ -94,7 +93,5 @@ opti = asb.Opti() alpha = opti.variable(init_guess=0, lower_bound=-20, upper_bound=20) LD = af.CL_function(alpha, 1e6) / af.CD_function(alpha, 1e6) - opti.minimize( - -LD - ) + opti.minimize(-LD) sol = opti.solve() diff --git a/aerosandbox/geometry/airfoil/test_airfoil/test_airfoil_polar_generation_caching.py b/aerosandbox/geometry/airfoil/test_airfoil/test_airfoil_polar_generation_caching.py index c288f3744..b91662e56 100644 --- a/aerosandbox/geometry/airfoil/test_airfoil/test_airfoil_polar_generation_caching.py +++ b/aerosandbox/geometry/airfoil/test_airfoil/test_airfoil_polar_generation_caching.py @@ -13,6 +13,6 @@ def test_load_cache(): af.generate_polars(cache_filename=cache) -if __name__ == '__main__': +if __name__ == "__main__": make_cache() test_load_cache() diff --git a/aerosandbox/geometry/airfoil/test_airfoil/test_airfoil_repaneling.py b/aerosandbox/geometry/airfoil/test_airfoil/test_airfoil_repaneling.py index 51a2bdba4..3abde534b 100644 --- a/aerosandbox/geometry/airfoil/test_airfoil/test_airfoil_repaneling.py +++ b/aerosandbox/geometry/airfoil/test_airfoil/test_airfoil_repaneling.py @@ -16,6 +16,7 @@ def get_airfoil_database() -> List[asb.Airfoil]: return afs + def test_repaneling_validity(): try: import shapely @@ -26,27 +27,24 @@ def test_repaneling_validity(): for af in afs: try: - similarity = af.jaccard_similarity( - af.repanel(n_points_per_side=100) - ) + similarity = af.jaccard_similarity(af.repanel(n_points_per_side=100)) except shapely.errors.GEOSException: - continue # Jaccard similarity is not defined for self-intersecting polygons + continue # Jaccard similarity is not defined for self-intersecting polygons # similarity = np.nan - assert similarity > 1 - 3 / af.n_points(), f"Airfoil {af.name} failed repaneling validity check with similarity {similarity}!" + assert ( + similarity > 1 - 3 / af.n_points() + ), f"Airfoil {af.name} failed repaneling validity check with similarity {similarity}!" + def debug_draw(af: asb.Airfoil): if isinstance(af, str): af = asb.Airfoil(af) for af in [af, af.repanel()]: - af.draw( - draw_mcl=False, backend='plotly', show=False - ).update_layout( + af.draw(draw_mcl=False, backend="plotly", show=False).update_layout( yaxis=dict(scaleanchor=None) ).show() - - -if __name__ == '__main__': - test_repaneling_validity() \ No newline at end of file +if __name__ == "__main__": + test_repaneling_validity() diff --git a/aerosandbox/geometry/airfoil/test_airfoil/test_kulfanairfoil_similarity.py b/aerosandbox/geometry/airfoil/test_airfoil/test_kulfanairfoil_similarity.py index 672a3c8c3..3adea5001 100644 --- a/aerosandbox/geometry/airfoil/test_airfoil/test_kulfanairfoil_similarity.py +++ b/aerosandbox/geometry/airfoil/test_airfoil/test_kulfanairfoil_similarity.py @@ -26,18 +26,21 @@ def test_TE_angle(): assert kaf.TE_angle() == pytest.approx(af.TE_angle(), rel=0.1) + def test_LE_radius(): kaf = asb.KulfanAirfoil("naca4412") af = kaf.to_airfoil() assert kaf.LE_radius() == pytest.approx(af.LE_radius(), rel=0.1) + def test_area(): kaf = asb.KulfanAirfoil("naca4412") af = kaf.to_airfoil() assert kaf.area() == pytest.approx(af.area(), rel=0.01) -if __name__ == '__main__': + +if __name__ == "__main__": test_roundtrip_conversion_similarity() pytest.main([__file__]) diff --git a/aerosandbox/geometry/airplane.py b/aerosandbox/geometry/airplane.py index 2ef280124..1e9859fec 100644 --- a/aerosandbox/geometry/airplane.py +++ b/aerosandbox/geometry/airplane.py @@ -22,17 +22,18 @@ class Airplane(AeroSandboxObject): """ - def __init__(self, - name: Optional[str] = None, - xyz_ref: Union[np.ndarray, List] = None, - wings: Optional[List[Wing]] = None, - fuselages: Optional[List[Fuselage]] = None, - propulsors: Optional[List[Propulsor]] = None, - s_ref: Optional[float] = None, - c_ref: Optional[float] = None, - b_ref: Optional[float] = None, - analysis_specific_options: Optional[Dict[type, Dict[str, Any]]] = None - ): + def __init__( + self, + name: Optional[str] = None, + xyz_ref: Union[np.ndarray, List] = None, + wings: Optional[List[Wing]] = None, + fuselages: Optional[List[Fuselage]] = None, + propulsors: Optional[List[Propulsor]] = None, + s_ref: Optional[float] = None, + c_ref: Optional[float] = None, + b_ref: Optional[float] = None, + analysis_specific_options: Optional[Dict[type, Dict[str, Any]]] = None, + ): """ Defines a new airplane. @@ -86,7 +87,7 @@ def __init__(self, if name is None: name = "Untitled" if xyz_ref is None: - xyz_ref = np.array([0., 0., 0.]) + xyz_ref = np.array([0.0, 0.0, 0.0]) if wings is None: wings: List[Wing] = [] if fuselages is None: @@ -126,7 +127,8 @@ def __init__(self, else: raise ValueError( "`s_ref` was not provided, and a value cannot be inferred automatically from wings or fuselages.\n" - "You must set this manually when instantiating your asb.Airplane object.") + "You must set this manually when instantiating your asb.Airplane object." + ) if c_ref is not None: self.c_ref = c_ref @@ -159,17 +161,20 @@ def __init__(self, def __repr__(self): n_wings = len(self.wings) n_fuselages = len(self.fuselages) - return f"Airplane '{self.name}' " \ - f"({n_wings} {'wing' if n_wings == 1 else 'wings'}, " \ - f"{n_fuselages} {'fuselage' if n_fuselages == 1 else 'fuselages'})" + return ( + f"Airplane '{self.name}' " + f"({n_wings} {'wing' if n_wings == 1 else 'wings'}, " + f"{n_fuselages} {'fuselage' if n_fuselages == 1 else 'fuselages'})" + ) # TODO def add_wing(wing: 'Wing') -> None - def mesh_body(self, - method="quad", - thin_wings=False, - stack_meshes=True, - ): + def mesh_body( + self, + method="quad", + thin_wings=False, + stack_meshes=True, + ): """ Returns a surface mesh of the Airplane, in (points, faces) format. For reference on this format, see the documentation in `aerosandbox.geometry.mesh_utilities`. @@ -204,12 +209,7 @@ def mesh_body(self, for wing in self.wings ] - fuse_meshes = [ - fuse.mesh_body( - method=method - ) - for fuse in self.fuselages - ] + fuse_meshes = [fuse.mesh_body(method=method) for fuse in self.fuselages] meshes = wing_meshes + fuse_meshes @@ -219,19 +219,20 @@ def mesh_body(self, else: return meshes - def draw(self, - backend: str = "pyvista", - thin_wings: bool = False, - ax=None, - use_preset_view_angle: str = None, - set_background_pane_color: Union[str, Tuple[float, float, float]] = None, - set_background_pane_alpha: float = None, - set_lims: bool = True, - set_equal: bool = True, - set_axis_visibility: bool = None, - show: bool = True, - show_kwargs: Dict = None, - ): + def draw( + self, + backend: str = "pyvista", + thin_wings: bool = False, + ax=None, + use_preset_view_angle: str = None, + set_background_pane_color: Union[str, Tuple[float, float, float]] = None, + set_background_pane_alpha: float = None, + set_lims: bool = True, + set_equal: bool = True, + set_axis_visibility: bool = None, + show: bool = True, + show_kwargs: Dict = None, + ): """ Produces an interactive 3D visualization of the airplane. @@ -290,8 +291,12 @@ def draw(self, ax.add_collection( Poly3DCollection( - points[faces], facecolors='lightgray', edgecolors=(0, 0, 0, 0.1), - linewidths=0.5, alpha=0.8, shade=True, + points[faces], + facecolors="lightgray", + edgecolors=(0, 0, 0, 0.1), + linewidths=0.5, + alpha=0.8, + shade=True, ), ) @@ -301,12 +306,16 @@ def draw(self, if prop.length == 0: ax.add_collection( Poly3DCollection( - np.stack([np.stack( - prop.get_disk_3D_coordinates(), - axis=1 - )], axis=0), - facecolors='darkgray', edgecolors=(0, 0, 0, 0.2), - linewidths=0.5, alpha=0.35, shade=True, zorder=4, + np.stack( + [np.stack(prop.get_disk_3D_coordinates(), axis=1)], + axis=0, + ), + facecolors="darkgray", + edgecolors=(0, 0, 0, 0.2), + linewidths=0.5, + alpha=0.35, + shade=True, + zorder=4, ) ) @@ -330,29 +339,31 @@ def draw(self, elif backend == "plotly": from aerosandbox.visualization.plotly_Figure3D import Figure3D + fig = Figure3D() for f in faces: - fig.add_quad(( - points[f[0]], - points[f[1]], - points[f[2]], - points[f[3]], - ), outline=True) - show_kwargs = { - "show": show, - **show_kwargs - } + fig.add_quad( + ( + points[f[0]], + points[f[1]], + points[f[2]], + points[f[3]], + ), + outline=True, + ) + show_kwargs = {"show": show, **show_kwargs} return fig.draw(**show_kwargs) elif backend == "pyvista": import pyvista as pv + fig = pv.PolyData( *mesh_utils.convert_mesh_to_polydata_format(points, faces) ) show_kwargs = { "show_edges": True, - "show_grid" : True, + "show_grid": True, **show_kwargs, } if show: @@ -362,6 +373,7 @@ def draw(self, elif backend == "trimesh": import trimesh as tri + fig = tri.Trimesh(points, faces) if show: fig.show(**show_kwargs) @@ -369,20 +381,21 @@ def draw(self, else: raise ValueError("Bad value of `backend`!") - def draw_wireframe(self, - ax=None, - color="k", - thin_linewidth=0.2, - thick_linewidth=0.5, - fuselage_longeron_theta=None, - use_preset_view_angle: str = None, - set_background_pane_color: Union[str, Tuple[float, float, float]] = None, - set_background_pane_alpha: float = None, - set_lims: bool = True, - set_equal: bool = True, - set_axis_visibility: bool = None, - show: bool = True, - ) -> "matplotlib.axes.Axes": + def draw_wireframe( + self, + ax=None, + color="k", + thin_linewidth=0.2, + thick_linewidth=0.5, + fuselage_longeron_theta=None, + use_preset_view_angle: str = None, + set_background_pane_color: Union[str, Tuple[float, float, float]] = None, + set_background_pane_alpha: float = None, + set_lims: bool = True, + set_equal: bool = True, + set_axis_visibility: bool = None, + show: bool = True, + ) -> "matplotlib.axes.Axes": """ Draws a wireframe of the airplane on a Matplotlib 3D axis. @@ -427,18 +440,17 @@ def draw_wireframe(self, ax.zaxis.pane.set_alpha(set_background_pane_alpha) def plot_line( - xyz: np.ndarray, - symmetric: bool = False, - color=color, - linewidth=0.4, - **kwargs + xyz: np.ndarray, + symmetric: bool = False, + color=color, + linewidth=0.4, + **kwargs, ): if symmetric: - xyz = np.concatenate([ - xyz, - np.array([[np.nan] * 3]), - xyz * np.array([[1, -1, 1]]) - ], axis=0) + xyz = np.concatenate( + [xyz, np.array([[np.nan] * 3]), xyz * np.array([[1, -1, 1]])], + axis=0, + ) ax.plot( xyz[:, 0], @@ -446,7 +458,7 @@ def plot_line( xyz[:, 2], color=color, linewidth=linewidth, - **kwargs + **kwargs, ) def reshape(x): @@ -472,7 +484,7 @@ def reshape(x): np.stack(wing.mesh_line(x_nondim=xy[0], z_nondim=xy[1]), axis=0), symmetric=wing.symmetric, linewidth=thick_linewidth, - color=color_to_use + color=color_to_use, ) ### Top and Bottom lines @@ -481,16 +493,26 @@ def reshape(x): thicknesses = np.array([af.local_thickness(x_over_c=x) for af in afs]) plot_line( - np.stack(wing.mesh_line(x_nondim=x, z_nondim=thicknesses / 2, add_camber=True), axis=0), + np.stack( + wing.mesh_line( + x_nondim=x, z_nondim=thicknesses / 2, add_camber=True + ), + axis=0, + ), symmetric=wing.symmetric, linewidth=thin_linewidth, - color=color_to_use + color=color_to_use, ) plot_line( - np.stack(wing.mesh_line(x_nondim=x, z_nondim=-thicknesses / 2, add_camber=True), axis=0), + np.stack( + wing.mesh_line( + x_nondim=x, z_nondim=-thicknesses / 2, add_camber=True + ), + axis=0, + ), symmetric=wing.symmetric, linewidth=thin_linewidth, - color=color_to_use + color=color_to_use, ) ### Airfoils @@ -502,21 +524,43 @@ def reshape(x): origin = reshape(xsec.xyz_le) scale = xsec.chord - line_upper = origin + ( - xsec.airfoil.upper_coordinates()[:, 0].reshape((-1, 1)) * scale * xg_local + - xsec.airfoil.upper_coordinates()[:, 1].reshape((-1, 1)) * scale * zg_local + line_upper = ( + origin + + ( + xsec.airfoil.upper_coordinates()[:, 0].reshape((-1, 1)) + * scale + * xg_local + ) + + ( + xsec.airfoil.upper_coordinates()[:, 1].reshape((-1, 1)) + * scale + * zg_local + ) ) - line_lower = origin + ( - xsec.airfoil.lower_coordinates()[:, 0].reshape((-1, 1)) * scale * xg_local + - xsec.airfoil.lower_coordinates()[:, 1].reshape((-1, 1)) * scale * zg_local + line_lower = ( + origin + + ( + xsec.airfoil.lower_coordinates()[:, 0].reshape((-1, 1)) + * scale + * xg_local + ) + + ( + xsec.airfoil.lower_coordinates()[:, 1].reshape((-1, 1)) + * scale + * zg_local + ) ) for line in [line_upper, line_lower]: plot_line( line, symmetric=wing.symmetric, - linewidth=thick_linewidth if i == 0 or i == len(wing.xsecs) - 1 else thin_linewidth, - color=color_to_use + linewidth=( + thick_linewidth + if i == 0 or i == len(wing.xsecs) - 1 + else thin_linewidth + ), + color=color_to_use, ) ##### Fuselages @@ -537,8 +581,12 @@ def reshape(x): for i, perim in enumerate(perimeters_xyz): plot_line( np.stack(perim, axis=1), - linewidth=thick_linewidth if i == 0 or i == len(fuse.xsecs) - 1 else thin_linewidth, - color=color_to_use + linewidth=( + thick_linewidth + if i == 0 or i == len(fuse.xsecs) - 1 + else thin_linewidth + ), + color=color_to_use, ) ### Centerline @@ -548,18 +596,21 @@ def reshape(x): axis=0, ), linewidth=thin_linewidth, - color=color_to_use + color=color_to_use, ) ### Longerons for theta in fuselage_longeron_theta: plot_line( - np.stack([ - np.array(xsec.get_3D_coordinates(theta=theta)) - for xsec in fuse.xsecs - ], axis=0), + np.stack( + [ + np.array(xsec.get_3D_coordinates(theta=theta)) + for xsec in fuse.xsecs + ], + axis=0, + ), linewidth=thick_linewidth, - color=color_to_use + color=color_to_use, ) ##### Propulsors @@ -575,11 +626,7 @@ def reshape(x): ### Disk if prop.length == 0: plot_line( - np.stack( - prop.get_disk_3D_coordinates(), - axis=1 - ), - color=color_to_use + np.stack(prop.get_disk_3D_coordinates(), axis=1), color=color_to_use ) if set_lims: @@ -602,11 +649,12 @@ def reshape(x): return ax - def draw_three_view(self, - axs=None, - style: str = "shaded", - show: bool = True, - ) -> np.ndarray: + def draw_three_view( + self, + axs=None, + style: str = "shaded", + show: bool = True, + ) -> np.ndarray: """ Draws a standard 4-panel three-view diagram of the airplane using Matplotlib backend. Creates a new figure. @@ -633,10 +681,9 @@ def draw_three_view(self, import matplotlib.pyplot as plt import aerosandbox.tools.pretty_plots as p - preset_view_angles = np.array([ - ["XZ", "-YZ"], - ["XY", "left_isometric"] - ], dtype="O") + preset_view_angles = np.array( + [["XZ", "-YZ"], ["XY", "left_isometric"]], dtype="O" + ) if axs is None: fig, axs = p.figure3d( @@ -647,13 +694,17 @@ def draw_three_view(self, ) else: if not len(axs.shape) == 2: - raise ValueError(f"`axs` must be a 2D array of axes; instead, it is: {axs}.") + raise ValueError( + f"`axs` must be a 2D array of axes; instead, it is: {axs}." + ) if not axs.shape[0] >= preset_view_angles.shape[0]: raise ValueError( - f"`axs` must have at least as many rows as preset_view_angles ({preset_view_angles.shape[0]}).") + f"`axs` must have at least as many rows as preset_view_angles ({preset_view_angles.shape[0]})." + ) if not axs.shape[1] >= preset_view_angles.shape[1]: raise ValueError( - f"`axs` must have at least as many columns as preset_view_angles ({preset_view_angles.shape[1]}).") + f"`axs` must have at least as many columns as preset_view_angles ({preset_view_angles.shape[1]})." + ) for i in range(axs.shape[0]): for j in range(axs.shape[1]): @@ -664,8 +715,10 @@ def draw_three_view(self, self.draw( backend="matplotlib", ax=ax, - set_axis_visibility=False if 'isometric' in preset_view else None, - show=False + set_axis_visibility=( + False if "isometric" in preset_view else None + ), + show=False, ) elif style == "wireframe": if preset_view == "XZ": @@ -677,18 +730,20 @@ def draw_three_view(self, self.draw_wireframe( ax=ax, - set_axis_visibility=False if 'isometric' in preset_view else None, + set_axis_visibility=( + False if "isometric" in preset_view else None + ), fuselage_longeron_theta=fuselage_longeron_theta, - show=False + show=False, ) p.set_preset_3d_view_angle(preset_view) - if preset_view == 'XY' or preset_view == '-XY': + if preset_view == "XY" or preset_view == "-XY": ax.set_zticks([]) - if preset_view == 'XZ' or preset_view == '-XZ': + if preset_view == "XZ" or preset_view == "-XZ": ax.set_yticks([]) - if preset_view == 'YZ' or preset_view == '-YZ': + if preset_view == "YZ" or preset_view == "-YZ": ax.set_xticks([]) axs[1, 0].set_xlabel("$x_g$ [m]") @@ -746,21 +801,15 @@ def aerodynamic_center(self, chord_fraction: float = 0.25): wing_areas = [wing.area(type="projected") for wing in self.wings] ACs = [wing.aerodynamic_center() for wing in self.wings] - wing_AC_area_products = [ - AC * area - for AC, area in zip( - ACs, - wing_areas - ) - ] + wing_AC_area_products = [AC * area for AC, area in zip(ACs, wing_areas)] aerodynamic_center = sum(wing_AC_area_products) / sum(wing_areas) return aerodynamic_center - def with_control_deflections(self, - control_surface_deflection_mappings: Dict[str, float] - ) -> "Airplane": + def with_control_deflections( + self, control_surface_deflection_mappings: Dict[str, float] + ) -> "Airplane": """ Returns a copy of the airplane with the specified control surface deflections applied. @@ -790,10 +839,11 @@ def with_control_deflections(self, return deflected_airplane - def generate_cadquery_geometry(self, - minimum_airfoil_TE_thickness: float = 0.001, - fuselage_tol: float = 1e-4, - ) -> "Workplane": + def generate_cadquery_geometry( + self, + minimum_airfoil_TE_thickness: float = 0.001, + fuselage_tol: float = 1e-4, + ) -> "Workplane": """ Uses the CADQuery library (OpenCASCADE backend) to generate a 3D CAD model of the airplane. @@ -813,7 +863,8 @@ def generate_cadquery_geometry(self, import cadquery as cq except ModuleNotFoundError: raise ModuleNotFoundError( - "The `cadquery` library is required to use this function. Please install it with `pip install cadquery`.") + "The `cadquery` library is required to use this function. Please install it with `pip install cadquery`." + ) solids = [] @@ -826,9 +877,7 @@ def generate_cadquery_geometry(self, af = xsec.airfoil if af.TE_thickness() < minimum_airfoil_TE_thickness: - af = af.set_TE_thickness( - thickness=minimum_airfoil_TE_thickness - ) + af = af.set_TE_thickness(thickness=minimum_airfoil_TE_thickness) LE_index = af.LE_index() @@ -837,19 +886,22 @@ def generate_cadquery_geometry(self, inPlane=cq.Plane( origin=tuple(xsec.xyz_le), xDir=tuple(csys[0]), - normal=tuple(-csys[1]) + normal=tuple(-csys[1]), ) - ).spline( + ) + .spline( listOfXYTuple=[ tuple(xy * xsec.chord) for xy in af.coordinates[:LE_index, :] ] - ).spline( + ) + .spline( listOfXYTuple=[ tuple(xy * xsec.chord) for xy in af.coordinates[LE_index:, :] ] - ).close() + ) + .close() ) wire_collection = xsec_wires[0] @@ -861,10 +913,7 @@ def generate_cadquery_geometry(self, solids.append(loft) if wing.symmetric: - loft = loft.mirror( - mirrorPlane='XZ', - union=False - ) + loft = loft.mirror(mirrorPlane="XZ", union=False) solids.append(loft) @@ -874,29 +923,34 @@ def generate_cadquery_geometry(self, for i, xsec in enumerate(fuse.xsecs): - if xsec.height < fuselage_tol or xsec.width < fuselage_tol: # If the xsec is so small as to effectively be a point - xsec = copy.deepcopy(xsec) # Modify the xsec to be big enough to not error out. + if ( + xsec.height < fuselage_tol or xsec.width < fuselage_tol + ): # If the xsec is so small as to effectively be a point + xsec = copy.deepcopy( + xsec + ) # Modify the xsec to be big enough to not error out. xsec.height = np.maximum(xsec.height, fuselage_tol) xsec.width = np.maximum(xsec.width, fuselage_tol) xsec_wires.append( cq.Workplane( inPlane=cq.Plane( - origin=tuple(xsec.xyz_c), - xDir=(0, 1, 0), - normal=(-1, 0, 0) + origin=tuple(xsec.xyz_c), xDir=(0, 1, 0), normal=(-1, 0, 0) ) - ).spline( + ) + .spline( listOfXYTuple=[ (y - xsec.xyz_c[1], z - xsec.xyz_c[2]) - for x, y, z in zip(*xsec.get_3D_coordinates( - theta=np.linspace( - np.pi / 2, np.pi / 2 + 2 * np.pi, - 181 + for x, y, z in zip( + *xsec.get_3D_coordinates( + theta=np.linspace( + np.pi / 2, np.pi / 2 + 2 * np.pi, 181 + ) ) - )) + ) ] - ).close() + ) + .close() ) wire_collection = xsec_wires[0] @@ -913,10 +967,9 @@ def generate_cadquery_geometry(self, return solid.clean() - def export_cadquery_geometry(self, - filename: Union[Path, str], - minimum_airfoil_TE_thickness: float = 0.001 - ) -> None: + def export_cadquery_geometry( + self, filename: Union[Path, str], minimum_airfoil_TE_thickness: float = 0.001 + ) -> None: """ Exports the airplane geometry to a STEP file. @@ -935,29 +988,20 @@ def export_cadquery_geometry(self, ) solid.objects = [ - o.scale(1000) - for o in solid.objects - ] + o.scale(1000) for o in solid.objects + ] # Default STEP units are mm from cadquery import exporters - exporters.export( - solid, - fname=filename - ) - def export_AVL(self, - filename, - include_fuselages: bool = True - ): + exporters.export(solid, fname=filename) + + def export_AVL(self, filename, include_fuselages: bool = True): # TODO include option for mass file export as well # Use MassProperties.export_AVL_mass... from aerosandbox.aerodynamics.aero_3D.avl import AVL - avl = AVL( - airplane=self, - op_point=None, - xyz_ref=self.xyz_ref - ) + + avl = AVL(airplane=self, op_point=None, xyz_ref=self.xyz_ref) avl.write_avl(filepath=filename) def export_XFLR(self, *args, **kwargs) -> str: @@ -970,18 +1014,19 @@ def export_XFLR(self, *args, **kwargs) -> str: "Please update your code to use `Airplane.export_XFLR5_xml()` instead.\n" "\n" "This function will be removed in a future version of AeroSandbox.", - PendingDeprecationWarning + PendingDeprecationWarning, ) return self.export_XFLR5_xml(*args, **kwargs) - def export_XFLR5_xml(self, - filename: Union[Path, str], - mass_props: MassProperties = None, - include_fuselages: bool = False, - mainwing: Wing = None, - elevator: Wing = None, - fin: Wing = None, - ) -> str: + def export_XFLR5_xml( + self, + filename: Union[Path, str], + mass_props: MassProperties = None, + include_fuselages: bool = False, + mainwing: Wing = None, + elevator: Wing = None, + fin: Wing = None, + ) -> str: """ Exports the airplane geometry to an XFLR5 `.xml` file. To import the `.xml` file into XFLR5, go to File -> Import -> Import from XML. @@ -1026,7 +1071,8 @@ def export_XFLR5_xml(self, pass elif any(wings_specified): raise ValueError( - "If any wings are specified (`mainwing`, `elevator`, `fin`), then all wings must be specified.") + "If any wings are specified (`mainwing`, `elevator`, `fin`), then all wings must be specified." + ) else: n_wings = len(self.wings) @@ -1034,6 +1080,7 @@ def export_XFLR5_xml(self, pass else: import warnings + warnings.warn( "No wings were specified (`mainwing`, `elevator`, `fin`). Automatically assigning the first wing " "to `mainwing`, the second wing to `elevator`, and the third wing to `fin`. If this is not " @@ -1095,8 +1142,8 @@ def export_XFLR5_xml(self, point_mass_xml = ET.SubElement(inertia, "Point_Mass") for k, v in { - "Tag" : f"pm{i}", - "Mass" : point_mass.mass, + "Tag": f"pm{i}", + "Mass": point_mass.mass, "coordinates": ",".join([str(x) for x in point_mass.xyz_cg]), }.items(): subelement = ET.SubElement(point_mass_xml, k) @@ -1110,13 +1157,13 @@ def export_XFLR5_xml(self, xyz_le_root = wing._compute_xyz_of_WingXSec(index=0, x_nondim=0, z_nondim=0) for k, v in { - "Name" : wing.name, - "Type" : "MAINWING", - "Position" : ",".join([str(x) for x in xyz_le_root]), - "Tilt_angle": 0., - "Symetric" : wing.symmetric, # This tag is a typo in XFLR... - "isFin" : "false", - "isSymFin" : "false", + "Name": wing.name, + "Type": "MAINWING", + "Position": ",".join([str(x) for x in xyz_le_root]), + "Tilt_angle": 0.0, + "Symetric": wing.symmetric, # This tag is a typo in XFLR... + "isFin": "false", + "isSymFin": "false", }.items(): subelement = ET.SubElement(wingxml, k) subelement.text = str(v) @@ -1124,7 +1171,8 @@ def export_XFLR5_xml(self, sections = ET.SubElement(wingxml, "Sections") xyz_le_sects_rel = [ - wing._compute_xyz_of_WingXSec(index=i, x_nondim=0, z_nondim=0) - xyz_le_root + wing._compute_xyz_of_WingXSec(index=i, x_nondim=0, z_nondim=0) + - xyz_le_root for i in range(len(wing.xsecs)) ] @@ -1141,15 +1189,15 @@ def export_XFLR5_xml(self, ) for k, v in { - "y_position" : xyz_le_sects_rel[i][1], - "Chord" : xsec.chord, - "xOffset" : xyz_le_sects_rel[i][0], - "Dihedral" : dihedral, - "Twist" : xsec.twist, - "Left_Side_FoilName" : xsec.airfoil.name, + "y_position": xyz_le_sects_rel[i][1], + "Chord": xsec.chord, + "xOffset": xyz_le_sects_rel[i][0], + "Dihedral": dihedral, + "Twist": xsec.twist, + "Left_Side_FoilName": xsec.airfoil.name, "Right_Side_FoilName": xsec.airfoil.name, - "x_number_of_panels" : 8, - "y_number_of_panels" : 8, + "x_number_of_panels": 8, + "y_number_of_panels": 8, }.items(): subelement = ET.SubElement(sect, k) subelement.text = str(v) @@ -1161,13 +1209,13 @@ def export_XFLR5_xml(self, xyz_le_root = wing._compute_xyz_of_WingXSec(index=0, x_nondim=0, z_nondim=0) for k, v in { - "Name" : wing.name, - "Type" : "ELEVATOR", - "Position" : ",".join([str(x) for x in xyz_le_root]), - "Tilt_angle": 0., - "Symetric" : wing.symmetric, # This tag is a typo in XFLR... - "isFin" : "false", - "isSymFin" : "false", + "Name": wing.name, + "Type": "ELEVATOR", + "Position": ",".join([str(x) for x in xyz_le_root]), + "Tilt_angle": 0.0, + "Symetric": wing.symmetric, # This tag is a typo in XFLR... + "isFin": "false", + "isSymFin": "false", }.items(): subelement = ET.SubElement(wingxml, k) subelement.text = str(v) @@ -1175,7 +1223,8 @@ def export_XFLR5_xml(self, sections = ET.SubElement(wingxml, "Sections") xyz_le_sects_rel = [ - wing._compute_xyz_of_WingXSec(index=i, x_nondim=0, z_nondim=0) - xyz_le_root + wing._compute_xyz_of_WingXSec(index=i, x_nondim=0, z_nondim=0) + - xyz_le_root for i in range(len(wing.xsecs)) ] @@ -1192,15 +1241,15 @@ def export_XFLR5_xml(self, ) for k, v in { - "y_position" : xyz_le_sects_rel[i][1], - "Chord" : xsec.chord, - "xOffset" : xyz_le_sects_rel[i][0], - "Dihedral" : dihedral, - "Twist" : xsec.twist, - "Left_Side_FoilName" : xsec.airfoil.name, + "y_position": xyz_le_sects_rel[i][1], + "Chord": xsec.chord, + "xOffset": xyz_le_sects_rel[i][0], + "Dihedral": dihedral, + "Twist": xsec.twist, + "Left_Side_FoilName": xsec.airfoil.name, "Right_Side_FoilName": xsec.airfoil.name, - "x_number_of_panels" : 8, - "y_number_of_panels" : 8, + "x_number_of_panels": 8, + "y_number_of_panels": 8, }.items(): subelement = ET.SubElement(sect, k) subelement.text = str(v) @@ -1212,13 +1261,13 @@ def export_XFLR5_xml(self, xyz_le_root = wing._compute_xyz_of_WingXSec(index=0, x_nondim=0, z_nondim=0) for k, v in { - "Name" : wing.name, - "Type" : "FIN", - "Position" : ",".join([str(x) for x in xyz_le_root]), - "Tilt_angle": 0., - "Symetric" : "true", # This tag is a typo in XFLR... - "isFin" : "true", - "isSymFin" : wing.symmetric, + "Name": wing.name, + "Type": "FIN", + "Position": ",".join([str(x) for x in xyz_le_root]), + "Tilt_angle": 0.0, + "Symetric": "true", # This tag is a typo in XFLR... + "isFin": "true", + "isSymFin": wing.symmetric, }.items(): subelement = ET.SubElement(wingxml, k) subelement.text = str(v) @@ -1226,7 +1275,8 @@ def export_XFLR5_xml(self, sections = ET.SubElement(wingxml, "Sections") xyz_le_sects_rel = [ - wing._compute_xyz_of_WingXSec(index=i, x_nondim=0, z_nondim=0) - xyz_le_root + wing._compute_xyz_of_WingXSec(index=i, x_nondim=0, z_nondim=0) + - xyz_le_root for i in range(len(wing.xsecs)) ] @@ -1243,15 +1293,15 @@ def export_XFLR5_xml(self, ) for k, v in { - "y_position" : xyz_le_sects_rel[i][2], - "Chord" : xsec.chord, - "xOffset" : xyz_le_sects_rel[i][0], - "Dihedral" : dihedral, - "Twist" : xsec.twist, - "Left_Side_FoilName" : xsec.airfoil.name, + "y_position": xyz_le_sects_rel[i][2], + "Chord": xsec.chord, + "xOffset": xyz_le_sects_rel[i][0], + "Dihedral": dihedral, + "Twist": xsec.twist, + "Left_Side_FoilName": xsec.airfoil.name, "Right_Side_FoilName": xsec.airfoil.name, - "x_number_of_panels" : 8, - "y_number_of_panels" : 8, + "x_number_of_panels": 8, + "y_number_of_panels": 8, }.items(): subelement = ET.SubElement(sect, k) subelement.text = str(v) @@ -1274,20 +1324,17 @@ def indent(elem, level=0): indent(root) - xml_string = ET.tostring( - root, - encoding="UTF-8", - xml_declaration=True - ).decode() + xml_string = ET.tostring(root, encoding="UTF-8", xml_declaration=True).decode() with open(filename, "w+") as f: f.write(xml_string) return xml_string - def export_OpenVSP_vspscript(self, - filename: Union[Path, str], - ) -> str: + def export_OpenVSP_vspscript( + self, + filename: Union[Path, str], + ) -> str: """ Exports the airplane geometry to a `*.vspscript` file compatible with OpenVSP. To import the `.vspscript` file into OpenVSP: @@ -1299,7 +1346,9 @@ def export_OpenVSP_vspscript(self, Returns: A string of the file contents, and also saves the file to the specified filename """ - from aerosandbox.geometry.openvsp_io.asb_to_openvsp.airplane_vspscript_generator import generate_airplane + from aerosandbox.geometry.openvsp_io.asb_to_openvsp.airplane_vspscript_generator import ( + generate_airplane, + ) vspscript_code = generate_airplane(self) @@ -1309,16 +1358,15 @@ def export_OpenVSP_vspscript(self, return vspscript_code -if __name__ == '__main__': +if __name__ == "__main__": import aerosandbox as asb + # import aerosandbox.numpy as np import aerosandbox.tools.units as u - def ft(feet, inches=0): # Converts feet (and inches) to meters return feet * u.foot + inches * u.inch - naca2412 = asb.Airfoil("naca2412") naca0012 = asb.Airfoil("naca0012") @@ -1328,11 +1376,7 @@ def ft(feet, inches=0): # Converts feet (and inches) to meters asb.Wing( name="Wing", xsecs=[ - asb.WingXSec( - xyz_le=[0, 0, 0], - chord=ft(5, 4), - airfoil=naca2412 - ), + asb.WingXSec(xyz_le=[0, 0, 0], chord=ft(5, 4), airfoil=naca2412), asb.WingXSec( xyz_le=[0, ft(7), ft(7) * np.sind(1)], chord=ft(5, 4), @@ -1342,21 +1386,21 @@ def ft(feet, inches=0): # Converts feet (and inches) to meters name="aileron", symmetric=False, hinge_point=0.8, - deflection=0 + deflection=0, ) - ] + ], ), asb.WingXSec( xyz_le=[ ft(4, 3 / 4) - ft(3, 8 + 1 / 2), ft(33, 4) / 2, - ft(33, 4) / 2 * np.sind(1) + ft(33, 4) / 2 * np.sind(1), ], chord=ft(3, 8 + 1 / 2), - airfoil=naca0012 - ) + airfoil=naca0012, + ), ], - symmetric=True + symmetric=True, ), asb.Wing( name="Horizontal Stabilizer", @@ -1371,18 +1415,18 @@ def ft(feet, inches=0): # Converts feet (and inches) to meters name="elevator", symmetric=True, hinge_point=0.75, - deflection=0 + deflection=0, ) - ] + ], ), asb.WingXSec( xyz_le=[ft(1), ft(10) / 2, 0], chord=ft(2, 4 + 3 / 8), airfoil=naca0012, - twist=-2 - ) + twist=-2, + ), ], - symmetric=True + symmetric=True, ).translate([ft(13, 3), 0, ft(-2)]), asb.Wing( name="Vertical Stabilizer", @@ -1398,19 +1442,17 @@ def ft(feet, inches=0): # Converts feet (and inches) to meters airfoil=naca0012, control_surfaces=[ asb.ControlSurface( - name="rudder", - hinge_point=0.75, - deflection=0 + name="rudder", hinge_point=0.75, deflection=0 ) - ] + ], ), asb.WingXSec( xyz_le=[ft(0, 8), 0, ft(5)], chord=ft(2, 8), airfoil=naca0012, ), - ] - ).translate([ft(16, 11) - ft(3, 8), 0, ft(-2)]) + ], + ).translate([ft(16, 11) - ft(3, 8), 0, ft(-2)]), ], fuselages=[ asb.Fuselage( @@ -1446,7 +1488,7 @@ def ft(feet, inches=0): # Converts feet (and inches) to meters ), ] ).translate([ft(-5), 0, ft(-3)]) - ] + ], ) airplane.draw_three_view() diff --git a/aerosandbox/geometry/common.py b/aerosandbox/geometry/common.py index e4a405e15..26e12c1cc 100644 --- a/aerosandbox/geometry/common.py +++ b/aerosandbox/geometry/common.py @@ -14,19 +14,28 @@ def reflect_over_XZ_plane(input_vector): return input_vector * np.array([1, -1, 1]) elif len(shape) == 2: if not shape[1] == 3: - raise ValueError("The function expected either a 3-element vector or a Nx3 array!") + raise ValueError( + "The function expected either a 3-element vector or a Nx3 array!" + ) return input_vector * np.array([1, -1, 1]) else: - raise ValueError("The function expected either a 3-element vector or a Nx3 array!") + raise ValueError( + "The function expected either a 3-element vector or a Nx3 array!" + ) else: if input_vector.shape[1] == 1: return input_vector * np.array([1, -1, 1]) elif input_vector.shape[1] == 3: - return np.stack(( - input_vector[:, 0], - -1 * input_vector[:, 1], - input_vector[:, 2], - ), axis=1) + return np.stack( + ( + input_vector[:, 0], + -1 * input_vector[:, 1], + input_vector[:, 2], + ), + axis=1, + ) else: - raise ValueError("This function expected either a 3-element vector or an Nx3 array!") + raise ValueError( + "This function expected either a 3-element vector or an Nx3 array!" + ) diff --git a/aerosandbox/geometry/fuselage.py b/aerosandbox/geometry/fuselage.py index 5256eb35f..15ff35b0c 100644 --- a/aerosandbox/geometry/fuselage.py +++ b/aerosandbox/geometry/fuselage.py @@ -26,13 +26,14 @@ class Fuselage(AeroSandboxObject): """ - def __init__(self, - name: Optional[str] = "Untitled", - xsecs: List['FuselageXSec'] = None, - color: Optional[Union[str, Tuple[float]]] = None, - analysis_specific_options: Optional[Dict[type, Dict[str, Any]]] = None, - **kwargs, # Only to allow for capturing of deprecated arguments, don't use this. - ): + def __init__( + self, + name: Optional[str] = "Untitled", + xsecs: List["FuselageXSec"] = None, + color: Optional[Union[str, Tuple[float]]] = None, + analysis_specific_options: Optional[Dict[type, Dict[str, Any]]] = None, + **kwargs, # Only to allow for capturing of deprecated arguments, don't use this. + ): """ Defines a new fuselage object. @@ -83,7 +84,7 @@ def __init__(self, """ ### Set defaults if xsecs is None: - xsecs: List['FuselageXSec'] = [] + xsecs: List["FuselageXSec"] = [] if analysis_specific_options is None: analysis_specific_options = {} @@ -94,42 +95,37 @@ def __init__(self, self.analysis_specific_options = analysis_specific_options ### Handle deprecated parameters - if 'symmetric' in locals(): + if "symmetric" in locals(): raise DeprecationWarning( - "The `symmetric` argument for Fuselage objects is deprecated. Make your fuselages separate instead!") + "The `symmetric` argument for Fuselage objects is deprecated. Make your fuselages separate instead!" + ) - if 'xyz_le' in locals(): + if "xyz_le" in locals(): import warnings + warnings.warn( "The `xyz_le` input for Fuselage is pending deprecation and will be removed in a future version. Use Fuselage().translate(xyz) instead.", - stacklevel=2 + stacklevel=2, ) - self.xsecs = [ - xsec.translate(xyz_le) - for xsec in self.xsecs - ] + self.xsecs = [xsec.translate(xyz_le) for xsec in self.xsecs] def __repr__(self) -> str: n_xsecs = len(self.xsecs) return f"Fuselage '{self.name}' ({len(self.xsecs)} {'xsec' if n_xsecs == 1 else 'xsecs'})" - def add_loft(self, - kind: str, - to_xsec: 'FuselageXSec', - from_xsec: 'FuselageXSec' = None, - n_points: int = 5, - spacing: Callable[[float, float, int], np.ndarray] = np.cosspace, - ) -> "Fuselage": - raise NotImplementedError # Function under construction! + def add_loft( + self, + kind: str, + to_xsec: "FuselageXSec", + from_xsec: "FuselageXSec" = None, + n_points: int = 5, + spacing: Callable[[float, float, int], np.ndarray] = np.cosspace, + ) -> "Fuselage": + raise NotImplementedError # Function under construction! ### Set defaults if from_xsec is None: if len(self.xsecs) == 0: - from_xsec = FuselageXSec( - xyz_c=[0, 0, 0], - width=0, - height=0, - shape=2 - ) + from_xsec = FuselageXSec(xyz_c=[0, 0, 0], width=0, height=0, shape=2) else: from_xsec = self.xsecs[-1] @@ -161,9 +157,7 @@ def add_loft(self, self.xsecs.extend(new_xsecs) - def translate(self, - xyz: Union[np.ndarray, List[float]] - ) -> "Fuselage": + def translate(self, xyz: Union[np.ndarray, List[float]]) -> "Fuselage": """ Translates the entire Fuselage by a certain amount. @@ -174,10 +168,7 @@ def translate(self, """ new_fuse = copy.copy(self) - new_fuse.xsecs = [ - xsec.translate(xyz) - for xsec in new_fuse.xsecs - ] + new_fuse.xsecs = [xsec.translate(xyz) for xsec in new_fuse.xsecs] return new_fuse def area_wetted(self) -> float: @@ -196,9 +187,10 @@ def area_wetted(self) -> float: return area - def area_projected(self, - type: str = "XY", - ) -> float: + def area_projected( + self, + type: str = "XY", + ) -> float: """ Returns the area of the fuselage as projected onto one of the principal planes. @@ -238,8 +230,8 @@ def area_base(self) -> float: return self.xsecs[-1].xsec_area() def fineness_ratio( - self, - assumed_shape="cylinder", + self, + assumed_shape="cylinder", ) -> float: """ Approximates the fineness ratio using the volume and length. The fineness ratio of a fuselage is defined as: @@ -258,15 +250,11 @@ def fineness_ratio( """ if assumed_shape == "cylinder": - return np.sqrt( - self.length() ** 3 / self.volume() * np.pi / 4 - ) + return np.sqrt(self.length() ** 3 / self.volume() * np.pi / 4) elif assumed_shape == "sears-haack": length = self.length() - r_max = np.sqrt( - self.volume() / length / (3 * np.pi ** 2 / 16) - ) + r_max = np.sqrt(self.volume() / length / (3 * np.pi**2 / 16)) return length / r_max def length(self) -> float: @@ -277,9 +265,7 @@ def length(self) -> float: """ return np.fabs(self.xsecs[-1].xyz_c[0] - self.xsecs[0].xyz_c[0]) - def volume(self, - _sectional: bool = False - ) -> Union[float, List[float]]: + def volume(self, _sectional: bool = False) -> Union[float, List[float]]: """ Computes the volume of the Fuselage. @@ -292,25 +278,17 @@ def volume(self, The computed volume. """ - xsec_areas = [ - xsec.xsec_area() - for xsec in self.xsecs - ] + xsec_areas = [xsec.xsec_area() for xsec in self.xsecs] separations = [ xsec_b.xyz_c[0] - xsec_a.xyz_c[0] - for xsec_a, xsec_b in zip( - self.xsecs[:-1], - self.xsecs[1:] - ) + for xsec_a, xsec_b in zip(self.xsecs[:-1], self.xsecs[1:]) ] sectional_volumes = [ separation / 3 * (area_a + area_b + (area_a * area_b + 1e-100) ** 0.5) for area_a, area_b, separation in zip( - xsec_areas[1:], - xsec_areas[:-1], - separations + xsec_areas[1:], xsec_areas[:-1], separations ) ] @@ -321,9 +299,10 @@ def volume(self, else: return volume - def x_centroid_projected(self, - type: str = "XY", - ) -> float: + def x_centroid_projected( + self, + type: str = "XY", + ) -> float: """ Returns the x_g coordinate of the centroid of the planform area. @@ -364,10 +343,11 @@ def x_centroid_projected(self, x_centroid = total_x_area_product / total_area return x_centroid - def mesh_body(self, - method="quad", - tangential_resolution: int = 36, - ) -> Tuple[np.ndarray, np.ndarray]: + def mesh_body( + self, + method="quad", + tangential_resolution: int = 36, + ) -> Tuple[np.ndarray, np.ndarray]: """ Meshes the fuselage as a solid (thickened) body. @@ -396,14 +376,9 @@ def mesh_body(self, t = np.linspace(0, 2 * np.pi, tangential_resolution + 1)[:-1] - points = np.concatenate([ - np.stack( - xsec.get_3D_coordinates(theta=t), - axis=1 - ) - for xsec in self.xsecs - ], - axis=0 + points = np.concatenate( + [np.stack(xsec.get_3D_coordinates(theta=t), axis=1) for xsec in self.xsecs], + axis=0, ) faces = [] @@ -435,10 +410,11 @@ def add_face(*indices): return points, faces - def mesh_line(self, - y_nondim: Union[float, List[float]] = 0., - z_nondim: Union[float, List[float]] = 0., - ) -> List[np.ndarray]: + def mesh_line( + self, + y_nondim: Union[float, List[float]] = 0.0, + z_nondim: Union[float, List[float]] = 0.0, + ) -> List[np.ndarray]: """ Returns points along a line that goes through each of the FuselageXSec objects in this Fuselage. @@ -491,8 +467,8 @@ def mesh_line(self, xsec_z_nondim = z_nondim xsec_point = origin + ( - xsec_y_nondim * (xsec.width / 2) * yg_local + - xsec_z_nondim * (xsec.height / 2) * zg_local + xsec_y_nondim * (xsec.width / 2) * yg_local + + xsec_z_nondim * (xsec.height / 2) * zg_local ) points_on_line.append(xsec_point) @@ -510,6 +486,7 @@ def draw(self, *args, **kwargs): """ from aerosandbox.geometry.airplane import Airplane + return Airplane(fuselages=[self]).draw(*args, **kwargs) def draw_wireframe(self, *args, **kwargs): @@ -524,6 +501,7 @@ def draw_wireframe(self, *args, **kwargs): """ from aerosandbox.geometry.airplane import Airplane + return Airplane(fuselages=[self]).draw_wireframe(*args, **kwargs) def draw_three_view(self, *args, **kwargs): @@ -538,12 +516,14 @@ def draw_three_view(self, *args, **kwargs): """ from aerosandbox.geometry.airplane import Airplane + return Airplane(fuselages=[self]).draw_three_view(*args, **kwargs) - def subdivide_sections(self, - ratio: int, - spacing_function: Callable[[float, float, float], np.ndarray] = np.linspace - ) -> "Fuselage": + def subdivide_sections( + self, + ratio: int, + spacing_function: Callable[[float, float, float], np.ndarray] = np.linspace, + ) -> "Fuselage": """ Generates a new Fuselage that subdivides the existing sections of this Fuselage into several smaller ones. Splits each section into N=`ratio` smaller subsections by inserting new cross-sections (xsecs) as needed. @@ -589,10 +569,12 @@ def subdivide_sections(self, return Fuselage( name=self.name, xsecs=new_xsecs, - analysis_specific_options=self.analysis_specific_options + analysis_specific_options=self.analysis_specific_options, ) - def _compute_frame_of_FuselageXSec(self, index: int) -> Tuple[np.ndarray, np.ndarray, np.ndarray]: + def _compute_frame_of_FuselageXSec( + self, index: int + ) -> Tuple[np.ndarray, np.ndarray, np.ndarray]: """ Computes the local frame of a FuselageXSec, given the index of the FuselageXSec in the Fuselage.xsecs list. @@ -605,10 +587,11 @@ def _compute_frame_of_FuselageXSec(self, index: int) -> Tuple[np.ndarray, np.nda zg_local: The z-axis of the local coordinate frame, in aircraft geometry axes. """ import warnings + warnings.warn( "Fuselage._compute_frame_of_FuselageXSec() is deprecated. " "Use FuselageXSec.compute_frame() instead.", - DeprecationWarning + DeprecationWarning, ) return self.xsecs[index].compute_frame() @@ -619,15 +602,16 @@ class FuselageXSec(AeroSandboxObject): Definition for a fuselage cross-section ("X-section"). """ - def __init__(self, - xyz_c: Union[np.ndarray, List[float]] = None, - xyz_normal: Union[np.ndarray, List[float]] = None, - radius: float = None, - width: float = None, - height: float = None, - shape: float = 2., - analysis_specific_options: Optional[Dict[type, Dict[str, Any]]] = None, - ): + def __init__( + self, + xyz_c: Union[np.ndarray, List[float]] = None, + xyz_normal: Union[np.ndarray, List[float]] = None, + radius: float = None, + width: float = None, + height: float = None, + shape: float = 2.0, + analysis_specific_options: Optional[Dict[type, Dict[str, Any]]] = None, + ): """ Defines a new Fuselage cross-section. @@ -712,18 +696,15 @@ def __init__(self, """ ### Set defaults if xyz_c is None: - xyz_c = np.array([0., 0., 0.]) + xyz_c = np.array([0.0, 0.0, 0.0]) if xyz_normal is None: - xyz_normal = np.array([1., 0., 0.]) # points backwards + xyz_normal = np.array([1.0, 0.0, 0.0]) # points backwards if analysis_specific_options is None: analysis_specific_options = {} ### Set width and height - radius_specified = (radius is not None) - width_height_specified = [ - (width is not None), - (height is not None) - ] + radius_specified = radius is not None + width_height_specified = [(width is not None), (height is not None)] if radius_specified: if any(width_height_specified): @@ -786,7 +767,7 @@ def xsec_area(self): Returns: """ - area = self.width * self.height / (self.shape ** -1.8717618013591173 + 1) + area = self.width * self.height / (self.shape**-1.8717618013591173 + 1) return area @@ -829,28 +810,26 @@ def xsec_perimeter(self): return 2 * self.height elif self.height == 0: return 2 * self.width - except RuntimeError: # Will error if width and height are optimization variables, as truthiness is indeterminate + except ( + RuntimeError + ): # Will error if width and height are optimization variables, as truthiness is indeterminate pass s = self.shape h = np.maximum( (self.width + 1e-16) / (self.height + 1e-16), - (self.height + 1e-16) / (self.width + 1e-16) - ) - nondim_quadrant_perimeter = ( - h + (((((s - 0.88487077) * h + 0.2588574 / h) ** np.exp(s / -0.90069205)) + h) + 0.09919785) ** ( - -1.4812293 / s) + (self.height + 1e-16) / (self.width + 1e-16), ) + nondim_quadrant_perimeter = h + ( + ((((s - 0.88487077) * h + 0.2588574 / h) ** np.exp(s / -0.90069205)) + h) + + 0.09919785 + ) ** (-1.4812293 / s) perimeter = 2 * nondim_quadrant_perimeter * np.minimum(self.width, self.height) return np.where( self.width == 0, 2 * self.height, - np.where( - self.height == 0, - 2 * self.width, - perimeter - ) + np.where(self.height == 0, 2 * self.width, perimeter), ) def compute_frame(self) -> Tuple[np.ndarray, np.ndarray, np.ndarray]: @@ -880,9 +859,9 @@ def compute_frame(self) -> Tuple[np.ndarray, np.ndarray, np.ndarray]: return xg_local, yg_local, zg_local - def get_3D_coordinates(self, - theta: Union[float, np.ndarray] = None - ) -> Tuple[Union[float, np.ndarray]]: + def get_3D_coordinates( + self, theta: Union[float, np.ndarray] = None + ) -> Tuple[Union[float, np.ndarray]]: """ Samples points from the perimeter of this FuselageXSec. @@ -910,11 +889,7 @@ def get_3D_coordinates(self, """ ### Set defaults if theta is None: - theta = np.linspace( - 0, - 2 * np.pi, - 60 + 1 - )[:-1] + theta = np.linspace(0, 2 * np.pi, 60 + 1)[:-1] st = np.sin(np.mod(theta, 2 * np.pi)) ct = np.cos(np.mod(theta, 2 * np.pi)) @@ -930,9 +905,7 @@ def get_3D_coordinates(self, self.xyz_c[2] + y * yg_local[2] + z * zg_local[2], ) - def equivalent_radius(self, - preserve="area" - ) -> float: + def equivalent_radius(self, preserve="area") -> float: """ Computes an equivalent radius for non-circular cross-sections. This may be necessary when doing analysis that uses axisymmetric assumptions. @@ -954,13 +927,11 @@ def equivalent_radius(self, if preserve == "area": return (self.xsec_area() / np.pi + 1e-16) ** 0.5 elif preserve == "perimeter": - return (self.xsec_perimeter() / (2 * np.pi)) + return self.xsec_perimeter() / (2 * np.pi) else: raise ValueError("Bad value of `preserve`!") - def translate(self, - xyz: Union[np.ndarray, List[float]] - ) -> "FuselageXSec": + def translate(self, xyz: Union[np.ndarray, List[float]]) -> "FuselageXSec": """ Returns a copy of this FuselageXSec that has been translated by `xyz`. @@ -975,23 +946,18 @@ def translate(self, return new_xsec -if __name__ == '__main__': +if __name__ == "__main__": fuse = Fuselage( xsecs=[ FuselageXSec( xyz_c=[0, 0, 1], radius=0, ), - FuselageXSec( - xyz_c=[1, 0, 1], - width=0.5, - height=0.2, - shape=5 - ), + FuselageXSec(xyz_c=[1, 0, 1], width=0.5, height=0.2, shape=5), FuselageXSec( xyz_c=[2, 0, 1], radius=0.2, - ) + ), ] ).translate([0, 0, 2]) fuse.draw() diff --git a/aerosandbox/geometry/mesh_utilities.py b/aerosandbox/geometry/mesh_utilities.py index f77374417..7d1c7967c 100644 --- a/aerosandbox/geometry/mesh_utilities.py +++ b/aerosandbox/geometry/mesh_utilities.py @@ -15,7 +15,7 @@ def stack_meshes( - *meshes: Tuple[Tuple[np.ndarray, np.ndarray]] + *meshes: Tuple[Tuple[np.ndarray, np.ndarray]] ) -> Tuple[np.ndarray, np.ndarray]: """ Takes in a series of tuples (points, faces) and merges them into a single tuple (points, faces). All (points, @@ -47,19 +47,12 @@ def stack_meshes( return points, faces else: - points, faces = stack_meshes( - meshes[0], - meshes[1] - ) - return stack_meshes( - (points, faces), - *meshes[2:] - ) + points, faces = stack_meshes(meshes[0], meshes[1]) + return stack_meshes((points, faces), *meshes[2:]) def convert_mesh_to_polydata_format( - points: np.ndarray, - faces: np.ndarray + points: np.ndarray, faces: np.ndarray ) -> Tuple[np.ndarray, np.ndarray]: """ PyVista uses a slightly different convention for the standard (points, faces) format as described above. They @@ -79,9 +72,6 @@ def convert_mesh_to_polydata_format( (points, faces), except that `faces` is now in a pyvista.PolyData compatible format. """ - faces = [ - [len(face), *face] - for face in faces - ] + faces = [[len(face), *face] for face in faces] faces = np.reshape(np.array(faces), -1) return points, faces diff --git a/aerosandbox/geometry/nosecone_shapes/haack.py b/aerosandbox/geometry/nosecone_shapes/haack.py index b8ea81b49..958651001 100644 --- a/aerosandbox/geometry/nosecone_shapes/haack.py +++ b/aerosandbox/geometry/nosecone_shapes/haack.py @@ -1,45 +1,31 @@ import aerosandbox.numpy as np -def haack_series( - x_over_L: np.ndarray, - C=1 / 3 -): +def haack_series(x_over_L: np.ndarray, C=1 / 3): theta = np.arccos(1 - 2 * x_over_L) - radius = (( - theta - np.sin(2 * theta) / 2 + C * np.sin(theta) ** 3 - ) / np.pi) ** 0.5 + radius = ((theta - np.sin(2 * theta) / 2 + C * np.sin(theta) ** 3) / np.pi) ** 0.5 return radius def karman( - x_over_L: np.ndarray, + x_over_L: np.ndarray, ): - return haack_series( - x_over_L=x_over_L, - C=0 - ) + return haack_series(x_over_L=x_over_L, C=0) def LV_haack( - x_over_L: np.ndarray, + x_over_L: np.ndarray, ): - return haack_series( - x_over_L=x_over_L, - C=1 / 3 - ) + return haack_series(x_over_L=x_over_L, C=1 / 3) def tangent( - x_over_L: np.ndarray, + x_over_L: np.ndarray, ): - return haack_series( - x_over_L=x_over_L, - C=2 / 3 - ) + return haack_series(x_over_L=x_over_L, C=2 / 3) -if __name__ == '__main__': +if __name__ == "__main__": import matplotlib.pyplot as plt import aerosandbox.tools.pretty_plots as p @@ -54,7 +40,5 @@ def tangent( p.equal() p.show_plot( - f"Nosecone Haack Series\nFineness Ratio $FR = {FR}$", - "Length $x$", - "Radius $r$" + f"Nosecone Haack Series\nFineness Ratio $FR = {FR}$", "Length $x$", "Radius $r$" ) diff --git a/aerosandbox/geometry/openvsp_io/asb_to_openvsp/_utilities.py b/aerosandbox/geometry/openvsp_io/asb_to_openvsp/_utilities.py index 76cb39627..6c5d7f56c 100644 --- a/aerosandbox/geometry/openvsp_io/asb_to_openvsp/_utilities.py +++ b/aerosandbox/geometry/openvsp_io/asb_to_openvsp/_utilities.py @@ -13,8 +13,8 @@ def wrap_script( - script: str, - set_geom_draw_type_to_shade: bool = True, + script: str, + set_geom_draw_type_to_shade: bool = True, ) -> str: """ Wraps the internal parts of a VSPScript file with a main() function. @@ -36,7 +36,9 @@ def wrap_script( Returns: The script, wrapped with a main() function. """ - script = script + """\ + script = ( + script + + """\ //==== Check For API Errors ====// while ( GetNumTotalErrors() > 0 ) @@ -45,9 +47,12 @@ def wrap_script( Print( err.GetErrorString() ); } """ + ) if set_geom_draw_type_to_shade: - script = script + """\ + script = ( + script + + """\ { array @geomids = FindGeoms(); @@ -59,8 +64,10 @@ def wrap_script( } """ + ) import aerosandbox as asb + return f"""\ // This *.vspscript file was automatically generated by AeroSandbox {asb.__version__} // using syntax tested on OpenVSP {_openvsp_version}. @@ -73,7 +80,7 @@ def wrap_script( """ -if __name__ == '__main__': +if __name__ == "__main__": print(wrap_script("hello\nworld")) # Expected: # // This file was automatically generated by AeroSandbox 4.2.0 diff --git a/aerosandbox/geometry/openvsp_io/asb_to_openvsp/airplane_vspscript_generator.py b/aerosandbox/geometry/openvsp_io/asb_to_openvsp/airplane_vspscript_generator.py index e5e5455ba..3aadde380 100644 --- a/aerosandbox/geometry/openvsp_io/asb_to_openvsp/airplane_vspscript_generator.py +++ b/aerosandbox/geometry/openvsp_io/asb_to_openvsp/airplane_vspscript_generator.py @@ -2,14 +2,20 @@ from aerosandbox.geometry.airplane import Airplane from textwrap import indent, dedent from aerosandbox.geometry.openvsp_io.asb_to_openvsp import _utilities -from aerosandbox.geometry.openvsp_io.asb_to_openvsp.wing_vspscript_generator import generate_wing -from aerosandbox.geometry.openvsp_io.asb_to_openvsp.fuselage_vspscript_generator import generate_fuselage -from aerosandbox.geometry.openvsp_io.asb_to_openvsp.propulsor_vspscript_generator import generate_propulsor +from aerosandbox.geometry.openvsp_io.asb_to_openvsp.wing_vspscript_generator import ( + generate_wing, +) +from aerosandbox.geometry.openvsp_io.asb_to_openvsp.fuselage_vspscript_generator import ( + generate_fuselage, +) +from aerosandbox.geometry.openvsp_io.asb_to_openvsp.propulsor_vspscript_generator import ( + generate_propulsor, +) def generate_airplane( - airplane: Airplane, - include_main=True, + airplane: Airplane, + include_main=True, ) -> str: """ Generates a VSPScript file for an Airplane object. @@ -49,7 +55,7 @@ def generate_airplane( return script -if __name__ == '__main__': +if __name__ == "__main__": from aerosandbox.geometry.wing import Wing, WingXSec from aerosandbox.geometry.airfoil.airfoil import Airfoil @@ -59,30 +65,21 @@ def generate_airplane( symmetric=True, xsecs=[ WingXSec( - xyz_le=[0.5, 0, 0], - chord=1.1, - twist=5, - airfoil=Airfoil(name="dae11") + xyz_le=[0.5, 0, 0], chord=1.1, twist=5, airfoil=Airfoil(name="dae11") ), WingXSec( - xyz_le=[1, 2, 0], - chord=0.9, - twist=5, - airfoil=Airfoil(name="NACA4412") + xyz_le=[1, 2, 0], chord=0.9, twist=5, airfoil=Airfoil(name="NACA4412") ), WingXSec( - xyz_le=[2, 5, 0], - chord=0.5, - twist=0, - airfoil=Airfoil(name="NACA3412") + xyz_le=[2, 5, 0], chord=0.5, twist=0, airfoil=Airfoil(name="NACA3412") ), WingXSec( xyz_le=[2.5, 5.5, 1], chord=0.25, twist=0, - airfoil=Airfoil(name="NACA2412") - ) - ] + airfoil=Airfoil(name="NACA2412"), + ), + ], ) from aerosandbox.geometry.fuselage import Fuselage, FuselageXSec @@ -94,13 +91,13 @@ def generate_airplane( name="Fuse", xsecs=[ FuselageXSec( - xyz_c=[xi, 0, 0.05 * xi ** 2], + xyz_c=[xi, 0, 0.05 * xi**2], width=2 * af.local_thickness(xi), height=af.local_thickness(xi), - shape=4 + shape=4, ) for xi in x - ] + ], ) from aerosandbox.geometry.propulsor import Propulsor @@ -113,10 +110,7 @@ def generate_airplane( ) airplane = Airplane( - name="Aircraft", - wings=[wing], - fuselages=[fuse], - propulsors=[prop] + name="Aircraft", wings=[wing], fuselages=[fuse], propulsors=[prop] ) print(generate_airplane(airplane)) diff --git a/aerosandbox/geometry/openvsp_io/asb_to_openvsp/fuselage_vspscript_generator.py b/aerosandbox/geometry/openvsp_io/asb_to_openvsp/fuselage_vspscript_generator.py index 254459205..6d7eeaa45 100644 --- a/aerosandbox/geometry/openvsp_io/asb_to_openvsp/fuselage_vspscript_generator.py +++ b/aerosandbox/geometry/openvsp_io/asb_to_openvsp/fuselage_vspscript_generator.py @@ -5,9 +5,9 @@ def generate_fuselage( - fuselage: Fuselage, - include_main=True, - continuity_type: str = "C2", + fuselage: Fuselage, + include_main=True, + continuity_type: str = "C2", ) -> str: """ Generates a VSPScript file for a Fuselage object. @@ -115,7 +115,7 @@ def generate_fuselage( return script -if __name__ == '__main__': +if __name__ == "__main__": import aerosandbox as asb import aerosandbox.numpy as np @@ -126,13 +126,13 @@ def generate_fuselage( name="Fuse", xsecs=[ FuselageXSec( - xyz_c=[xi, 0, 0.05 * xi ** 2], + xyz_c=[xi, 0, 0.05 * xi**2], width=2 * af.local_thickness(xi), height=af.local_thickness(xi), - shape=4 + shape=4, ) for xi in x - ] + ], ) print(generate_fuselage(fuse)) diff --git a/aerosandbox/geometry/openvsp_io/asb_to_openvsp/propulsor_vspscript_generator.py b/aerosandbox/geometry/openvsp_io/asb_to_openvsp/propulsor_vspscript_generator.py index e01584c39..54bc4c98a 100644 --- a/aerosandbox/geometry/openvsp_io/asb_to_openvsp/propulsor_vspscript_generator.py +++ b/aerosandbox/geometry/openvsp_io/asb_to_openvsp/propulsor_vspscript_generator.py @@ -4,10 +4,7 @@ from aerosandbox.geometry.openvsp_io.asb_to_openvsp import _utilities -def generate_propulsor( - propulsor: Propulsor, - include_main=True -) -> str: +def generate_propulsor(propulsor: Propulsor, include_main=True) -> str: """ Generates a VSPScript file for a Wing object. @@ -26,26 +23,26 @@ def generate_propulsor( """ ### Compute the orientation of the propulsor - desired_normal = np.array(propulsor.xyz_normal) / np.linalg.norm(propulsor.xyz_normal) + desired_normal = np.array(propulsor.xyz_normal) / np.linalg.norm( + propulsor.xyz_normal + ) if np.allclose(desired_normal, np.array([1, 0, 0]), atol=1e-8): y_rot_deg = 180 z_rot_deg = 0 else: import aerosandbox as asb + opti = asb.Opti() y_rot = opti.variable(init_guess=0, lower_bound=-np.pi, upper_bound=np.pi) z_rot = opti.variable(init_guess=0, lower_bound=-np.pi, upper_bound=np.pi) - rot = ( - np.rotation_matrix_3D(angle=y_rot, axis="y") @ - np.rotation_matrix_3D(angle=z_rot, axis="z") + rot = np.rotation_matrix_3D(angle=y_rot, axis="y") @ np.rotation_matrix_3D( + angle=z_rot, axis="z" ) normal = rot @ np.array([-1, 0, 0]) - opti.maximize( - np.dot(normal, desired_normal) - ) + opti.maximize(np.dot(normal, desired_normal)) sol = opti.solve(verbose=False) y_rot_deg = np.degrees(sol(y_rot)) z_rot_deg = np.degrees(sol(z_rot)) @@ -75,7 +72,7 @@ def generate_propulsor( return script -if __name__ == '__main__': +if __name__ == "__main__": prop = Propulsor( name="Prop", diff --git a/aerosandbox/geometry/openvsp_io/asb_to_openvsp/wing_vspscript_generator.py b/aerosandbox/geometry/openvsp_io/asb_to_openvsp/wing_vspscript_generator.py index b8eb5bfb6..c478825d9 100644 --- a/aerosandbox/geometry/openvsp_io/asb_to_openvsp/wing_vspscript_generator.py +++ b/aerosandbox/geometry/openvsp_io/asb_to_openvsp/wing_vspscript_generator.py @@ -5,10 +5,7 @@ from aerosandbox.geometry.openvsp_io.asb_to_openvsp import _utilities -def generate_wing( - wing: Wing, - include_main=True -) -> str: +def generate_wing(wing: Wing, include_main=True) -> str: """ Generates a VSPScript file for a Wing object. @@ -60,7 +57,7 @@ def generate_wing( dx_le = dxyz_le[:, 0] dy_le = dxyz_le[:, 1] dz_le = dxyz_le[:, 2] - dyz_le = np.sqrt(dy_le ** 2 + dz_le ** 2) + dyz_le = np.sqrt(dy_le**2 + dz_le**2) dihedrals = np.arctan2d(dz_le, dy_le) sweep_le = np.arctan2d(dx_le, dyz_le) @@ -90,12 +87,8 @@ def generate_wing( upper = xsec.airfoil.upper_coordinates()[::-1] # VSP wants front to back order lower = xsec.airfoil.lower_coordinates() # Front to back order - up_pnt_vecs = ", ".join([ - f"vec3d({p[0]:.8g}, {p[1]:.8g}, 0.0)" for p in upper - ]) - lo_pnt_vecs = ", ".join([ - f"vec3d({p[0]:.8g}, {p[1]:.8g}, 0.0)" for p in lower - ]) + up_pnt_vecs = ", ".join([f"vec3d({p[0]:.8g}, {p[1]:.8g}, 0.0)" for p in upper]) + lo_pnt_vecs = ", ".join([f"vec3d({p[0]:.8g}, {p[1]:.8g}, 0.0)" for p in lower]) script += f"""\ {{ @@ -126,35 +119,23 @@ def generate_wing( return script -if __name__ == '__main__': +if __name__ == "__main__": wing = Wing( name="Main Wing", xsecs=[ WingXSec( - xyz_le=[1, 0, 0], - chord=1.1, - twist=-20, - airfoil=Airfoil(name="dae11") + xyz_le=[1, 0, 0], chord=1.1, twist=-20, airfoil=Airfoil(name="dae11") ), WingXSec( - xyz_le=[1, 2, 0], - chord=0.9, - twist=40, - airfoil=Airfoil(name="NACA4412") + xyz_le=[1, 2, 0], chord=0.9, twist=40, airfoil=Airfoil(name="NACA4412") ), WingXSec( - xyz_le=[2, 5, 0], - chord=0.5, - twist=0, - airfoil=Airfoil(name="NACA3412") + xyz_le=[2, 5, 0], chord=0.5, twist=0, airfoil=Airfoil(name="NACA3412") ), WingXSec( - xyz_le=[2, 10, 5], - chord=0.25, - twist=0, - airfoil=Airfoil(name="NACA2412") - ) - ] + xyz_le=[2, 10, 5], chord=0.25, twist=0, airfoil=Airfoil(name="NACA2412") + ), + ], ) print(generate_wing(wing)) diff --git a/aerosandbox/geometry/polygon.py b/aerosandbox/geometry/polygon.py index b7dfa5442..a29575a08 100644 --- a/aerosandbox/geometry/polygon.py +++ b/aerosandbox/geometry/polygon.py @@ -5,9 +5,7 @@ class Polygon(AeroSandboxObject): - def __init__(self, - coordinates: np.ndarray - ): + def __init__(self, coordinates: np.ndarray): """ Creates a polygon object. @@ -49,10 +47,11 @@ def n_points(self) -> int: except AttributeError: return 0 - def scale(self, - scale_x: float = 1., - scale_y: float = 1., - ) -> 'Polygon': + def scale( + self, + scale_x: float = 1.0, + scale_y: float = 1.0, + ) -> "Polygon": """ Scales a Polygon about the origin. Args: @@ -64,14 +63,13 @@ def scale(self, x = self.x() * scale_x y = self.y() * scale_y - return Polygon( - coordinates=np.stack((x, y), axis=1) - ) + return Polygon(coordinates=np.stack((x, y), axis=1)) - def translate(self, - translate_x: float = 0., - translate_y: float = 0., - ) -> 'Polygon': + def translate( + self, + translate_x: float = 0.0, + translate_y: float = 0.0, + ) -> "Polygon": """ Translates a Polygon by a given amount. Args: @@ -84,15 +82,11 @@ def translate(self, x = self.x() + translate_x y = self.y() + translate_y - return Polygon( - coordinates=np.stack((x, y), axis=1) - ) + return Polygon(coordinates=np.stack((x, y), axis=1)) - def rotate(self, - angle: float, - x_center: float = 0., - y_center: float = 0. - ) -> 'Polygon': + def rotate( + self, angle: float, x_center: float = 0.0, y_center: float = 0.0 + ) -> "Polygon": """ Rotates a Polygon clockwise by the specified amount, in radians. @@ -121,9 +115,7 @@ def rotate(self, ### Translate coordinates = coordinates + translation - return Polygon( - coordinates=coordinates - ) + return Polygon(coordinates=coordinates) def area(self) -> float: """ @@ -134,7 +126,9 @@ def area(self) -> float: x_n = np.roll(x, -1) # x_next, or x_i+1 y_n = np.roll(y, -1) # y_next, or y_i+1 - a = x * y_n - x_n * y # a is the area of the triangle bounded by a given point, the next point, and the origin. + a = ( + x * y_n - x_n * y + ) # a is the area of the triangle bounded by a given point, the next point, and the origin. A = 0.5 * np.sum(a) # area @@ -146,10 +140,7 @@ def perimeter(self) -> float: """ dx = np.diff(self.x()) dy = np.diff(self.y()) - ds = ( - dx ** 2 + - dy ** 2 - ) ** 0.5 + ds = (dx**2 + dy**2) ** 0.5 return np.sum(ds) @@ -162,7 +153,9 @@ def centroid(self) -> np.ndarray: x_n = np.roll(x, -1) # x_next, or x_i+1 y_n = np.roll(y, -1) # y_next, or y_i+1 - a = x * y_n - x_n * y # a is the area of the triangle bounded by a given point, the next point, and the origin. + a = ( + x * y_n - x_n * y + ) # a is the area of the triangle bounded by a given point, the next point, and the origin. A = 0.5 * np.sum(a) # area @@ -181,7 +174,9 @@ def Ixx(self): x_n = np.roll(x, -1) # x_next, or x_i+1 y_n = np.roll(y, -1) # y_next, or y_i+1 - a = x * y_n - x_n * y # a is the area of the triangle bounded by a given point, the next point, and the origin. + a = ( + x * y_n - x_n * y + ) # a is the area of the triangle bounded by a given point, the next point, and the origin. A = 0.5 * np.sum(a) # area @@ -189,7 +184,7 @@ def Ixx(self): y_c = 1 / (6 * A) * np.sum(a * (y + y_n)) centroid = np.array([x_c, y_c]) - Ixx = 1 / 12 * np.sum(a * (y ** 2 + y * y_n + y_n ** 2)) + Ixx = 1 / 12 * np.sum(a * (y**2 + y * y_n + y_n**2)) Iuu = Ixx - A * centroid[1] ** 2 @@ -204,7 +199,9 @@ def Iyy(self): x_n = np.roll(x, -1) # x_next, or x_i+1 y_n = np.roll(y, -1) # y_next, or y_i+1 - a = x * y_n - x_n * y # a is the area of the triangle bounded by a given point, the next point, and the origin. + a = ( + x * y_n - x_n * y + ) # a is the area of the triangle bounded by a given point, the next point, and the origin. A = 0.5 * np.sum(a) # area @@ -212,7 +209,7 @@ def Iyy(self): y_c = 1 / (6 * A) * np.sum(a * (y + y_n)) centroid = np.array([x_c, y_c]) - Iyy = 1 / 12 * np.sum(a * (x ** 2 + x * x_n + x_n ** 2)) + Iyy = 1 / 12 * np.sum(a * (x**2 + x * x_n + x_n**2)) Ivv = Iyy - A * centroid[0] ** 2 @@ -227,7 +224,9 @@ def Ixy(self): x_n = np.roll(x, -1) # x_next, or x_i+1 y_n = np.roll(y, -1) # y_next, or y_i+1 - a = x * y_n - x_n * y # a is the area of the triangle bounded by a given point, the next point, and the origin. + a = ( + x * y_n - x_n * y + ) # a is the area of the triangle bounded by a given point, the next point, and the origin. A = 0.5 * np.sum(a) # area @@ -250,7 +249,9 @@ def J(self): x_n = np.roll(x, -1) # x_next, or x_i+1 y_n = np.roll(y, -1) # y_next, or y_i+1 - a = x * y_n - x_n * y # a is the area of the triangle bounded by a given point, the next point, and the origin. + a = ( + x * y_n - x_n * y + ) # a is the area of the triangle bounded by a given point, the next point, and the origin. A = 0.5 * np.sum(a) # area @@ -258,17 +259,15 @@ def J(self): y_c = 1 / (6 * A) * np.sum(a * (y + y_n)) centroid = np.array([x_c, y_c]) - Ixx = 1 / 12 * np.sum(a * (y ** 2 + y * y_n + y_n ** 2)) + Ixx = 1 / 12 * np.sum(a * (y**2 + y * y_n + y_n**2)) - Iyy = 1 / 12 * np.sum(a * (x ** 2 + x * x_n + x_n ** 2)) + Iyy = 1 / 12 * np.sum(a * (x**2 + x * x_n + x_n**2)) J = Ixx + Iyy return J - def write_sldcrv(self, - filepath: str = None - ): + def write_sldcrv(self, filepath: str = None): """ Writes a .sldcrv (SolidWorks curve) file corresponding to this Polygon to a filepath. @@ -280,10 +279,7 @@ def write_sldcrv(self, """ string = "\n".join( - [ - "%f %f 0" % tuple(coordinate) - for coordinate in self.coordinates - ] + ["%f %f 0" % tuple(coordinate) for coordinate in self.coordinates] ) if filepath is not None: @@ -292,10 +288,11 @@ def write_sldcrv(self, return string - def contains_points(self, - x: Union[float, np.ndarray], - y: Union[float, np.ndarray], - ) -> Union[float, np.ndarray]: + def contains_points( + self, + x: Union[float, np.ndarray], + y: Union[float, np.ndarray], + ) -> Union[float, np.ndarray]: """ Returns a boolean array of whether some (x, y) point(s) are contained within the Polygon. @@ -323,11 +320,7 @@ def contains_points(self, points = np.hstack((x, y)) - contained = path.Path( - vertices=self.coordinates - ).contains_points( - points - ) + contained = path.Path(vertices=self.coordinates).contains_points(points) contained = np.array(contained).reshape(input_shape) return contained @@ -340,11 +333,10 @@ def as_shapely_polygon(self): allows for union/intersection calculation between Polygons), it is not automatic-differentiable. """ import shapely + return shapely.Polygon(self.coordinates) - def jaccard_similarity(self, - other: "Polygon" - ): + def jaccard_similarity(self, other: "Polygon"): """ Calculates the Jaccard similarity between this polygon and another polygon. @@ -366,11 +358,7 @@ def jaccard_similarity(self, similarity = intersection / union if union != 0 else 0 return similarity - def draw(self, - set_equal=True, - color=None, - **kwargs - ): + def draw(self, set_equal=True, color=None, **kwargs): """ Draws the Polygon on the current matplotlib axis. @@ -389,22 +377,20 @@ def draw(self, if color is None: color = plt.gca()._get_lines.get_next_color() - plt.fill( - self.x(), - self.y(), - color=color, - alpha=0.5, - **kwargs - ) + plt.fill(self.x(), self.y(), color=color, alpha=0.5, **kwargs) if set_equal: - plt.gca().set_aspect("equal", adjustable='box') + plt.gca().set_aspect("equal", adjustable="box") -if __name__ == '__main__': +if __name__ == "__main__": theta = np.linspace(0, 2 * np.pi, 1000) - r = np.sin(theta) * np.sqrt(np.abs(np.cos(theta))) / (np.sin(theta) + 7 / 5) - 2 * np.sin(theta) + 2 + r = ( + np.sin(theta) * np.sqrt(np.abs(np.cos(theta))) / (np.sin(theta) + 7 / 5) + - 2 * np.sin(theta) + + 2 + ) heart = Polygon(np.stack((r * np.cos(theta), r * np.sin(theta)), axis=1)) import matplotlib.pyplot as plt diff --git a/aerosandbox/geometry/propulsor.py b/aerosandbox/geometry/propulsor.py index 7cc8176b7..ef4847545 100644 --- a/aerosandbox/geometry/propulsor.py +++ b/aerosandbox/geometry/propulsor.py @@ -11,15 +11,16 @@ class Propulsor(AeroSandboxObject): Assumes a disk- or cylinder-shaped propulsor. """ - def __init__(self, - name: Optional[str] = "Untitled", - xyz_c: Union[np.ndarray, List[float]] = None, - xyz_normal: Union[np.ndarray, List[float]] = None, - radius: float = 1., - length: float = 0., - color: Optional[Union[str, Tuple[float]]] = None, - analysis_specific_options: Optional[Dict[type, Dict[str, Any]]] = None, - ): + def __init__( + self, + name: Optional[str] = "Untitled", + xyz_c: Union[np.ndarray, List[float]] = None, + xyz_normal: Union[np.ndarray, List[float]] = None, + radius: float = 1.0, + length: float = 0.0, + color: Optional[Union[str, Tuple[float]]] = None, + analysis_specific_options: Optional[Dict[type, Dict[str, Any]]] = None, + ): """ Defines a new propulsor object. @@ -27,9 +28,9 @@ def __init__(self, """ ### Set defaults if xyz_c is None: - xyz_c = np.array([0., 0., 0.]) + xyz_c = np.array([0.0, 0.0, 0.0]) if xyz_normal is None: - xyz_normal = np.array([-1., 0., 0.]) + xyz_normal = np.array([-1.0, 0.0, 0.0]) if analysis_specific_options is None: analysis_specific_options = {} @@ -48,7 +49,7 @@ def xsec_area(self) -> float: """ Returns the cross-sectional area of the propulsor, in m^2. """ - return np.pi * self.radius ** 2 + return np.pi * self.radius**2 def xsec_perimeter(self) -> float: """ @@ -89,26 +90,19 @@ def compute_frame(self) -> Tuple[np.ndarray, np.ndarray, np.ndarray]: return xg_local, yg_local, zg_local - def get_disk_3D_coordinates(self, - theta: Union[float, np.ndarray] = None, - l_over_length: Union[float, np.ndarray] = None, - ) -> Tuple[Union[float, np.ndarray]]: + def get_disk_3D_coordinates( + self, + theta: Union[float, np.ndarray] = None, + l_over_length: Union[float, np.ndarray] = None, + ) -> Tuple[Union[float, np.ndarray]]: ### Set defaults if theta is None: - theta = np.linspace( - 0, - 2 * np.pi, - 60 + 1 - )[:-1] + theta = np.linspace(0, 2 * np.pi, 60 + 1)[:-1] if l_over_length is None: if self.length == 0: l_over_length = 0 else: - l_over_length = np.linspace( - 0, - 1, - 4 - ).reshape((1, -1)) + l_over_length = np.linspace(0, 1, 4).reshape((1, -1)) theta = np.array(theta).reshape((-1, 1)) @@ -127,9 +121,10 @@ def get_disk_3D_coordinates(self, self.xyz_c[2] + x * xg_local[2] + y * yg_local[2] + z * zg_local[2], ) - def translate(self, - xyz: Union[np.ndarray, List[float]], - ) -> 'Propulsor': + def translate( + self, + xyz: Union[np.ndarray, List[float]], + ) -> "Propulsor": """ Returns a copy of this propulsor that has been translated by `xyz`. @@ -143,6 +138,6 @@ def translate(self, return new_propulsor -if __name__ == '__main__': +if __name__ == "__main__": p_disk = Propulsor(radius=3) p_can = Propulsor(length=1) diff --git a/aerosandbox/geometry/test_geometry/test_airfoil.py b/aerosandbox/geometry/test_geometry/test_airfoil.py index 3d73e58ac..d1ad85576 100644 --- a/aerosandbox/geometry/test_geometry/test_airfoil.py +++ b/aerosandbox/geometry/test_geometry/test_airfoil.py @@ -44,13 +44,11 @@ def test_repanel(naca4412): def test_containts_points(naca4412): - assert naca4412.contains_points( - x=0.5, y=0 - ) == True - assert np.all(naca4412.contains_points( - x=np.array([0.5, 0.5]), - y=np.array([0, -0.1]) - ) == np.array([True, False])) + assert naca4412.contains_points(x=0.5, y=0) == True + assert np.all( + naca4412.contains_points(x=np.array([0.5, 0.5]), y=np.array([0, -0.1])) + == np.array([True, False]) + ) shape = (1, 2, 3, 4) x_points = np.random.randn(*shape) y_points = np.random.randn(*shape) @@ -65,13 +63,9 @@ def test_optimize_through_control_surface_deflections(): d = opti.variable(init_guess=5, lower_bound=-90, upper_bound=90) - afd = af.add_control_surface( - deflection=d, hinge_point_x=0.75 - ) + afd = af.add_control_surface(deflection=d, hinge_point_x=0.75) - opti.minimize( - (afd.coordinates[0, 1] - 0.2) ** 2 - ) + opti.minimize((afd.coordinates[0, 1] - 0.2) ** 2) sol = opti.solve() # print(sol(d)) @@ -93,23 +87,16 @@ def test_optimize_through_control_surface_deflections_for_CL(): alpha=0, Re=1e6, mach=0, - control_surfaces=[ - asb.ControlSurface( - deflection=d, - hinge_point=0.75 - ) - ] + control_surfaces=[asb.ControlSurface(deflection=d, hinge_point=0.75)], ) - opti.minimize( - (aero["CL"] - 0.5) ** 2 - ) + opti.minimize((aero["CL"] - 0.5) ** 2) sol = opti.solve() assert sol(d) == pytest.approx(8.34, abs=1) -if __name__ == '__main__': +if __name__ == "__main__": test_optimize_through_control_surface_deflections() # pytest.main() diff --git a/aerosandbox/geometry/test_geometry/test_airfoil_families.py b/aerosandbox/geometry/test_geometry/test_airfoil_families.py index 332aeb099..212927f4d 100644 --- a/aerosandbox/geometry/test_geometry/test_airfoil_families.py +++ b/aerosandbox/geometry/test_geometry/test_airfoil_families.py @@ -3,10 +3,7 @@ def test_get_NACA_coordinates(): - coords = get_NACA_coordinates( - name='naca4408', - n_points_per_side=100 - ) + coords = get_NACA_coordinates(name="naca4408", n_points_per_side=100) assert len(coords) == 199 @@ -17,5 +14,5 @@ def test_get_UIUC_coordinates(): assert len(coords) != 0 -if __name__ == '__main__': +if __name__ == "__main__": pytest.main() diff --git a/aerosandbox/geometry/test_geometry/test_airplane.py b/aerosandbox/geometry/test_geometry/test_airplane.py index c1100ac6e..913ec1ace 100644 --- a/aerosandbox/geometry/test_geometry/test_airplane.py +++ b/aerosandbox/geometry/test_geometry/test_airplane.py @@ -9,7 +9,7 @@ def a() -> asb.Airplane: fuselage_cabin_diameter = 20.4 * u.foot fuselage_cabin_radius = fuselage_cabin_diameter / 2 - fuselage_cabin_xsec_area = np.pi * fuselage_cabin_radius ** 2 + fuselage_cabin_xsec_area = np.pi * fuselage_cabin_radius**2 fuselage_cabin_length = 123.2 * u.foot fwd_fuel_tank_length = 6 @@ -32,11 +32,11 @@ def a() -> asb.Airplane: r_fuse_sections = [] def linear_map( - f_in: Union[float, np.ndarray], - min_in: Union[float, np.ndarray], - max_in: Union[float, np.ndarray], - min_out: Union[float, np.ndarray], - max_out: Union[float, np.ndarray], + f_in: Union[float, np.ndarray], + min_in: Union[float, np.ndarray], + max_in: Union[float, np.ndarray], + min_out: Union[float, np.ndarray], + max_out: Union[float, np.ndarray], ) -> Union[float, np.ndarray]: """ Linearly maps an input `f_in` from range (`min_in`, `max_in`) to (`min_out`, `max_out`). @@ -72,22 +72,28 @@ def linear_map( x_fuse_sections.append( linear_map( f_in=x_sect_nondim, - min_in=0, max_in=1, - min_out=x_nose, max_out=x_nose_to_fwd_tank + min_in=0, + max_in=1, + min_out=x_nose, + max_out=x_nose_to_fwd_tank, ) ) z_fuse_sections.append( linear_map( f_in=z_sect_nondim, - min_in=0, max_in=1, - min_out=0, max_out=fuselage_cabin_radius + min_in=0, + max_in=1, + min_out=0, + max_out=fuselage_cabin_radius, ) ) r_fuse_sections.append( linear_map( f_in=r_sect_nondim, - min_in=0, max_in=1, - min_out=0, max_out=fuselage_cabin_radius + min_in=0, + max_in=1, + min_out=0, + max_out=fuselage_cabin_radius, ) ) @@ -99,22 +105,28 @@ def linear_map( x_fuse_sections.append( linear_map( f_in=x_sect_nondim, - min_in=0, max_in=1, - min_out=x_nose_to_fwd_tank, max_out=x_fwd_tank_to_cabin + min_in=0, + max_in=1, + min_out=x_nose_to_fwd_tank, + max_out=x_fwd_tank_to_cabin, ) ) z_fuse_sections.append( linear_map( f_in=z_sect_nondim, - min_in=0, max_in=1, - min_out=0, max_out=fuselage_cabin_radius + min_in=0, + max_in=1, + min_out=0, + max_out=fuselage_cabin_radius, ) ) r_fuse_sections.append( linear_map( f_in=r_sect_nondim, - min_in=0, max_in=1, - min_out=0, max_out=fuselage_cabin_radius + min_in=0, + max_in=1, + min_out=0, + max_out=fuselage_cabin_radius, ) ) @@ -126,22 +138,28 @@ def linear_map( x_fuse_sections.append( linear_map( f_in=x_sect_nondim, - min_in=0, max_in=1, - min_out=x_fwd_tank_to_cabin, max_out=x_cabin_to_aft_tank + min_in=0, + max_in=1, + min_out=x_fwd_tank_to_cabin, + max_out=x_cabin_to_aft_tank, ) ) z_fuse_sections.append( linear_map( f_in=z_sect_nondim, - min_in=0, max_in=1, - min_out=0, max_out=fuselage_cabin_radius + min_in=0, + max_in=1, + min_out=0, + max_out=fuselage_cabin_radius, ) ) r_fuse_sections.append( linear_map( f_in=r_sect_nondim, - min_in=0, max_in=1, - min_out=0, max_out=fuselage_cabin_radius + min_in=0, + max_in=1, + min_out=0, + max_out=fuselage_cabin_radius, ) ) @@ -153,84 +171,96 @@ def linear_map( x_fuse_sections.append( linear_map( f_in=x_sect_nondim, - min_in=0, max_in=1, - min_out=x_cabin_to_aft_tank, max_out=x_aft_tank_to_tail + min_in=0, + max_in=1, + min_out=x_cabin_to_aft_tank, + max_out=x_aft_tank_to_tail, ) ) z_fuse_sections.append( linear_map( f_in=z_sect_nondim, - min_in=0, max_in=1, - min_out=0, max_out=fuselage_cabin_radius + min_in=0, + max_in=1, + min_out=0, + max_out=fuselage_cabin_radius, ) ) r_fuse_sections.append( linear_map( f_in=r_sect_nondim, - min_in=0, max_in=1, - min_out=0, max_out=fuselage_cabin_radius + min_in=0, + max_in=1, + min_out=0, + max_out=fuselage_cabin_radius, ) ) # Tail x_sect_nondim = np.linspace(0, 1, 10) - z_sect_nondim = 1 * x_sect_nondim ** 1.5 - r_sect_nondim = 1 - x_sect_nondim ** 1.5 + z_sect_nondim = 1 * x_sect_nondim**1.5 + r_sect_nondim = 1 - x_sect_nondim**1.5 x_fuse_sections.append( linear_map( f_in=x_sect_nondim, - min_in=0, max_in=1, - min_out=x_aft_tank_to_tail, max_out=x_tail + min_in=0, + max_in=1, + min_out=x_aft_tank_to_tail, + max_out=x_tail, ) ) z_fuse_sections.append( linear_map( f_in=z_sect_nondim, - min_in=0, max_in=1, - min_out=0, max_out=fuselage_cabin_radius + min_in=0, + max_in=1, + min_out=0, + max_out=fuselage_cabin_radius, ) ) r_fuse_sections.append( linear_map( f_in=r_sect_nondim, - min_in=0, max_in=1, - min_out=0, max_out=fuselage_cabin_radius + min_in=0, + max_in=1, + min_out=0, + max_out=fuselage_cabin_radius, ) ) # Compile Fuselage - x_fuse_sections = np.concatenate([ - x_fuse_section[:-1] if i != len(x_fuse_sections) - 1 else x_fuse_section - for i, x_fuse_section in enumerate(x_fuse_sections) - ]) - z_fuse_sections = np.concatenate([ - z_fuse_section[:-1] if i != len(z_fuse_sections) - 1 else z_fuse_section - for i, z_fuse_section in enumerate(z_fuse_sections) - ]) - r_fuse_sections = np.concatenate([ - r_fuse_section[:-1] if i != len(r_fuse_sections) - 1 else r_fuse_section - for i, r_fuse_section in enumerate(r_fuse_sections) - ]) + x_fuse_sections = np.concatenate( + [ + x_fuse_section[:-1] if i != len(x_fuse_sections) - 1 else x_fuse_section + for i, x_fuse_section in enumerate(x_fuse_sections) + ] + ) + z_fuse_sections = np.concatenate( + [ + z_fuse_section[:-1] if i != len(z_fuse_sections) - 1 else z_fuse_section + for i, z_fuse_section in enumerate(z_fuse_sections) + ] + ) + r_fuse_sections = np.concatenate( + [ + r_fuse_section[:-1] if i != len(r_fuse_sections) - 1 else r_fuse_section + for i, r_fuse_section in enumerate(r_fuse_sections) + ] + ) fuse = asb.Fuselage( name="Fuselage", xsecs=[ asb.FuselageXSec( - xyz_c=[ - x_fuse_sections[i], - 0, - z_fuse_sections[i] - ], - radius=r_fuse_sections[i] + xyz_c=[x_fuse_sections[i], 0, z_fuse_sections[i]], + radius=r_fuse_sections[i], ) for i in range(np.length(x_fuse_sections)) ], analysis_specific_options={ - asb.AeroBuildup: dict( - nose_fineness_ratio=nose_fineness_ratio - ) - } + asb.AeroBuildup: dict(nose_fineness_ratio=nose_fineness_ratio) + }, ) ### Wing @@ -265,42 +295,30 @@ def linear_map( airfoil=wing_airfoil, ) wing_yehudi = asb.WingXSec( - xyz_le=[ - wing_yehudi_x, - wing_yehudi_y, - wing_yehudi_y * np.tand(wing_dihedral) - ], + xyz_le=[wing_yehudi_x, wing_yehudi_y, wing_yehudi_y * np.tand(wing_dihedral)], chord=wing_yehudi_chord, airfoil=wing_airfoil, ) wing_tip = asb.WingXSec( - xyz_le=[ - wing_tip_x, - wing_tip_y, - wing_tip_y * np.tand(wing_dihedral) - ], + xyz_le=[wing_tip_x, wing_tip_y, wing_tip_y * np.tand(wing_dihedral)], chord=wing_tip_chord, - airfoil=wing_airfoil + airfoil=wing_airfoil, ) # Assemble the wing - wing_x_le = 0.5 * x_fwd_tank_to_cabin + 0.5 * x_cabin_to_aft_tank - 0.5 * wing_root_chord + wing_x_le = ( + 0.5 * x_fwd_tank_to_cabin + 0.5 * x_cabin_to_aft_tank - 0.5 * wing_root_chord + ) wing_z_le = -0.5 * fuselage_cabin_radius - wing = asb.Wing( - name="Main Wing", - symmetric=True, - xsecs=[ - wing_root, - wing_yehudi, - wing_tip - ] - ).translate([ - wing_x_le, - 0, - wing_z_le - ]).subdivide_sections(3) + wing = ( + asb.Wing( + name="Main Wing", symmetric=True, xsecs=[wing_root, wing_yehudi, wing_tip] + ) + .translate([wing_x_le, 0, wing_z_le]) + .subdivide_sections(3) + ) ### Horizontal Stabilizer hstab_span = 70.8 * u.foot @@ -314,39 +332,25 @@ def linear_map( xyz_le=[0, 0, 0], chord=hstab_root_chord, airfoil=asb.Airfoil("naca0012"), - control_surfaces=[ - asb.ControlSurface( - name="All-moving Elevator", - deflection=0 - ) - ] + control_surfaces=[asb.ControlSurface(name="All-moving Elevator", deflection=0)], ) hstab_tip = asb.WingXSec( - xyz_le=[ - hstab_half_span * np.tand(hstab_LE_sweep_deg), - hstab_half_span, - 0 - ], + xyz_le=[hstab_half_span * np.tand(hstab_LE_sweep_deg), hstab_half_span, 0], chord=0.35 * hstab_root_chord, - airfoil=asb.Airfoil("naca0012") + airfoil=asb.Airfoil("naca0012"), ) # Assemble the hstab hstab_x_le = x_tail - 2 * hstab_root_chord hstab_z_le = 0.5 * fuselage_cabin_radius - hstab = asb.Wing( - name="Horizontal Stabilizer", - symmetric=True, - xsecs=[ - hstab_root, - hstab_tip - ] - ).translate([ - hstab_x_le, - 0, - hstab_z_le - ]).subdivide_sections(3) + hstab = ( + asb.Wing( + name="Horizontal Stabilizer", symmetric=True, xsecs=[hstab_root, hstab_tip] + ) + .translate([hstab_x_le, 0, hstab_z_le]) + .subdivide_sections(3) + ) ### Vertical Stabilizer vstab_span = 29.6 * u.foot @@ -356,9 +360,7 @@ def linear_map( vstab_LE_sweep_deg = 45 vstab_root = asb.WingXSec( - xyz_le=[0, 0, 0], - chord=vstab_root_chord, - airfoil=asb.Airfoil("naca0008") + xyz_le=[0, 0, 0], chord=vstab_root_chord, airfoil=asb.Airfoil("naca0008") ) vstab_tip = asb.WingXSec( xyz_le=[ @@ -367,43 +369,31 @@ def linear_map( vstab_span, ], chord=0.35 * vstab_root_chord, - airfoil=asb.Airfoil("naca0008") + airfoil=asb.Airfoil("naca0008"), ) # Assemble the vstab vstab_x_le = x_tail - 2 * vstab_root_chord vstab_z_le = 1 * fuselage_cabin_radius - vstab = asb.Wing( - name="Vertical Stabilizer", - xsecs=[ - vstab_root, - vstab_tip - ] - ).translate([ - vstab_x_le, - 0, - vstab_z_le - ]).subdivide_sections(3) + vstab = ( + asb.Wing(name="Vertical Stabilizer", xsecs=[vstab_root, vstab_tip]) + .translate([vstab_x_le, 0, vstab_z_le]) + .subdivide_sections(3) + ) ### Airplane airplane = asb.Airplane( name="Airplane", xyz_ref=[], - wings=[ - wing, - hstab, - vstab - ], - fuselages=[ - fuse - ], + wings=[wing, hstab, vstab], + fuselages=[fuse], ) return airplane -if __name__ == '__main__': +if __name__ == "__main__": # import matplotlib # matplotlib.use("TkAgg") import matplotlib.pyplot as plt diff --git a/aerosandbox/geometry/test_geometry/test_polygon.py b/aerosandbox/geometry/test_geometry/test_polygon.py index 4a37b8a8d..c1cd3e8a7 100644 --- a/aerosandbox/geometry/test_geometry/test_polygon.py +++ b/aerosandbox/geometry/test_geometry/test_polygon.py @@ -5,28 +5,14 @@ def test_polygon_creation(): - p = Polygon( - coordinates=np.array([ - [0, 0], - [1, 0], - [1, 1], - [0, 1] - ]) - ) + p = Polygon(coordinates=np.array([[0, 0], [1, 0], [1, 1], [0, 1]])) assert p.n_points() == 4 assert np.all(p.x() == np.array([0, 1, 1, 0])) assert np.all(p.y() == np.array([0, 0, 1, 1])) def test_contains_points(): - p = Polygon( - coordinates=np.array([ - [0, 0], - [1, 0], - [1, 1], - [0, 1] - ]) - ) + p = Polygon(coordinates=np.array([[0, 0], [1, 0], [1, 1], [0, 1]])) assert p.contains_points(0.5, 0.5) == True assert p.contains_points(-0.1, 0.5) == False @@ -37,10 +23,12 @@ def test_contains_points(): assert p.contains_points(0.5, 1.0) == True assert p.contains_points(0.5, 1.1) == False - assert np.all(p.contains_points( - x=np.array([0.5, 0.5, -0.1, -0.1]), - y=np.array([0.5, -0.1, 0.5, -0.1]) - ) == np.array([True, False, False, False])) + assert np.all( + p.contains_points( + x=np.array([0.5, 0.5, -0.1, -0.1]), y=np.array([0.5, -0.1, 0.5, -0.1]) + ) + == np.array([True, False, False, False]) + ) shape = (1, 2, 3, 4) x_points = np.random.randn(*shape) @@ -50,14 +38,7 @@ def test_contains_points(): def test_equality(): - p1 = Polygon( - coordinates=np.array([ - [0, 0], - [1, 0], - [1, 1], - [0, 1] - ]) - ) + p1 = Polygon(coordinates=np.array([[0, 0], [1, 0], [1, 1], [0, 1]])) p2 = p1.deepcopy() @@ -67,17 +48,9 @@ def test_equality(): def test_translate_scale_rotate(): - p1 = Polygon( - coordinates=np.array([ - [0, 0], - [1, 0], - [1, 1], - [0, 1] - ]) - ) + p1 = Polygon(coordinates=np.array([[0, 0], [1, 0], [1, 1], [0, 1]])) p2 = ( - p1 - .translate(1, 1) + p1.translate(1, 1) .rotate(np.pi / 2, 1, 1) .scale(2, 1) .rotate(3 * np.pi / 2, 2, 1) @@ -85,10 +58,7 @@ def test_translate_scale_rotate(): .translate(-2, -0.5) ) - assert np.allclose( - p1.coordinates, - p2.coordinates - ) + assert np.allclose(p1.coordinates, p2.coordinates) def test_jaccard_similarity(): @@ -98,14 +68,7 @@ def test_jaccard_similarity(): print("Shapely (optional) not installed; skipping test_jaccard_similarity.") return - p1 = Polygon( - coordinates=np.array([ - [0, 0], - [1, 0], - [1, 1], - [0, 1] - ]) - ) + p1 = Polygon(coordinates=np.array([[0, 0], [1, 0], [1, 1], [0, 1]])) p2 = p1.copy() assert p1.jaccard_similarity(p2) == pytest.approx(1) @@ -118,6 +81,6 @@ def test_jaccard_similarity(): assert p1.jaccard_similarity(p2.rotate(np.pi / 2, 0.5, 0.5)) == pytest.approx(1) -if __name__ == '__main__': +if __name__ == "__main__": # test_translate_scale_rotate() pytest.main() diff --git a/aerosandbox/geometry/test_geometry/test_reflection.py b/aerosandbox/geometry/test_geometry/test_reflection.py index 9ead65be4..e5c5c75dd 100644 --- a/aerosandbox/geometry/test_geometry/test_reflection.py +++ b/aerosandbox/geometry/test_geometry/test_reflection.py @@ -10,26 +10,17 @@ def test_np_vector(): - assert np.all( - reflect_over_XZ_plane(vec) == - np.array([0, -1, 2]) - ) + assert np.all(reflect_over_XZ_plane(vec) == np.array([0, -1, 2])) def test_cas_vector(): output = reflect_over_XZ_plane(cas.DM(vec)) assert isinstance(output, cas.DM) - assert np.all( - output == - np.array([0, -1, 2]) - ) + assert np.all(output == np.array([0, -1, 2])) def test_np_vector_2D_wide(): - assert np.all( - reflect_over_XZ_plane(np.expand_dims(vec, 0)) == - np.array([0, -1, 2]) - ) + assert np.all(reflect_over_XZ_plane(np.expand_dims(vec, 0)) == np.array([0, -1, 2])) def test_np_vector_2D_tall(): @@ -39,37 +30,27 @@ def test_np_vector_2D_tall(): def test_np_square(): assert np.all( - reflect_over_XZ_plane(square) == - np.array([ - [0, -1, 2], - [3, -4, 5], - [6, -7, 8] - ]) + reflect_over_XZ_plane(square) == np.array([[0, -1, 2], [3, -4, 5], [6, -7, 8]]) ) def test_cas_square(): output = reflect_over_XZ_plane(cas.DM(square)) assert isinstance(output, cas.DM) - assert np.all( - output == - np.array([ - [0, -1, 2], - [3, -4, 5], - [6, -7, 8] - ]) - ) + assert np.all(output == np.array([[0, -1, 2], [3, -4, 5], [6, -7, 8]])) def test_np_rectangular_tall(): assert np.all( - reflect_over_XZ_plane(rectangular_tall) == - np.array([ - [0, -1, 2], - [3, -4, 5], - [6, -7, 8], - [9, -10, 11], - ]) + reflect_over_XZ_plane(rectangular_tall) + == np.array( + [ + [0, -1, 2], + [3, -4, 5], + [6, -7, 8], + [9, -10, 11], + ] + ) ) @@ -77,13 +58,15 @@ def test_cas_rectangular_tall(): output = reflect_over_XZ_plane(cas.DM(rectangular_tall)) assert isinstance(output, cas.DM) assert np.all( - output == - np.array([ - [0, -1, 2], - [3, -4, 5], - [6, -7, 8], - [9, -10, 11], - ]) + output + == np.array( + [ + [0, -1, 2], + [3, -4, 5], + [6, -7, 8], + [9, -10, 11], + ] + ) ) @@ -102,5 +85,5 @@ def test_np_3D(): reflect_over_XZ_plane(np.arange(2 * 3 * 4).reshape((2, 3, 4))) -if __name__ == '__main__': +if __name__ == "__main__": pytest.main() diff --git a/aerosandbox/geometry/test_geometry/test_wing.py b/aerosandbox/geometry/test_geometry/test_wing.py index 7a485c5c5..0a6df9765 100644 --- a/aerosandbox/geometry/test_geometry/test_wing.py +++ b/aerosandbox/geometry/test_geometry/test_wing.py @@ -15,21 +15,17 @@ def w() -> Wing: ControlSurface( symmetric=True, ) - ] + ], ), WingXSec( xyz_le=np.array([2, 2, 0]), chord=0.5, twist=5, airfoil=Airfoil("mh60"), - control_surfaces=[ - ControlSurface( - symmetric=True - ) - ] - ) + control_surfaces=[ControlSurface(symmetric=True)], + ), ], - symmetric=True + symmetric=True, ).translate(np.array([1, 2, 3])) return wing @@ -74,5 +70,5 @@ def test_aerodynamic_center(): assert ac[2] == pytest.approx(3, abs=2e-2) -if __name__ == '__main__': +if __name__ == "__main__": pytest.main() diff --git a/aerosandbox/geometry/test_geometry/test_wingxsec.py b/aerosandbox/geometry/test_geometry/test_wingxsec.py index da7becbdc..a0ec2fe1f 100644 --- a/aerosandbox/geometry/test_geometry/test_wingxsec.py +++ b/aerosandbox/geometry/test_geometry/test_wingxsec.py @@ -5,14 +5,14 @@ def test_init(): # TODO actually test this xsec = WingXSec( xyz_le=np.array([0, 0, 0]), - chord=1., + chord=1.0, twist=0, airfoil=Airfoil("naca0012"), control_surface_is_symmetric=True, control_surface_hinge_point=0.75, - control_surface_deflection=0., + control_surface_deflection=0.0, ) -if __name__ == '__main__': +if __name__ == "__main__": pytest.main() diff --git a/aerosandbox/geometry/wing.py b/aerosandbox/geometry/wing.py index 5069baf78..399e37f5d 100644 --- a/aerosandbox/geometry/wing.py +++ b/aerosandbox/geometry/wing.py @@ -32,14 +32,15 @@ class Wing(AeroSandboxObject): If the wing is not symmetric across the XZ plane (e.g., a single vertical stabilizer), just define the wing. """ - def __init__(self, - name: Optional[str] = None, - xsecs: List['WingXSec'] = None, - symmetric: bool = False, - color: Optional[Union[str, Tuple[float]]] = None, - analysis_specific_options: Optional[Dict[type, Dict[str, Any]]] = None, - **kwargs, # Only to allow for capturing of deprecated arguments, don't use this. - ): + def __init__( + self, + name: Optional[str] = None, + xsecs: List["WingXSec"] = None, + symmetric: bool = False, + color: Optional[Union[str, Tuple[float]]] = None, + analysis_specific_options: Optional[Dict[type, Dict[str, Any]]] = None, + **kwargs, # Only to allow for capturing of deprecated arguments, don't use this. + ): """ Defines a new wing object. @@ -94,7 +95,7 @@ def __init__(self, if name is None: name = "Untitled" if xsecs is None: - xsecs: List['WingXSec'] = [] + xsecs: List["WingXSec"] = [] if analysis_specific_options is None: analysis_specific_options = {} @@ -106,25 +107,21 @@ def __init__(self, self.analysis_specific_options = analysis_specific_options ### Handle deprecated parameters - if 'xyz_le' in locals(): + if "xyz_le" in locals(): import warnings + warnings.warn( "The `xyz_le` input for Wing is pending deprecation and will be removed in a future version. Use Wing().translate(xyz) instead.", - stacklevel=2 + stacklevel=2, ) - self.xsecs = [ - xsec.translate(xyz_le) - for xsec in self.xsecs - ] + self.xsecs = [xsec.translate(xyz_le) for xsec in self.xsecs] def __repr__(self) -> str: n_xsecs = len(self.xsecs) symmetry_description = "symmetric" if self.symmetric else "asymmetric" return f"Wing '{self.name}' ({len(self.xsecs)} {'xsec' if n_xsecs == 1 else 'xsecs'}, {symmetry_description})" - def translate(self, - xyz: Union[np.ndarray, List[float]] - ) -> 'Wing': + def translate(self, xyz: Union[np.ndarray, List[float]]) -> "Wing": """ Translates the entire Wing by a certain amount. @@ -135,17 +132,15 @@ def translate(self, """ new_wing = copy.copy(self) - new_wing.xsecs = [ - xsec.translate(xyz) - for xsec in new_wing.xsecs - ] + new_wing.xsecs = [xsec.translate(xyz) for xsec in new_wing.xsecs] return new_wing - def span(self, - type: str = "yz", - include_centerline_distance=False, - _sectional: bool = False, - ) -> Union[float, List[float]]: + def span( + self, + type: str = "yz", + include_centerline_distance=False, + _sectional: bool = False, + ) -> Union[float, List[float]]: """ Computes the span, with options for various ways of measuring this (see `type` argument). @@ -199,7 +194,9 @@ def span(self, """ # Check inputs if include_centerline_distance and _sectional: - raise ValueError("Cannot use `_sectional` with `include_centerline_distance`!") + raise ValueError( + "Cannot use `_sectional` with `include_centerline_distance`!" + ) # Handle overloaded names if type == "top": @@ -226,34 +223,30 @@ def span(self, for inner_i, outer_i in zip(i_range, i_range[1:]): quarter_chord_vector = ( - quarter_chord_locations[outer_i] - - quarter_chord_locations[inner_i] + quarter_chord_locations[outer_i] - quarter_chord_locations[inner_i] ) if type == "xyz": section_span = ( - quarter_chord_vector[0] ** 2 + - quarter_chord_vector[1] ** 2 + - quarter_chord_vector[2] ** 2 - ) ** 0.5 + quarter_chord_vector[0] ** 2 + + quarter_chord_vector[1] ** 2 + + quarter_chord_vector[2] ** 2 + ) ** 0.5 elif type == "xy": section_span = ( - quarter_chord_vector[0] ** 2 + - quarter_chord_vector[1] ** 2 - ) ** 0.5 + quarter_chord_vector[0] ** 2 + quarter_chord_vector[1] ** 2 + ) ** 0.5 elif type == "yz": section_span = ( - quarter_chord_vector[1] ** 2 + - quarter_chord_vector[2] ** 2 - ) ** 0.5 + quarter_chord_vector[1] ** 2 + quarter_chord_vector[2] ** 2 + ) ** 0.5 elif type == "xz": section_span = ( - quarter_chord_vector[0] ** 2 + - quarter_chord_vector[2] ** 2 - ) ** 0.5 + quarter_chord_vector[0] ** 2 + quarter_chord_vector[2] ** 2 + ) ** 0.5 elif type == "x": section_span = quarter_chord_vector[0] @@ -280,8 +273,7 @@ def span(self, for i in i_range: half_span_to_XZ_plane = np.minimum( - half_span_to_XZ_plane, - np.abs(quarter_chord_locations[i][1]) + half_span_to_XZ_plane, np.abs(quarter_chord_locations[i][1]) ) half_span = half_span + half_span_to_XZ_plane @@ -293,11 +285,12 @@ def span(self, return span - def area(self, - type: str = "planform", - include_centerline_distance=False, - _sectional: bool = False, - ) -> Union[float, List[float]]: + def area( + self, + type: str = "planform", + include_centerline_distance=False, + _sectional: bool = False, + ) -> Union[float, List[float]]: """ Computes the wing area, with options for various ways of measuring this (see `type` argument): @@ -342,7 +335,9 @@ def area(self, """ # Check inputs if include_centerline_distance and _sectional: - raise ValueError("`include_centerline_distance` and `_sectional` cannot both be True!") + raise ValueError( + "`include_centerline_distance` and `_sectional` cannot both be True!" + ) # Handle overloaded names if type == "projected" or type == "top": @@ -359,10 +354,7 @@ def area(self, elif type == "wetted": sectional_spans = self.span(type="yz", _sectional=True) - xsec_chords = [ - xsec.chord * xsec.airfoil.perimeter() - for xsec in self.xsecs - ] + xsec_chords = [xsec.chord * xsec.airfoil.perimeter() for xsec in self.xsecs] elif type == "xy": sectional_spans = self.span(type="y", _sectional=True) @@ -383,18 +375,11 @@ def area(self, sectional_chords = [ (inner_chord + outer_chord) / 2 - for inner_chord, outer_chord in zip( - xsec_chords[1:], - xsec_chords[:-1] - ) + for inner_chord, outer_chord in zip(xsec_chords[1:], xsec_chords[:-1]) ] sectional_areas = [ - span * chord - for span, chord in zip( - sectional_spans, - sectional_chords - ) + span * chord for span, chord in zip(sectional_spans, sectional_chords) ] if _sectional: return sectional_areas @@ -413,24 +398,26 @@ def area(self, ) half_span_to_centerline = np.minimum( - half_span_to_centerline, - np.abs(quarter_chord_location[1]) + half_span_to_centerline, np.abs(quarter_chord_location[1]) ) half_area = half_area + ( - half_span_to_centerline * self.mean_geometric_chord() + half_span_to_centerline * self.mean_geometric_chord() ) - if self.symmetric: # Returns the total area of both the left and right wing halves on mirrored wings. + if ( + self.symmetric + ): # Returns the total area of both the left and right wing halves on mirrored wings. area = 2 * half_area else: area = half_area return area - def aspect_ratio(self, - type: str = "geometric", - ) -> float: + def aspect_ratio( + self, + type: str = "geometric", + ) -> float: """ Computes the aspect ratio of the wing, with options for various ways of measuring this. @@ -448,10 +435,9 @@ def aspect_ratio(self, return self.span() ** 2 / self.area() elif type == "effective": - return ( - self.span(type="yz", include_centerline_distance=True) ** 2 / - self.area(type="planform", include_centerline_distance=True) - ) + return self.span( + type="yz", include_centerline_distance=True + ) ** 2 / self.area(type="planform", include_centerline_distance=True) else: raise ValueError("Bad value of `type`!") @@ -463,13 +449,17 @@ def is_entirely_symmetric(self) -> bool: if not (surf.symmetric or surf.deflection == 0): return False - if not self.symmetric: # If the wing itself isn't mirrored (e.g., vertical stabilizer), check that it's symmetric + if ( + not self.symmetric + ): # If the wing itself isn't mirrored (e.g., vertical stabilizer), check that it's symmetric for xsec in self.xsecs: if not xsec.xyz_le[1] == 0: # Surface has to be right on the centerline return False if not xsec.twist == 0: # Surface has to be untwisted return False - if not np.allclose(xsec.airfoil.local_camber(), 0): # Surface has to have a symmetric airfoil. + if not np.allclose( + xsec.airfoil.local_camber(), 0 + ): # Surface has to have a symmetric airfoil. return False return True @@ -495,9 +485,13 @@ def mean_aerodynamic_chord(self) -> float: for inner_xsec, outer_xsec in zip(self.xsecs[:-1], self.xsecs[1:]): section_taper_ratio = outer_xsec.chord / inner_xsec.chord - section_MAC_length = (2 / 3) * inner_xsec.chord * ( - (1 + section_taper_ratio + section_taper_ratio ** 2) / - (1 + section_taper_ratio) + section_MAC_length = ( + (2 / 3) + * inner_xsec.chord + * ( + (1 + section_taper_ratio + section_taper_ratio**2) + / (1 + section_taper_ratio) + ) ) sectional_MAC_lengths.append(section_MAC_length) @@ -522,27 +516,19 @@ def mean_twist_angle(self) -> float: sectional_twists = [ (inner_xsec.twist + outer_xsec.twist) / 2 - for inner_xsec, outer_xsec in zip( - self.xsecs[1:], - self.xsecs[:-1] - ) + for inner_xsec, outer_xsec in zip(self.xsecs[1:], self.xsecs[:-1]) ] sectional_areas = self.area(_sectional=True) sectional_twist_area_products = [ - twist * area - for twist, area in zip( - sectional_twists, sectional_areas - ) + twist * area for twist, area in zip(sectional_twists, sectional_areas) ] mean_twist = sum(sectional_twist_area_products) / sum(sectional_areas) return mean_twist - def mean_sweep_angle(self, - x_nondim=0.25 - ) -> float: + def mean_sweep_angle(self, x_nondim=0.25) -> float: """ Returns the mean sweep angle (in degrees) of the wing, relative to the x-axis. Positive sweep is backwards, negative sweep is forward. @@ -565,14 +551,10 @@ def mean_sweep_angle(self, The mean sweep angle, in degrees. """ root_quarter_chord = self._compute_xyz_of_WingXSec( - 0, - x_nondim=x_nondim, - z_nondim=0 + 0, x_nondim=x_nondim, z_nondim=0 ) tip_quarter_chord = self._compute_xyz_of_WingXSec( - -1, - x_nondim=x_nondim, - z_nondim=0 + -1, x_nondim=x_nondim, z_nondim=0 ) vec = tip_quarter_chord - root_quarter_chord @@ -584,9 +566,7 @@ def mean_sweep_angle(self, return sweep_deg - def mean_dihedral_angle(self, - x_nondim=0.25 - ) -> float: + def mean_dihedral_angle(self, x_nondim=0.25) -> float: """ Returns the mean dihedral angle (in degrees) of the wing, relative to the XY plane. Positive dihedral is bending up, negative dihedral is bending down. @@ -610,14 +590,10 @@ def mean_dihedral_angle(self, """ root_quarter_chord = self._compute_xyz_of_WingXSec( - 0, - x_nondim=x_nondim, - z_nondim=0 + 0, x_nondim=x_nondim, z_nondim=0 ) tip_quarter_chord = self._compute_xyz_of_WingXSec( - -1, - x_nondim=x_nondim, - z_nondim=0 + -1, x_nondim=x_nondim, z_nondim=0 ) vec = tip_quarter_chord - root_quarter_chord @@ -628,7 +604,9 @@ def mean_dihedral_angle(self, vec_norm[1], ) - def aerodynamic_center(self, chord_fraction: float = 0.25, _sectional=False) -> np.ndarray: + def aerodynamic_center( + self, chord_fraction: float = 0.25, _sectional=False + ) -> np.ndarray: """ Computes the location of the aerodynamic center of the wing. Uses the generalized methodology described here: @@ -650,21 +628,24 @@ def aerodynamic_center(self, chord_fraction: float = 0.25, _sectional=False) -> for inner_xsec, outer_xsec in zip(self.xsecs[:-1], self.xsecs[1:]): section_taper_ratio = outer_xsec.chord / inner_xsec.chord - section_MAC_length = (2 / 3) * inner_xsec.chord * ( - (1 + section_taper_ratio + section_taper_ratio ** 2) / - (1 + section_taper_ratio) + section_MAC_length = ( + (2 / 3) + * inner_xsec.chord + * ( + (1 + section_taper_ratio + section_taper_ratio**2) + / (1 + section_taper_ratio) + ) ) - section_MAC_le = ( - inner_xsec.xyz_le + - (outer_xsec.xyz_le - inner_xsec.xyz_le) * - (1 + 2 * section_taper_ratio) / - (3 + 3 * section_taper_ratio) + section_MAC_le = inner_xsec.xyz_le + ( + outer_xsec.xyz_le - inner_xsec.xyz_le + ) * (1 + 2 * section_taper_ratio) / (3 + 3 * section_taper_ratio) + section_AC = section_MAC_le + np.array( + [ # TODO rotate this vector by the local twist angle + chord_fraction * section_MAC_length, + 0, + 0, + ] ) - section_AC = section_MAC_le + np.array([ # TODO rotate this vector by the local twist angle - chord_fraction * section_MAC_length, - 0, - 0 - ]) sectional_ACs.append(section_AC) @@ -696,9 +677,7 @@ def taper_ratio(self) -> float: """ return self.xsecs[-1].chord / self.xsecs[0].chord - def volume(self, - _sectional: bool = False - ) -> Union[float, List[float]]: + def volume(self, _sectional: bool = False) -> Union[float, List[float]]: """ Computes the volume of the Wing. @@ -711,21 +690,13 @@ def volume(self, The computed volume. """ - xsec_areas = [ - xsec.xsec_area() - for xsec in self.xsecs - ] - separations = self.span( - type="yz", - _sectional=True - ) + xsec_areas = [xsec.xsec_area() for xsec in self.xsecs] + separations = self.span(type="yz", _sectional=True) sectional_volumes = [ separation / 3 * (area_a + area_b + (area_a * area_b + 1e-100) ** 0.5) for area_a, area_b, separation in zip( - xsec_areas[1:], - xsec_areas[:-1], - separations + xsec_areas[1:], xsec_areas[:-1], separations ) ] @@ -755,9 +726,10 @@ def get_control_surface_names(self) -> List[str]: return control_surface_names - def set_control_surface_deflections(self, - control_surface_mappings: Dict[str, float], - ) -> None: + def set_control_surface_deflections( + self, + control_surface_mappings: Dict[str, float], + ) -> None: """ Sets the deflection of all control surfaces on this wing, based on the provided mapping. @@ -773,12 +745,15 @@ def set_control_surface_deflections(self, for xsec in self.xsecs: for control_surface in xsec.control_surfaces: if control_surface.name in control_surface_mappings.keys(): - control_surface.deflection = control_surface_mappings[control_surface.name] - - def control_surface_area(self, - by_name: Optional[str] = None, - type: Optional[str] = "planform", - ) -> float: + control_surface.deflection = control_surface_mappings[ + control_surface.name + ] + + def control_surface_area( + self, + by_name: Optional[str] = None, + type: Optional[str] = "planform", + ) -> float: """ Computes the total area of all control surfaces on this wing, optionally filtered by their name. @@ -816,12 +791,10 @@ def control_surface_area(self, """ sectional_areas = self.area( - type=type, - include_centerline_distance=False, - _sectional=True + type=type, include_centerline_distance=False, _sectional=True ) - control_surface_area = 0. + control_surface_area = 0.0 for xsec, sect_area in zip(self.xsecs[:-1], sectional_areas): for control_surface in xsec.control_surfaces: @@ -829,13 +802,11 @@ def control_surface_area(self, if control_surface.trailing_edge: control_surface_chord_fraction = np.maximum( - 1 - control_surface.hinge_point, - 0 + 1 - control_surface.hinge_point, 0 ) else: control_surface_chord_fraction = np.maximum( - control_surface.hinge_point, - 0 + control_surface.hinge_point, 0 ) control_surface_area += control_surface_chord_fraction * sect_area @@ -845,15 +816,18 @@ def control_surface_area(self, return control_surface_area - def mesh_body(self, - method="quad", - chordwise_resolution: int = 36, - chordwise_spacing_function_per_side: Callable[[float, float, float], np.ndarray] = np.cosspace, - mesh_surface: bool = True, - mesh_tips: bool = True, - mesh_trailing_edge: bool = True, - mesh_symmetric: bool = True, - ) -> Tuple[np.ndarray, np.ndarray]: + def mesh_body( + self, + method="quad", + chordwise_resolution: int = 36, + chordwise_spacing_function_per_side: Callable[ + [float, float, float], np.ndarray + ] = np.cosspace, + mesh_surface: bool = True, + mesh_tips: bool = True, + mesh_trailing_edge: bool = True, + mesh_symmetric: bool = True, + ) -> Tuple[np.ndarray, np.ndarray]: """ Meshes the outer mold line surface of the wing. @@ -920,15 +894,15 @@ def mesh_body(self, """ - airfoil_nondim_coordinates = np.array([ - xsec.airfoil - .repanel( - n_points_per_side=chordwise_resolution + 1, - spacing_function_per_side=chordwise_spacing_function_per_side, - ) - .coordinates - for xsec in self.xsecs - ]) + airfoil_nondim_coordinates = np.array( + [ + xsec.airfoil.repanel( + n_points_per_side=chordwise_resolution + 1, + spacing_function_per_side=chordwise_spacing_function_per_side, + ).coordinates + for xsec in self.xsecs + ] + ) x_nondim = airfoil_nondim_coordinates[:, :, 0].T y_nondim = airfoil_nondim_coordinates[:, :, 1].T @@ -942,7 +916,7 @@ def mesh_body(self, z_nondim=y_n, add_camber=False, ), - axis=0 + axis=0, ) ) @@ -950,7 +924,7 @@ def mesh_body(self, faces = [] - num_i = (len(self.xsecs) - 1) + num_i = len(self.xsecs) - 1 num_j = len(spanwise_strips) - 1 def index_of(iloc, jloc): @@ -1000,26 +974,23 @@ def add_face(*indices): faces = np.array(faces) if mesh_symmetric and self.symmetric: - flipped_points = np.multiply( - points, - np.array([ - [1, -1, 1] - ]) - ) + flipped_points = np.multiply(points, np.array([[1, -1, 1]])) points, faces = mesh_utils.stack_meshes( - (points, faces), - (flipped_points, faces) + (points, faces), (flipped_points, faces) ) return points, faces - def mesh_thin_surface(self, - method="tri", - chordwise_resolution: int = 36, - chordwise_spacing_function: Callable[[float, float, float], np.ndarray] = np.cosspace, - add_camber: bool = True, - ) -> Tuple[np.ndarray, np.ndarray]: + def mesh_thin_surface( + self, + method="tri", + chordwise_resolution: int = 36, + chordwise_spacing_function: Callable[ + [float, float, float], np.ndarray + ] = np.cosspace, + add_camber: bool = True, + ) -> Tuple[np.ndarray, np.ndarray]: """ Meshes the mean camber line of the wing as a thin-sheet body. @@ -1074,11 +1045,7 @@ def mesh_thin_surface(self, """ - x_nondim = chordwise_spacing_function( - 0, - 1, - chordwise_resolution + 1 - ) + x_nondim = chordwise_spacing_function(0, 1, chordwise_resolution + 1) spanwise_strips = [] for x_n in x_nondim: @@ -1089,7 +1056,7 @@ def mesh_thin_surface(self, z_nondim=0, add_camber=add_camber, ), - axis=0 + axis=0, ) ) @@ -1123,10 +1090,9 @@ def add_face(*indices): if self.symmetric: index_offset = np.length(points) - points = np.concatenate([ - points, - np.multiply(points, np.array([[1, -1, 1]])) - ]) + points = np.concatenate( + [points, np.multiply(points, np.array([[1, -1, 1]]))] + ) def index_of(iloc, jloc): return index_offset + iloc + jloc * num_i @@ -1144,11 +1110,12 @@ def index_of(iloc, jloc): return points, faces - def mesh_line(self, - x_nondim: Union[float, List[float]] = 0.25, - z_nondim: Union[float, List[float]] = 0, - add_camber: bool = True, - ) -> List[np.ndarray]: + def mesh_line( + self, + x_nondim: Union[float, List[float]] = 0.25, + z_nondim: Union[float, List[float]] = 0, + add_camber: bool = True, + ) -> List[np.ndarray]: """ Meshes a line that goes through each of the WingXSec objects in this wing. @@ -1201,7 +1168,9 @@ def mesh_line(self, xsec_z_nondim = z_nondim if add_camber: - xsec_z_nondim = xsec_z_nondim + xsec.airfoil.local_camber(x_over_c=x_nondim) + xsec_z_nondim = xsec_z_nondim + xsec.airfoil.local_camber( + x_over_c=x_nondim + ) points_on_line.append( self._compute_xyz_of_WingXSec( @@ -1225,6 +1194,7 @@ def draw(self, *args, **kwargs): """ from aerosandbox.geometry.airplane import Airplane + return Airplane(wings=[self]).draw(*args, **kwargs) def draw_wireframe(self, *args, **kwargs): @@ -1239,6 +1209,7 @@ def draw_wireframe(self, *args, **kwargs): """ from aerosandbox.geometry.airplane import Airplane + return Airplane(wings=[self]).draw_wireframe(*args, **kwargs) def draw_three_view(self, *args, **kwargs): @@ -1253,12 +1224,14 @@ def draw_three_view(self, *args, **kwargs): """ from aerosandbox.geometry.airplane import Airplane + return Airplane(wings=[self]).draw_three_view(*args, **kwargs) - def subdivide_sections(self, - ratio: int, - spacing_function: Callable[[float, float, float], np.ndarray] = np.linspace - ) -> "Wing": + def subdivide_sections( + self, + ratio: int, + spacing_function: Callable[[float, float, float], np.ndarray] = np.linspace, + ) -> "Wing": """ Generates a new Wing that subdivides the existing sections of this Wing into several smaller ones. Splits each section into N=`ratio` smaller sub-sections by inserting new cross-sections (xsecs) as needed. @@ -1297,8 +1270,7 @@ def subdivide_sections(self, blended_airfoil = xsec_b.airfoil else: blended_airfoil = xsec_a.airfoil.blend_with_another_airfoil( - airfoil=xsec_b.airfoil, - blend_fraction=b_weight + airfoil=xsec_b.airfoil, blend_fraction=b_weight ) new_xsecs.append( @@ -1318,7 +1290,7 @@ def subdivide_sections(self, name=self.name, xsecs=new_xsecs, symmetric=self.symmetric, - analysis_specific_options=self.analysis_specific_options + analysis_specific_options=self.analysis_specific_options, ) def _compute_xyz_le_of_WingXSec(self, index: int): @@ -1331,21 +1303,22 @@ def _compute_xyz_te_of_WingXSec(self, index: int): z_nondim=0, ) - def _compute_xyz_of_WingXSec(self, - index, - x_nondim, - z_nondim, - ): + def _compute_xyz_of_WingXSec( + self, + index, + x_nondim, + z_nondim, + ): xg_local, yg_local, zg_local = self._compute_frame_of_WingXSec(index) origin = self.xsecs[index].xyz_le xsec = self.xsecs[index] return origin + ( - x_nondim * xsec.chord * xg_local + - z_nondim * xsec.chord * zg_local + x_nondim * xsec.chord * xg_local + z_nondim * xsec.chord * zg_local ) def _compute_frame_of_WingXSec( - self, index: int) -> Tuple[np.ndarray, np.ndarray, np.ndarray]: + self, index: int + ) -> Tuple[np.ndarray, np.ndarray, np.ndarray]: """ Computes the local reference frame associated with a particular cross-section (XSec) of this wing. @@ -1391,16 +1364,15 @@ def project_to_YZ_plane_and_normalize(vector): zg_local = np.cross(xg_local, yg_local) * z_scale ### Twist the reference frame by the WingXSec twist angle - rot = np.rotation_matrix_3D( - self.xsecs[index].twist * pi / 180, - yg_local - ) + rot = np.rotation_matrix_3D(self.xsecs[index].twist * pi / 180, yg_local) xg_local = rot @ xg_local zg_local = rot @ zg_local return xg_local, yg_local, zg_local - def _compute_frame_of_section(self, index: int) -> Tuple[np.ndarray, np.ndarray, np.ndarray]: + def _compute_frame_of_section( + self, index: int + ) -> Tuple[np.ndarray, np.ndarray, np.ndarray]: """ Computes the local reference frame associated with a particular section. (Note that sections and cross sections are different! cross-sections, or xsecs, are the vertices, and sections are the parts in between. In @@ -1428,11 +1400,9 @@ def _compute_frame_of_section(self, index: int) -> Tuple[np.ndarray, np.ndarray, zg_local = cross / np.linalg.norm(cross) - quarter_chord_vector = ( - 0.75 * out_front + 0.25 * out_back - ) - ( - 0.75 * in_front + 0.25 * in_back - ) + quarter_chord_vector = (0.75 * out_front + 0.25 * out_back) - ( + 0.75 * in_front + 0.25 * in_back + ) quarter_chord_vector[0] = 0 yg_local = quarter_chord_vector / np.linalg.norm(quarter_chord_vector) @@ -1447,15 +1417,16 @@ class WingXSec(AeroSandboxObject): Definition for a wing cross-section ("X-section"). """ - def __init__(self, - xyz_le: Union[np.ndarray, List] = None, - chord: float = 1., - twist: float = 0., - airfoil: Airfoil = None, - control_surfaces: Optional[List['ControlSurface']] = None, - analysis_specific_options: Optional[Dict[type, Dict[str, Any]]] = None, - **deprecated_kwargs, - ): + def __init__( + self, + xyz_le: Union[np.ndarray, List] = None, + chord: float = 1.0, + twist: float = 0.0, + airfoil: Airfoil = None, + control_surfaces: Optional[List["ControlSurface"]] = None, + analysis_specific_options: Optional[Dict[type, Dict[str, Any]]] = None, + **deprecated_kwargs, + ): """ Defines a new wing cross-section. @@ -1508,10 +1479,10 @@ def __init__(self, Note: Control surface definition through WingXSec properties (control_surface_is_symmetric, control_surface_hinge_point, control_surface_deflection) is deprecated. Control surfaces should be handled according to the following protocol: - + 1. If control_surfaces is an empty list (default, user does not specify any control surfaces), use deprecated WingXSec control surface definition properties. This will result in 1 control surface at this xsec. - + Usage example: >>> xsecs = asb.WingXSec( @@ -1541,17 +1512,18 @@ def __init__(self, >>> chord = 2, >>> control_surfaces = None >>>) - + See avl.py for example of control_surface handling using this protocol. """ ### Set defaults if xyz_le is None: - xyz_le = np.array([0., 0., 0.]) + xyz_le = np.array([0.0, 0.0, 0.0]) if airfoil is None: import warnings + warnings.warn( "An airfoil is not specified for WingXSec. Defaulting to NACA 0012.", - stacklevel=2 + stacklevel=2, ) airfoil = Airfoil("naca0012") if control_surfaces is None: @@ -1567,28 +1539,30 @@ def __init__(self, self.analysis_specific_options = analysis_specific_options ### Handle deprecated arguments - if 'twist_angle' in deprecated_kwargs.keys(): + if "twist_angle" in deprecated_kwargs.keys(): import warnings + warnings.warn( "DEPRECATED: 'twist_angle' has been renamed 'twist', and will break in future versions.", - stacklevel=2 + stacklevel=2, ) - self.twist = deprecated_kwargs['twist_angle'] + self.twist = deprecated_kwargs["twist_angle"] if ( - 'control_surface_is_symmetric' in locals() or - 'control_surface_hinge_point' in locals() or - 'control_surface_deflection' in locals() + "control_surface_is_symmetric" in locals() + or "control_surface_hinge_point" in locals() + or "control_surface_deflection" in locals() ): import warnings + warnings.warn( "DEPRECATED: Define control surfaces using the `control_surfaces` parameter, which takes in a list of asb.ControlSurface objects.", - stacklevel=2 + stacklevel=2, ) - if 'control_surface_is_symmetric' not in locals(): + if "control_surface_is_symmetric" not in locals(): control_surface_is_symmetric = True - if 'control_surface_hinge_point' not in locals(): + if "control_surface_hinge_point" not in locals(): control_surface_hinge_point = 0.75 - if 'control_surface_deflection' not in locals(): + if "control_surface_deflection" not in locals(): control_surface_deflection = 0 self.control_surfaces.append( @@ -1602,9 +1576,7 @@ def __init__(self, def __repr__(self) -> str: return f"WingXSec (Airfoil: {self.airfoil.name}, chord: {self.chord}, twist: {self.twist})" - def translate(self, - xyz: Union[np.ndarray, List] - ) -> "WingXSec": + def translate(self, xyz: Union[np.ndarray, List]) -> "WingXSec": """ Returns a copy of this WingXSec that has been translated by `xyz`. @@ -1624,7 +1596,7 @@ def xsec_area(self): Returns: The (dimensional) cross-sectional area of the WingXSec. """ - return self.airfoil.area() * self.chord ** 2 + return self.airfoil.area() * self.chord**2 class ControlSurface(AeroSandboxObject): @@ -1632,14 +1604,15 @@ class ControlSurface(AeroSandboxObject): Definition for a control surface, which is attached to a particular WingXSec via WingXSec's `control_surfaces=[]` parameter. """ - def __init__(self, - name: str = "Untitled", - symmetric: bool = True, - deflection: float = 0.0, - hinge_point: float = 0.75, - trailing_edge: bool = True, - analysis_specific_options: Optional[Dict[type, Dict[str, Any]]] = None, - ): + def __init__( + self, + name: str = "Untitled", + symmetric: bool = True, + deflection: float = 0.0, + hinge_point: float = 0.75, + trailing_edge: bool = True, + analysis_specific_options: Optional[Dict[type, Dict[str, Any]]] = None, + ): """ Define a new control surface. @@ -1705,15 +1678,12 @@ def __repr__(self) -> str: if not self.trailing_edge: keys += ["trailing_edge"] - info = ", ".join([ - f"{k}={self.__dict__[k]}" - for k in keys - ]) + info = ", ".join([f"{k}={self.__dict__[k]}" for k in keys]) return f"ControlSurface ({info})" -if __name__ == '__main__': +if __name__ == "__main__": wing = Wing( xsecs=[ WingXSec( @@ -1726,9 +1696,9 @@ def __repr__(self) -> str: name="Elevator", trailing_edge=True, hinge_point=0.75, - deflection=5 + deflection=5, ) - ] + ], ), WingXSec( xyz_le=[0.5, 1, 0], @@ -1741,7 +1711,7 @@ def __repr__(self) -> str: chord=0.3, airfoil=Airfoil("naca0012"), twist=0, - ) + ), ] ).translate([1, 0, 0]) # wing.subdivide_sections(5).draw() diff --git a/aerosandbox/library/aerodynamics/components.py b/aerosandbox/library/aerodynamics/components.py index f7a8b10b5..bce7e32ed 100644 --- a/aerosandbox/library/aerodynamics/components.py +++ b/aerosandbox/library/aerodynamics/components.py @@ -3,10 +3,10 @@ def CDA_control_linkage( - Re_l: Union[float, np.ndarray], - linkage_length: Union[float, np.ndarray], - is_covered: Union[bool, np.ndarray] = False, - is_top: Union[bool, np.ndarray] = False, + Re_l: Union[float, np.ndarray], + linkage_length: Union[float, np.ndarray], + is_covered: Union[bool, np.ndarray] = False, + is_top: Union[bool, np.ndarray] = False, ) -> Union[float, np.ndarray]: """ Computes the drag area (CDA) of a typical control usage as used on a well-manufactured RC airplane. @@ -40,18 +40,15 @@ def CDA_control_linkage( """ x = dict( - Re_l=Re_l, - linkage_length=linkage_length, - is_covered=is_covered, - is_top=is_top + Re_l=Re_l, linkage_length=linkage_length, is_covered=is_covered, is_top=is_top ) p = { - 'CD0' : 7.833083680086374e-05, - 'CD1' : 0.0001216877860785463, - 'c_length' : 30.572471745477774, - 'covered_drag_ratio': 0.7520722978405192, - 'top_drag_ratio' : 1.1139040832208857 + "CD0": 7.833083680086374e-05, + "CD1": 0.0001216877860785463, + "c_length": 30.572471745477774, + "covered_drag_ratio": 0.7520722978405192, + "top_drag_ratio": 1.1139040832208857, } Re = x["Re_l"] @@ -59,34 +56,28 @@ def CDA_control_linkage( is_covered = x["is_covered"] is_top = x["is_top"] - side_drag_multiplier = np.where( - is_top, - p["top_drag_ratio"], - 1 - ) - covered_drag_multiplier = np.where( - is_covered, - p["covered_drag_ratio"], - 1 - ) + side_drag_multiplier = np.where(is_top, p["top_drag_ratio"], 1) + covered_drag_multiplier = np.where(is_covered, p["covered_drag_ratio"], 1) linkage_length_multiplier = 1 + p["c_length"] * linkage_length - CDA_raw = ( - p["CD1"] / (Re / 1e5) + - p["CD0"] - ) + CDA_raw = p["CD1"] / (Re / 1e5) + p["CD0"] - return side_drag_multiplier * covered_drag_multiplier * linkage_length_multiplier * CDA_raw + return ( + side_drag_multiplier + * covered_drag_multiplier + * linkage_length_multiplier + * CDA_raw + ) def CDA_control_surface_gaps( - local_chord: float, - control_surface_span: float, - local_thickness_over_chord: float = 0.12, - control_surface_hinge_x: float = 0.75, - n_side_gaps: int = 2, - side_gap_width: float = None, - hinge_gap_width: float = None, + local_chord: float, + control_surface_span: float, + local_thickness_over_chord: float = 0.12, + control_surface_hinge_x: float = 0.75, + n_side_gaps: int = 2, + side_gap_width: float = None, + hinge_gap_width: float = None, ) -> float: """ Computes the drag area (CDA) of the gaps associated with a typical wing control surface. @@ -138,11 +129,7 @@ def CDA_control_surface_gaps( """ if side_gap_width is None: side_gap_width = np.maximum( - np.maximum( - 0.002, - 0.006 * local_chord - ), - control_surface_span * 0.01 + np.maximum(0.002, 0.006 * local_chord), control_surface_span * 0.01 ) if hinge_gap_width is None: hinge_gap_width = 0.03 * local_chord @@ -153,7 +140,9 @@ def CDA_control_surface_gaps( tested on 2412 airfoil at C_L = 0.1 and Re_c = 2 * 10^6" """ - CDA_side_gaps = n_side_gaps * (side_gap_width * local_chord * local_thickness_over_chord) * 0.50 + CDA_side_gaps = ( + n_side_gaps * (side_gap_width * local_chord * local_thickness_over_chord) * 0.50 + ) ### Spanwise gaps (at hinge line of control surface) """ @@ -166,10 +155,7 @@ def CDA_control_surface_gaps( return CDA_side_gaps + CDA_hinge_gap -def CDA_protruding_bolt_or_rivet( - diameter: float, - kind: str = "flush_rivet" -): +def CDA_protruding_bolt_or_rivet(diameter: float, kind: str = "flush_rivet"): """ Computes the drag area (CDA) of a protruding bolt or rivet. @@ -197,15 +183,15 @@ def CDA_protruding_bolt_or_rivet( Returns: The drag area [m^2] of the bolt or rivet. """ - S_ref = np.pi * diameter ** 2 / 4 + S_ref = np.pi * diameter**2 / 4 CD_factors = { - "flush_rivet" : 0.002, - "round_rivet" : 0.04, - "flat_head_bolt" : 0.02, - "round_head_bolt" : 0.32, + "flush_rivet": 0.002, + "round_rivet": 0.04, + "flat_head_bolt": 0.02, + "round_head_bolt": 0.32, "cylindrical_bolt": 0.42, - "hex_bolt" : 0.80, + "hex_bolt": 0.80, } try: @@ -217,9 +203,9 @@ def CDA_protruding_bolt_or_rivet( def CDA_perpendicular_sheet_metal_joint( - joint_width: float, - sheet_metal_thickness: float, - kind: str = "butt_joint_with_inside_joiner" + joint_width: float, + sheet_metal_thickness: float, + kind: str = "butt_joint_with_inside_joiner", ): """ Computes the drag area (CDA) of a sheet metal joint that is perpendicular to the flow. @@ -270,18 +256,18 @@ def CDA_perpendicular_sheet_metal_joint( S_ref = joint_width * sheet_metal_thickness CD_factors = { - "butt_joint_with_inside_joiner" : 0.01, - "butt_joint_with_inside_weld" : 0.01, - "butt_joint_with_outside_joiner" : 0.70, - "butt_joint_with_outside_weld" : 0.51, - "lap_joint_forward_facing_step" : 0.40, - "lap_joint_backward_facing_step" : 0.22, - "lap_joint_forward_facing_step_with_bevel" : 0.11, - "lap_joint_backward_facing_step_with_bevel" : 0.24, - "lap joint_forward_facing_step_with_rounded_bevel" : 0.04, + "butt_joint_with_inside_joiner": 0.01, + "butt_joint_with_inside_weld": 0.01, + "butt_joint_with_outside_joiner": 0.70, + "butt_joint_with_outside_weld": 0.51, + "lap_joint_forward_facing_step": 0.40, + "lap_joint_backward_facing_step": 0.22, + "lap_joint_forward_facing_step_with_bevel": 0.11, + "lap_joint_backward_facing_step_with_bevel": 0.24, + "lap joint_forward_facing_step_with_rounded_bevel": 0.04, "lap_joint_backward_facing_step_with_rounded_bevel": 0.16, - "flush_lap_joint_forward_facing_step" : 0.13, - "flush_lap_joint_backward_facing_step" : 0.07, + "flush_lap_joint_forward_facing_step": 0.13, + "flush_lap_joint_backward_facing_step": 0.07, } try: diff --git a/aerosandbox/library/aerodynamics/inviscid.py b/aerosandbox/library/aerodynamics/inviscid.py index ea0161979..091f92955 100644 --- a/aerosandbox/library/aerodynamics/inviscid.py +++ b/aerosandbox/library/aerodynamics/inviscid.py @@ -2,10 +2,10 @@ def induced_drag( - lift, - span, - dynamic_pressure, - oswalds_efficiency=1, + lift, + span, + dynamic_pressure, + oswalds_efficiency=1, ): """ Computes the induced drag associated with a lifting planar wing. @@ -19,17 +19,15 @@ def induced_drag( Returns: Induced drag force [Newtons] """ - return lift ** 2 / ( - dynamic_pressure * np.pi * span ** 2 * oswalds_efficiency - ) + return lift**2 / (dynamic_pressure * np.pi * span**2 * oswalds_efficiency) def oswalds_efficiency( - taper_ratio: float, - aspect_ratio: float, - sweep: float = 0., - fuselage_diameter_to_span_ratio: float = 0., - method="nita_scholz", + taper_ratio: float, + aspect_ratio: float, + sweep: float = 0.0, + fuselage_diameter_to_span_ratio: float = 0.0, + method="nita_scholz", ) -> float: """ Computes the Oswald's efficiency factor for a planar, tapered, swept wing. @@ -52,32 +50,28 @@ def oswalds_efficiency( """ sweep = np.clip(sweep, 0, 90) # TODO input proper analytic continuation - def f(l): # f(lambda), given as Eq. 36 in the Nita and Scholz paper (see parent docstring). - return ( - 0.0524 * l ** 4 - - 0.15 * l ** 3 - + 0.1659 * l ** 2 - - 0.0706 * l - + 0.0119 - ) + def f( + l, + ): # f(lambda), given as Eq. 36 in the Nita and Scholz paper (see parent docstring). + return 0.0524 * l**4 - 0.15 * l**3 + 0.1659 * l**2 - 0.0706 * l + 0.0119 delta_lambda = -0.357 + 0.45 * np.exp(-0.0375 * sweep) # Eq. 37 in Nita & Scholz. # Note: there is a typo in the cited paper; the negative in the exponent was omitted. # A bit of thinking about this reveals that this omission must be erroneous. - e_theo = 1 / ( - 1 + f(taper_ratio - delta_lambda) * aspect_ratio - ) + e_theo = 1 / (1 + f(taper_ratio - delta_lambda) * aspect_ratio) ### Correction factors, with nomenclature from Nita & Scholz k_e_F = 1 - 2 * (fuselage_diameter_to_span_ratio) ** 2 - k_e_D0 = np.mean([ - 0.873, # jet transport - 0.864, # business jet - 0.804, # turboprop - 0.804, # general aviation - ]) + k_e_D0 = np.mean( + [ + 0.873, # jet transport + 0.864, # business jet + 0.804, # turboprop + 0.804, # general aviation + ] + ) k_e_M = 1 # Compressibility correction not added because it only becomes significant well after M_crit, after which wave # drag dominates. Nita & Scholz also do not provide a model that extrapolates sensibly beyond M=0.9 or so, @@ -89,17 +83,16 @@ def f(l): # f(lambda), given as Eq. 36 in the Nita and Scholz paper (see parent mach_correction_factor = 1 Q = 1 / (e_theo * k_e_F) from aerosandbox.library.aerodynamics.viscous import Cf_flat_plate + P = 0.38 * Cf_flat_plate(Re_L=1e6) - e = mach_correction_factor / ( - Q + P * np.pi * aspect_ratio - ) + e = mach_correction_factor / (Q + P * np.pi * aspect_ratio) return e def optimal_taper_ratio( - sweep=0., + sweep=0.0, ) -> float: """ Computes the optimal (minimum-induced-drag) taper ratio for a given quarter-chord sweep angle. @@ -120,10 +113,10 @@ def optimal_taper_ratio( def CL_over_Cl( - aspect_ratio: float, - mach: float = 0., - sweep: float = 0., - Cl_is_compressible: bool = True + aspect_ratio: float, + mach: float = 0.0, + sweep: float = 0.0, + Cl_is_compressible: bool = True, ) -> float: """ Returns the ratio of 3D lift coefficient (with compressibility) to the 2D lift coefficient. @@ -153,13 +146,13 @@ def CL_over_Cl( For most accurate results, set this flag to True, and then model profile characteristics separately. """ - prandtl_glauert_beta_squared_ideal = 1 - mach ** 2 + prandtl_glauert_beta_squared_ideal = 1 - mach**2 # beta_squared = 1 - mach ** 2 beta_squared = np.softmax( prandtl_glauert_beta_squared_ideal, -prandtl_glauert_beta_squared_ideal, - hardness=3.0 + hardness=3.0, ) ### Alternate formulations @@ -171,19 +164,23 @@ def CL_over_Cl( # Symbolically simplified to remove the PG singularity. eta = 1.0 CL_ratio = aspect_ratio / ( - 2 + ( - 4 + (aspect_ratio ** 2 * beta_squared / eta ** 2) + (np.tand(sweep) * aspect_ratio / eta) ** 2 - ) ** 0.5 + 2 + + ( + 4 + + (aspect_ratio**2 * beta_squared / eta**2) + + (np.tand(sweep) * aspect_ratio / eta) ** 2 + ) + ** 0.5 ) if Cl_is_compressible: - CL_ratio = CL_ratio * beta_squared ** 0.5 + CL_ratio = CL_ratio * beta_squared**0.5 return CL_ratio def induced_drag_ratio_from_ground_effect( - h_over_b # type: float + h_over_b, # type: float ): """ Gives the ratio of actual induced drag to free-flight induced drag experienced by a wing in ground effect. @@ -194,17 +191,11 @@ def induced_drag_ratio_from_ground_effect( :param h_over_b: (Height above ground) divided by (wingspan). :return: Ratio of induced drag in ground effect to induced drag out of ground effect [unitless] """ - h_over_b = np.softmax( - h_over_b, - 0, - hardness=1 / 0.03 - ) - return 1 - np.exp( - -4.01 * (2 * h_over_b) ** 0.717 - ) + h_over_b = np.softmax(h_over_b, 0, hardness=1 / 0.03) + return 1 - np.exp(-4.01 * (2 * h_over_b) ** 0.717) -if __name__ == '__main__': +if __name__ == "__main__": import matplotlib.pyplot as plt import aerosandbox.tools.pretty_plots as p diff --git a/aerosandbox/library/aerodynamics/normal_shock_relations.py b/aerosandbox/library/aerodynamics/normal_shock_relations.py index a9b66b345..459a34e95 100644 --- a/aerosandbox/library/aerodynamics/normal_shock_relations.py +++ b/aerosandbox/library/aerodynamics/normal_shock_relations.py @@ -1,8 +1,9 @@ # From https://www.grc.nasa.gov/WWW/K-12/airplane/normal.html + def mach_number_after_normal_shock( - mach_upstream, - gamma=1.4, + mach_upstream, + gamma=1.4, ): """ Computes the mach number immediately after a normal shock wave. @@ -15,17 +16,12 @@ def mach_number_after_normal_shock( """ gm1 = gamma - 1 - m2 = mach_upstream ** 2 + m2 = mach_upstream**2 - return ( - (gm1 * m2 + 2) / (2 * gamma * m2 - gm1) - ) ** 0.5 + return ((gm1 * m2 + 2) / (2 * gamma * m2 - gm1)) ** 0.5 -def density_ratio_across_normal_shock( - mach_upstream, - gamma=1.4 -): +def density_ratio_across_normal_shock(mach_upstream, gamma=1.4): """ Computes the ratio of fluid density across a normal shock. @@ -38,17 +34,10 @@ def density_ratio_across_normal_shock( Returns: rho_after_shock / rho_before_shock """ - return ( - (gamma + 1) * mach_upstream ** 2 - ) / ( - (gamma - 1) * mach_upstream ** 2 + 2 - ) + return ((gamma + 1) * mach_upstream**2) / ((gamma - 1) * mach_upstream**2 + 2) -def temperature_ratio_across_normal_shock( - mach_upstream, - gamma=1.4 -): +def temperature_ratio_across_normal_shock(mach_upstream, gamma=1.4): """ Computes the ratio of fluid temperature across a normal shock. @@ -62,18 +51,11 @@ def temperature_ratio_across_normal_shock( """ gm1 = gamma - 1 - m2 = mach_upstream ** 2 - return ( - (2 * gamma * m2 - gm1) * (gm1 * m2 + 2) - ) / ( - (gamma + 1) ** 2 * m2 - ) + m2 = mach_upstream**2 + return ((2 * gamma * m2 - gm1) * (gm1 * m2 + 2)) / ((gamma + 1) ** 2 * m2) -def pressure_ratio_across_normal_shock( - mach_upstream, - gamma=1.4 -): +def pressure_ratio_across_normal_shock(mach_upstream, gamma=1.4): """ Computes the ratio of fluid static pressure across a normal shock. @@ -86,18 +68,11 @@ def pressure_ratio_across_normal_shock( Returns: P_after_shock / P_before_shock """ - m2 = mach_upstream ** 2 - return ( - 2 * gamma * m2 - (gamma - 1) - ) / ( - (gamma + 1) - ) + m2 = mach_upstream**2 + return (2 * gamma * m2 - (gamma - 1)) / ((gamma + 1)) -def total_pressure_ratio_across_normal_shock( - mach_upstream, - gamma=1.4 -): +def total_pressure_ratio_across_normal_shock(mach_upstream, gamma=1.4): """ Computes the ratio of fluid total pressure across a normal shock. @@ -111,22 +86,24 @@ def total_pressure_ratio_across_normal_shock( """ return density_ratio_across_normal_shock( - mach_upstream=mach_upstream, - gamma=gamma + mach_upstream=mach_upstream, gamma=gamma ) ** (gamma / (gamma - 1)) * ( - (gamma + 1) / (2 * gamma * mach_upstream ** 2 - (gamma - 1)) - ) ** (1 / (gamma - 1)) + (gamma + 1) / (2 * gamma * mach_upstream**2 - (gamma - 1)) + ) ** ( + 1 / (gamma - 1) + ) -if __name__ == '__main__': +if __name__ == "__main__": + def q_ratio(mach): return ( - density_ratio_across_normal_shock(mach) * - ( - mach_number_after_normal_shock(mach) * - temperature_ratio_across_normal_shock(mach) ** 0.5 - ) ** 2 + density_ratio_across_normal_shock(mach) + * ( + mach_number_after_normal_shock(mach) + * temperature_ratio_across_normal_shock(mach) ** 0.5 + ) + ** 2 ) - q_ratio(2) diff --git a/aerosandbox/library/aerodynamics/test_aerodynamics/test_Cf_flat_plate.py b/aerosandbox/library/aerodynamics/test_aerodynamics/test_Cf_flat_plate.py index 5e4ae6a15..ca076ba5d 100644 --- a/aerosandbox/library/aerodynamics/test_aerodynamics/test_Cf_flat_plate.py +++ b/aerosandbox/library/aerodynamics/test_aerodynamics/test_Cf_flat_plate.py @@ -16,11 +16,7 @@ def plot_Cf_flat_plates(): "hybrid-sharpe-convex", "hybrid-sharpe-nonconvex", ]: - plt.loglog( - Res, - aero.Cf_flat_plate(Res, method=method), - label=method - ) + 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", @@ -29,5 +25,5 @@ def plot_Cf_flat_plates(): ) -if __name__ == '__main__': +if __name__ == "__main__": plot_Cf_flat_plates() diff --git a/aerosandbox/library/aerodynamics/test_aerodynamics/test_transonic_optimization.py b/aerosandbox/library/aerodynamics/test_aerodynamics/test_transonic_optimization.py index e647c8146..8f5cf7ce6 100644 --- a/aerosandbox/library/aerodynamics/test_aerodynamics/test_transonic_optimization.py +++ b/aerosandbox/library/aerodynamics/test_aerodynamics/test_transonic_optimization.py @@ -7,7 +7,9 @@ def test_simple_scalar_optimization(): opti = asb.Opti() - mach = opti.variable(init_guess=0.8, ) + mach = opti.variable( + init_guess=0.8, + ) CD_induced = 0.1 / mach CD_wave = transonic.approximate_CD_wave( mach=mach, @@ -19,5 +21,5 @@ def test_simple_scalar_optimization(): sol = opti.solve() -if __name__ == '__main__': +if __name__ == "__main__": pytest.main() diff --git a/aerosandbox/library/aerodynamics/transonic.py b/aerosandbox/library/aerodynamics/transonic.py index daa16a170..564c5a1af 100644 --- a/aerosandbox/library/aerodynamics/transonic.py +++ b/aerosandbox/library/aerodynamics/transonic.py @@ -1,11 +1,11 @@ import aerosandbox.numpy as np -from aerosandbox.modeling.splines.hermite import linear_hermite_patch, cubic_hermite_patch +from aerosandbox.modeling.splines.hermite import ( + linear_hermite_patch, + cubic_hermite_patch, +) -def sears_haack_drag( - radius_max: float, - length: float -) -> float: +def sears_haack_drag(radius_max: float, length: float) -> float: """ Yields the idealized drag area (denoted CDA, or equivalently, D/q) of a Sears-Haack body. @@ -22,14 +22,11 @@ def sears_haack_drag( Returns: The drag area (CDA, or D/q) of the body. To get the drag force, multiply by the dynamic pressure. """ - CDA = 9 * np.pi ** 2 * radius_max ** 2 / (2 * length ** 2) + CDA = 9 * np.pi**2 * radius_max**2 / (2 * length**2) return CDA -def sears_haack_drag_from_volume( - volume: float, - length: float -) -> float: +def sears_haack_drag_from_volume(volume: float, length: float) -> float: """ See documentation for sears_haack_drag() in this same file. @@ -37,16 +34,11 @@ def sears_haack_drag_from_volume( Also returns a drag area (denoted CDA, or equivalently, D/q). """ - CDA = 128 * volume ** 2 / (np.pi * length ** 4) + CDA = 128 * volume**2 / (np.pi * length**4) return CDA -def mach_crit_Korn( - CL, - t_over_c, - sweep=0, - kappa_A=0.95 -): +def mach_crit_Korn(CL, t_over_c, sweep=0, kappa_A=0.95): """ Wave drag_force coefficient prediction using the low-fidelity Korn Equation method; derived in "Configuration Aerodynamics" by W.H. Mason, Sect. 7.5.2, pg. 7-18 @@ -62,7 +54,11 @@ def mach_crit_Korn( """ smooth_abs_CL = np.softmax(CL, -CL, hardness=10) - M_dd = kappa_A / np.cosd(sweep) - t_over_c / np.cosd(sweep) ** 2 - smooth_abs_CL / (10 * np.cosd(sweep) ** 3) + M_dd = ( + kappa_A / np.cosd(sweep) + - t_over_c / np.cosd(sweep) ** 2 + - smooth_abs_CL / (10 * np.cosd(sweep) ** 3) + ) M_crit = M_dd - (0.1 / 80) ** (1 / 3) return M_crit @@ -81,21 +77,21 @@ def Cd_wave_Korn(Cl, t_over_c, mach, sweep=0, kappa_A=0.95): smooth_abs_Cl = np.softmax(Cl, -Cl, hardness=10) mach = np.fmax(mach, 0) - Mdd = kappa_A / np.cosd(sweep) - t_over_c / np.cosd(sweep) ** 2 - smooth_abs_Cl / (10 * np.cosd(sweep) ** 3) - Mcrit = Mdd - (0.1 / 80) ** (1 / 3) - Cd_wave = np.where( - mach > Mcrit, - 20 * (mach - Mcrit) ** 4, - 0 + Mdd = ( + kappa_A / np.cosd(sweep) + - t_over_c / np.cosd(sweep) ** 2 + - smooth_abs_Cl / (10 * np.cosd(sweep) ** 3) ) + Mcrit = Mdd - (0.1 / 80) ** (1 / 3) + Cd_wave = np.where(mach > Mcrit, 20 * (mach - Mcrit) ** 4, 0) return Cd_wave def approximate_CD_wave( - mach, - mach_crit, - CD_wave_at_fully_supersonic, + mach, + mach_crit, + CD_wave_at_fully_supersonic, ): """ An approximate relation for computing transonic wave drag, based on an object's Mach number. @@ -138,11 +134,7 @@ def approximate_CD_wave( """ mach_crit_max = 1 - (0.1 / 80) ** (1 / 3) - mach_crit = -np.softmax( - -mach_crit, - -mach_crit_max, - hardness=50 - ) + mach_crit = -np.softmax(-mach_crit, -mach_crit_max, hardness=50) ### The following approximate relation is derived in W.H. Mason, "Configuration Aerodynamics", Chapter 7. Transonic Aerodynamics of Airfoils and Wings. ### Equation 7-8 on Page 7-19. @@ -165,32 +157,26 @@ def approximate_CD_wave( f_a=20 * (0.1 / 80) ** (4 / 3), f_b=1, dfdx_a=0.1, - dfdx_b=8 + dfdx_b=8, ), np.where( mach < 1.2, cubic_hermite_patch( - mach, - x_a=1.05, - x_b=1.2, - f_a=1, - f_b=1, - dfdx_a=8, - dfdx_b=-4 + mach, x_a=1.05, x_b=1.2, f_a=1, f_b=1, dfdx_a=8, dfdx_b=-4 ), np.blend( switch=4 * 2 * (mach - 1.2) / (1.2 - 0.8), value_switch_high=0.8, value_switch_low=1.2, - ) + ), # 0.8 + 0.2 * np.exp(20 * (1.2 - mach)) - ) - ) - ) + ), + ), + ), ) -if __name__ == '__main__': +if __name__ == "__main__": mc = 0.6 drag = lambda mach: approximate_CD_wave( mach, @@ -203,7 +189,7 @@ def approximate_CD_wave( fig, ax = plt.subplots(1, 3, figsize=(10, 5)) - mach = np.linspace(0., 2, 10000) + mach = np.linspace(0.0, 2, 10000) drag = drag(mach) ddragdm = np.gradient(drag, np.diff(mach)[0]) dddragdm = np.gradient(ddragdm, np.diff(mach)[0]) diff --git a/aerosandbox/library/aerodynamics/unsteady.py b/aerosandbox/library/aerodynamics/unsteady.py index ca5de7de1..489e3c30a 100644 --- a/aerosandbox/library/aerodynamics/unsteady.py +++ b/aerosandbox/library/aerodynamics/unsteady.py @@ -5,95 +5,98 @@ # Welcome to the unsteady aerodynamics library! # In here you will find analytical, time-domain models for the -# unsteady lift response of thin airfoils. Here is a quick overview +# unsteady lift response of thin airfoils. Here is a quick overview # of what's been implemented so far: # 1) Unsteady pitching (Wagner's problem) # 2) Transverse wing-gust encounters (Kussner's problem) -# 3) Added mass +# 3) Added mass # 4) Pitching maneuver through a gust (Combination of all 3 models above) -# The models usually take Callable objects as arguments which given the reduced time, return the quantity of +# The models usually take Callable objects as arguments which given the reduced time, return the quantity of # interest (Velocity profile, angle of attack etc.). For an explanation of reduced time see function calculate_reduced_time. # In main() you will find some example gusts as well as example pitchig profiles. -# You can easily build your own and pass them to the appropriate functions -# to instantly get the lift response! Although not yet implemented, it is possible to +# You can easily build your own and pass them to the appropriate functions +# to instantly get the lift response! Although not yet implemented, it is possible to # calculate an optimal unsteady maneuver through any known disturbance. -# If you run this file as is, the lift history of a flat plate pitching through a +# If you run this file as is, the lift history of a flat plate pitching through a # top hat gust will be computed. def calculate_reduced_time( - time: Union[float, np.ndarray], - velocity: Union[float, np.ndarray], - chord: float + time: Union[float, np.ndarray], velocity: Union[float, np.ndarray], chord: float ) -> Union[float, np.ndarray]: - """ - Calculates reduced time from time in seconds and velocity history in m/s. + """ + Calculates reduced time from time in seconds and velocity history in m/s. For constant velocity it reduces to s = 2*U*t/c - The reduced time is the number of semichords travelled by the airfoil/aircaft - i.e. 2 / chord * integral from t0 to t of velocity dt - - + The reduced time is the number of semichords travelled by the airfoil/aircaft + i.e. 2 / chord * integral from t0 to t of velocity dt + + Args: - time (float,np.ndarray) : Time in seconds + time (float,np.ndarray) : Time in seconds velocity (float,np.ndarray): Either a constant velocity or array of velocities at corresponding reduced times chord (float) : The chord of the airfoil - + Returns: - The reduced time as an ndarray or float similar to the input. The first element is 0. + The reduced time as an ndarray or float similar to the input. The first element is 0. """ if type(velocity) == float or type(velocity) == int: return 2 * velocity * time / chord else: - assert np.size(velocity) == np.size(time), "The velocity history and time must have the same length" + assert np.size(velocity) == np.size( + time + ), "The velocity history and time must have the same length" reduced_time = np.zeros_like(time) for i in range(len(time) - 1): - reduced_time[i + 1] = reduced_time[i] + (velocity[i + 1] + velocity[i]) / 2 * (time[i + 1] - time[i]) + reduced_time[i + 1] = reduced_time[i] + ( + velocity[i + 1] + velocity[i] + ) / 2 * (time[i + 1] - time[i]) return 2 / chord * reduced_time def wagners_function(reduced_time: Union[float, np.ndarray]): - """ - A commonly used approximation to Wagner's function + """ + A commonly used approximation to Wagner's function (Jones, R.T. The Unsteady Lift of a Finite Wing; Technical Report NACA TN-682; NACA: Washington, DC, USA, 1939) - + Args: reduced_time (float,np.ndarray) : Equal to the number of semichords travelled. See function calculate_reduced_time """ - wagner = (1 - 0.165 * np.exp(-0.0455 * reduced_time) - - 0.335 * np.exp(-0.3 * reduced_time)) * np.where(reduced_time >= 0, 1, 0) + wagner = ( + 1 - 0.165 * np.exp(-0.0455 * reduced_time) - 0.335 * np.exp(-0.3 * reduced_time) + ) * np.where(reduced_time >= 0, 1, 0) return wagner def kussners_function(reduced_time: Union[float, np.ndarray]): - """ + """ A commonly used approximation to Kussner's function (Sears and Sparks 1941) - + Args: reduced_time (float,np.ndarray) : This is equal to the number of semichords travelled. See function calculate_reduced_time """ - kussner = (1 - 0.5 * np.exp(-0.13 * reduced_time) - - 0.5 * np.exp(-reduced_time)) * np.where(reduced_time >= 0, 1, 0) + kussner = ( + 1 - 0.5 * np.exp(-0.13 * reduced_time) - 0.5 * np.exp(-reduced_time) + ) * np.where(reduced_time >= 0, 1, 0) return kussner def indicial_pitch_response( - reduced_time: Union[float, np.ndarray], - angle_of_attack: float # In degrees + reduced_time: Union[float, np.ndarray], angle_of_attack: float # In degrees ): """ Computes the evolution of the lift coefficient in Wagner's problem which can be interpreted as follows 1) An impulsively started flat plate at constant angle of attack 2) An impuslive change in the angle of attack of a flat plate at constant velocity - + The model predicts infinite added mass at the first instant due to the infinite acceleration The delta function term (and therefore added mass) has been ommited in this case. Reduced_time = 0 corresponds to the instance the airfoil pitches/accelerates - + Args: reduced_time (float,np.ndarray) : Reduced time, equal to the number of semichords travelled. See function reduced_time angle_of_attack (float) : The angle of attack, in degrees @@ -102,20 +105,20 @@ def indicial_pitch_response( def indicial_gust_response( - reduced_time: Union[float, np.ndarray], - gust_velocity: float, - plate_velocity: float, - angle_of_attack: float = 0, # In degrees - chord: float = 1 + reduced_time: Union[float, np.ndarray], + gust_velocity: float, + plate_velocity: float, + angle_of_attack: float = 0, # In degrees + chord: float = 1, ): """ - Computes the evolution of the lift coefficient of a flat plate entering a - an infinitely long, sharp step gust (Heaveside function) at a constant angle of attack. + Computes the evolution of the lift coefficient of a flat plate entering a + an infinitely long, sharp step gust (Heaveside function) at a constant angle of attack. Reduced_time = 0 corresponds to the instance the gust is entered - - + + (Leishman, Principles of Helicopter Aerodynamics, S8.10,S8.11) - + Args: reduced_time (float,np.ndarray) : Reduced time, equal to the number of semichords travelled. See function reduced_time gust_velocity (float) : velocity in m/s of the top hat gust @@ -125,22 +128,26 @@ def indicial_gust_response( """ angle_of_attack_radians = np.deg2rad(angle_of_attack) offset = chord / 2 * (1 - np.cos(angle_of_attack_radians)) - return (2 * np.pi * np.arctan(gust_velocity / plate_velocity) * - np.cos(angle_of_attack_radians) * - kussners_function(reduced_time - offset)) + return ( + 2 + * np.pi + * np.arctan(gust_velocity / plate_velocity) + * np.cos(angle_of_attack_radians) + * kussners_function(reduced_time - offset) + ) def calculate_lift_due_to_transverse_gust( - reduced_time: np.ndarray, - gust_velocity_profile: Callable[[float], float], - plate_velocity: float, - angle_of_attack: Union[float, Callable[[float], float]] = 0, # In Degrees - chord: float = 1 + reduced_time: np.ndarray, + gust_velocity_profile: Callable[[float], float], + plate_velocity: float, + angle_of_attack: Union[float, Callable[[float], float]] = 0, # In Degrees + chord: float = 1, ): """ Calculates the lift (as a function of reduced time) caused by an arbitrary transverse gust profile by computing duhamel superposition integral of Kussner's problem at a constant angle of attack - + Args: reduced_time (float,np.ndarray) : Reduced time, equal to the number of semichords travelled. See function reduced_time gust_velocity_profile (Callable[[float],float]) : The transverse velocity profile that the flate plate experiences. Must be a function that takes reduced time and returns a velocity @@ -148,26 +155,32 @@ def calculate_lift_due_to_transverse_gust( angle_of_attack (Union[float,Callable[[float],float]]) : The angle of attack, in degrees. Can either be a float for constant angle of attack or a Callable that takes reduced time and returns angle of attack chord (float) : The chord of the plate in meters Returns: - lift_coefficient (np.ndarray) : The lift coefficient history of the flat plate + lift_coefficient (np.ndarray) : The lift coefficient history of the flat plate """ - assert type(angle_of_attack) != np.ndarray, "Please provide either a Callable or a float for the angle of attack" + assert ( + type(angle_of_attack) != np.ndarray + ), "Please provide either a Callable or a float for the angle of attack" if isinstance(angle_of_attack, float) or isinstance(angle_of_attack, int): + def AoA_function(reduced_time): return np.deg2rad(angle_of_attack) + else: + def AoA_function(reduced_time): return np.deg2rad(angle_of_attack(reduced_time)) def dK_ds(reduced_time): - return (0.065 * np.exp(-0.13 * reduced_time) + - 0.5 * np.exp(-reduced_time)) + return 0.065 * np.exp(-0.13 * reduced_time) + 0.5 * np.exp(-reduced_time) def integrand(sigma, s, chord): offset = chord / 2 * (1 - np.cos(AoA_function(s - sigma))) - return (dK_ds(sigma) * - gust_velocity_profile(s - sigma - offset) * - np.cos(AoA_function(s - sigma))) + return ( + dK_ds(sigma) + * gust_velocity_profile(s - sigma - offset) + * np.cos(AoA_function(s - sigma)) + ) lift_coefficient = np.zeros_like(reduced_time) for i, s in enumerate(reduced_time): @@ -178,34 +191,40 @@ def integrand(sigma, s, chord): def calculate_lift_due_to_pitching_profile( - reduced_time: np.ndarray, - angle_of_attack: Union[Callable[[float], float], float] # In degrees + reduced_time: np.ndarray, + angle_of_attack: Union[Callable[[float], float], float], # In degrees ): """ - Calculates the duhamel superposition integral of Wagner's problem. - Given some arbitrary pitching profile. The lift coefficient as a function + Calculates the duhamel superposition integral of Wagner's problem. + Given some arbitrary pitching profile. The lift coefficient as a function of reduced time of a flat plate can be computed using this function - - + + Args: reduced_time (float,np.ndarray) : Reduced time, equal to the number of semichords travelled. See function reduced_time angle_of_attack (Callable[[float],float]) : The angle of attack as a function of reduced time of the flat plate. Must be a Callable that takes reduced time and returns angle of attack Returns: - lift_coefficient (np.ndarray) : The lift coefficient history of the flat plate + lift_coefficient (np.ndarray) : The lift coefficient history of the flat plate """ - assert (reduced_time >= 0).all(), "Please use positive time. Negative time not supported" + assert ( + reduced_time >= 0 + ).all(), "Please use positive time. Negative time not supported" if isinstance(angle_of_attack, float) or isinstance(angle_of_attack, int): + def AoA_function(reduced_time): return np.deg2rad(angle_of_attack) + else: + def AoA_function(reduced_time): return np.deg2rad(angle_of_attack(reduced_time)) def dW_ds(reduced_time): - return (0.1005 * np.exp(-0.3 * reduced_time) + - 0.00750075 * np.exp(-0.0455 * reduced_time)) + return 0.1005 * np.exp(-0.3 * reduced_time) + 0.00750075 * np.exp( + -0.0455 * reduced_time + ) def integrand(sigma, s): if dW_ds(sigma) < 0: @@ -218,26 +237,23 @@ def integrand(sigma, s): I = quad(integrand, 0, s, args=s)[0] # print(I) - lift_coefficient[i] = 2 * np.pi * (AoA_function(s) * - wagners_function(0) + - I) + lift_coefficient[i] = 2 * np.pi * (AoA_function(s) * wagners_function(0) + I) return lift_coefficient def added_mass_due_to_pitching( - reduced_time: np.ndarray, - angle_of_attack: Callable[[float], float] # In degrees + reduced_time: np.ndarray, angle_of_attack: Callable[[float], float] # In degrees ): """ This function calculate the lift coefficient due to the added mass of a flat plate - pitching about its midchord while moving at constant velocity. - + pitching about its midchord while moving at constant velocity. + Args: reduced_time (np.ndarray) : Reduced time, equal to the number of semichords travelled. See function reduced_time angle_of_attack (Callable[[float],float]) : The angle of attack as a function of reduced time of the flat plate Returns: - lift_coefficient (np.ndarray) : The lift coefficient history of the flat plate + lift_coefficient (np.ndarray) : The lift coefficient history of the flat plate """ AoA = np.array([np.deg2rad(angle_of_attack(s)) for s in reduced_time]) @@ -249,40 +265,41 @@ def added_mass_due_to_pitching( def pitching_through_transverse_gust( - reduced_time: np.ndarray, - gust_velocity_profile: Callable[[float], float], - plate_velocity: float, - angle_of_attack: Union[Callable[[float], float], float], # In degrees - chord: float = 1 + reduced_time: np.ndarray, + gust_velocity_profile: Callable[[float], float], + plate_velocity: float, + angle_of_attack: Union[Callable[[float], float], float], # In degrees + chord: float = 1, ): """ This function calculates the lift as a function of time of a flat plate pitching about its midchord through an arbitrary transverse gust. It combines Kussner's gust response with - wagners pitch response as well as added mass. - + wagners pitch response as well as added mass. + The following physics are accounted for 1) Vorticity shed from the trailing edge due to gust profile 2) Vorticity shed from the trailing edge due to pitching profile 3) Added mass (non-circulatory force) due to pitching about midchord - + The following physics are NOT taken accounted for 1) Any type of flow separation 2) Leading edge vorticity shedding 3) Deflected wake due to gust (flat wake assumption) - - + + Args: reduced_time (float,np.ndarray) : Reduced time, equal to the number of semichords travelled. See function reduced_time gust_velocity_profile (Callable[[float],float]) : The transverse velocity profile that the flate plate experiences. Must be a function that takes reduced time and returns a velocity plate_velocity (float) :The velocity by which the flat plate enters the gust angle_of_attack (Union[float,Callable[[float],float]]) : The angle of attack, in degrees. Can either be a float for constant angle of attack or a Callable that takes reduced time and returns angle of attack chord (float) : The chord of the plate in meters - + Returns: - lift_coefficient (np.ndarray) : The lift coefficient history of the flat plate + lift_coefficient (np.ndarray) : The lift coefficient history of the flat plate """ - gust_lift = calculate_lift_due_to_transverse_gust(reduced_time, gust_velocity_profile, plate_velocity, - angle_of_attack, chord) + gust_lift = calculate_lift_due_to_transverse_gust( + reduced_time, gust_velocity_profile, plate_velocity, angle_of_attack, chord + ) pitch_lift = calculate_lift_due_to_pitching_profile(reduced_time, angle_of_attack) added_mass_lift = added_mass_due_to_pitching(reduced_time, angle_of_attack) @@ -307,13 +324,13 @@ def top_hat_gust(reduced_time: float) -> float: def sine_squared_gust(reduced_time: float) -> float: """ - A canonical gust of used by the FAA to show 'compliance with the - requirements of Title 14, Code of Federal Regulations (14 CFR) 25.341, - Gust and turbulence loads. Section 25.341 specifies the discrete gust - and continuous turbulence dynamic load conditions that apply to the + A canonical gust of used by the FAA to show 'compliance with the + requirements of Title 14, Code of Federal Regulations (14 CFR) 25.341, + Gust and turbulence loads. Section 25.341 specifies the discrete gust + and continuous turbulence dynamic load conditions that apply to the airplane and engines.' Args: - reduced_time (float) + reduced_time (float) Returns: gust_velocity (float) """ @@ -322,9 +339,10 @@ def sine_squared_gust(reduced_time: float) -> float: finish = 10 gust_width_to_chord_ratio = 5 if start <= reduced_time <= finish: - gust_velocity = (gust_strength * - np.sin((np.pi * reduced_time) / - gust_width_to_chord_ratio) ** 2) + gust_velocity = ( + gust_strength + * np.sin((np.pi * reduced_time) / gust_width_to_chord_ratio) ** 2 + ) else: gust_velocity = 0 @@ -335,18 +353,18 @@ def gaussian_pitch(reduced_time: float) -> float: """ A pitch maneuver resembling a guassian curve Args: - reduced_time (float) + reduced_time (float) Returns: angle_of_attack (float) : in degrees """ - return -25 * np.exp(-((reduced_time - 7.5) / 3) ** 2) + return -25 * np.exp(-(((reduced_time - 7.5) / 3) ** 2)) def linear_ramp_pitch(reduced_time: float) -> float: """ A pitch maneuver resembling a linear ramp Args: - reduced_time (float) + reduced_time (float) Returns: angle_of_attack (float) : in degrees """ @@ -365,26 +383,47 @@ def linear_ramp_pitch(reduced_time: float) -> float: time = np.linspace(0, 10, 100) # Time in seconds wing_velocity = 2 # Wing horizontal velocity in m/s chord = 2 - reduced_time = calculate_reduced_time(time, wing_velocity, chord) # Number of semi chords travelled + reduced_time = calculate_reduced_time( + time, wing_velocity, chord + ) # Number of semi chords travelled # Visualize the gust profiles as well as the pitch maneuvers fig, ax1 = plt.subplots(dpi=300) - ln1 = ax1.plot(reduced_time, np.array([top_hat_gust(s) for s in reduced_time]), label="Top-Hat Gust", lw=3) - ln2 = ax1.plot(reduced_time, np.array([sine_squared_gust(s) for s in reduced_time]), label="Sine-Squared Gust", - lw=3) + ln1 = ax1.plot( + reduced_time, + np.array([top_hat_gust(s) for s in reduced_time]), + label="Top-Hat Gust", + lw=3, + ) + ln2 = ax1.plot( + reduced_time, + np.array([sine_squared_gust(s) for s in reduced_time]), + label="Sine-Squared Gust", + lw=3, + ) ax1.set_xlabel("Reduced time") ax1.set_ylabel("Velocity (m/s)") ax2 = ax1.twinx() - ln3 = ax2.plot(reduced_time, np.array([gaussian_pitch(s) for s in reduced_time]), label="Guassian Pitch", c="red", - ls="--", lw=3) + ln3 = ax2.plot( + reduced_time, + np.array([gaussian_pitch(s) for s in reduced_time]), + label="Guassian Pitch", + c="red", + ls="--", + lw=3, + ) ax2.set_ylabel("Angle of Attack, degrees") lns = ln1 + ln2 + ln3 labs = [l.get_label() for l in lns] ax2.legend(lns, labs, loc="lower right") plt.title("Gust and pitch example profiles") - total_lift = pitching_through_transverse_gust(reduced_time, top_hat_gust, wing_velocity, gaussian_pitch) - gust_lift = calculate_lift_due_to_transverse_gust(reduced_time, top_hat_gust, wing_velocity, gaussian_pitch) + total_lift = pitching_through_transverse_gust( + reduced_time, top_hat_gust, wing_velocity, gaussian_pitch + ) + gust_lift = calculate_lift_due_to_transverse_gust( + reduced_time, top_hat_gust, wing_velocity, gaussian_pitch + ) pitch_lift = calculate_lift_due_to_pitching_profile(reduced_time, gaussian_pitch) added_mass_lift = added_mass_due_to_pitching(reduced_time, gaussian_pitch) @@ -398,4 +437,4 @@ def linear_ramp_pitch(reduced_time: float) -> float: plt.xlabel("Reduced time") plt.ylabel("$C_\ell$") plt.title("Guassian Pitch Maneuver Through Top-Hat Gust") - plt.show() \ No newline at end of file + plt.show() diff --git a/aerosandbox/library/aerodynamics/viscous.py b/aerosandbox/library/aerodynamics/viscous.py index b8209a5e0..4eedbb161 100644 --- a/aerosandbox/library/aerodynamics/viscous.py +++ b/aerosandbox/library/aerodynamics/viscous.py @@ -2,10 +2,7 @@ def Cd_cylinder( - Re_D: float, - mach: float = 0., - include_mach_effects=True, - subcritical_only=False + Re_D: float, mach: float = 0.0, include_mach_effects=True, subcritical_only=False ) -> float: """ Returns the drag coefficient of a cylinder in crossflow as a function of its Reynolds number and Mach. @@ -40,31 +37,35 @@ def Cd_cylinder( if subcritical_only: Cd_mach_0 = 10 ** (csub0 * x + csub1) + csub2 + csub3 * x else: - log10_Cd = ( - (np.log10(10 ** (csub0 * x + csub1) + csub2 + csub3 * x)) - * (1 - 1 / (1 + np.exp(-csigh * (x - csigc)))) - + (csup0 + csupscl / csuph * np.log(np.exp(csuph * (csupc - x)) + 1)) - * (1 / (1 + np.exp(-csigh * (x - csigc)))) + log10_Cd = (np.log10(10 ** (csub0 * x + csub1) + csub2 + csub3 * x)) * ( + 1 - 1 / (1 + np.exp(-csigh * (x - csigc))) + ) + (csup0 + csupscl / csuph * np.log(np.exp(csuph * (csupc - x)) + 1)) * ( + 1 / (1 + np.exp(-csigh * (x - csigc))) ) - Cd_mach_0 = 10 ** log10_Cd + Cd_mach_0 = 10**log10_Cd ##### Do the compressible part of the computation if include_mach_effects: m = mach - p = {'a_sub' : 0.03458900259594298, - 'a_sup' : -0.7129528087049688, - 'cd_sub' : 1.163206940186374, - 'cd_sup' : 1.2899213533122527, - 's_sub' : 3.436601777569716, - 's_sup' : -1.37123096976983, - 'trans' : 1.022819211244295, - 'trans_str': 19.017600596069848} - - Cd_over_Cd_mach_0 = np.blend( - p["trans_str"] * (m - p["trans"]), - p["cd_sup"] + np.exp(p["a_sup"] + p["s_sup"] * (m - p["trans"])), - p["cd_sub"] + np.exp(p["a_sub"] + p["s_sub"] * (m - p["trans"])) - ) / 1.1940010047391572 + p = { + "a_sub": 0.03458900259594298, + "a_sup": -0.7129528087049688, + "cd_sub": 1.163206940186374, + "cd_sup": 1.2899213533122527, + "s_sub": 3.436601777569716, + "s_sup": -1.37123096976983, + "trans": 1.022819211244295, + "trans_str": 19.017600596069848, + } + + Cd_over_Cd_mach_0 = ( + np.blend( + p["trans_str"] * (m - p["trans"]), + p["cd_sup"] + np.exp(p["a_sup"] + p["s_sup"] * (m - p["trans"])), + p["cd_sub"] + np.exp(p["a_sub"] + p["s_sub"] * (m - p["trans"])), + ) + / 1.1940010047391572 + ) Cd = Cd_mach_0 * Cd_over_Cd_mach_0 @@ -74,10 +75,7 @@ def Cd_cylinder( return Cd -def Cf_flat_plate( - Re_L: float, - method="hybrid-sharpe-convex" -) -> float: +def Cf_flat_plate(Re_L: float, method="hybrid-sharpe-convex") -> float: """ Returns the mean skin friction coefficient over a flat plate. @@ -125,24 +123,24 @@ def Cf_flat_plate( Re_L = np.abs(Re_L) if method == "blasius": - return 1.328 / Re_L ** 0.5 + return 1.328 / Re_L**0.5 elif method == "turbulent": return 0.074 / Re_L ** (1 / 5) elif method == "hybrid-cengel": return 0.074 / Re_L ** (1 / 5) - 1742 / Re_L elif method == "hybrid-schlichting": - return 0.02666 * Re_L ** -0.139 + return 0.02666 * Re_L**-0.139 elif method == "hybrid-sharpe-convex": return np.softmax( Cf_flat_plate(Re_L, method="blasius"), Cf_flat_plate(Re_L, method="hybrid-schlichting"), - hardness=1e3 + hardness=1e3, ) elif method == "hybrid-sharpe-nonconvex": return np.softmax( Cf_flat_plate(Re_L, method="blasius"), Cf_flat_plate(Re_L, method="hybrid-cengel"), - hardness=1e3 + hardness=1e3, ) @@ -155,6 +153,7 @@ def Cl_flat_plate(alpha, Re_c=None): """ if Re_c is not None: from warnings import warn + warn("`Re_c` input will be deprecated in a future version.") alpha_rad = alpha * np.pi / 180 @@ -188,7 +187,7 @@ def Cl_2412(alpha, Re_c): "This function is deprecated. Use `asb.Airfoil.get_aero_from_neuralfoil()` instead.", DeprecationWarning, ) - return 0.2568 + 0.1206 * alpha - 0.002018 * alpha ** 2 + return 0.2568 + 0.1206 * alpha - 0.002018 * alpha**2 def Cd_profile_2412(alpha, Re_c): @@ -211,8 +210,12 @@ def Cd_profile_2412(alpha, Re_c): cxy = -0.00588 cy = 0.04838 - log_CD = CD0 + cx * (alpha - alpha0) ** 2 + cy * (log_Re - Re0) ** 2 + cxy * (alpha - alpha1) * ( - log_Re - Re1) # basically, a rotated paraboloid in logspace + log_CD = ( + CD0 + + cx * (alpha - alpha0) ** 2 + + cy * (log_Re - Re0) ** 2 + + cxy * (alpha - alpha1) * (log_Re - Re1) + ) # basically, a rotated paraboloid in logspace CD = np.exp(log_CD) return CD @@ -239,18 +242,20 @@ def Cl_e216(alpha, Re_c): atr = 3.6775107602844948e-01 c0l = -2.5909363461176749e-01 c0t = 8.3824440586718862e-01 - ctr = 1.1431810545735890e+02 + ctr = 1.1431810545735890e02 ksl = 5.3416670116733611e-01 - rtr = 3.9713338634462829e+01 - rtr2 = -3.3634858542657771e+00 + rtr = 3.9713338634462829e01 + rtr2 = -3.3634858542657771e00 xsl = -1.2220899840236835e-01 a = alpha r = log10_Re - Cl = (c0t + a1t * a + a4t * a ** 4) * 1 / (1 + np.exp(ctr - rtr * r - atr * a - rtr2 * r ** 2)) + ( - c0l + a1l * a + asl / (1 + np.exp(-ksl * (a - xsl)))) * ( - 1 - 1 / (1 + np.exp(ctr - rtr * r - atr * a - rtr2 * r ** 2))) + Cl = (c0t + a1t * a + a4t * a**4) * 1 / ( + 1 + np.exp(ctr - rtr * r - atr * a - rtr2 * r**2) + ) + (c0l + a1l * a + asl / (1 + np.exp(-ksl * (a - xsl)))) * ( + 1 - 1 / (1 + np.exp(ctr - rtr * r - atr * a - rtr2 * r**2)) + ) return Cl @@ -274,24 +279,27 @@ def Cd_profile_e216(alpha, Re_c): a2l = 8.7552076545610764e-04 a4t = 1.1220763679805319e-05 atr = 4.2456038382581129e-01 - c0l = -1.4099657419753771e+00 - c0t = -2.3855286371940609e+00 - ctr = 9.1474872611212135e+01 - rtr = 3.0218483612170434e+01 - rtr2 = -2.4515094313899279e+00 + c0l = -1.4099657419753771e00 + c0t = -2.3855286371940609e00 + ctr = 9.1474872611212135e01 + rtr = 3.0218483612170434e01 + rtr2 = -2.4515094313899279e00 a = alpha r = log10_Re - log10_Cd = (c0t + a1t * a + a4t * a ** 4) * 1 / (1 + np.exp(ctr - rtr * r - atr * a - rtr2 * r ** 2)) + ( - c0l + a1l * a + a2l * a ** 2) * (1 - 1 / (1 + np.exp(ctr - rtr * r - atr * a - rtr2 * r ** 2))) + log10_Cd = (c0t + a1t * a + a4t * a**4) * 1 / ( + 1 + np.exp(ctr - rtr * r - atr * a - rtr2 * r**2) + ) + (c0l + a1l * a + a2l * a**2) * ( + 1 - 1 / (1 + np.exp(ctr - rtr * r - atr * a - rtr2 * r**2)) + ) - Cd = 10 ** log10_Cd + Cd = 10**log10_Cd return Cd -def Cd_wave_e216(Cl, mach, sweep=0.): +def Cd_wave_e216(Cl, mach, sweep=0.0): r""" A curve fit I did to Eppler 216 airfoil data. Within -0.4 < CL < 0.75 and 0 < mach < ~0.9, has R^2 = 0.9982. @@ -316,13 +324,15 @@ def Cd_wave_e216(Cl, mach, sweep=0.): c3 = 2.1305118052118968e-01 c4 = 7.8812272501525316e-01 c5 = 3.3888938102072169e-03 - l0 = 1.5298928303149546e+00 + l0 = 1.5298928303149546e00 l1 = 5.2389999717540392e-01 m = mach_perpendicular l = Cl_perpendicular - Cd_wave = (np.fmax(m - (c0 + c1 * np.sqrt(c3 + (l - c4) ** 2) + c5 * l), 0) * (l0 + l1 * l)) ** 2 + Cd_wave = ( + np.fmax(m - (c0 + c1 * np.sqrt(c3 + (l - c4) ** 2) + c5 * l), 0) * (l0 + l1 * l) + ) ** 2 return Cd_wave @@ -349,14 +359,17 @@ def Cl_rae2822(alpha, Re_c): atr2 = -8.3128119739031697e-02 c0l = -4.9103908291438701e-02 c0t = 2.3903424824298553e-01 - ctr = 1.3082854754897108e+01 - rtr = 2.6963082864300731e+00 + ctr = 1.3082854754897108e01 + rtr = 2.6963082864300731e00 a = alpha r = log10_Re - Cl = (c0t + a1t * a + a4t * a ** 4) * 1 / (1 + np.exp(ctr - rtr * r - atr * a - atr2 * a ** 2)) + ( - c0l + a1l * a + a4l * a ** 4) * (1 - 1 / (1 + np.exp(ctr - rtr * r - atr * a - atr2 * a ** 2))) + Cl = (c0t + a1t * a + a4t * a**4) * 1 / ( + 1 + np.exp(ctr - rtr * r - atr * a - atr2 * a**2) + ) + (c0l + a1l * a + a4l * a**4) * ( + 1 - 1 / (1 + np.exp(ctr - rtr * r - atr * a - atr2 * a**2)) + ) return Cl @@ -375,30 +388,32 @@ def Cd_profile_rae2822(alpha, Re_c): log10_Re = np.log10(Re_c) # Coeffs - at = 8.1034027621509015e+00 + at = 8.1034027621509015e00 c0l = -8.4296746456429639e-01 - c0t = -1.3700609138855402e+00 + c0t = -1.3700609138855402e00 kart = -4.1609994062600880e-01 kat = 5.9510959342452441e-01 krt = -7.1938030052506197e-01 r1l = 1.1548628822014631e-01 r1t = -4.9133662875044504e-01 - rt = 5.0070459892411696e+00 + rt = 5.0070459892411696e00 a = alpha r = log10_Re log10_Cd = (c0t + r1t * (r - 4)) * ( - 1 / (1 + np.exp(kat * (a - at) + krt * (r - rt) + kart * (a - at) * (r - rt)))) + ( - c0l + r1l * (r - 4)) * ( - 1 - 1 / (1 + np.exp(kat * (a - at) + krt * (r - rt) + kart * (a - at) * (r - rt)))) + 1 / (1 + np.exp(kat * (a - at) + krt * (r - rt) + kart * (a - at) * (r - rt))) + ) + (c0l + r1l * (r - 4)) * ( + 1 + - 1 / (1 + np.exp(kat * (a - at) + krt * (r - rt) + kart * (a - at) * (r - rt))) + ) - Cd = 10 ** log10_Cd + Cd = 10**log10_Cd return Cd -def Cd_wave_rae2822(Cl, mach, sweep=0.): +def Cd_wave_rae2822(Cl, mach, sweep=0.0): r""" A curve fit I did to RAE2822 airfoil data. Within -0.4 < CL < 0.75 and 0 < mach < ~0.9, has R^2 = 0.9982. @@ -418,10 +433,10 @@ def Cd_wave_rae2822(Cl, mach, sweep=0.): Cl_perpendicular = Cl / np.cosd(sweep) ** 2 # Relation from FVA Eq. 8.177 # Coeffs - c2 = 4.5776476424519119e+00 + c2 = 4.5776476424519119e00 mc0 = 9.5623337929607111e-01 mc1 = 2.0552787101770234e-01 - mc2 = 1.1259268018737063e+00 + mc2 = 1.1259268018737063e00 mc3 = 1.9538856688443659e-01 m = mach_perpendicular @@ -433,8 +448,8 @@ def Cd_wave_rae2822(Cl, mach, sweep=0.): def fuselage_upsweep_drag_area( - upsweep_angle_rad: float, - fuselage_xsec_area_max: float, + upsweep_angle_rad: float, + fuselage_xsec_area_max: float, ) -> float: """ Calculates the drag area (in m^2) of the aft end of a fuselage with a given upsweep angle. diff --git a/aerosandbox/library/airfoils.py b/aerosandbox/library/airfoils.py index c12a4945a..83e3a1dd8 100644 --- a/aerosandbox/library/airfoils.py +++ b/aerosandbox/library/airfoils.py @@ -1,13 +1,15 @@ ### This file contains an assortment of random airfoils to use from aerosandbox.geometry.airfoil import Airfoil from aerosandbox.library.aerodynamics.viscous import * -from aerosandbox.geometry.airfoil.airfoil_families import get_NACA_coordinates, \ - get_UIUC_coordinates +from aerosandbox.geometry.airfoil.airfoil_families import ( + get_NACA_coordinates, + get_UIUC_coordinates, +) def diamond_airfoil( - t_over_c: float, - n_points_per_panel=2, + t_over_c: float, + n_points_per_panel=2, ) -> Airfoil: x_nondim = [1, 0.5, 0, 0.5, 1] y_nondim = [0, 1, 0, -1, 0] @@ -16,13 +18,15 @@ def diamond_airfoil( [ list(np.cosspace(a, b, n_points_per_panel))[:-1] for a, b in zip(x_nondim[:-1], x_nondim[1:]) - ] + [[x_nondim[-1]]] + ] + + [[x_nondim[-1]]] ) y = np.concatenate( [ list(np.cosspace(a, b, n_points_per_panel))[:-1] for a, b in zip(y_nondim[:-1], y_nondim[1:]) - ] + [[y_nondim[-1]]] + ] + + [[y_nondim[-1]]] ) y = y * (t_over_c / 2) @@ -32,4 +36,3 @@ def diamond_airfoil( name="Diamond", coordinates=coordinates, ) - diff --git a/aerosandbox/library/costs.py b/aerosandbox/library/costs.py index 4d71c9e25..25f10aae2 100644 --- a/aerosandbox/library/costs.py +++ b/aerosandbox/library/costs.py @@ -4,22 +4,22 @@ def modified_DAPCA_IV_production_cost_analysis( - design_empty_weight: float, - design_maximum_airspeed: float, - n_airplanes_produced: int, - n_engines_per_aircraft: int, - cost_per_engine: float, - cost_avionics_per_airplane: float, - n_pax: int, - cpi_relative_to_2012_dollars: float = 1.327, # updated for 2024 - n_flight_test_aircraft: int = 4, - is_cargo_airplane: bool = False, - primary_structure_material: str = "aluminum", - per_passenger_cost_model: str = "general_aviation", - engineering_wrap_rate_2012_dollars: float = 115., - tooling_wrap_rate_2012_dollars: float = 118., - quality_control_wrap_rate_2012_dollars: float = 108., - manufacturing_wrap_rate_2012_dollars: float = 98., + design_empty_weight: float, + design_maximum_airspeed: float, + n_airplanes_produced: int, + n_engines_per_aircraft: int, + cost_per_engine: float, + cost_avionics_per_airplane: float, + n_pax: int, + cpi_relative_to_2012_dollars: float = 1.327, # updated for 2024 + n_flight_test_aircraft: int = 4, + is_cargo_airplane: bool = False, + primary_structure_material: str = "aluminum", + per_passenger_cost_model: str = "general_aviation", + engineering_wrap_rate_2012_dollars: float = 115.0, + tooling_wrap_rate_2012_dollars: float = 118.0, + quality_control_wrap_rate_2012_dollars: float = 108.0, + manufacturing_wrap_rate_2012_dollars: float = 98.0, ) -> Dict[str, float]: """ Computes the cost of an aircraft in present-day dollars, using the Modified DAPCA IV cost model. @@ -125,10 +125,12 @@ def modified_DAPCA_IV_production_cost_analysis( ### Estimate labor hours hours = dict() - hours["engineering"] = 5.18 * W ** 0.777 * V ** 0.894 * Q ** 0.163 - hours["tooling"] = 7.22 * W ** 0.777 * V ** 0.696 * Q ** 0.263 - hours["manufacturing"] = 10.5 * W ** 0.82 * V ** 0.484 * Q ** 0.641 - hours["quality_control"] = hours["manufacturing"] * (0.076 if is_cargo_airplane else 0.133) + hours["engineering"] = 5.18 * W**0.777 * V**0.894 * Q**0.163 + hours["tooling"] = 7.22 * W**0.777 * V**0.696 * Q**0.263 + hours["manufacturing"] = 10.5 * W**0.82 * V**0.484 * Q**0.641 + hours["quality_control"] = hours["manufacturing"] * ( + 0.076 if is_cargo_airplane else 0.133 + ) ### Account for materials difficulties if primary_structure_material == "aluminum": @@ -144,22 +146,31 @@ def modified_DAPCA_IV_production_cost_analysis( else: raise ValueError("Invalid value of `primary_structure_material`.") - hours = { - k: v * materials_hourly_multiplier - for k, v in hours.items() - } + hours = {k: v * materials_hourly_multiplier for k, v in hours.items()} ### Convert labor hours to labor costs in 2012 dollars costs_2012_dollars = dict() - costs_2012_dollars["engineering_labor"] = hours["engineering"] * engineering_wrap_rate_2012_dollars - costs_2012_dollars["tooling_labor"] = hours["tooling"] * tooling_wrap_rate_2012_dollars - costs_2012_dollars["manufacturing_labor"] = hours["manufacturing"] * manufacturing_wrap_rate_2012_dollars - costs_2012_dollars["quality_control_labor"] = hours["quality_control"] * quality_control_wrap_rate_2012_dollars + costs_2012_dollars["engineering_labor"] = ( + hours["engineering"] * engineering_wrap_rate_2012_dollars + ) + costs_2012_dollars["tooling_labor"] = ( + hours["tooling"] * tooling_wrap_rate_2012_dollars + ) + costs_2012_dollars["manufacturing_labor"] = ( + hours["manufacturing"] * manufacturing_wrap_rate_2012_dollars + ) + costs_2012_dollars["quality_control_labor"] = ( + hours["quality_control"] * quality_control_wrap_rate_2012_dollars + ) - costs_2012_dollars["development_support"] = 67.4 * W ** 0.630 * V ** 1.3 - costs_2012_dollars["flight_test"] = 1947 * W ** 0.325 * V ** 0.822 * n_flight_test_aircraft ** 1.21 - costs_2012_dollars["manufacturing_materials"] = 31.2 * W ** 0.921 * V ** 0.621 * Q ** 0.799 + costs_2012_dollars["development_support"] = 67.4 * W**0.630 * V**1.3 + costs_2012_dollars["flight_test"] = ( + 1947 * W**0.325 * V**0.822 * n_flight_test_aircraft**1.21 + ) + costs_2012_dollars["manufacturing_materials"] = ( + 31.2 * W**0.921 * V**0.621 * Q**0.799 + ) ### Add in the per-passenger cost for aircraft interiors: # Seats, luggage bins, closets, lavatories, insulation, ceilings, floors, walls, etc. @@ -174,10 +185,7 @@ def modified_DAPCA_IV_production_cost_analysis( raise ValueError(f"Invalid value of `per_passenger_cost_model`!") ### Convert all costs to present-day dollars - costs = { - k: v * cpi_relative_to_2012_dollars - for k, v in costs_2012_dollars.items() - } + costs = {k: v * cpi_relative_to_2012_dollars for k, v in costs_2012_dollars.items()} ### Add the engine(s) and avionics costs costs["engines"] = cost_per_engine * n_engines_per_aircraft * n_airplanes_produced @@ -190,24 +198,24 @@ def modified_DAPCA_IV_production_cost_analysis( def electric_aircraft_direct_operating_cost_analysis( - production_cost_per_airframe: float, - nominal_cruise_airspeed: float, - nominal_mission_range: float, - battery_capacity: float, - num_passengers_nominal: int, - num_crew: int = 1, - battery_fraction_used_on_nominal_mission: float = 0.8, - typical_passenger_utilization: float = 0.8, - flight_hours_per_year: float = 1200, - airframe_lifetime_years: float = 20, - airframe_eol_resale_value_fraction: float = 0.4, - battery_cost_per_kWh_capacity: float = 500.0, - battery_cycle_life: float = 1500, - real_interest_rate: float = 0.04, - electricity_cost_per_kWh: float = 0.145, - annual_expenses_per_crew: float = 100000 * 1.5, - ascent_time: float = 0.2 * u.hour, - descent_time: float = 0.2 * u.hour, + production_cost_per_airframe: float, + nominal_cruise_airspeed: float, + nominal_mission_range: float, + battery_capacity: float, + num_passengers_nominal: int, + num_crew: int = 1, + battery_fraction_used_on_nominal_mission: float = 0.8, + typical_passenger_utilization: float = 0.8, + flight_hours_per_year: float = 1200, + airframe_lifetime_years: float = 20, + airframe_eol_resale_value_fraction: float = 0.4, + battery_cost_per_kWh_capacity: float = 500.0, + battery_cycle_life: float = 1500, + real_interest_rate: float = 0.04, + electricity_cost_per_kWh: float = 0.145, + annual_expenses_per_crew: float = 100000 * 1.5, + ascent_time: float = 0.2 * u.hour, + descent_time: float = 0.2 * u.hour, ) -> Dict[str, float]: """ Estimates the overall operating cost of an electric aircraft. Includes both direct and indirect operating costs. @@ -323,26 +331,30 @@ def electric_aircraft_direct_operating_cost_analysis( ### Airframe depreciation costs num_airframe_flights_lifetime = flights_per_year * airframe_lifetime_years net_value_per_airframe_over_lifetime = ( - production_cost_per_airframe # Production cost - - (production_cost_per_airframe * airframe_eol_resale_value_fraction) # End-of-life resale value + production_cost_per_airframe # Production cost + - ( + production_cost_per_airframe * airframe_eol_resale_value_fraction + ) # End-of-life resale value ) costs_per_flight["airframe_depreciation"] = ( - net_value_per_airframe_over_lifetime / num_airframe_flights_lifetime + net_value_per_airframe_over_lifetime / num_airframe_flights_lifetime ) ### Airframe financing cost airframe_financing_lifetime_cost = production_cost_per_airframe * ( - np.exp(real_interest_rate * airframe_lifetime_years) - 1 + np.exp(real_interest_rate * airframe_lifetime_years) - 1 ) costs_per_flight["airframe_financing"] = ( - airframe_financing_lifetime_cost / num_airframe_flights_lifetime + airframe_financing_lifetime_cost / num_airframe_flights_lifetime ) ### Insurance cost insurance_cost_per_year = production_cost_per_airframe * ( - 0.025 # Base rate of 2% on average, adjusted higher for perceived higher risk of new electric technology - * ((num_passengers_nominal + num_crew + 1) / 11) ** 0.5 # Adj. for number of souls on board; but sublinear - * (flight_hours_per_year / 1200) ** 0.5 # Normalizing to industry average; but again, sublinear + 0.025 # Base rate of 2% on average, adjusted higher for perceived higher risk of new electric technology + * ((num_passengers_nominal + num_crew + 1) / 11) + ** 0.5 # Adj. for number of souls on board; but sublinear + * (flight_hours_per_year / 1200) + ** 0.5 # Normalizing to industry average; but again, sublinear ) costs_per_flight["insurance"] = insurance_cost_per_year / flights_per_year @@ -350,14 +362,14 @@ def electric_aircraft_direct_operating_cost_analysis( # maintenance_cost_per_year = production_cost_per_airframe * 0.04 # costs_per_flight["maintenance"] = maintenance_cost_per_year / flights_per_year costs_per_flight["airframe_maintenance"] = (production_cost_per_airframe / 3e6) * ( - 65 * (mission_time / u.hour) + - (65) # per cycle + 65 * (mission_time / u.hour) + (65) # per cycle ) ### Propulsion maintenance cost - costs_per_flight["propulsion_maintenance"] = (production_cost_per_airframe / 3e6) * ( - (58 * (mission_time / u.hour)) + - (50) # per cycle + costs_per_flight["propulsion_maintenance"] = ( + production_cost_per_airframe / 3e6 + ) * ( + (58 * (mission_time / u.hour)) + (50) # per cycle ) ### Battery replacement cost @@ -366,39 +378,36 @@ def electric_aircraft_direct_operating_cost_analysis( costs_per_flight["battery_replacement"] = battery_cost / battery_cycle_life ### Energy Cost - electric_energy_per_flight = battery_capacity * battery_fraction_used_on_nominal_mission + electric_energy_per_flight = ( + battery_capacity * battery_fraction_used_on_nominal_mission + ) electric_energy_per_flight_kWh = electric_energy_per_flight / (u.kilo * u.watt_hour) - costs_per_flight["energy"] = electric_energy_per_flight_kWh * electricity_cost_per_kWh + costs_per_flight["energy"] = ( + electric_energy_per_flight_kWh * electricity_cost_per_kWh + ) ### Crew cost - costs_per_flight["crew"] = ( - num_crew * - annual_expenses_per_crew / - flights_per_year - ) + costs_per_flight["crew"] = num_crew * annual_expenses_per_crew / flights_per_year ### Airport landing fees estimated_max_gross_landing_weight = ( # Model is very approximate, because landing fees are small - production_cost_per_airframe / 266 # typical cost per kg empty weight - * 1.2 # Rough ratio of landing weight to empty weight + production_cost_per_airframe + / 266 # typical cost per kg empty weight + * 1.2 # Rough ratio of landing weight to empty weight ) costs_per_flight["airport_landing_fees"] = ( - 1.20 * estimated_max_gross_landing_weight / 1000 + 1.20 * estimated_max_gross_landing_weight / 1000 ) ### Airport terminal use fees - costs_per_flight["airport_terminal_fees"] = ( - 50 # Cost per turn-around - ) + costs_per_flight["airport_terminal_fees"] = 50 # Cost per turn-around ### Airport aircraft parking fees - costs_per_flight["airport_parking_fees"] = ( - 35 # Cost per turn-around - ) + costs_per_flight["airport_parking_fees"] = 35 # Cost per turn-around ### Airport passenger facility charge costs_per_flight["airport_passenger_facility_charge"] = ( - 4.50 * num_passengers_nominal + 4.50 * num_passengers_nominal ) ### Indirect Costs @@ -411,13 +420,13 @@ def electric_aircraft_direct_operating_cost_analysis( # Return same dictionary as before costs_per_paxmi = { - k: v / passenger_miles_per_flight - for k, v in costs_per_flight.items() + k: v / passenger_miles_per_flight for k, v in costs_per_flight.items() } return costs_per_paxmi -if __name__ == '__main__': + +if __name__ == "__main__": res = electric_aircraft_direct_operating_cost_analysis( production_cost_per_airframe=3.0e6, nominal_cruise_airspeed=250 * u.knot, diff --git a/aerosandbox/library/gust_pitch_control.py b/aerosandbox/library/gust_pitch_control.py index 70b22b4c6..f84a9dfd9 100644 --- a/aerosandbox/library/gust_pitch_control.py +++ b/aerosandbox/library/gust_pitch_control.py @@ -20,11 +20,9 @@ class TransverseGustPitchControl(ImplicitAnalysis): """ @ImplicitAnalysis.initialize - def __init__(self, - reduced_time: np.ndarray, - gust_profile: np.ndarray, - velocity: float - ): + def __init__( + self, reduced_time: np.ndarray, gust_profile: np.ndarray, velocity: float + ): self.reduced_time = reduced_time self.gust_profile = gust_profile self.timesteps = len(reduced_time) @@ -35,10 +33,12 @@ def __init__(self, def _setup_unknowns(self): self.angles_of_attack = self.opti.variable(init_guess=1, n_vars=self.timesteps) - self.lift_coefficients = self.opti.variable(init_guess=1, n_vars=self.timesteps - 1) + self.lift_coefficients = self.opti.variable( + init_guess=1, n_vars=self.timesteps - 1 + ) def _enforce_governing_equations(self): - # Calculate unsteady lift due to pitching + # Calculate unsteady lift due to pitching wagner = wagners_function(self.reduced_time) ds = self.reduced_time[1:] - self.reduced_time[:-1] da_ds = (self.angles_of_attack[1:] - self.angles_of_attack[:-1]) / ds @@ -47,7 +47,7 @@ def _enforce_governing_equations(self): integral_term = np.sum(da_ds[j] * wagner[i - j] * ds[j] for j in range(i)) self.lift_coefficients[i] = 2 * np.pi * (integral_term + init_term[i]) - # Calculate unsteady lift due to transverse gust + # Calculate unsteady lift due to transverse gust kussner = kussners_function(self.reduced_time) dw_ds = (self.gust_profile[1:] - self.gust_profile[:-1]) / ds init_term = self.gust_profile[0] * kussner @@ -55,13 +55,17 @@ def _enforce_governing_equations(self): integral_term = 0 for j in range(i): integral_term += dw_ds[j] * kussner[i - j] * ds[j] - self.lift_coefficients[i] += 2 * np.pi / self.velocity * (init_term[i] + integral_term) + self.lift_coefficients[i] += ( + 2 * np.pi / self.velocity * (init_term[i] + integral_term) + ) # Calculate unsteady lift due to added mass - self.lift_coefficients += np.pi / 2 * np.cos(self.angles_of_attack[:-1]) ** 2 * da_ds + self.lift_coefficients += ( + np.pi / 2 * np.cos(self.angles_of_attack[:-1]) ** 2 * da_ds + ) # Integral of lift to be minimized - lift_squared_integral = np.sum(self.lift_coefficients ** 2) + lift_squared_integral = np.sum(self.lift_coefficients**2) # Constraints and objective to minimize self.opti.subject_to(self.angles_of_attack[0] == 0) @@ -69,21 +73,26 @@ def _enforce_governing_equations(self): def calculate_transients(self): self.optimal_pitching_profile_rad = self.opti.value(self.angles_of_attack) - self.optimal_pitching_profile_deg = np.rad2deg(self.optimal_pitching_profile_rad) + self.optimal_pitching_profile_deg = np.rad2deg( + self.optimal_pitching_profile_rad + ) self.optimal_lift_history = self.opti.value(self.lift_coefficients) self.pitching_lift = np.zeros(self.timesteps - 1) - # Calculate unsteady lift due to pitching + # Calculate unsteady lift due to pitching wagner = wagners_function(self.reduced_time) ds = self.reduced_time[1:] - self.reduced_time[:-1] - da_ds = (self.optimal_pitching_profile_rad[1:] - self.optimal_pitching_profile_rad[:-1]) / ds + da_ds = ( + self.optimal_pitching_profile_rad[1:] + - self.optimal_pitching_profile_rad[:-1] + ) / ds init_term = self.optimal_pitching_profile_rad[0] * wagner[:-1] for i in range(self.timesteps - 1): integral_term = np.sum(da_ds[j] * wagner[i - j] * ds[j] for j in range(i)) self.pitching_lift[i] = 2 * np.pi * (integral_term + init_term[i]) self.gust_lift = np.zeros(self.timesteps - 1) - # Calculate unsteady lift due to transverse gust + # Calculate unsteady lift due to transverse gust kussner = kussners_function(self.reduced_time) dw_ds = (self.gust_profile[1:] - self.gust_profile[:-1]) / ds init_term = self.gust_profile[0] * kussner @@ -91,10 +100,14 @@ def calculate_transients(self): integral_term = 0 for j in range(i): integral_term += dw_ds[j] * kussner[i - j] * ds[j] - self.gust_lift[i] += 2 * np.pi / self.velocity * (init_term[i] + integral_term) + self.gust_lift[i] += ( + 2 * np.pi / self.velocity * (init_term[i] + integral_term) + ) # Calculate unsteady lift due to added mass - self.added_mass_lift = np.pi / 2 * np.cos(self.optimal_pitching_profile_rad[:-1]) ** 2 * da_ds + self.added_mass_lift = ( + np.pi / 2 * np.cos(self.optimal_pitching_profile_rad[:-1]) ** 2 * da_ds + ) if __name__ == "__main__": @@ -111,11 +124,19 @@ def calculate_transients(self): fig, ax1 = plt.subplots(dpi=300) ax2 = ax1.twinx() - ax1.plot(reduced_time[:-1], optimal.optimal_lift_history, label="Total Lift", lw=2, c="k") + ax1.plot( + reduced_time[:-1], optimal.optimal_lift_history, label="Total Lift", lw=2, c="k" + ) ax1.plot(reduced_time[:-1], optimal.gust_lift, label="Gust Lift", lw=2) ax1.plot(reduced_time[:-1], optimal.pitching_lift, label="Pitching Lift", lw=2) ax1.plot(reduced_time[:-1], optimal.added_mass_lift, label="Added Mass Lift", lw=2) - ax2.plot(reduced_time, optimal.optimal_pitching_profile_deg, label="Angle of attack", lw=2, ls="--") + ax2.plot( + reduced_time, + optimal.optimal_pitching_profile_deg, + label="Angle of attack", + lw=2, + ls="--", + ) ax2.set_ylim([-40, 40]) ax1.legend(loc="lower left") diff --git a/aerosandbox/library/landing_gear.py b/aerosandbox/library/landing_gear.py index 8f2b3ea23..a0de56ae0 100644 --- a/aerosandbox/library/landing_gear.py +++ b/aerosandbox/library/landing_gear.py @@ -4,8 +4,7 @@ def tire_size( - mass_supported_by_each_tire: float, - aircraft_type="general_aviation" + mass_supported_by_each_tire: float, aircraft_type="general_aviation" ) -> float: """ Computes the required diameter and width of a tire for an airplane, from statistical regression to historical data. @@ -41,7 +40,7 @@ def tire_size( else: raise ValueError("Invalid `aircraft_type`.") - tire_diameter_in = A * mass_supported_by_tire_lbm ** B + tire_diameter_in = A * mass_supported_by_tire_lbm**B if aircraft_type == "general_aviation": A = 0.7150 @@ -56,7 +55,7 @@ def tire_size( A = 0.0980 B = 0.467 - tire_width_in = A * mass_supported_by_tire_lbm ** B + tire_width_in = A * mass_supported_by_tire_lbm**B tire_diameter = tire_diameter_in * u.inch tire_width = tire_width_in * u.inch diff --git a/aerosandbox/library/mass_structural.py b/aerosandbox/library/mass_structural.py index 9eb12724d..e8f6a5bb2 100644 --- a/aerosandbox/library/mass_structural.py +++ b/aerosandbox/library/mass_structural.py @@ -1,14 +1,14 @@ def mass_hpa_wing( - span, - chord, - vehicle_mass, - n_ribs, # You should optimize on this, there's a trade between rib weight and LE sheeting weight! - n_wing_sections=1, # defaults to a single-section wing (be careful: can you disassemble/transport this?) - ultimate_load_factor=1.75, # default taken from Daedalus design - type="cantilevered", # "cantilevered", "one-wire", "multi-wire" - t_over_c=0.128, # default from DAE11 - include_spar=True, - # Should we include the mass of the spar? Useful if you want to do your own primary structure calculations. + span, + chord, + vehicle_mass, + n_ribs, # You should optimize on this, there's a trade between rib weight and LE sheeting weight! + n_wing_sections=1, # defaults to a single-section wing (be careful: can you disassemble/transport this?) + ultimate_load_factor=1.75, # default taken from Daedalus design + type="cantilevered", # "cantilevered", "one-wire", "multi-wire" + t_over_c=0.128, # default from DAE11 + include_spar=True, + # Should we include the mass of the spar? Useful if you want to do your own primary structure calculations. ): """ Finds the mass of the wing structure of a human powered aircraft (HPA), following Juan Cruz's correlations in @@ -27,25 +27,23 @@ def mass_hpa_wing( ### Primary structure if include_spar: if type == "cantilevered": - mass_primary_spar = ( - (span * 1.17e-1 + span ** 2 * 1.10e-2) * - (1 + (ultimate_load_factor * vehicle_mass / 100 - 2) / 4) + mass_primary_spar = (span * 1.17e-1 + span**2 * 1.10e-2) * ( + 1 + (ultimate_load_factor * vehicle_mass / 100 - 2) / 4 ) elif type == "one-wire": - mass_primary_spar = ( - (span * 3.10e-2 + span ** 2 * 7.56e-3) * - (1 + (ultimate_load_factor * vehicle_mass / 100 - 2) / 4) + mass_primary_spar = (span * 3.10e-2 + span**2 * 7.56e-3) * ( + 1 + (ultimate_load_factor * vehicle_mass / 100 - 2) / 4 ) elif type == "multi-wire": - mass_primary_spar = ( - (span * 1.35e-1 + span ** 2 * 1.68e-3) * - (1 + (ultimate_load_factor * vehicle_mass / 100 - 2) / 4) + mass_primary_spar = (span * 1.35e-1 + span**2 * 1.68e-3) * ( + 1 + (ultimate_load_factor * vehicle_mass / 100 - 2) / 4 ) else: raise ValueError("Bad input for 'type'!") mass_primary = mass_primary_spar * ( - 11382.3 / 9222.2) # accounts for rear spar, struts, fittings, kevlar x-bracing, and wing-fuselage mounts + 11382.3 / 9222.2 + ) # accounts for rear spar, struts, fittings, kevlar x-bracing, and wing-fuselage mounts else: mass_primary = 0 @@ -55,13 +53,13 @@ def mass_hpa_wing( area = span * chord # Rib mass - W_wr = n_ribs * (chord ** 2 * t_over_c * 5.50e-2 + chord * 1.91e-3) + W_wr = n_ribs * (chord**2 * t_over_c * 5.50e-2 + chord * 1.91e-3) # End rib mass - W_wer = n_end_ribs * (chord ** 2 * t_over_c * 6.62e-1 + chord * 6.57e-3) + W_wer = n_end_ribs * (chord**2 * t_over_c * 6.62e-1 + chord * 6.57e-3) # LE sheeting mass - W_wLE = 0.456 * (span ** 2 * ratio_of_rib_spacing_to_chord ** (4 / 3) / span) + W_wLE = 0.456 * (span**2 * ratio_of_rib_spacing_to_chord ** (4 / 3) / span) # TE mass W_wTE = span * 2.77e-2 @@ -75,10 +73,10 @@ def mass_hpa_wing( def mass_wing_spar( - span, - mass_supported, - ultimate_load_factor=1.75, # default taken from Daedalus design - n_booms=1, + span, + mass_supported, + ultimate_load_factor=1.75, # default taken from Daedalus design + n_booms=1, ): """ Finds the mass of the spar for a wing on a single- or multi-boom lightweight aircraft. Model originally designed for solar aircraft. @@ -118,13 +116,13 @@ def mass_wing_spar( def mass_hpa_stabilizer( - span, - chord, - dynamic_pressure_at_manuever_speed, - n_ribs, # You should optimize on this, there's a trade between rib weight and LE sheeting weight! - t_over_c=0.128, # default from DAE11 - include_spar=True, - # Should we include the mass of the spar? Useful if you want to do your own primary structure calculations. + span, + chord, + dynamic_pressure_at_manuever_speed, + n_ribs, # You should optimize on this, there's a trade between rib weight and LE sheeting weight! + t_over_c=0.128, # default from DAE11 + include_spar=True, + # Should we include the mass of the spar? Useful if you want to do your own primary structure calculations. ): """ Finds the mass of a stabilizer structure of a human powered aircraft (HPA), following Juan Cruz's correlations in @@ -142,10 +140,7 @@ def mass_hpa_stabilizer( area = span * chord q = dynamic_pressure_at_manuever_speed if include_spar: - W_tss = ( - (span * 4.15e-2 + span ** 2 * 3.91e-3) * - (1 + ((q * area) / 78.5 - 1) / 2) - ) + W_tss = (span * 4.15e-2 + span**2 * 3.91e-3) * (1 + ((q * area) / 78.5 - 1) / 2) mass_primary = W_tss else: @@ -155,10 +150,10 @@ def mass_hpa_stabilizer( ratio_of_rib_spacing_to_chord = (span / n_ribs) / chord # Rib mass - W_tsr = n_ribs * (chord ** 2 * t_over_c * 1.16e-1 + chord * 4.01e-3) + W_tsr = n_ribs * (chord**2 * t_over_c * 1.16e-1 + chord * 4.01e-3) # Leading edge sheeting - W_tsLE = 0.174 * (area ** 2 * ratio_of_rib_spacing_to_chord ** (4 / 3) / span) + W_tsLE = 0.174 * (area**2 * ratio_of_rib_spacing_to_chord ** (4 / 3) / span) # Covering W_tsc = area * 1.93e-2 @@ -166,16 +161,18 @@ def mass_hpa_stabilizer( mass_secondary = W_tsr + W_tsLE + W_tsc ### Totaling - correction_factor = ((537.8 / (537.8 - 23.7 - 15.1)) * (623.3 / (623.3 - 63.2 - 8.1))) ** 0.5 + correction_factor = ( + (537.8 / (537.8 - 23.7 - 15.1)) * (623.3 / (623.3 - 63.2 - 8.1)) + ) ** 0.5 # geometric mean of Daedalus elevator and rudder corrections from misc. weight return (mass_primary + mass_secondary) * correction_factor def mass_hpa_tail_boom( - length_tail_boom, - dynamic_pressure_at_manuever_speed, - mean_tail_surface_area, + length_tail_boom, + dynamic_pressure_at_manuever_speed, + mean_tail_surface_area, ): """ Finds the mass of a tail boom structure of a human powered aircraft (HPA), following Juan Cruz's correlations in @@ -189,16 +186,12 @@ def mass_hpa_tail_boom( l = length_tail_boom q = dynamic_pressure_at_manuever_speed area = mean_tail_surface_area - w_tb = (l * 1.14e-1 + l ** 2 * 1.96e-2) * (1 + ((q * area) / 78.5 - 1) / 2) + w_tb = (l * 1.14e-1 + l**2 * 1.96e-2) * (1 + ((q * area) / 78.5 - 1) / 2) return w_tb -def mass_surface_balsa_monokote_cf( - chord, - span, - mean_t_over_c=0.08 -): +def mass_surface_balsa_monokote_cf(chord, span, mean_t_over_c=0.08): """ Estimates the mass of a lifting surface constructed with balsa-monokote-carbon-fiber construction techniques. Warning: Not well validated; spar sizing is a guessed scaling and not based on structural analysis. @@ -215,23 +208,24 @@ def mass_surface_balsa_monokote_cf( rib_spacing = 0.1 # one rib every x meters rib_width = 0.003 # width of an individual rib ribs_mass = ( - (mean_t * chord * rib_width) * # volume of a rib - rib_density * # density of a rib - (span / rib_spacing) # number of ribs + (mean_t * chord * rib_width) # volume of a rib + * rib_density # density of a rib + * (span / rib_spacing) # number of ribs ) spar_mass_1_inch = 0.2113 * span * 1.5 # assuming 1.5x 1" CF tube spar - spar_mass = spar_mass_1_inch * ( - mean_t / 0.0254) ** 2 # Rough GUESS for scaling, FIX THIS before using seriously! + spar_mass = ( + spar_mass_1_inch * (mean_t / 0.0254) ** 2 + ) # Rough GUESS for scaling, FIX THIS before using seriously! return (monokote_mass + ribs_mass + spar_mass) * 1.2 # for glue def mass_surface_solid( - chord, - span, - density=2700, # kg/m^3, defaults to that of aluminum - mean_t_over_c=0.08 + chord, + span, + density=2700, # kg/m^3, defaults to that of aluminum + mean_t_over_c=0.08, ): """ Estimates the mass of a lifting surface constructed out of a solid piece of material. @@ -256,19 +250,17 @@ def mass_surface_solid( # Daedalus wing mass validation print( - "Daedalus wing, estimated mass: %f" % - mass_hpa_wing( + "Daedalus wing, estimated mass: %f" + % mass_hpa_wing( span=34, chord=0.902, vehicle_mass=104.1, n_ribs=100, n_wing_sections=5, - type="one-wire" + type="one-wire", ) ) - print( - "Daedalus wing, actual mass: %f" % 18.9854 - ) + print("Daedalus wing, actual mass: %f" % 18.9854) nr = np.linspace(1, 400, 401) m = mass_hpa_wing( @@ -277,7 +269,7 @@ def mass_surface_solid( vehicle_mass=104.1, n_ribs=nr, n_wing_sections=5, - type="one-wire" + type="one-wire", ) plt.plot(nr, m) plt.ylim([15, 20]) @@ -290,23 +282,25 @@ def mass_surface_solid( # Test rib number optimization opti = asb.Opti() nr_opt = opti.variable(init_guess=100) - opti.minimize(mass_hpa_wing( - span=34, - chord=0.902, - vehicle_mass=104.1, - n_ribs=nr_opt, - n_wing_sections=5, - type="one-wire" - )) + opti.minimize( + mass_hpa_wing( + span=34, + chord=0.902, + vehicle_mass=104.1, + n_ribs=nr_opt, + n_wing_sections=5, + type="one-wire", + ) + ) sol = opti.solve() print(f"Optimal number of ribs: {sol(nr_opt)}") print( - "Daedalus elevator, estimated mass: %f" % - mass_hpa_stabilizer( + "Daedalus elevator, estimated mass: %f" + % mass_hpa_stabilizer( span=4.26, chord=0.6, - dynamic_pressure_at_manuever_speed=1 / 2 * 1.225 * 7 ** 2, + dynamic_pressure_at_manuever_speed=1 / 2 * 1.225 * 7**2, n_ribs=20, ) ) @@ -321,7 +315,7 @@ def mass_surface_solid( vehicle_mass=mass_total, n_ribs=sol(nr_opt), n_wing_sections=1, - type="cantilevered" + type="cantilevered", ) - mass_hpa_wing( span=span, chord=0.902, @@ -329,10 +323,7 @@ def mass_surface_solid( n_ribs=sol(nr_opt), n_wing_sections=1, type="cantilevered", - include_spar=False + include_spar=False, ) - mass_wing_primary_physics = mass_wing_spar( - span=span, - mass_supported=mass_total - ) + mass_wing_primary_physics = mass_wing_spar(span=span, mass_supported=mass_total) diff --git a/aerosandbox/library/power_gas.py b/aerosandbox/library/power_gas.py index 6b99e84ad..6349dc7b7 100644 --- a/aerosandbox/library/power_gas.py +++ b/aerosandbox/library/power_gas.py @@ -6,7 +6,7 @@ def mass_gas_engine(max_power): :return: Estimated motor mass [kg] """ max_power_hp = max_power / 745.7 - mass_lbm = 6.12 * max_power_hp ** 0.588 + mass_lbm = 6.12 * max_power_hp**0.588 mass = mass_lbm * 0.453592 # to kilograms return mass diff --git a/aerosandbox/library/power_human.py b/aerosandbox/library/power_human.py index cf46e7b7f..866c774bf 100644 --- a/aerosandbox/library/power_human.py +++ b/aerosandbox/library/power_human.py @@ -2,8 +2,8 @@ def power_human( - duration, # type: float - dataset="Healthy Men" # type: str + duration, # type: float + dataset="Healthy Men", # type: str ): """ Finds the power output that a human can sustain for a given duration. @@ -45,13 +45,11 @@ def power_human( log_duration_mins = np.log10(duration_mins) return a * duration_mins ** ( - b0 + - b1 * log_duration_mins + - b2 * log_duration_mins ** 2 + b0 + b1 * log_duration_mins + b2 * log_duration_mins**2 ) # essentially, a cubic in log-log space -if __name__ == '__main__': +if __name__ == "__main__": print( power_human( duration=60, diff --git a/aerosandbox/library/power_nuclear_rtg.py b/aerosandbox/library/power_nuclear_rtg.py index f7e3d02b7..c6a4d5f9d 100644 --- a/aerosandbox/library/power_nuclear_rtg.py +++ b/aerosandbox/library/power_nuclear_rtg.py @@ -36,12 +36,13 @@ def po210_specific_power( - days_after_formation=0, + days_after_formation=0, ): half_life = 138.376 # days # Source: https://en.wikipedia.org/wiki/Polonium-210 pure_specific_energy = ( - (po_210_mass_amu - alpha_particle_mass_amu - pb_206_mass_amu) * c ** 2 - / po_210_mass_amu + (po_210_mass_amu - alpha_particle_mass_amu - pb_206_mass_amu) + * c**2 + / po_210_mass_amu ) # J/kg # TODO finish diff --git a/aerosandbox/library/power_solar.py b/aerosandbox/library/power_solar.py index f1b3ff204..e93455f43 100644 --- a/aerosandbox/library/power_solar.py +++ b/aerosandbox/library/power_solar.py @@ -20,14 +20,11 @@ def _prepare_for_inverse_trig(x: Union[float, np.ndarray]) -> Union[float, np.nd Returns: A clipped version of the number, constrained to be in the open interval (-1, 1). """ - return ( - np.nextafter(1, -1) * - np.clip(x, -1, 1) - ) + return np.nextafter(1, -1) * np.clip(x, -1, 1) def solar_flux_outside_atmosphere_normal( - day_of_year: Union[int, float, np.ndarray] + day_of_year: Union[int, float, np.ndarray] ) -> Union[float, np.ndarray]: """ Computes the normal solar flux at the top of the atmosphere ("Airmass 0"). @@ -41,13 +38,11 @@ def solar_flux_outside_atmosphere_normal( Returns: The normal solar flux [W/m^2] at the top of the atmosphere. """ - return 1367 * ( - 1 + 0.034 * np.cosd(360 * (day_of_year) / 365.25) - ) + return 1367 * (1 + 0.034 * np.cosd(360 * (day_of_year) / 365.25)) def declination_angle( - day_of_year: Union[int, float, np.ndarray] + day_of_year: Union[int, float, np.ndarray] ) -> Union[float, np.ndarray]: """ Computes the solar declination angle, in degrees, as a function of day of year. @@ -66,9 +61,9 @@ def declination_angle( def solar_elevation_angle( - latitude: Union[float, np.ndarray], - day_of_year: Union[int, float, np.ndarray], - time: Union[float, np.ndarray] + latitude: Union[float, np.ndarray], + day_of_year: Union[int, float, np.ndarray], + time: Union[float, np.ndarray], ) -> Union[float, np.ndarray]: """ Elevation angle of the sun [degrees] for a local observer. @@ -89,10 +84,9 @@ def solar_elevation_angle( """ declination = declination_angle(day_of_year) - sin_solar_elevation_angle = ( - np.sind(declination) * np.sind(latitude) + - np.cosd(declination) * np.cosd(latitude) * np.cosd(time / 86400 * 360) - ) + sin_solar_elevation_angle = np.sind(declination) * np.sind(latitude) + np.cosd( + declination + ) * np.cosd(latitude) * np.cosd(time / 86400 * 360) solar_elevation_angle = np.arcsind( _prepare_for_inverse_trig(sin_solar_elevation_angle) @@ -101,9 +95,9 @@ def solar_elevation_angle( def solar_azimuth_angle( - latitude: Union[float, np.ndarray], - day_of_year: Union[int, float, np.ndarray], - time: Union[float, np.ndarray] + latitude: Union[float, np.ndarray], + day_of_year: Union[int, float, np.ndarray], + time: Union[float, np.ndarray], ) -> Union[float, np.ndarray]: """ Azimuth angle of the sun [degrees] for a local observer. @@ -136,19 +130,15 @@ def solar_azimuth_angle( is_solar_morning = np.mod(time, 86400) > 43200 - solar_azimuth_angle = np.where( - is_solar_morning, - azimuth_raw, - 360 - azimuth_raw - ) + solar_azimuth_angle = np.where(is_solar_morning, azimuth_raw, 360 - azimuth_raw) return solar_azimuth_angle def airmass( - solar_elevation_angle: Union[float, np.ndarray], - altitude: Union[float, np.ndarray] = 0., - method='Young' + solar_elevation_angle: Union[float, np.ndarray], + altitude: Union[float, np.ndarray] = 0.0, + method="Young", ) -> Union[float, np.ndarray]: """ Computes the (relative) airmass as a function of the (true) solar elevation angle and observer altitude. @@ -206,40 +196,37 @@ def airmass( """ true_zenith_angle = 90 - solar_elevation_angle - if method == 'Young': + if method == "Young": cos_zt = np.cosd(true_zenith_angle) - cos2_zt = cos_zt ** 2 - cos3_zt = cos_zt ** 3 + cos2_zt = cos_zt**2 + cos3_zt = cos_zt**3 numerator = 1.002432 * cos2_zt + 0.148386 * cos_zt + 0.0096467 denominator = cos3_zt + 0.149864 * cos2_zt + 0.0102963 * cos_zt + 0.000303978 sea_level_airmass = np.where( - denominator > 0, - numerator / denominator, - 1e100 # Essentially, infinity. + denominator > 0, numerator / denominator, 1e100 # Essentially, infinity. ) else: raise ValueError("Bad value of `method`!") airmass_at_altitude = sea_level_airmass * ( - Atmosphere(altitude=altitude).pressure() / - 101325. + Atmosphere(altitude=altitude).pressure() / 101325.0 ) return airmass_at_altitude def solar_flux( - latitude: Union[float, np.ndarray], - day_of_year: Union[int, float, np.ndarray], - time: Union[float, np.ndarray], - altitude: Union[float, np.ndarray] = 0., - panel_azimuth_angle: Union[float, np.ndarray] = 0., - panel_tilt_angle: Union[float, np.ndarray] = 0., - air_quality: str = 'typical', - albedo: Union[float, np.ndarray] = 0.2, - **deprecated_kwargs + latitude: Union[float, np.ndarray], + day_of_year: Union[int, float, np.ndarray], + time: Union[float, np.ndarray], + altitude: Union[float, np.ndarray] = 0.0, + panel_azimuth_angle: Union[float, np.ndarray] = 0.0, + panel_tilt_angle: Union[float, np.ndarray] = 0.0, + air_quality: str = "typical", + albedo: Union[float, np.ndarray] = 0.2, + **deprecated_kwargs, ) -> Union[float, np.ndarray]: """ Computes the solar power flux (power per unit area) on a flat (possibly tilted) panel. Accounts for atmospheric @@ -292,11 +279,11 @@ def solar_flux( air_quality: Indicates the amount of pollution in the air. A string, one of: * 'clean': Pristine atmosphere conditions. - + * 'typical': Corresponds to "rural aerosol loading" following ASTM G-173. * 'polluted': Urban atmosphere conditions. - + Note: in very weird edge cases, a polluted atmosphere can actually result in slightly higher solar flux than clean air, due to increased back-scattering. For example, imagine it's near sunset, with the sun in the west, and your panel normal vector points east. Increased pollution can, in some edge cases, @@ -328,7 +315,9 @@ def solar_flux( * Note: does not account for any potential reflectivity of the solar panel's coating itself. """ - flux_outside_atmosphere = solar_flux_outside_atmosphere_normal(day_of_year=day_of_year) + flux_outside_atmosphere = solar_flux_outside_atmosphere_normal( + day_of_year=day_of_year + ) solar_elevation = solar_elevation_angle(latitude, day_of_year, time) solar_azimuth = solar_azimuth_angle(latitude, day_of_year, time) @@ -339,24 +328,27 @@ def solar_flux( ) # Source: "Planning and installing..." Earthscan. Full citation in docstring above. - if air_quality == 'typical': - atmospheric_transmission_fraction = 0.70 ** (relative_airmass ** 0.678) - elif air_quality == 'clean': - atmospheric_transmission_fraction = 0.76 ** (relative_airmass ** 0.618) - elif air_quality == 'polluted': - atmospheric_transmission_fraction = 0.56 ** (relative_airmass ** 0.715) + if air_quality == "typical": + atmospheric_transmission_fraction = 0.70 ** (relative_airmass**0.678) + elif air_quality == "clean": + atmospheric_transmission_fraction = 0.76 ** (relative_airmass**0.618) + elif air_quality == "polluted": + atmospheric_transmission_fraction = 0.56 ** (relative_airmass**0.715) else: raise ValueError("Bad value of `air_quality`!") direct_normal_irradiance = np.where( - solar_elevation > 0., + solar_elevation > 0.0, flux_outside_atmosphere * atmospheric_transmission_fraction, - 0. + 0.0, ) - absorption_and_scattering_losses = flux_outside_atmosphere - flux_outside_atmosphere * atmospheric_transmission_fraction + absorption_and_scattering_losses = ( + flux_outside_atmosphere + - flux_outside_atmosphere * atmospheric_transmission_fraction + ) - scattering_losses = absorption_and_scattering_losses * (10. / 28.) + scattering_losses = absorption_and_scattering_losses * (10.0 / 28.0) # Source: https://www.pveducation.org/pvcdrom/properties-of-sunlight/air-mass # Indicates that absorption and scattering happen in a 18:10 ratio, at least in AM1.5 conditions. We extrapolate # this to all conditions. @@ -368,39 +360,40 @@ def solar_flux( -1 + panel_tilt_angle / 180, ) - diffuse_irradiance = scattering_losses * atmospheric_transmission_fraction * ( - fraction_of_panel_facing_sky + albedo * (1 - fraction_of_panel_facing_sky) + diffuse_irradiance = ( + scattering_losses + * atmospheric_transmission_fraction + * (fraction_of_panel_facing_sky + albedo * (1 - fraction_of_panel_facing_sky)) ) # We assume that the in-scattering (i.e., diffuse irradiance) and the out-scattering (i.e., scattering losses in # the direct irradiance calculation) are equal, by argument of approximately parallel incident rays. # We further assume that any in-scattering must then once-again go through the absorption / re-scattering process, # which is identical to the original atmospheric transmission fraction. - cosine_of_angle_between_panel_normal_and_sun = ( - np.cosd(solar_elevation) * - np.sind(panel_tilt_angle) * - np.cosd(panel_azimuth_angle - solar_azimuth) - + np.sind(solar_elevation) * np.cosd(panel_tilt_angle) + cosine_of_angle_between_panel_normal_and_sun = np.cosd(solar_elevation) * np.sind( + panel_tilt_angle + ) * np.cosd(panel_azimuth_angle - solar_azimuth) + np.sind( + solar_elevation + ) * np.cosd( + panel_tilt_angle ) cosine_of_angle_between_panel_normal_and_sun = np.fmax( - cosine_of_angle_between_panel_normal_and_sun, - 0 + cosine_of_angle_between_panel_normal_and_sun, 0 ) # Accounts for if you have a downwards-pointing panel while the sun is above you. # Source: https://www.pveducation.org/pvcdrom/properties-of-sunlight/arbitrary-orientation-and-tilt # Author of this code (Peter Sharpe) has manually verified correctness of this vector math. flux_on_panel = ( - direct_normal_irradiance * cosine_of_angle_between_panel_normal_and_sun - + diffuse_irradiance + direct_normal_irradiance * cosine_of_angle_between_panel_normal_and_sun + + diffuse_irradiance ) return flux_on_panel def peak_sun_hours_per_day_on_horizontal( - latitude: Union[float, np.ndarray], - day_of_year: Union[int, float, np.ndarray] + latitude: Union[float, np.ndarray], day_of_year: Union[int, float, np.ndarray] ) -> Union[float, np.ndarray]: """ How many hours of equivalent peak sun do you get per day? @@ -410,15 +403,18 @@ def peak_sun_hours_per_day_on_horizontal( :return: """ import warnings + warnings.warn( "Use `solar_flux()` function from this module instead and integrate, which allows far greater granularity.", - DeprecationWarning + DeprecationWarning, ) time = np.linspace(0, 86400, 1000) fluxes = solar_flux(latitude, day_of_year, time) energy_per_area = np.sum(np.trapz(fluxes) * np.diff(time)) - duration_of_equivalent_peak_sun = energy_per_area / solar_flux(latitude, day_of_year, time=0.) + duration_of_equivalent_peak_sun = energy_per_area / solar_flux( + latitude, day_of_year, time=0.0 + ) sun_hours = duration_of_equivalent_peak_sun / 3600 @@ -426,8 +422,7 @@ def peak_sun_hours_per_day_on_horizontal( def length_day( - latitude: Union[float, np.ndarray], - day_of_year: Union[int, float, np.ndarray] + latitude: Union[float, np.ndarray], day_of_year: Union[int, float, np.ndarray] ) -> Union[float, np.ndarray]: """ Gives the duration where the sun is above the horizon on a given day. @@ -451,9 +446,7 @@ def length_day( return sun_time -def mass_MPPT( - power: Union[float, np.ndarray] -) -> Union[float, np.ndarray]: +def mass_MPPT(power: Union[float, np.ndarray]) -> Union[float, np.ndarray]: """ Gives the estimated mass of a Maximum Power Point Tracking (MPPT) unit for solar energy collection. Based on regressions at AeroSandbox/studies/SolarMPPTMasses. @@ -466,7 +459,7 @@ def mass_MPPT( """ constant = 0.066343 exponent = 0.515140 - return constant * power ** exponent + return constant * power**exponent if __name__ == "__main__": @@ -475,11 +468,11 @@ def mass_MPPT( # plt.switch_backend('WebAgg') - base_color = p.palettes['categorical'][0] + base_color = p.palettes["categorical"][0] quality_colors = { - 'clean' : p.adjust_lightness(base_color, amount=1.2), - 'typical' : p.adjust_lightness(base_color, amount=0.7), - 'polluted': p.adjust_lightness(base_color, amount=0.2), + "clean": p.adjust_lightness(base_color, amount=1.2), + "typical": p.adjust_lightness(base_color, amount=0.7), + "polluted": p.adjust_lightness(base_color, amount=0.2), } ##### Plot solar_flux() over the course of a day @@ -494,20 +487,21 @@ def mass_MPPT( fig, ax = plt.subplots(2, 1, figsize=(7, 6.5)) plt.sca(ax[0]) - plt.title(f"Solar Flux on a Horizontal Surface Over A Day\n(Tropic of Cancer, Summer Solstice, Sea Level)") + plt.title( + f"Solar Flux on a Horizontal Surface Over A Day\n(Tropic of Cancer, Summer Solstice, Sea Level)" + ) for q in quality_colors.keys(): plt.plot( hour, - solar_flux( - **base_kwargs, - air_quality=q - ), + solar_flux(**base_kwargs, air_quality=q), color=quality_colors[q], - label=f'ASB Model: {q.capitalize()} air' + label=f"ASB Model: {q.capitalize()} air", ) plt.sca(ax[1]) - plt.title(f"Solar Flux on a Sun-Tracking Surface Over A Day\n(Tropic of Cancer, Summer Solstice, Sea Level)") + plt.title( + f"Solar Flux on a Sun-Tracking Surface Over A Day\n(Tropic of Cancer, Summer Solstice, Sea Level)" + ) for q in quality_colors.keys(): plt.plot( hour, @@ -515,10 +509,10 @@ def mass_MPPT( **base_kwargs, panel_tilt_angle=90 - solar_elevation_angle(**base_kwargs), panel_azimuth_angle=solar_azimuth_angle(**base_kwargs), - air_quality=q + air_quality=q, ), color=quality_colors[q], - label=f'ASB Model: {q.capitalize()} air' + label=f"ASB Model: {q.capitalize()} air", ) for a in ax: @@ -556,20 +550,17 @@ def mass_MPPT( from io import StringIO delimiter = "\t" - df = pd.read_csv( - StringIO(raw_data), - delimiter=',' - ) - df["Solar Flux [W/m^2]"] = (df['Solar Flux Lower Bound [W/m^2]'] + df['Solar Flux Upper Bound [W/m^2]']) / 2 + df = pd.read_csv(StringIO(raw_data), delimiter=",") + df["Solar Flux [W/m^2]"] = ( + df["Solar Flux Lower Bound [W/m^2]"] + df["Solar Flux Upper Bound [W/m^2]"] + ) / 2 fluxes = solar_flux( **base_kwargs, panel_tilt_angle=90 - solar_elevation_angle(**base_kwargs), panel_azimuth_angle=solar_azimuth_angle(**base_kwargs), ) - elevations = solar_elevation_angle( - **base_kwargs - ) + elevations = solar_elevation_angle(**base_kwargs) fig, ax = plt.subplots() for q in quality_colors.keys(): @@ -579,32 +570,32 @@ def mass_MPPT( **base_kwargs, panel_tilt_angle=90 - solar_elevation_angle(**base_kwargs), panel_azimuth_angle=solar_azimuth_angle(**base_kwargs), - air_quality=q + air_quality=q, ), color=quality_colors[q], - label=f'ASB Model: {q.capitalize()} air', - zorder=3 + label=f"ASB Model: {q.capitalize()} air", + zorder=3, ) - data_color = p.palettes['categorical'][1] + data_color = p.palettes["categorical"][1] plt.fill_between( - x=90 - df['z [deg]'].values, - y1=df['Solar Flux Lower Bound [W/m^2]'], - y2=df['Solar Flux Upper Bound [W/m^2]'], + x=90 - df["z [deg]"].values, + y1=df["Solar Flux Lower Bound [W/m^2]"], + y2=df["Solar Flux Upper Bound [W/m^2]"], color=data_color, alpha=0.4, - label='Experimental Data Range\n(due to Pollution)', + label="Experimental Data Range\n(due to Pollution)", zorder=2.9, ) - for d in ['Lower', 'Upper']: + for d in ["Lower", "Upper"]: plt.plot( - 90 - df['z [deg]'].values, - df[f'Solar Flux {d} Bound [W/m^2]'], + 90 - df["z [deg]"].values, + df[f"Solar Flux {d} Bound [W/m^2]"], ".", color=data_color, alpha=0.7, - zorder=2.95 + zorder=2.95, ) plt.annotate( @@ -612,8 +603,8 @@ def mass_MPPT( xy=(0.02, 0.98), xycoords="axes fraction", ha="left", - va='top', - fontsize=9 + va="top", + fontsize=9, ) plt.xlim(-5, 90) p.set_ticks(15, 5, 200, 50) @@ -621,5 +612,5 @@ def mass_MPPT( p.show_plot( f"Sun Position vs. Solar Flux on a Sun-Tracking Surface", f"Solar Elevation Angle [deg]", - "Solar Flux [$W/m^2$]" + "Solar Flux [$W/m^2$]", ) diff --git a/aerosandbox/library/power_turboshaft.py b/aerosandbox/library/power_turboshaft.py index dd20694cc..65ca6e057 100644 --- a/aerosandbox/library/power_turboshaft.py +++ b/aerosandbox/library/power_turboshaft.py @@ -1,9 +1,7 @@ import aerosandbox.numpy as np -def overall_pressure_ratio_turboshaft_technology_limit( - mass_turboshaft: float -) -> float: +def overall_pressure_ratio_turboshaft_technology_limit(mass_turboshaft: float) -> float: """ Estimates the maximum-practically-achievable overall pressure ratio (OPR) of a turboshaft engine, as a function of its mass. A regression to historical data. @@ -21,7 +19,11 @@ def overall_pressure_ratio_turboshaft_technology_limit( The maximum-practically-achievable overall pressure ratio (OPR) of the turboshaft engine. [-] """ - p = {'scl': 1.0222956615376533, 'cen': 1.6535195257959798, 'high': 23.957335474997656} + p = { + "scl": 1.0222956615376533, + "cen": 1.6535195257959798, + "high": 23.957335474997656, + } return np.blend( np.log10(mass_turboshaft) / p["scl"] - p["cen"], value_switch_high=p["high"], @@ -30,8 +32,8 @@ def overall_pressure_ratio_turboshaft_technology_limit( def power_turboshaft( - mass_turboshaft: float, - overall_pressure_ratio: float = None, + mass_turboshaft: float, + overall_pressure_ratio: float = None, ) -> float: """ Estimates the maximum rated power of a turboshaft engine, given its mass. A regression to historical data. @@ -54,22 +56,26 @@ def power_turboshaft( """ if overall_pressure_ratio is None: - overall_pressure_ratio = overall_pressure_ratio_turboshaft_technology_limit( - mass_turboshaft - ) * 0.7 + overall_pressure_ratio = ( + overall_pressure_ratio_turboshaft_technology_limit(mass_turboshaft) * 0.7 + ) - p = {'a': 1674.9411795202134, 'OPR': 0.5090953411025091, 'Weight [kg]': 0.9418482117552568} + p = { + "a": 1674.9411795202134, + "OPR": 0.5090953411025091, + "Weight [kg]": 0.9418482117552568, + } return ( - p["a"] - * mass_turboshaft ** p["Weight [kg]"] - * overall_pressure_ratio ** p["OPR"] + p["a"] + * mass_turboshaft ** p["Weight [kg]"] + * overall_pressure_ratio ** p["OPR"] ) def thermal_efficiency_turboshaft( - mass_turboshaft: float, - overall_pressure_ratio: float = None, - throttle_setting: float = 1, + mass_turboshaft: float, + overall_pressure_ratio: float = None, + throttle_setting: float = 1, ) -> float: """ Estimates the thermal efficiency of a turboshaft engine. A regression to historical data. @@ -98,11 +104,11 @@ def thermal_efficiency_turboshaft( """ if overall_pressure_ratio is None: - overall_pressure_ratio = overall_pressure_ratio_turboshaft_technology_limit( - mass_turboshaft - ) * 0.7 + overall_pressure_ratio = ( + overall_pressure_ratio_turboshaft_technology_limit(mass_turboshaft) * 0.7 + ) - p = {'a': 0.12721246565294902, 'wcen': 2.679474077211383, 'wscl': 4.10824884208311} + p = {"a": 0.12721246565294902, "wcen": 2.679474077211383, "wscl": 4.10824884208311} ideal_efficiency = 1 - (1 / overall_pressure_ratio) ** ((1.4 - 1) / 1.4) @@ -113,26 +119,26 @@ def thermal_efficiency_turboshaft( ) p = { - 'B0': 0.0592, # Modified from Geiß thesis such that B values sum to 1 by construction. Orig: 0.05658 - 'B1': 2.567, - 'B2': -2.612, - 'B3': 0.9858 + "B0": 0.0592, # Modified from Geiß thesis such that B values sum to 1 by construction. Orig: 0.05658 + "B1": 2.567, + "B2": -2.612, + "B3": 0.9858, } thermal_efficiency_knockdown_from_partial_power = ( - p["B0"] - + p["B1"] * throttle_setting - + p["B2"] * throttle_setting ** 2 - + p["B3"] * throttle_setting ** 3 + p["B0"] + + p["B1"] * throttle_setting + + p["B2"] * throttle_setting**2 + + p["B3"] * throttle_setting**3 ) return ( - thermal_efficiency_at_full_power - * thermal_efficiency_knockdown_from_partial_power + thermal_efficiency_at_full_power + * thermal_efficiency_knockdown_from_partial_power ) -if __name__ == '__main__': +if __name__ == "__main__": import matplotlib.pyplot as plt import aerosandbox.tools.pretty_plots as p @@ -141,7 +147,8 @@ def thermal_efficiency_turboshaft( x = np.linspace(0, 1) plt.plot( x, - thermal_efficiency_turboshaft(1000, throttle_setting=x) / thermal_efficiency_turboshaft(1000), + thermal_efficiency_turboshaft(1000, throttle_setting=x) + / thermal_efficiency_turboshaft(1000), ) ax.xaxis.set_major_formatter(p.mpl.ticker.PercentFormatter(1)) plt.xlim(0, 1) @@ -151,7 +158,7 @@ def thermal_efficiency_turboshaft( p.show_plot( "Turboshaft: Thermal Efficiency at Partial Power", "Throttle Setting [-]", - "Thermal Efficiency Knockdown relative to Design Point [-] $\eta / \eta_\mathrm{max}$" + "Thermal Efficiency Knockdown relative to Design Point [-] $\eta / \eta_\mathrm{max}$", ) ##### Do Weight/OPR Efficiency Plot ##### @@ -183,5 +190,5 @@ def thermal_efficiency_turboshaft( "Turboshaft Model: Thermal Efficiency vs. Weight and OPR", "Engine Weight [kg]", "Overall Pressure Ratio [-]", - dpi=300 + dpi=300, ) diff --git a/aerosandbox/library/propulsion_electric.py b/aerosandbox/library/propulsion_electric.py index 380671bcd..f603746a2 100644 --- a/aerosandbox/library/propulsion_electric.py +++ b/aerosandbox/library/propulsion_electric.py @@ -5,49 +5,49 @@ def motor_electric_performance( - voltage: Union[float, np.ndarray] = None, - current: Union[float, np.ndarray] = None, - rpm: Union[float, np.ndarray] = None, - torque: Union[float, np.ndarray] = None, - kv: float = 1000., # rpm/volt - resistance: float = 0.1, # ohms - no_load_current: float = 0.4 # amps + voltage: Union[float, np.ndarray] = None, + current: Union[float, np.ndarray] = None, + rpm: Union[float, np.ndarray] = None, + torque: Union[float, np.ndarray] = None, + kv: float = 1000.0, # rpm/volt + resistance: float = 0.1, # ohms + no_load_current: float = 0.4, # amps ) -> Dict[str, Union[float, np.ndarray]]: """ A function for predicting the performance of an electric motor. - + Performance equations based on Mark Drela's First Order Motor Model: http://web.mit.edu/drela/Public/web/qprop/motor1_theory.pdf - + Instructions: Input EXACTLY TWO of the following parameters: voltage, current, rpm, torque. - + Exception: You cannot supply the combination of current and torque - this makes for an ill-posed problem. - + Note that this function is fully vectorized, so arrays can be supplied to any of the inputs. - + Args: voltage: Voltage across motor terminals [Volts] - + current: Current through motor [Amps] - + rpm: Motor rotation speed [rpm] - + torque: Motor torque [N m] - + kv: voltage constant, in rpm/volt - + resistance: resistance, in ohms - + no_load_current: no-load current, in amps - + Returns: - A dictionary where keys are: - "voltage", - "current", - "rpm", + A dictionary where keys are: + "voltage", + "current", + "rpm", "torque", - "shaft power", - "electrical power", + "shaft power", + "electrical power", "efficiency" "waste heat" @@ -64,14 +64,13 @@ def motor_electric_performance( rpm_known = rpm is not None torque_known = torque is not None - if not ( - voltage_known + current_known + rpm_known + torque_known == 2 - ): + if not (voltage_known + current_known + rpm_known + torque_known == 2): raise ValueError("You must give exactly two input arguments.") if current_known and torque_known: raise ValueError( - "You cannot supply the combination of current and torque - this makes for an ill-posed problem.") + "You cannot supply the combination of current and torque - this makes for an ill-posed problem." + ) kv_rads_per_sec_per_volt = kv * np.pi / 30 # rads/sec/volt @@ -112,32 +111,32 @@ def motor_electric_performance( waste_heat = np.fabs(electrical_power - shaft_power) return { - "voltage" : voltage, - "current" : current, - "rpm" : rpm, - "torque" : torque, - "shaft power" : shaft_power, + "voltage": voltage, + "current": current, + "rpm": rpm, + "torque": torque, + "shaft power": shaft_power, "electrical power": electrical_power, - "efficiency" : efficiency, - "waste heat" : waste_heat, + "efficiency": efficiency, + "waste heat": waste_heat, } def electric_propeller_propulsion_analysis( - total_thrust: float, - n_engines: int, - propeller_diameter: float, - op_point: OperatingPoint, - motor_kv: float, - motor_no_load_current: float, - motor_resistance: float, - wire_resistance: float, - battery_voltage: float, - propeller_tip_mach: float = 0.50, - gearbox_ratio: float = 1, - gearbox_efficiency: float = 1, - esc_efficiency: float = 0.98, - battery_discharge_efficiency: float = 0.985, + total_thrust: float, + n_engines: int, + propeller_diameter: float, + op_point: OperatingPoint, + motor_kv: float, + motor_no_load_current: float, + motor_resistance: float, + wire_resistance: float, + battery_voltage: float, + propeller_tip_mach: float = 0.50, + gearbox_ratio: float = 1, + gearbox_efficiency: float = 1, + esc_efficiency: float = 0.98, + battery_discharge_efficiency: float = 0.985, ) -> Dict[str, float]: """ Performs a propulsion analysis for an electric propeller-driven aircraft. @@ -201,15 +200,17 @@ def electric_propeller_propulsion_analysis( """ ### Propeller Analysis - propulsive_area_per_propeller = (np.pi / 4) * propeller_diameter ** 2 + propulsive_area_per_propeller = (np.pi / 4) * propeller_diameter**2 propulsive_area_total = propulsive_area_per_propeller * n_engines - propeller_wake_dynamic_pressure = op_point.dynamic_pressure() + total_thrust / propulsive_area_total + propeller_wake_dynamic_pressure = ( + op_point.dynamic_pressure() + total_thrust / propulsive_area_total + ) propeller_wake_velocity = ( - # Derived from the above pressure jump relation, with adjustments to avoid singularity at zero velocity - 2 * total_thrust / (propulsive_area_total * op_point.atmosphere.density()) - + op_point.velocity ** 2 - ) ** 0.5 + # Derived from the above pressure jump relation, with adjustments to avoid singularity at zero velocity + 2 * total_thrust / (propulsive_area_total * op_point.atmosphere.density()) + + op_point.velocity**2 + ) ** 0.5 propeller_tip_speed = propeller_tip_mach * op_point.atmosphere.speed_of_sound() propeller_rads_per_sec = propeller_tip_speed / (propeller_diameter / 2) @@ -219,7 +220,9 @@ def electric_propeller_propulsion_analysis( air_power = total_thrust * op_point.velocity - from aerosandbox.library.propulsion_propeller import propeller_shaft_power_from_thrust + from aerosandbox.library.propulsion_propeller import ( + propeller_shaft_power_from_thrust, + ) shaft_power = propeller_shaft_power_from_thrust( thrust_force=total_thrust, @@ -235,7 +238,9 @@ def electric_propeller_propulsion_analysis( motor_rpm = propeller_rpm / gearbox_ratio motor_rads_per_sec = motor_rpm * 2 * np.pi / 60 - motor_torque_per_motor = shaft_power / n_engines / motor_rads_per_sec / gearbox_efficiency + motor_torque_per_motor = ( + shaft_power / n_engines / motor_rads_per_sec / gearbox_efficiency + ) motor_parameters_per_motor = motor_electric_performance( rpm=motor_rpm, @@ -257,7 +262,9 @@ def electric_propeller_propulsion_analysis( wire_efficiency = esc_electrical_power / (esc_electrical_power + wire_power_loss) ### Battery Analysis - battery_power = (esc_electrical_power + wire_power_loss) / battery_discharge_efficiency + battery_power = ( + esc_electrical_power + wire_power_loss + ) / battery_discharge_efficiency battery_current = battery_power / battery_voltage ### Overall @@ -266,9 +273,7 @@ def electric_propeller_propulsion_analysis( return locals() -def motor_resistance_from_no_load_current( - no_load_current -): +def motor_resistance_from_no_load_current(no_load_current): """ Estimates the internal resistance of a motor from its no_load_current. Gates quotes R^2=0.93 for this model. @@ -281,11 +286,11 @@ def motor_resistance_from_no_load_current( Returns: motor internal resistance [ohms] """ - return 0.0467 * no_load_current ** -1.892 + return 0.0467 * no_load_current**-1.892 def mass_ESC( - max_power, + max_power, ): """ Estimates the mass of an ESC. @@ -302,9 +307,9 @@ def mass_ESC( def mass_battery_pack( - battery_capacity_Wh, - battery_cell_specific_energy_Wh_kg=240, - battery_pack_cell_fraction=0.7, + battery_capacity_Wh, + battery_cell_specific_energy_Wh_kg=240, + battery_pack_cell_fraction=0.7, ): """ Estimates the mass of a lithium-polymer battery. @@ -322,14 +327,18 @@ def mass_battery_pack( Returns: Estimated battery mass [kg] """ - return battery_capacity_Wh / battery_cell_specific_energy_Wh_kg / battery_pack_cell_fraction + return ( + battery_capacity_Wh + / battery_cell_specific_energy_Wh_kg + / battery_pack_cell_fraction + ) def mass_motor_electric( - max_power, - kv_rpm_volt=1000, # This is in rpm/volt, not rads/sec/volt! - voltage=20, - method="hobbyking" + max_power, + kv_rpm_volt=1000, # This is in rpm/volt, not rads/sec/volt! + voltage=20, + method="hobbyking", ): """ Estimates the mass of a brushless DC electric motor. @@ -358,26 +367,30 @@ def mass_motor_electric( Estimated motor mass [kg] """ if method == "burton": - return max_power / 4128 # Less sophisticated model. 95% CI (3992, 4263), R^2 = 0.866 + return ( + max_power / 4128 + ) # Less sophisticated model. 95% CI (3992, 4263), R^2 = 0.866 elif method == "hobbyking": return 10 ** (0.8205 * np.log10(max_power) - 3.155) # More sophisticated model elif method == "astroflight": max_current = max_power / voltage - return 2.464 * max_current / kv_rpm_volt + 0.368 # Even more sophisticated model + return ( + 2.464 * max_current / kv_rpm_volt + 0.368 + ) # Even more sophisticated model def mass_wires( - wire_length, - max_current, - allowable_voltage_drop, - material="aluminum", - insulated=True, - max_voltage=600, - wire_packing_factor=1, - insulator_density=1700, - insulator_dielectric_strength=12e6, - insulator_min_thickness=0.2e-3, # silicone wire - return_dict: bool = False + wire_length, + max_current, + allowable_voltage_drop, + material="aluminum", + insulated=True, + max_voltage=600, + wire_packing_factor=1, + insulator_density=1700, + insulator_dielectric_strength=12e6, + insulator_min_thickness=0.2e-3, # silicone wire + return_dict: bool = False, ): """ Estimates the mass of wires used for power transmission. @@ -438,16 +451,24 @@ def mass_wires( Returns: If `return_dict` is False (default), returns the wire mass as a single number. If `return_dict` is True, returns a dictionary of all local variables. """ - if material == "sodium": # highly reactive with water & oxygen, low physical strength + if ( + material == "sodium" + ): # highly reactive with water & oxygen, low physical strength density = 970 # kg/m^3 resistivity = 47.7e-9 # ohm-meters - elif material == "lithium": # highly reactive with water & oxygen, low physical strength + elif ( + material == "lithium" + ): # highly reactive with water & oxygen, low physical strength density = 530 # kg/m^3 resistivity = 92.8e-9 # ohm-meters - elif material == "calcium": # highly reactive with water & oxygen, low physical strength + elif ( + material == "calcium" + ): # highly reactive with water & oxygen, low physical strength density = 1550 # kg/m^3 resistivity = 33.6e-9 # ohm-meters - elif material == "potassium": # highly reactive with water & oxygen, low physical strength + elif ( + material == "potassium" + ): # highly reactive with water & oxygen, low physical strength density = 890 # kg/m^3 resistivity = 72.0e-9 # ohm-meters elif material == "beryllium": # toxic, brittle @@ -459,13 +480,17 @@ def mass_wires( elif material == "magnesium": # worse specific conductivity than aluminum density = 1740 # kg/m^3 resistivity = 43.90e-9 # ohm-meters - elif material == "copper": # worse specific conductivity than aluminum, moderately expensive + elif ( + material == "copper" + ): # worse specific conductivity than aluminum, moderately expensive density = 8960 # kg/m^3 resistivity = 16.78e-9 # ohm-meters elif material == "silver": # worse specific conductivity than aluminum, expensive density = 10490 # kg/m^3 resistivity = 15.87e-9 # ohm-meters - elif material == "gold": # worse specific conductivity than aluminum, very expensive + elif ( + material == "gold" + ): # worse specific conductivity than aluminum, very expensive density = 19300 # kg/m^3 resistivity = 22.14e-9 # ohm-meters elif material == "iron": # worse specific conductivity than aluminum @@ -489,7 +514,7 @@ def mass_wires( ) radius_conductor = (area_conductor / wire_packing_factor / np.pi) ** 0.5 radius_insulator = radius_conductor + insulator_thickness - area_insulator = np.pi * radius_insulator ** 2 - area_conductor + area_insulator = np.pi * radius_insulator**2 - area_conductor volume_insulator = area_insulator * wire_length mass_insulator = insulator_density * volume_insulator else: @@ -504,19 +529,11 @@ def mass_wires( return mass_total -if __name__ == '__main__': - print(motor_electric_performance( - rpm=100, - current=3 - )) - print(motor_electric_performance( - rpm=4700, - torque=0.02482817 - )) +if __name__ == "__main__": + print(motor_electric_performance(rpm=100, current=3)) + print(motor_electric_performance(rpm=4700, torque=0.02482817)) - print( - mass_battery_pack(100) - ) + print(mass_battery_pack(100)) pows = np.logspace(2, 5, 300) mass_mot_burton = mass_motor_electric(pows, method="burton") @@ -533,12 +550,14 @@ def mass_wires( p.show_plot( "Small Electric Motor Mass Models\n(500 kv, 100 V)", "Motor Power [W]", - "Motor Mass [kg]" + "Motor Mass [kg]", ) - print(mass_wires( - wire_length=1, - max_current=100, - allowable_voltage_drop=1, - material="aluminum" - )) + print( + mass_wires( + wire_length=1, + max_current=100, + allowable_voltage_drop=1, + material="aluminum", + ) + ) diff --git a/aerosandbox/library/propulsion_propeller.py b/aerosandbox/library/propulsion_propeller.py index 535ce1ec6..1dc03fa42 100644 --- a/aerosandbox/library/propulsion_propeller.py +++ b/aerosandbox/library/propulsion_propeller.py @@ -2,11 +2,11 @@ def propeller_shaft_power_from_thrust( - thrust_force, - area_propulsive, - airspeed, - rho, - propeller_coefficient_of_performance=0.8, + thrust_force, + area_propulsive, + airspeed, + rho, + propeller_coefficient_of_performance=0.8, ): """ Using dynamic disc actuator theory, gives the shaft power required to generate @@ -21,18 +21,16 @@ def propeller_shaft_power_from_thrust( :param propeller_coefficient_of_performance: propeller coeff. of performance (due to viscous losses) [unitless] :return: Shaft power [W] """ - return 0.5 * thrust_force * airspeed * ( - np.sqrt( - thrust_force / (area_propulsive * airspeed ** 2 * rho / 2) + 1 - ) + 1 - ) / propeller_coefficient_of_performance + return ( + 0.5 + * thrust_force + * airspeed + * (np.sqrt(thrust_force / (area_propulsive * airspeed**2 * rho / 2) + 1) + 1) + / propeller_coefficient_of_performance + ) -def mass_hpa_propeller( - diameter, - max_power, - include_variable_pitch_mechanism=False -): +def mass_hpa_propeller(diameter, max_power, include_variable_pitch_mechanism=False): """ Returns the estimated mass of a propeller assembly for low-disc-loading applications (human powered airplane, paramotor, etc.) @@ -43,9 +41,9 @@ def mass_hpa_propeller( """ mass_propeller = ( - 0.495 * - (diameter / 1.25) ** 1.6 * - np.softmax(0.6, max_power / 14914, hardness=5) ** 2 + 0.495 + * (diameter / 1.25) ** 1.6 + * np.softmax(0.6, max_power / 14914, hardness=5) ** 2 ) # Baselining to a 125cm E-Props Top 80 Propeller for paramotor, with some sketchy scaling assumptions # Parameters on diameter exponent and min power were chosen such that Daedalus propeller is roughly on the curve. @@ -58,9 +56,9 @@ def mass_hpa_propeller( def mass_gearbox( - power, - rpm_in, - rpm_out, + power, + rpm_in, + rpm_out, ): """ Estimates the mass of a gearbox. @@ -98,7 +96,7 @@ def mass_gearbox( return mass -if __name__ == '__main__': +if __name__ == "__main__": import matplotlib.style as style style.use("seaborn") @@ -108,12 +106,8 @@ def mass_gearbox( mass_hpa_propeller( diameter=3.4442, max_power=177.93 * 8.2, # max thrust at cruise speed - include_variable_pitch_mechanism=False + include_variable_pitch_mechanism=False, ) ) # Should weight ca. 800 grams - print(mass_gearbox( - power=3000, - rpm_in=6000, - rpm_out=600 - )) + print(mass_gearbox(power=3000, rpm_in=6000, rpm_out=600)) diff --git a/aerosandbox/library/propulsion_small_solid_rocket.py b/aerosandbox/library/propulsion_small_solid_rocket.py index 315972429..15eac5fc6 100644 --- a/aerosandbox/library/propulsion_small_solid_rocket.py +++ b/aerosandbox/library/propulsion_small_solid_rocket.py @@ -41,7 +41,8 @@ # [units: dimensionless] W_OM_VALID_RANGE = (0, 0.22) OUT_OF_RANGE_ERROR_STRING = ( - '{:.3f} is outside the model valid range of {:.3f} <= w_om <= {:.3f}') + "{:.3f} is outside the model valid range of {:.3f} <= w_om <= {:.3f}" +) def burn_rate_coefficient(oxamide_fraction): @@ -66,7 +67,7 @@ def c_star(oxamide_fraction): """ # oxamide_fraction = cas.fmax(oxamide_fraction, 0) coefs = [1380.2, -983.3, -697.1] - return coefs[0] + coefs[1] * oxamide_fraction + coefs[2] * oxamide_fraction ** 2 + return coefs[0] + coefs[1] * oxamide_fraction + coefs[2] * oxamide_fraction**2 def dubious_min_combustion_pressure(oxamide_fraction): @@ -74,8 +75,8 @@ def dubious_min_combustion_pressure(oxamide_fraction): Note: this model is of DUBIOUS accuracy. Don't trust it. """ - coefs = [7.73179444e+00, 3.60886970e-01, 7.64587936e-03] - p_min_MPa = coefs[0] * oxamide_fraction ** 2 + coefs[1] * oxamide_fraction + coefs[2] + coefs = [7.73179444e00, 3.60886970e-01, 7.64587936e-03] + p_min_MPa = coefs[0] * oxamide_fraction**2 + coefs[1] * oxamide_fraction + coefs[2] p_min = 1e6 * p_min_MPa return p_min # Pa @@ -89,10 +90,12 @@ def gamma(oxamide_fraction): # oxamide_fraction = cas.fmax(oxamide_fraction, 0) coefs = [1.238, 0.216, -0.351] - return coefs[0] + coefs[1] * oxamide_fraction + coefs[2] * oxamide_fraction ** 2 + return coefs[0] + coefs[1] * oxamide_fraction + coefs[2] * oxamide_fraction**2 -def expansion_ratio_from_pressure(chamber_pressure, exit_pressure, gamma, oxamide_fraction): +def expansion_ratio_from_pressure( + chamber_pressure, exit_pressure, gamma, oxamide_fraction +): """Find the nozzle expansion ratio from the chamber and exit pressures. See :ref:`expansion-ratio-tutorial-label` for a physical description of the @@ -108,12 +111,20 @@ def expansion_ratio_from_pressure(chamber_pressure, exit_pressure, gamma, oxamid Returns: scalar: Expansion ratio :math:`\\epsilon = A_e / A_t` [units: dimensionless] """ - chamber_pressure = np.fmax(chamber_pressure, dubious_min_combustion_pressure(oxamide_fraction)) + chamber_pressure = np.fmax( + chamber_pressure, dubious_min_combustion_pressure(oxamide_fraction) + ) chamber_pressure = np.fmax(chamber_pressure, exit_pressure * 1.5) - AtAe = ((gamma + 1) / 2) ** (1 / (gamma - 1)) \ - * (exit_pressure / chamber_pressure) ** (1 / gamma) \ - * np.sqrt((gamma + 1) / (gamma - 1) * (1 - (exit_pressure / chamber_pressure) ** ((gamma - 1) / gamma))) + AtAe = ( + ((gamma + 1) / 2) ** (1 / (gamma - 1)) + * (exit_pressure / chamber_pressure) ** (1 / gamma) + * np.sqrt( + (gamma + 1) + / (gamma - 1) + * (1 - (exit_pressure / chamber_pressure) ** ((gamma - 1) / gamma)) + ) + ) er = 1 / AtAe return er @@ -141,10 +152,13 @@ def thrust_coefficient(chamber_pressure, exit_pressure, gamma, p_a=None, er=None """ # if (p_a is None and er is not None) or (er is None and p_a is not None): # raise ValueError('Both p_a and er must be provided.') - C_F = (2 * gamma ** 2 / (gamma - 1) - * (2 / (gamma + 1)) ** ((gamma + 1) / (gamma - 1)) - * (1 - (exit_pressure / chamber_pressure) ** ((gamma - 1) / gamma)) - ) ** 0.5 + C_F = ( + 2 + * gamma**2 + / (gamma - 1) + * (2 / (gamma + 1)) ** ((gamma + 1) / (gamma - 1)) + * (1 - (exit_pressure / chamber_pressure) ** ((gamma - 1) / gamma)) + ) ** 0.5 # if p_a is not None and er is not None: C_F += er * (exit_pressure - p_a) / chamber_pressure @@ -161,9 +175,17 @@ def thrust_coefficient(chamber_pressure, exit_pressure, gamma, p_a=None, er=None c_stars = c_star(oxamides) min_combustion_pressures = dubious_min_combustion_pressure(oxamides) gammas = gamma(oxamides) - px.scatter(x=oxamides, y=burn_rate_coefficients, labels={"x": "Oxamide", "y": "Burn Rate Coeff"}).show() + px.scatter( + x=oxamides, + y=burn_rate_coefficients, + labels={"x": "Oxamide", "y": "Burn Rate Coeff"}, + ).show() px.scatter(x=oxamides, y=c_stars, labels={"x": "Oxamide", "y": "c_star"}).show() - px.scatter(x=oxamides, y=min_combustion_pressures, labels={"x": "Oxamide", "y": "Min. Combustion Pressure"}).show() + px.scatter( + x=oxamides, + y=min_combustion_pressures, + labels={"x": "Oxamide", "y": "Min. Combustion Pressure"}, + ).show() px.scatter(x=oxamides, y=gammas, labels={"x": "Oxamide", "y": "Gamma"}).show() # # ER_from_P test @@ -177,11 +199,25 @@ def thrust_coefficient(chamber_pressure, exit_pressure, gamma, p_a=None, er=None for exit_pressure in exit_pressure_inputs: chamber_pressures.append(chamber_pressure) exit_pressures.append(exit_pressure) - ers.append(expansion_ratio_from_pressure(chamber_pressure, exit_pressure, gamma(ox_for_test), ox_for_test)) - data = pd.DataFrame({ - 'chamber_pressure': chamber_pressures, - 'exit_pressure' : exit_pressures, - 'ers' : ers - }) - px.scatter_3d(data, x='chamber_pressure', y='exit_pressure', z='ers', color='ers', log_x=True, log_y=True, - log_z=True).show() + ers.append( + expansion_ratio_from_pressure( + chamber_pressure, exit_pressure, gamma(ox_for_test), ox_for_test + ) + ) + data = pd.DataFrame( + { + "chamber_pressure": chamber_pressures, + "exit_pressure": exit_pressures, + "ers": ers, + } + ) + px.scatter_3d( + data, + x="chamber_pressure", + y="exit_pressure", + z="ers", + color="ers", + log_x=True, + log_y=True, + log_z=True, + ).show() diff --git a/aerosandbox/library/propulsion_turbofan.py b/aerosandbox/library/propulsion_turbofan.py index ec6f34f1c..37ffd9d22 100644 --- a/aerosandbox/library/propulsion_turbofan.py +++ b/aerosandbox/library/propulsion_turbofan.py @@ -2,7 +2,7 @@ def thrust_turbofan( - mass_turbofan: float, + mass_turbofan: float, ) -> float: """ Estimates the maximum rated dry thrust of a turbofan engine. A regression to historical data. @@ -23,16 +23,14 @@ def thrust_turbofan( The maximum (rated takeoff) dry thrust of the turbofan engine. [N] """ - p = {'a': 12050.719283568596, 'w': 0.9353861810025565} + p = {"a": 12050.719283568596, "w": 0.9353861810025565} - return ( - p["a"] * mass_turbofan ** p["w"] - ) + return p["a"] * mass_turbofan ** p["w"] def thrust_specific_fuel_consumption_turbofan( - mass_turbofan: float, - bypass_ratio: float, + mass_turbofan: float, + bypass_ratio: float, ) -> float: """ Estimates the thrust-specific fuel consumption (TSFC) of a turbofan engine. A regression to historical data. @@ -46,22 +44,25 @@ def thrust_specific_fuel_consumption_turbofan( See studies in `/AeroSandbox/studies/TurbofanStudies/make_fit_tsfc.py` for model details. """ - p = {'a' : 3.2916082331121034e-05, 'Weight [kg]': -0.07792863839756586, 'BPR': -0.3438158689838915, - 'BPR2': 0.29880079602955967} + p = { + "a": 3.2916082331121034e-05, + "Weight [kg]": -0.07792863839756586, + "BPR": -0.3438158689838915, + "BPR2": 0.29880079602955967, + } return ( - p["a"] - * mass_turbofan ** p["Weight [kg]"] - * (bypass_ratio + p["BPR2"]) ** p["BPR"] + p["a"] + * mass_turbofan ** p["Weight [kg]"] + * (bypass_ratio + p["BPR2"]) ** p["BPR"] ) def mass_turbofan( - m_dot_core_corrected, - overall_pressure_ratio, - bypass_ratio, - diameter_fan, - + m_dot_core_corrected, + overall_pressure_ratio, + bypass_ratio, + diameter_fan, ): """ Computes the combined mass of a bare turbofan, nacelle, and accessory and pylon weights. @@ -90,7 +91,9 @@ def mass_turbofan( m_to_ft = 1 / 0.3048 ##### Compute bare turbofan weight - m_dot_core_corrected_lbm_per_sec = m_dot_core_corrected * kg_to_lbm # Converts from kg/s to lbm/s + m_dot_core_corrected_lbm_per_sec = ( + m_dot_core_corrected * kg_to_lbm + ) # Converts from kg/s to lbm/s ### Parameters determined via least-squares fitting by Drela in TASOPT doc. b_m = 1 @@ -100,13 +103,11 @@ def mass_turbofan( W_pi_lbm = 17.7 W_alpha_lbm = 1662.2 - W_bare_lbm = ( - m_dot_core_corrected_lbm_per_sec / 100 - ) ** b_m * ( - W_0_lbm + - W_pi_lbm * (overall_pressure_ratio / 30) ** b_pi + - W_alpha_lbm * (bypass_ratio / 5) ** b_alpha - ) + W_bare_lbm = (m_dot_core_corrected_lbm_per_sec / 100) ** b_m * ( + W_0_lbm + + W_pi_lbm * (overall_pressure_ratio / 30) ** b_pi + + W_alpha_lbm * (bypass_ratio / 5) ** b_alpha + ) W_bare = W_bare_lbm / kg_to_lbm ##### Compute nacelle weight @@ -157,8 +158,8 @@ def mass_turbofan( def m_dot_corrected_over_m_dot( - temperature_total_2, - pressure_total_2, + temperature_total_2, + pressure_total_2, ): """ Computes the ratio `m_dot_corrected / m_dot`, where: @@ -181,19 +182,19 @@ def m_dot_corrected_over_m_dot( """ temperature_standard = 273.15 + 15 pressure_standard = 101325 - return ( - temperature_total_2 / temperature_standard - ) ** 0.5 / (pressure_total_2 / pressure_standard) + return (temperature_total_2 / temperature_standard) ** 0.5 / ( + pressure_total_2 / pressure_standard + ) -if __name__ == '__main__': +if __name__ == "__main__": import aerosandbox as asb atmo = asb.Atmosphere(altitude=10668) op_point = asb.OperatingPoint(atmo, velocity=0.80 * atmo.speed_of_sound()) m_dot_corrected_over_m_dot_ratio = m_dot_corrected_over_m_dot( temperature_total_2=op_point.total_temperature(), - pressure_total_2=op_point.total_pressure() + pressure_total_2=op_point.total_pressure(), ) ### CFM56-2 engine test @@ -201,5 +202,5 @@ def m_dot_corrected_over_m_dot( m_dot_core_corrected=364 / (5.95 + 1), overall_pressure_ratio=31.2, bypass_ratio=5.95, - diameter_fan=1.73 + diameter_fan=1.73, ) # real mass: (2139 to 2200 kg bare, ~3400 kg installed) diff --git a/aerosandbox/library/regulations/far_part_23.py b/aerosandbox/library/regulations/far_part_23.py index 603fac248..de849cb01 100644 --- a/aerosandbox/library/regulations/far_part_23.py +++ b/aerosandbox/library/regulations/far_part_23.py @@ -7,9 +7,10 @@ ### See also: # https://www.risingup.com/fars/info/23-index.shtml + def limit_load_factors( - design_mass_TOGW: float, - category: str = "normal", + design_mass_TOGW: float, + category: str = "normal", ) -> Tuple[float, float]: """ Computes the required limit load factors for FAR Part 23 certification. @@ -36,9 +37,7 @@ def limit_load_factors( ### Compute positive load factor if category == "normal" or category == "commuter": positive_load_factor = np.softmin( - 2.1 + (24000 / (design_mass_TOGW / u.lbm + 10000)), - 3.8, - softness=0.01 + 2.1 + (24000 / (design_mass_TOGW / u.lbm + 10000)), 3.8, softness=0.01 ) elif category == "utility": positive_load_factor = 4.4 @@ -47,7 +46,9 @@ def limit_load_factors( positive_load_factor = 6.0 else: - raise ValueError("Bad value of `category`. Valid values are 'normal', 'utility', 'acrobatic', and 'commuter'.") + raise ValueError( + "Bad value of `category`. Valid values are 'normal', 'utility', 'acrobatic', and 'commuter'." + ) ### Compute negative load factor if category == "normal" or category == "commuter" or category == "utility": diff --git a/aerosandbox/library/weights/nicolai_weights_light_metal_utility.py b/aerosandbox/library/weights/nicolai_weights_light_metal_utility.py index 1d1664bb6..0633a01a2 100644 --- a/aerosandbox/library/weights/nicolai_weights_light_metal_utility.py +++ b/aerosandbox/library/weights/nicolai_weights_light_metal_utility.py @@ -11,9 +11,9 @@ def mass_landing_gear( - gear_length: float, - design_mass_TOGW: float, - ultimate_load_factor: float, + gear_length: float, + design_mass_TOGW: float, + ultimate_load_factor: float, ): """ Calculates the mass of the landing gear. @@ -29,7 +29,7 @@ def mass_landing_gear( Returns: The mass of the landing gear, in kg. """ return ( - 0.054 * - (gear_length / u.inch) ** 0.501 * - (design_mass_TOGW / u.lbm * ultimate_load_factor) ** 0.684 + 0.054 + * (gear_length / u.inch) ** 0.501 + * (design_mass_TOGW / u.lbm * ultimate_load_factor) ** 0.684 ) * u.lbm diff --git a/aerosandbox/library/weights/raymer_cargo_transport_weights.py b/aerosandbox/library/weights/raymer_cargo_transport_weights.py index 5fe7fd920..c549251cb 100644 --- a/aerosandbox/library/weights/raymer_cargo_transport_weights.py +++ b/aerosandbox/library/weights/raymer_cargo_transport_weights.py @@ -8,11 +8,12 @@ # From Raymer, Aircraft Design: A Conceptual Approach, 5th Ed. # Section 15.3.2: Cargo/Transport Weights + def mass_wing( - wing: asb.Wing, - design_mass_TOGW: float, - ultimate_load_factor: float, - use_advanced_composites: bool = False, + wing: asb.Wing, + design_mass_TOGW: float, + ultimate_load_factor: float, + use_advanced_composites: bool = False, ) -> float: """ Computes the mass of the wing for a cargo/transport aircraft, according to Raymer's Aircraft Design: A Conceptual @@ -34,34 +35,31 @@ def mass_wing( Returns: Wing mass [kg]. """ - airfoil_thicknesses = [ - xsec.airfoil.max_thickness() - for xsec in wing.xsecs - ] + airfoil_thicknesses = [xsec.airfoil.max_thickness() for xsec in wing.xsecs] airfoil_t_over_c = np.min(airfoil_thicknesses) return ( - 0.0051 * - (design_mass_TOGW / u.lbm * ultimate_load_factor) ** 0.557 * - (wing.area('planform') / u.foot ** 2) ** 0.649 * - wing.aspect_ratio() ** 0.5 * - airfoil_t_over_c ** -0.4 * - (1 + wing.taper_ratio()) ** 0.1 * - np.cosd(wing.mean_sweep_angle()) ** -1 * - (wing.control_surface_area() / u.foot ** 2) ** 0.1 * - (advanced_composites["wing"] if use_advanced_composites else 1) + 0.0051 + * (design_mass_TOGW / u.lbm * ultimate_load_factor) ** 0.557 + * (wing.area("planform") / u.foot**2) ** 0.649 + * wing.aspect_ratio() ** 0.5 + * airfoil_t_over_c**-0.4 + * (1 + wing.taper_ratio()) ** 0.1 + * np.cosd(wing.mean_sweep_angle()) ** -1 + * (wing.control_surface_area() / u.foot**2) ** 0.1 + * (advanced_composites["wing"] if use_advanced_composites else 1) ) * u.lbm def mass_hstab( - hstab: asb.Wing, - design_mass_TOGW: float, - ultimate_load_factor: float, - wing_to_hstab_distance: float, - fuselage_width_at_hstab_intersection: float, - aircraft_y_radius_of_gyration: float = None, - use_advanced_composites: bool = False, + hstab: asb.Wing, + design_mass_TOGW: float, + ultimate_load_factor: float, + wing_to_hstab_distance: float, + fuselage_width_at_hstab_intersection: float, + aircraft_y_radius_of_gyration: float = None, + use_advanced_composites: bool = False, ) -> float: """ Computes the mass of the horizontal stabilizer for a cargo/transport aircraft, according to Raymer's Aircraft @@ -97,37 +95,36 @@ def mass_hstab( all_moving = True for xsec in hstab.xsecs: for control_surface in xsec.control_surfaces: - if ( - (control_surface.trailing_edge and control_surface.hinge_point > 0) or - (not control_surface.trailing_edge and control_surface.hinge_point < 1) + if (control_surface.trailing_edge and control_surface.hinge_point > 0) or ( + not control_surface.trailing_edge and control_surface.hinge_point < 1 ): all_moving = False break return ( - 0.0379 * - (1.143 if all_moving else 1) * - (1 + fuselage_width_at_hstab_intersection / hstab.span()) ** -0.25 * - (design_mass_TOGW / u.lbm) ** 0.639 * - ultimate_load_factor ** 0.10 * - (area / u.foot ** 2) ** 0.75 * - (wing_to_hstab_distance / u.foot) ** -1 * - (aircraft_y_radius_of_gyration / u.foot) ** 0.704 * - np.cosd(hstab.mean_sweep_angle()) ** -1 * - hstab.aspect_ratio() ** 0.166 * - (1 + hstab.control_surface_area() / area) ** 0.1 * - (advanced_composites["tails"] if use_advanced_composites else 1) + 0.0379 + * (1.143 if all_moving else 1) + * (1 + fuselage_width_at_hstab_intersection / hstab.span()) ** -0.25 + * (design_mass_TOGW / u.lbm) ** 0.639 + * ultimate_load_factor**0.10 + * (area / u.foot**2) ** 0.75 + * (wing_to_hstab_distance / u.foot) ** -1 + * (aircraft_y_radius_of_gyration / u.foot) ** 0.704 + * np.cosd(hstab.mean_sweep_angle()) ** -1 + * hstab.aspect_ratio() ** 0.166 + * (1 + hstab.control_surface_area() / area) ** 0.1 + * (advanced_composites["tails"] if use_advanced_composites else 1) ) * u.lbm def mass_vstab( - vstab: asb.Wing, - design_mass_TOGW: float, - ultimate_load_factor: float, - wing_to_vstab_distance: float, - is_t_tail: bool = False, - aircraft_z_radius_of_gyration: float = None, - use_advanced_composites: bool = False, + vstab: asb.Wing, + design_mass_TOGW: float, + ultimate_load_factor: float, + wing_to_vstab_distance: float, + is_t_tail: bool = False, + aircraft_z_radius_of_gyration: float = None, + use_advanced_composites: bool = False, ) -> float: """ Computes the mass of the vertical stabilizer for a cargo/transport aircraft, according to Raymer's Aircraft @@ -155,10 +152,7 @@ def mass_vstab( The mass of the vertical stabilizer [kg]. """ - airfoil_thicknesses = [ - xsec.airfoil.max_thickness() - for xsec in vstab.xsecs - ] + airfoil_thicknesses = [xsec.airfoil.max_thickness() for xsec in vstab.xsecs] airfoil_t_over_c = np.min(airfoil_thicknesses) @@ -166,30 +160,30 @@ def mass_vstab( aircraft_z_radius_of_gyration = 1 * wing_to_vstab_distance return ( - 0.0026 * - (1 + (1 if is_t_tail else 0)) ** 0.225 * - (design_mass_TOGW / u.lbm) ** 0.556 * - ultimate_load_factor ** 0.536 * - (wing_to_vstab_distance / u.foot) ** -0.5 * - (vstab.area('planform') / u.foot ** 2) ** 0.5 * - (aircraft_z_radius_of_gyration / u.foot) ** 0.875 * - np.cosd(vstab.mean_sweep_angle()) ** -1 * - vstab.aspect_ratio() ** 0.35 * - airfoil_t_over_c ** -0.5 * - (advanced_composites["tails"] if use_advanced_composites else 1) + 0.0026 + * (1 + (1 if is_t_tail else 0)) ** 0.225 + * (design_mass_TOGW / u.lbm) ** 0.556 + * ultimate_load_factor**0.536 + * (wing_to_vstab_distance / u.foot) ** -0.5 + * (vstab.area("planform") / u.foot**2) ** 0.5 + * (aircraft_z_radius_of_gyration / u.foot) ** 0.875 + * np.cosd(vstab.mean_sweep_angle()) ** -1 + * vstab.aspect_ratio() ** 0.35 + * airfoil_t_over_c**-0.5 + * (advanced_composites["tails"] if use_advanced_composites else 1) ) * u.lbm def mass_fuselage( - fuselage: asb.Fuselage, - design_mass_TOGW: float, - ultimate_load_factor: float, - L_over_D: float, - main_wing: asb.Wing, - n_cargo_doors: int = 1, - has_aft_clamshell_door: bool = False, - landing_gear_mounted_on_fuselage: bool = False, - use_advanced_composites: bool = False, + fuselage: asb.Fuselage, + design_mass_TOGW: float, + ultimate_load_factor: float, + L_over_D: float, + main_wing: asb.Wing, + n_cargo_doors: int = 1, + has_aft_clamshell_door: bool = False, + landing_gear_mounted_on_fuselage: bool = False, + use_advanced_composites: bool = False, ) -> float: """ Computes the mass of the fuselage for a cargo/transport aircraft, according to Raymer's Aircraft Design: A @@ -230,41 +224,39 @@ def mass_fuselage( if main_wing is not None: K_ws = ( - 0.75 * - ( - (1 + 2 * main_wing.taper_ratio()) / - (1 + main_wing.taper_ratio()) - ) * - ( - main_wing.span() / fuselage_structural_length * - np.tand(main_wing.mean_sweep_angle()) - ) + 0.75 + * ((1 + 2 * main_wing.taper_ratio()) / (1 + main_wing.taper_ratio())) + * ( + main_wing.span() + / fuselage_structural_length + * np.tand(main_wing.mean_sweep_angle()) + ) ) else: K_ws = 0 return ( - 0.3280 * - K_door * - K_lg * - (design_mass_TOGW / u.lbm * ultimate_load_factor) ** 0.5 * - (fuselage_structural_length / u.foot) ** 0.25 * - (fuselage.area_wetted() / u.foot ** 2) ** 0.302 * - (1 + K_ws) ** 0.04 * - L_over_D ** 0.10 * # L/D - (advanced_composites["fuselage/nacelle"] if use_advanced_composites else 1) + 0.3280 + * K_door + * K_lg + * (design_mass_TOGW / u.lbm * ultimate_load_factor) ** 0.5 + * (fuselage_structural_length / u.foot) ** 0.25 + * (fuselage.area_wetted() / u.foot**2) ** 0.302 + * (1 + K_ws) ** 0.04 + * L_over_D**0.10 # L/D + * (advanced_composites["fuselage/nacelle"] if use_advanced_composites else 1) ) * u.lbm def mass_main_landing_gear( - main_gear_length: float, - landing_speed: float, - design_mass_TOGW: float, - is_kneeling: bool = False, - n_gear: int = 2, - n_wheels: int = 12, - n_shock_struts: int = 4, - use_advanced_composites: bool = False, + main_gear_length: float, + landing_speed: float, + design_mass_TOGW: float, + is_kneeling: bool = False, + n_gear: int = 2, + n_wheels: int = 12, + n_shock_struts: int = 4, + use_advanced_composites: bool = False, ) -> float: """ Computes the mass of the main landing gear for a cargo/transport aircraft, according to Raymer's Aircraft Design: @@ -297,25 +289,25 @@ def mass_main_landing_gear( ultimate_landing_load_factor = n_gear * 1.5 return ( - 0.0106 * - K_mp * # non-kneeling LG - (design_mass_TOGW / u.lbm) ** 0.888 * - ultimate_landing_load_factor ** 0.25 * - (main_gear_length / u.inch) ** 0.4 * - n_wheels ** 0.321 * - n_shock_struts ** -0.5 * - (landing_speed / u.knot) ** 0.1 * - (advanced_composites["landing_gear"] if use_advanced_composites else 1) + 0.0106 + * K_mp # non-kneeling LG + * (design_mass_TOGW / u.lbm) ** 0.888 + * ultimate_landing_load_factor**0.25 + * (main_gear_length / u.inch) ** 0.4 + * n_wheels**0.321 + * n_shock_struts**-0.5 + * (landing_speed / u.knot) ** 0.1 + * (advanced_composites["landing_gear"] if use_advanced_composites else 1) ) * u.lbm def mass_nose_landing_gear( - nose_gear_length: float, - design_mass_TOGW: float, - is_kneeling: bool = False, - n_gear: int = 1, - n_wheels: int = 2, - use_advanced_composites: bool = False, + nose_gear_length: float, + design_mass_TOGW: float, + is_kneeling: bool = False, + n_gear: int = 1, + n_wheels: int = 2, + use_advanced_composites: bool = False, ) -> float: """ Computes the mass of the nose landing gear for a cargo/transport aircraft, according to Raymer's Aircraft @@ -343,27 +335,27 @@ def mass_nose_landing_gear( ultimate_landing_load_factor = n_gear * 1.5 return ( - 0.032 * - K_np * - (design_mass_TOGW / u.lbm) ** 0.646 * - ultimate_landing_load_factor ** 0.2 * - (nose_gear_length / u.inch) ** 0.5 * - n_wheels ** 0.45 * - (advanced_composites["landing_gear"] if use_advanced_composites else 1) + 0.032 + * K_np + * (design_mass_TOGW / u.lbm) ** 0.646 + * ultimate_landing_load_factor**0.2 + * (nose_gear_length / u.inch) ** 0.5 + * n_wheels**0.45 + * (advanced_composites["landing_gear"] if use_advanced_composites else 1) ) * u.lbm def mass_nacelles( - nacelle_length: float, - nacelle_width: float, - nacelle_height: float, - ultimate_load_factor: float, - mass_per_engine: float, - n_engines: int, - is_pylon_mounted: bool = False, - engines_have_propellers: bool = False, - engines_have_thrust_reversers: bool = False, - use_advanced_composites: bool = False, + nacelle_length: float, + nacelle_width: float, + nacelle_height: float, + ultimate_load_factor: float, + mass_per_engine: float, + n_engines: int, + is_pylon_mounted: bool = False, + engines_have_propellers: bool = False, + engines_have_thrust_reversers: bool = False, + use_advanced_composites: bool = False, ) -> float: """ Computes the mass of the nacelles for a cargo/transport aircraft, according to Raymer's Aircraft @@ -402,30 +394,29 @@ def mass_nacelles( mass_per_engine_with_contents = np.softmax( (2.331 * (mass_per_engine / u.lbm) ** 0.901) * K_p * K_tr * u.lbm, mass_per_engine, - hardness=10 / mass_per_engine + hardness=10 / mass_per_engine, ) nacelle_wetted_area = ( - nacelle_length * nacelle_height * 2 + - nacelle_width * nacelle_height * 2 + nacelle_length * nacelle_height * 2 + nacelle_width * nacelle_height * 2 ) return ( - 0.6724 * - K_ng * - (nacelle_length / u.foot) ** 0.10 * - (nacelle_width / u.foot) ** 0.294 * - (ultimate_load_factor) ** 0.119 * - (mass_per_engine_with_contents / u.lbm) ** 0.611 * - (n_engines) ** 0.984 * - (nacelle_wetted_area / u.foot ** 2) ** 0.224 * - (advanced_composites["fuselage/nacelle"] if use_advanced_composites else 1) + 0.6724 + * K_ng + * (nacelle_length / u.foot) ** 0.10 + * (nacelle_width / u.foot) ** 0.294 + * (ultimate_load_factor) ** 0.119 + * (mass_per_engine_with_contents / u.lbm) ** 0.611 + * (n_engines) ** 0.984 + * (nacelle_wetted_area / u.foot**2) ** 0.224 + * (advanced_composites["fuselage/nacelle"] if use_advanced_composites else 1) ) def mass_engine_controls( - n_engines: int, - cockpit_to_engine_length: float, + n_engines: int, + cockpit_to_engine_length: float, ) -> float: """ Computes the mass of the engine controls for a cargo/transport aircraft, according to Raymer's Aircraft @@ -440,14 +431,13 @@ def mass_engine_controls( The mass of the engine controls [kg]. """ return ( - 5 * n_engines + - 0.80 * (cockpit_to_engine_length / u.foot) * n_engines + 5 * n_engines + 0.80 * (cockpit_to_engine_length / u.foot) * n_engines ) * u.lbm def mass_starter( - n_engines: int, - mass_per_engine: float, + n_engines: int, + mass_per_engine: float, ) -> float: """ Computes the mass of the engine starter for a cargo/transport aircraft, according to Raymer's Aircraft @@ -461,18 +451,13 @@ def mass_starter( Returns: The mass of the engine starter [kg]. """ - return ( - 49.19 * ( - mass_per_engine / u.lbm * n_engines - / 1000 - ) ** 0.541 - ) * u.lbm + return (49.19 * (mass_per_engine / u.lbm * n_engines / 1000) ** 0.541) * u.lbm def mass_fuel_system( - fuel_volume: float, - n_tanks: int, - fraction_in_integral_tanks: float = 0.5, + fuel_volume: float, + n_tanks: int, + fraction_in_integral_tanks: float = 0.5, ) -> float: """ Computes the mass of the fuel system (e.g., tanks, pumps, but not the fuel itself) for a cargo/transport @@ -492,18 +477,18 @@ def mass_fuel_system( fraction_in_protected_tanks = 1 - fraction_in_integral_tanks return ( - 2.405 * - (fuel_volume / u.gallon) ** 0.606 * - (1 + fraction_in_integral_tanks) ** -1 * - (1 + fraction_in_protected_tanks) * - n_tanks ** 0.5 + 2.405 + * (fuel_volume / u.gallon) ** 0.606 + * (1 + fraction_in_integral_tanks) ** -1 + * (1 + fraction_in_protected_tanks) + * n_tanks**0.5 ) * u.lbm def mass_flight_controls( - airplane: asb.Airplane, - aircraft_Iyy: float, - fraction_of_mechanical_controls: int = 0, + airplane: asb.Airplane, + aircraft_Iyy: float, + fraction_of_mechanical_controls: int = 0, ) -> float: """ Computes the added mass of the flight control surfaces (and any applicable linkages, in the case of mechanical @@ -532,16 +517,17 @@ def mass_flight_controls( control_surface_area += wing.control_surface_area() return ( - 145.9 * - N_functions_performed_by_controls ** 0.554 * # number of functions performed by controls - (1 + fraction_of_mechanical_controls) ** -1 * - (control_surface_area / u.foot ** 2) ** 0.20 * - (aircraft_Iyy / (u.lbm * u.foot ** 2) * 1e-6) ** 0.07 + 145.9 + * N_functions_performed_by_controls + ** 0.554 # number of functions performed by controls + * (1 + fraction_of_mechanical_controls) ** -1 + * (control_surface_area / u.foot**2) ** 0.20 + * (aircraft_Iyy / (u.lbm * u.foot**2) * 1e-6) ** 0.07 ) * u.lbm def mass_APU( - mass_APU_uninstalled: float, + mass_APU_uninstalled: float, ): """ Computes the mass of the auxiliary power unit (APU) for a cargo/transport aircraft, according to Raymer's Aircraft @@ -557,12 +543,12 @@ def mass_APU( def mass_instruments( - fuselage: asb.Fuselage, - main_wing: asb.Wing, - n_engines: int, - n_crew: Union[int, float], - engine_is_reciprocating: bool = False, - engine_is_turboprop: bool = False, + fuselage: asb.Fuselage, + main_wing: asb.Wing, + n_engines: int, + n_crew: Union[int, float], + engine_is_reciprocating: bool = False, + engine_is_turboprop: bool = False, ): """ Computes the mass of the flight instruments for a cargo/transport aircraft, according to Raymer's Aircraft @@ -589,19 +575,19 @@ def mass_instruments( K_tp = 0.793 if engine_is_turboprop else 1 return ( - 4.509 * - K_r * - K_tp * - n_crew ** 0.541 * - n_engines * - (fuselage.length() / u.foot * main_wing.span() / u.foot) ** 0.5 + 4.509 + * K_r + * K_tp + * n_crew**0.541 + * n_engines + * (fuselage.length() / u.foot * main_wing.span() / u.foot) ** 0.5 ) * u.lbm def mass_hydraulics( - airplane: asb.Airplane, - fuselage: asb.Fuselage, - main_wing: asb.Wing, + airplane: asb.Airplane, + fuselage: asb.Fuselage, + main_wing: asb.Wing, ): """ Computes the mass of the hydraulic system for a cargo/transport aircraft, according to Raymer's Aircraft @@ -622,16 +608,16 @@ def mass_hydraulics( N_functions_performed_by_controls += len(wing.get_control_surface_names()) return ( - 0.2673 * - N_functions_performed_by_controls * - (fuselage.length() / u.foot * main_wing.span() / u.foot) ** 0.937 + 0.2673 + * N_functions_performed_by_controls + * (fuselage.length() / u.foot * main_wing.span() / u.foot) ** 0.937 ) * u.lbm def mass_electrical( - system_electrical_power_rating: float, - electrical_routing_distance: float, - n_engines: int, + system_electrical_power_rating: float, + electrical_routing_distance: float, + n_engines: int, ): """ Computes the mass of the electrical system for a cargo/transport aircraft, according to Raymer's Aircraft @@ -655,15 +641,15 @@ def mass_electrical( """ return ( - 7.291 * - (system_electrical_power_rating / 1e3) ** 0.782 * - (electrical_routing_distance / u.foot) ** 0.346 * - (n_engines) ** 0.10 + 7.291 + * (system_electrical_power_rating / 1e3) ** 0.782 + * (electrical_routing_distance / u.foot) ** 0.346 + * (n_engines) ** 0.10 ) * u.lbm def mass_avionics( - mass_uninstalled_avionics: float, + mass_uninstalled_avionics: float, ): """ Computes the mass of the avionics for a cargo/transport aircraft, according to Raymer's Aircraft @@ -675,16 +661,13 @@ def mass_avionics( Returns: The mass of the avionics, as installed [kg]. """ - return ( - 1.73 * - (mass_uninstalled_avionics / u.lbm) ** 0.983 - ) * u.lbm + return (1.73 * (mass_uninstalled_avionics / u.lbm) ** 0.983) * u.lbm def mass_furnishings( - n_crew: Union[int, float], - mass_cargo: float, - fuselage: asb.Fuselage, + n_crew: Union[int, float], + mass_cargo: float, + fuselage: asb.Fuselage, ): """ Computes the mass of the furnishings for a cargo/transport aircraft, according to Raymer's Aircraft @@ -701,18 +684,18 @@ def mass_furnishings( The mass of the furnishings [kg]. """ return ( - 0.0577 * - n_crew ** 0.1 * - (mass_cargo / u.lbm) ** 0.393 * - (fuselage.area_wetted() / u.foot ** 2) ** 0.75 + 0.0577 + * n_crew**0.1 + * (mass_cargo / u.lbm) ** 0.393 + * (fuselage.area_wetted() / u.foot**2) ** 0.75 ) * u.lbm def mass_air_conditioning( - n_crew: int, - n_pax: int, - volume_pressurized: float, - mass_uninstalled_avionics: float, + n_crew: int, + n_pax: int, + volume_pressurized: float, + mass_uninstalled_avionics: float, ): """ Computes the mass of the air conditioning system for a cargo/transport aircraft, according to Raymer's Aircraft @@ -731,15 +714,15 @@ def mass_air_conditioning( The mass of the air conditioning system [kg]. """ return ( - 62.36 * - (n_crew + n_pax) ** 0.25 * - (volume_pressurized / u.foot ** 3 / 1e3) ** 0.604 * - (mass_uninstalled_avionics / u.lbm) ** 0.10 + 62.36 + * (n_crew + n_pax) ** 0.25 + * (volume_pressurized / u.foot**3 / 1e3) ** 0.604 + * (mass_uninstalled_avionics / u.lbm) ** 0.10 ) * u.lbm def mass_anti_ice( - design_mass_TOGW: float, + design_mass_TOGW: float, ): """ Computes the mass of the anti-ice system for a cargo/transport aircraft, according to Raymer's Aircraft @@ -755,7 +738,7 @@ def mass_anti_ice( def mass_handling_gear( - design_mass_TOGW: float, + design_mass_TOGW: float, ): """ Computes the mass of the handling gear for a cargo/transport aircraft, according to Raymer's Aircraft @@ -771,7 +754,7 @@ def mass_handling_gear( def mass_military_cargo_handling_system( - cargo_floor_area: float, + cargo_floor_area: float, ): """ Computes the mass of the military cargo handling system for a cargo/transport aircraft, according to Raymer's @@ -783,7 +766,4 @@ def mass_military_cargo_handling_system( Returns: The mass of the military cargo handling system [kg]. """ - return ( - 2.4 * - (cargo_floor_area / u.foot ** 2) - ) * u.lbm + return (2.4 * (cargo_floor_area / u.foot**2)) * u.lbm diff --git a/aerosandbox/library/weights/raymer_fudge_factors.py b/aerosandbox/library/weights/raymer_fudge_factors.py index ad3cb45d0..619cdc3b9 100644 --- a/aerosandbox/library/weights/raymer_fudge_factors.py +++ b/aerosandbox/library/weights/raymer_fudge_factors.py @@ -3,16 +3,15 @@ advanced_composites = { # Format: # "component" : (low, high) - "wing" : (0.85, 0.90), - "tails" : (0.83, 0.88), - "fuselage/nacelle" : (0.90, 0.95), - "landing_gear" : (0.95, 1), + "wing": (0.85, 0.90), + "tails": (0.83, 0.88), + "fuselage/nacelle": (0.90, 0.95), + "landing_gear": (0.95, 1), "air_induction_system": (0.85, 0.90), } -advanced_composites = { # Here, we convert this to a dictionary of average values. - k: (v[0] + v[1]) / 2 - for k, v in advanced_composites.items() +advanced_composites = { # Here, we convert this to a dictionary of average values. + k: (v[0] + v[1]) / 2 for k, v in advanced_composites.items() } braced_wing = 0.82 diff --git a/aerosandbox/library/weights/raymer_general_aviation_weights.py b/aerosandbox/library/weights/raymer_general_aviation_weights.py index 1e0915962..c3d1b33af 100644 --- a/aerosandbox/library/weights/raymer_general_aviation_weights.py +++ b/aerosandbox/library/weights/raymer_general_aviation_weights.py @@ -7,13 +7,14 @@ # From Raymer: "Aircraft Design: A Conceptual Approach", 5th Ed. # Section 15.3.3: General Aviation Weights + def mass_wing( - wing: asb.Wing, - design_mass_TOGW: float, - ultimate_load_factor: float, - mass_fuel_in_wing: float, - cruise_op_point: asb.OperatingPoint, - use_advanced_composites: bool = False, + wing: asb.Wing, + design_mass_TOGW: float, + ultimate_load_factor: float, + mass_fuel_in_wing: float, + cruise_op_point: asb.OperatingPoint, + use_advanced_composites: bool = False, ) -> float: """ Computes the mass of a wing of a general aviation aircraft, according to Raymer's Aircraft Design: A Conceptual @@ -49,41 +50,36 @@ def mass_wing( if fuel_is_in_wing: fuel_weight_factor = np.softmax( - (mass_fuel_in_wing / u.lbm) ** 0.0035, - 1, - hardness=1000 + (mass_fuel_in_wing / u.lbm) ** 0.0035, 1, hardness=1000 ) else: fuel_weight_factor = 1 - airfoil_thicknesses = [ - xsec.airfoil.max_thickness() - for xsec in wing.xsecs - ] + airfoil_thicknesses = [xsec.airfoil.max_thickness() for xsec in wing.xsecs] airfoil_t_over_c = np.min(airfoil_thicknesses) cos_sweep = np.cosd(wing.mean_sweep_angle()) return ( - 0.036 * - (wing.area('planform') / u.foot ** 2) ** 0.758 * - fuel_weight_factor * - (wing.aspect_ratio() / cos_sweep ** 2) ** 0.6 * - (cruise_op_point.dynamic_pressure() / u.psf) ** 0.006 * - wing.taper_ratio() ** 0.04 * - (100 * airfoil_t_over_c / cos_sweep) ** -0.3 * - (design_mass_TOGW / u.lbm * ultimate_load_factor) ** 0.49 * - (advanced_composites["wing"] if use_advanced_composites else 1) + 0.036 + * (wing.area("planform") / u.foot**2) ** 0.758 + * fuel_weight_factor + * (wing.aspect_ratio() / cos_sweep**2) ** 0.6 + * (cruise_op_point.dynamic_pressure() / u.psf) ** 0.006 + * wing.taper_ratio() ** 0.04 + * (100 * airfoil_t_over_c / cos_sweep) ** -0.3 + * (design_mass_TOGW / u.lbm * ultimate_load_factor) ** 0.49 + * (advanced_composites["wing"] if use_advanced_composites else 1) ) * u.lbm def mass_hstab( - hstab: asb.Wing, - design_mass_TOGW: float, - ultimate_load_factor: float, - cruise_op_point: asb.OperatingPoint, - use_advanced_composites: bool = False, + hstab: asb.Wing, + design_mass_TOGW: float, + ultimate_load_factor: float, + cruise_op_point: asb.OperatingPoint, + use_advanced_composites: bool = False, ) -> float: """ Computes the mass of a horizontal stabilizer of a general aviation aircraft, according to Raymer's Aircraft Design: @@ -103,34 +99,31 @@ def mass_hstab( Returns: The mass of the horizontal stabilizer [kg]. """ - airfoil_thicknesses = [ - xsec.airfoil.max_thickness() - for xsec in hstab.xsecs - ] + airfoil_thicknesses = [xsec.airfoil.max_thickness() for xsec in hstab.xsecs] airfoil_t_over_c = np.min(airfoil_thicknesses) cos_sweep = np.cosd(hstab.mean_sweep_angle()) return ( - 0.016 * - (design_mass_TOGW / u.lbm * ultimate_load_factor) ** 0.414 * - (cruise_op_point.dynamic_pressure() / u.psf) ** 0.168 * - (hstab.area('planform') / u.foot ** 2) ** 0.896 * - (100 * airfoil_t_over_c / cos_sweep) ** -0.12 * - (hstab.aspect_ratio() / cos_sweep ** 2) ** 0.043 * - hstab.taper_ratio() ** -0.02 * - (advanced_composites["tails"] if use_advanced_composites else 1) + 0.016 + * (design_mass_TOGW / u.lbm * ultimate_load_factor) ** 0.414 + * (cruise_op_point.dynamic_pressure() / u.psf) ** 0.168 + * (hstab.area("planform") / u.foot**2) ** 0.896 + * (100 * airfoil_t_over_c / cos_sweep) ** -0.12 + * (hstab.aspect_ratio() / cos_sweep**2) ** 0.043 + * hstab.taper_ratio() ** -0.02 + * (advanced_composites["tails"] if use_advanced_composites else 1) ) * u.lbm def mass_vstab( - vstab: asb.Wing, - design_mass_TOGW: float, - ultimate_load_factor: float, - cruise_op_point: asb.OperatingPoint, - is_t_tail: bool = False, - use_advanced_composites: bool = False, + vstab: asb.Wing, + design_mass_TOGW: float, + ultimate_load_factor: float, + cruise_op_point: asb.OperatingPoint, + is_t_tail: bool = False, + use_advanced_composites: bool = False, ) -> float: """ Computes the mass of a vertical stabilizer of a general aviation aircraft, according to Raymer's Aircraft Design: @@ -152,37 +145,34 @@ def mass_vstab( Returns: The mass of the vertical stabilizer [kg]. """ - airfoil_thicknesses = [ - xsec.airfoil.max_thickness() - for xsec in vstab.xsecs - ] + airfoil_thicknesses = [xsec.airfoil.max_thickness() for xsec in vstab.xsecs] airfoil_t_over_c = np.min(airfoil_thicknesses) cos_sweep = np.cosd(vstab.mean_sweep_angle()) return ( - 0.073 * - (1 + (0.2 if is_t_tail else 0)) * - (design_mass_TOGW / u.lbm * ultimate_load_factor) ** 0.376 * - (cruise_op_point.dynamic_pressure() / u.psf) ** 0.122 * - (vstab.area('planform') / u.foot ** 2) ** 0.876 * - (100 * airfoil_t_over_c / cos_sweep) ** -0.49 * - (vstab.aspect_ratio() / cos_sweep ** 2) ** 0.357 * - vstab.taper_ratio() ** 0.039 * - (advanced_composites["tails"] if use_advanced_composites else 1) + 0.073 + * (1 + (0.2 if is_t_tail else 0)) + * (design_mass_TOGW / u.lbm * ultimate_load_factor) ** 0.376 + * (cruise_op_point.dynamic_pressure() / u.psf) ** 0.122 + * (vstab.area("planform") / u.foot**2) ** 0.876 + * (100 * airfoil_t_over_c / cos_sweep) ** -0.49 + * (vstab.aspect_ratio() / cos_sweep**2) ** 0.357 + * vstab.taper_ratio() ** 0.039 + * (advanced_composites["tails"] if use_advanced_composites else 1) ) * u.lbm def mass_fuselage( - fuselage: asb.Fuselage, - design_mass_TOGW: float, - ultimate_load_factor: float, - L_over_D: float, - cruise_op_point: asb.OperatingPoint, - wing_to_tail_distance: float, - pressure_differential: float = 0.0, - use_advanced_composites: bool = False, + fuselage: asb.Fuselage, + design_mass_TOGW: float, + ultimate_load_factor: float, + L_over_D: float, + cruise_op_point: asb.OperatingPoint, + wing_to_tail_distance: float, + pressure_differential: float = 0.0, + use_advanced_composites: bool = False, ) -> float: """ Computes the mass of a fuselage of a general aviation aircraft, according to Raymer's Aircraft Design: A Conceptual @@ -211,36 +201,28 @@ def mass_fuselage( """ mass_fuselage_without_pressurization = ( - 0.052 * - (fuselage.area_wetted() / u.foot ** 2) ** 1.086 * - (design_mass_TOGW / u.lbm * ultimate_load_factor) ** 0.177 * - (wing_to_tail_distance / u.foot) ** -0.051 * - (L_over_D) ** -0.072 * - (cruise_op_point.dynamic_pressure() / u.psf) ** 0.241 * - (advanced_composites["fuselage/nacelle"] - if use_advanced_composites else 1) - ) * u.lbm + 0.052 + * (fuselage.area_wetted() / u.foot**2) ** 1.086 + * (design_mass_TOGW / u.lbm * ultimate_load_factor) ** 0.177 + * (wing_to_tail_distance / u.foot) ** -0.051 + * (L_over_D) ** -0.072 + * (cruise_op_point.dynamic_pressure() / u.psf) ** 0.241 + * (advanced_composites["fuselage/nacelle"] if use_advanced_composites else 1) + ) * u.lbm mass_pressurization_components = ( - 11.9 * - ( - fuselage.volume() / u.foot ** 3 * - pressure_differential / u.psi - ) ** 0.271 - ) * u.lbm + 11.9 * (fuselage.volume() / u.foot**3 * pressure_differential / u.psi) ** 0.271 + ) * u.lbm - return ( - mass_fuselage_without_pressurization + - mass_pressurization_components - ) + return mass_fuselage_without_pressurization + mass_pressurization_components def mass_main_landing_gear( - main_gear_length: float, - design_mass_TOGW: float, - n_gear: int = 2, - is_retractable: bool = True, - use_advanced_composites: bool = False, + main_gear_length: float, + design_mass_TOGW: float, + n_gear: int = 2, + is_retractable: bool = True, + use_advanced_composites: bool = False, ) -> float: """ Computes the mass of the main landing gear of a general aviation aircraft, according to Raymer's Aircraft Design: @@ -264,20 +246,22 @@ def mass_main_landing_gear( ultimate_landing_load_factor = n_gear * 1.5 return ( - 0.095 * - (ultimate_landing_load_factor * design_mass_TOGW / u.lbm) ** 0.768 * - (main_gear_length / u.foot / 12) ** 0.409 * - (advanced_composites["landing_gear"] if use_advanced_composites else 1) * - (((5.7 - 1.4 / 2) / 5.7) if not is_retractable else 1) # derived from Raymer Section 15.2 and 15.3.3 together. + 0.095 + * (ultimate_landing_load_factor * design_mass_TOGW / u.lbm) ** 0.768 + * (main_gear_length / u.foot / 12) ** 0.409 + * (advanced_composites["landing_gear"] if use_advanced_composites else 1) + * ( + ((5.7 - 1.4 / 2) / 5.7) if not is_retractable else 1 + ) # derived from Raymer Section 15.2 and 15.3.3 together. ) * u.lbm def mass_nose_landing_gear( - nose_gear_length: float, - design_mass_TOGW: float, - n_gear: int = 1, - is_retractable: bool = True, - use_advanced_composites: bool = False, + nose_gear_length: float, + design_mass_TOGW: float, + n_gear: int = 1, + is_retractable: bool = True, + use_advanced_composites: bool = False, ) -> float: """ Computes the mass of the nose landing gear of a general aviation aircraft, according to Raymer's Aircraft Design: @@ -301,17 +285,19 @@ def mass_nose_landing_gear( ultimate_landing_load_factor = n_gear * 1.5 return ( - 0.125 * - (ultimate_landing_load_factor * design_mass_TOGW / u.lbm) ** 0.566 * - (nose_gear_length / u.foot / 12) ** 0.845 * - (advanced_composites["landing_gear"] if use_advanced_composites else 1) * - (((5.7 - 1.4 / 2) / 5.7) if not is_retractable else 1) # derived from Raymer Section 15.2 and 15.3.3 together. + 0.125 + * (ultimate_landing_load_factor * design_mass_TOGW / u.lbm) ** 0.566 + * (nose_gear_length / u.foot / 12) ** 0.845 + * (advanced_composites["landing_gear"] if use_advanced_composites else 1) + * ( + ((5.7 - 1.4 / 2) / 5.7) if not is_retractable else 1 + ) # derived from Raymer Section 15.2 and 15.3.3 together. ) * u.lbm def mass_engines_installed( - n_engines: int, - mass_per_engine: float, + n_engines: int, + mass_per_engine: float, ) -> float: """ Computes the mass of the engines installed on a general aviation aircraft, according to Raymer's Aircraft Design: @@ -324,18 +310,14 @@ def mass_engines_installed( Returns: The mass of the engines installed on the aircraft [kg]. """ - return ( - 2.575 * - (mass_per_engine / u.lbm) ** 0.922 * - n_engines - ) * u.lbm + return (2.575 * (mass_per_engine / u.lbm) ** 0.922 * n_engines) * u.lbm def mass_fuel_system( - fuel_volume: float, - n_tanks: int, - n_engines: int, - fraction_in_integral_tanks: float = 0.5, + fuel_volume: float, + n_tanks: int, + n_engines: int, + fraction_in_integral_tanks: float = 0.5, ) -> float: """ Computes the mass of the fuel system (e.g., tanks, pumps, but not the fuel itself) for a general aviation @@ -354,20 +336,20 @@ def mass_fuel_system( Returns: The mass of the fuel system [kg]. """ return ( - 2.49 * - (fuel_volume / u.gallon) ** 0.726 * - (1 + fraction_in_integral_tanks) ** -0.363 * - n_tanks ** 0.242 * - n_engines ** 0.157 + 2.49 + * (fuel_volume / u.gallon) ** 0.726 + * (1 + fraction_in_integral_tanks) ** -0.363 + * n_tanks**0.242 + * n_engines**0.157 ) * u.lbm def mass_flight_controls( - airplane: asb.Airplane, - design_mass_TOGW: float, - ultimate_load_factor: float, - fuselage: asb.Fuselage = None, - main_wing: asb.Wing = None, + airplane: asb.Airplane, + design_mass_TOGW: float, + ultimate_load_factor: float, + fuselage: asb.Fuselage = None, + main_wing: asb.Wing = None, ) -> float: """ Computes the mass of the flight controls for a general aviation aircraft, according to Raymer's Aircraft Design: @@ -396,8 +378,10 @@ def mass_flight_controls( elif len(airplane.fuselages) == 1: fuselage = airplane.fuselages[0] else: - raise ValueError('More than one fuselage is present in the airplane. Please specify which fuselage to use ' - 'for computing flight control system mass.') + raise ValueError( + "More than one fuselage is present in the airplane. Please specify which fuselage to use " + "for computing flight control system mass." + ) if fuselage is not None: fuselage_length_factor = (fuselage.length() / u.foot) ** 1.536 @@ -411,8 +395,10 @@ def mass_flight_controls( elif len(airplane.wings) == 1: main_wing = airplane.wings[0] else: - raise ValueError('More than one wing is present in the airplane. Please specify which wing is the main' - 'wing using the `main_wing` argument.') + raise ValueError( + "More than one wing is present in the airplane. Please specify which wing is the main" + "wing using the `main_wing` argument." + ) if main_wing is not None: wing_span_factor = (main_wing.span() / u.foot) ** 0.371 @@ -430,16 +416,16 @@ def mass_flight_controls( # control_surface_area += wing.control_surface_area() return ( - 0.053 * - fuselage_length_factor * - wing_span_factor * - (design_mass_TOGW / u.lbm * ultimate_load_factor * 1e-4) ** 0.80 + 0.053 + * fuselage_length_factor + * wing_span_factor + * (design_mass_TOGW / u.lbm * ultimate_load_factor * 1e-4) ** 0.80 ) * u.lbm def mass_hydraulics( - fuselage_width: float, - cruise_op_point: asb.OperatingPoint, + fuselage_width: float, + cruise_op_point: asb.OperatingPoint, ) -> float: """ Computes the mass of the hydraulics for a general aviation aircraft, according to Raymer's Aircraft Design: @@ -454,7 +440,7 @@ def mass_hydraulics( """ mach = cruise_op_point.mach() - K_h = 0.16472092991402892 * mach ** 0.8327375101470056 + K_h = 0.16472092991402892 * mach**0.8327375101470056 # This is a curve fit to a few points that Raymer gives in his book. The points are: # { # 0.1 : 0.013, @@ -469,15 +455,11 @@ def mass_hydraulics( # for flaps; 0.12 for high subsonic with hydraulic flight controls; 0.013 for light plane with hydraulic brakes # only (and use M=0.1)" - return ( - K_h * - (fuselage_width / u.foot) ** 0.8 * - mach ** 0.5 - ) * u.lbm + return (K_h * (fuselage_width / u.foot) ** 0.8 * mach**0.5) * u.lbm def mass_avionics( - mass_uninstalled_avionics: float, + mass_uninstalled_avionics: float, ) -> float: """ Computes the mass of the avionics for a general aviation aircraft, according to Raymer's Aircraft Design: A @@ -488,15 +470,12 @@ def mass_avionics( Returns: The mass of the avionics, as installed [kg]. """ - return ( - 2.117 * - (mass_uninstalled_avionics / u.lbm) ** 0.933 - ) * u.lbm + return (2.117 * (mass_uninstalled_avionics / u.lbm) ** 0.933) * u.lbm def mass_electrical( - fuel_system_mass: float, - avionics_mass: float, + fuel_system_mass: float, + avionics_mass: float, ) -> float: """ Computes the mass of the electrical system for a general aviation aircraft, according to Raymer's Aircraft Design: @@ -512,18 +491,15 @@ def mass_electrical( fuel_and_avionics_masses = fuel_system_mass + avionics_mass - return ( - 12.57 * - (fuel_and_avionics_masses / u.lbm) ** 0.51 - ) * u.lbm + return (12.57 * (fuel_and_avionics_masses / u.lbm) ** 0.51) * u.lbm def mass_air_conditioning_and_anti_ice( - design_mass_TOGW: float, - n_crew: int, - n_pax: int, - mass_avionics: float, - cruise_op_point: asb.OperatingPoint, + design_mass_TOGW: float, + n_crew: int, + n_pax: int, + mass_avionics: float, + cruise_op_point: asb.OperatingPoint, ): """ Computes the mass of the air conditioning and anti-ice system for a general aviation aircraft, according to @@ -545,16 +521,16 @@ def mass_air_conditioning_and_anti_ice( mach = cruise_op_point.mach() return ( - 0.265 * - (design_mass_TOGW / u.lbm) ** 0.52 * - (n_crew + n_pax) ** 0.68 * - (mass_avionics / u.lbm) ** 0.17 * - mach ** 0.08 + 0.265 + * (design_mass_TOGW / u.lbm) ** 0.52 + * (n_crew + n_pax) ** 0.68 + * (mass_avionics / u.lbm) ** 0.17 + * mach**0.08 ) * u.lbm def mass_furnishings( - design_mass_TOGW: float, + design_mass_TOGW: float, ): """ Computes the mass of the furnishings for a general aviation aircraft, according to Raymer's Aircraft Design: A diff --git a/aerosandbox/library/weights/raymer_miscellaneous.py b/aerosandbox/library/weights/raymer_miscellaneous.py index ff3cf11ba..2136a5bcf 100644 --- a/aerosandbox/library/weights/raymer_miscellaneous.py +++ b/aerosandbox/library/weights/raymer_miscellaneous.py @@ -8,9 +8,7 @@ mass_passenger = 215 * u.lbm # includes carry-on -def mass_seat( - kind="passenger" -) -> float: +def mass_seat(kind="passenger") -> float: """ Computes the mass of an individual seat on an airplane. @@ -37,10 +35,7 @@ def mass_seat( raise ValueError("Bad value of `kind`!") -def mass_lavatories( - n_pax, - aircraft_type="short-haul" -) -> float: +def mass_lavatories(n_pax, aircraft_type="short-haul") -> float: """ Computes the required mass of all lavatories on an airplane. @@ -59,11 +54,10 @@ def mass_lavatories( """ if aircraft_type == "long-haul": - return (1.11 * n_pax ** 1.33) * u.lbm + return (1.11 * n_pax**1.33) * u.lbm elif aircraft_type == "short-haul": - return (0.31 * n_pax ** 1.33) * u.lbm + return (0.31 * n_pax**1.33) * u.lbm elif aircraft_type == "business-jet": - return (3.90 * n_pax ** 1.33) * u.lbm + return (3.90 * n_pax**1.33) * u.lbm else: raise ValueError("Bad value of `aircraft_type`!") - diff --git a/aerosandbox/library/weights/torenbeek_weights.py b/aerosandbox/library/weights/torenbeek_weights.py index 2bd0934bc..c944a5a7e 100644 --- a/aerosandbox/library/weights/torenbeek_weights.py +++ b/aerosandbox/library/weights/torenbeek_weights.py @@ -7,15 +7,16 @@ # From Torenbeek: "Synthesis of Subsonic Airplane Design", 1976, Delft University Press # Chapter 8: "Airplane Weight and Balance" + def mass_wing_simple( - wing: asb.Wing, - design_mass_TOGW: float, - ultimate_load_factor: float, - suspended_mass: float, - main_gear_mounted_to_wing: bool = True, + wing: asb.Wing, + design_mass_TOGW: float, + ultimate_load_factor: float, + suspended_mass: float, + main_gear_mounted_to_wing: bool = True, ) -> float: """ - Computes the mass of a wing of an aircraft, according to Torenbeek's "Synthesis of Subsonic + Computes the mass of a wing of an aircraft, according to Torenbeek's "Synthesis of Subsonic Airplane Design". This is the simple version of the wing weight model, which is found in: @@ -41,32 +42,28 @@ def mass_wing_simple( """ - k_w = np.blend( - (design_mass_TOGW - 5670) / 2000, - 6.67e-3, - 4.90e-3 - ) + k_w = np.blend((design_mass_TOGW - 5670) / 2000, 6.67e-3, 4.90e-3) span = wing.span() / np.cosd(wing.mean_sweep_angle(x_nondim=0.5)) wing_root_thickness = wing.xsecs[0].airfoil.max_thickness() * wing.xsecs[0].chord return suspended_mass * ( - k_w * - span ** 0.75 * - (1 + (1.905 / span) ** 0.5) * - ultimate_load_factor ** 0.55 * - ((span / wing_root_thickness) / (suspended_mass / wing.area())) ** 0.30 * - (1 if main_gear_mounted_to_wing else 0.95) + k_w + * span**0.75 + * (1 + (1.905 / span) ** 0.5) + * ultimate_load_factor**0.55 + * ((span / wing_root_thickness) / (suspended_mass / wing.area())) ** 0.30 + * (1 if main_gear_mounted_to_wing else 0.95) ) def mass_wing_high_lift_devices( - wing: asb.Wing, - max_airspeed_for_flaps: float, - flap_deflection_angle: float = 30, - k_f1: float = 1.0, - k_f2: float = 1.0 + wing: asb.Wing, + max_airspeed_for_flaps: float, + flap_deflection_angle: float = 30, + k_f1: float = 1.0, + k_f2: float = 1.0, ) -> float: """ The function mass_high_lift() is designed to estimate the weight of the high-lift devices @@ -114,14 +111,16 @@ def mass_wing_high_lift_devices( k_f = k_f1 * k_f2 mass_trailing_edge_flaps = S_flaps * ( - 2.706 * k_f * - (S_flaps * span_structural) ** (3 / 16) * - ( - (max_airspeed_for_flaps / 100) ** 2 * - np.sind(flap_deflection_angle) * - np.cosd(wing.mean_sweep_angle(x_nondim=1)) / - root_t_over_c - ) ** (3 / 4) + 2.706 + * k_f + * (S_flaps * span_structural) ** (3 / 16) + * ( + (max_airspeed_for_flaps / 100) ** 2 + * np.sind(flap_deflection_angle) + * np.cosd(wing.mean_sweep_angle(x_nondim=1)) + / root_t_over_c + ) + ** (3 / 4) ) mass_leading_edge_devices = 0 @@ -132,18 +131,18 @@ def mass_wing_high_lift_devices( def mass_wing_basic_structure( - wing: asb.Wing, - design_mass_TOGW: float, - ultimate_load_factor: float, - suspended_mass: float, - never_exceed_airspeed: float, - main_gear_mounted_to_wing: bool = True, - strut_y_location: float = None, - k_e: float = 0.95, - return_dict: bool = False, + wing: asb.Wing, + design_mass_TOGW: float, + ultimate_load_factor: float, + suspended_mass: float, + never_exceed_airspeed: float, + main_gear_mounted_to_wing: bool = True, + strut_y_location: float = None, + k_e: float = 0.95, + return_dict: bool = False, ) -> Union[float, Dict[str, float]]: """ - Computes the mass of the basic structure of the wing of an aircraft, according to + Computes the mass of the basic structure of the wing of an aircraft, according to Torenbeek's "Synthesis of Subsonic Airplane Design", 1976, Appendix C: "Prediction of Wing Structural Weight". This is the basic wing structure without movables like spoilers, high-lift devices, etc. @@ -158,7 +157,7 @@ def mass_wing_basic_structure( ultimate_load_factor: The ultimate load factor of the aircraft [-]. 1.5x the limit load factor. - suspended_mass: The mass of the aircraft that is suspended from the wing [kg]. It should exclude + suspended_mass: The mass of the aircraft that is suspended from the wing [kg]. It should exclude any wing attachments that are not part of the wing structure. never_exceed_airspeed: The never-exceed airspeed of the aircraft [m/s]. Used for flutter calculations. @@ -171,7 +170,7 @@ def mass_wing_basic_structure( k_e: represents weight knockdowns due to bending moment relief from engines mounted in front of elastic axis. see Torenbeek unlabeled equations, between C-3 and C-4. k_e = 1.0 if engines are not wing mounted, - k_e = 0.95 (default) two wing mounted engines in front of the elastic axis and + k_e = 0.95 (default) two wing mounted engines in front of the elastic axis and k_e = 0.90 four wing-mounted engines in front of the elastic axis return_dict: Whether to return a dictionary of all the intermediate values, or just the final mass. Defaults @@ -209,14 +208,11 @@ def mass_wing_basic_structure( # Torenbeek Eq. C-4 # `k_st` represents weight excrescence due to structural stiffness against flutter. k_st = ( - 1 + - 9.06e-4 * ( - (span * np.cosd(wing.mean_sweep_angle(x_nondim=0))) ** 3 / - design_mass_TOGW - ) * ( - never_exceed_airspeed / 100 / root_t_over_c - ) ** 2 * - cos_sweep_half_chord + 1 + + 9.06e-4 + * ((span * np.cosd(wing.mean_sweep_angle(x_nondim=0))) ** 3 / design_mass_TOGW) + * (never_exceed_airspeed / 100 / root_t_over_c) ** 2 + * cos_sweep_half_chord ) # Torenbeek Eq. C-5 @@ -228,18 +224,17 @@ def mass_wing_basic_structure( ### Use all the above to compute the basic wing structural mass mass_wing_basic = ( - 4.58e-3 * - k_no * - k_lambda * - k_e * - k_uc * - k_st * - ( - k_b * ultimate_load_factor * (0.8 * suspended_mass + 0.2 * design_mass_TOGW) - ) ** 0.55 * - span ** 1.675 * - root_t_over_c ** -0.45 * - cos_sweep_half_chord ** -1.325 + 4.58e-3 + * k_no + * k_lambda + * k_e + * k_uc + * k_st + * (k_b * ultimate_load_factor * (0.8 * suspended_mass + 0.2 * design_mass_TOGW)) + ** 0.55 + * span**1.675 + * root_t_over_c**-0.45 + * cos_sweep_half_chord**-1.325 ) if return_dict: @@ -248,10 +243,7 @@ def mass_wing_basic_structure( return mass_wing_basic -def mass_wing_spoilers_and_speedbrakes( - wing: asb.Wing, - mass_basic_wing: float -) -> float: +def mass_wing_spoilers_and_speedbrakes(wing: asb.Wing, mass_basic_wing: float) -> float: """ The function mass_spoilers_and_speedbrakes() estimates the weight of the spoilers and speedbrakes according to Torenbeek's "Synthesis of Subsonic Airplane Design", 1976, Appendix C: "Prediction @@ -282,16 +274,16 @@ def mass_wing_spoilers_and_speedbrakes( def mass_wing( - wing: asb.Wing, - design_mass_TOGW: float, - ultimate_load_factor: float, - suspended_mass: float, - never_exceed_airspeed: float, - max_airspeed_for_flaps: float, - main_gear_mounted_to_wing: bool = True, - flap_deflection_angle: float = 30, - strut_y_location: float = None, - return_dict: bool = False, + wing: asb.Wing, + design_mass_TOGW: float, + ultimate_load_factor: float, + suspended_mass: float, + never_exceed_airspeed: float, + max_airspeed_for_flaps: float, + main_gear_mounted_to_wing: bool = True, + flap_deflection_angle: float = 30, + strut_y_location: float = None, + return_dict: bool = False, ) -> Union[float, Dict[str, float]]: """ Computes the mass of a wing of an aircraft, according to Torenbeek's "Synthesis of Subsonic Airplane Design", @@ -347,13 +339,11 @@ def mass_wing( ) # spoilers and speedbrake mass estimation mass_spoilers_speedbrakes = mass_wing_spoilers_and_speedbrakes( - wing=wing, - mass_basic_wing=mass_basic_wing + wing=wing, mass_basic_wing=mass_basic_wing ) - mass_wing_total = ( - mass_basic_wing + - 1.2 * (mass_high_lift_devices + mass_spoilers_speedbrakes) + mass_wing_total = mass_basic_wing + 1.2 * ( + mass_high_lift_devices + mass_spoilers_speedbrakes ) if return_dict: @@ -372,10 +362,11 @@ def mass_wing( # # k_wt = 0.64 + def mass_fuselage_simple( - fuselage: asb.Fuselage, - never_exceed_airspeed: float, - wing_to_tail_distance: float, + fuselage: asb.Fuselage, + never_exceed_airspeed: float, + wing_to_tail_distance: float, ): """ Computes the mass of the fuselage, using Torenbeek's simple version of the calculation. @@ -398,43 +389,28 @@ def mass_fuselage_simple( Returns: The mass of the fuselage, in kg. """ - widths = [ - xsec.width - for xsec in fuselage.xsecs - ] - - max_width = np.softmax( - *widths, - softness=np.mean(np.array(widths)) * 0.01 - ) + widths = [xsec.width for xsec in fuselage.xsecs] - heights = [ - xsec.height - for xsec in fuselage.xsecs - ] + max_width = np.softmax(*widths, softness=np.mean(np.array(widths)) * 0.01) - max_height = np.softmax( - *heights, - softness=np.mean(np.array(heights)) * 0.01 - ) + heights = [xsec.height for xsec in fuselage.xsecs] + + max_height = np.softmax(*heights, softness=np.mean(np.array(heights)) * 0.01) return ( - 0.23 * - ( - never_exceed_airspeed * - wing_to_tail_distance / - (max_width + max_height) - ) ** 0.5 * - fuselage.area_wetted() ** 1.2 + 0.23 + * (never_exceed_airspeed * wing_to_tail_distance / (max_width + max_height)) + ** 0.5 + * fuselage.area_wetted() ** 1.2 ) def mass_fuselage( - fuselage: asb.Fuselage, - design_mass_TOGW: float, - ultimate_load_factor: float, - never_exceed_airspeed: float, - wing_to_tail_distance: float, + fuselage: asb.Fuselage, + design_mass_TOGW: float, + ultimate_load_factor: float, + never_exceed_airspeed: float, + wing_to_tail_distance: float, ): # TODO Torenbeek Appendix D (PDF page 477) @@ -444,21 +420,21 @@ def mass_fuselage( # Torenbeek Eq. D-3 fuselage.fineness_ratio() - fuselage_quasi_slenderness_ratio = fuselage.fineness_ratio(assumed_shape="sears_haack") - - k_lambda = np.softmin( - 0.56 * fuselage.fineness_ratio(assumed_shape="sears_haack") + fuselage_quasi_slenderness_ratio = fuselage.fineness_ratio( + assumed_shape="sears_haack" ) - W_sk = 0.05428 * k_lambda * S_g ** 1.07 * never_exceed_airspeed ** 0.743 + k_lambda = np.softmin(0.56 * fuselage.fineness_ratio(assumed_shape="sears_haack")) + + W_sk = 0.05428 * k_lambda * S_g**1.07 * never_exceed_airspeed**0.743 W_g = W_sk + W_str + W_fr def mass_propeller( - propeller_diameter: float, - propeller_power: float, - n_blades: int, + propeller_diameter: float, + propeller_power: float, + n_blades: int, ) -> float: """ Computes the mass of a propeller. @@ -478,10 +454,7 @@ def mass_propeller( """ return ( - 0.108 * - n_blades * - ( - (propeller_diameter / u.foot) * - (propeller_power / u.horsepower) - ) ** 0.78174 + 0.108 + * n_blades + * ((propeller_diameter / u.foot) * (propeller_power / u.horsepower)) ** 0.78174 ) * u.lbm diff --git a/aerosandbox/library/winds.py b/aerosandbox/library/winds.py index aee6bfcd1..c5ed8f26a 100644 --- a/aerosandbox/library/winds.py +++ b/aerosandbox/library/winds.py @@ -35,10 +35,23 @@ def wind_speed_conus_summer_99(altitude, latitude): lgs = 0.9805766577269118 lqc = 4.0356834595743214 - s = c0 + cql * (l - lqc) ** 2 + cqa * (a - aqc) ** 2 + cqla * a * l + cg * np.exp( - -(np.fabs(l - lgc) ** lgh / (2 * lgs ** 2) + np.fabs(a - agc) ** agh / ( - 2 * ags ** 2) + cgc * a * l)) + c4a * ( - a - c4c) ** 4 + c12 * l * a ** 2 + c21 * l ** 2 * a + s = ( + c0 + + cql * (l - lqc) ** 2 + + cqa * (a - aqc) ** 2 + + cqla * a * l + + cg + * np.exp( + -( + np.fabs(l - lgc) ** lgh / (2 * lgs**2) + + np.fabs(a - agc) ** agh / (2 * ags**2) + + cgc * a * l + ) + ) + + c4a * (a - c4c) ** 4 + + c12 * l * a**2 + + c21 * l**2 * a + ) speed = s * 56 + 7 return speed @@ -47,11 +60,22 @@ def wind_speed_conus_summer_99(altitude, latitude): ### Prep data for global wind speed function # Import data root = Path(os.path.abspath(__file__)).parent -altitudes_world = np.load(root / "datasets" / "winds_and_tropopause_global" / "altitudes.npy") -latitudes_world = np.load(root / "datasets" / "winds_and_tropopause_global" / "latitudes.npy") +altitudes_world = np.load( + root / "datasets" / "winds_and_tropopause_global" / "altitudes.npy" +) +latitudes_world = np.load( + root / "datasets" / "winds_and_tropopause_global" / "latitudes.npy" +) day_of_year_world_boundaries = np.linspace(0, 365, 13) -day_of_year_world = (day_of_year_world_boundaries[1:] + day_of_year_world_boundaries[:-1]) / 2 -winds_95_world = np.load(root / "datasets" / "winds_and_tropopause_global" / "winds_95_vs_altitude_latitude_day.npy") +day_of_year_world = ( + day_of_year_world_boundaries[1:] + day_of_year_world_boundaries[:-1] +) / 2 +winds_95_world = np.load( + root + / "datasets" + / "winds_and_tropopause_global" + / "winds_95_vs_altitude_latitude_day.npy" +) # Trim the poles latitudes_world = latitudes_world[1:-1] @@ -85,33 +109,33 @@ def wind_speed_conus_summer_99(altitude, latitude): # Extend boundaries so that cubic spline interpolates around day_of_year appropriately. extend_bounds = 3 -day_of_year_world = np.hstack(( - day_of_year_world[-extend_bounds:] - 365, - day_of_year_world, - day_of_year_world[:extend_bounds] + 365 -)) -winds_95_world = np.dstack(( - winds_95_world[:, :, -extend_bounds:], - winds_95_world, - winds_95_world[:, :, :extend_bounds] -)) +day_of_year_world = np.hstack( + ( + day_of_year_world[-extend_bounds:] - 365, + day_of_year_world, + day_of_year_world[:extend_bounds] + 365, + ) +) +winds_95_world = np.dstack( + ( + winds_95_world[:, :, -extend_bounds:], + winds_95_world, + winds_95_world[:, :, :extend_bounds], + ) +) # Make the model winds_95_world_model = InterpolatedModel( x_data_coordinates={ - "altitude" : altitudes_world, - "latitude" : latitudes_world, + "altitude": altitudes_world, + "latitude": latitudes_world, "day of year": day_of_year_world, }, y_data_structured=winds_95_world, ) -def wind_speed_world_95( - altitude, - latitude, - day_of_year -): +def wind_speed_world_95(altitude, latitude, day_of_year): """ Gives the 95th-percentile wind speed as a function of altitude, latitude, and day of year. Args: @@ -123,50 +147,48 @@ def wind_speed_world_95( """ - return winds_95_world_model({ - "altitude" : altitude, - "latitude" : latitude, - "day of year": day_of_year - }) + return winds_95_world_model( + {"altitude": altitude, "latitude": latitude, "day of year": day_of_year} + ) ### Prep data for tropopause altitude function # Import data latitudes_trop = np.linspace(-80, 80, 50) day_of_year_trop_boundaries = np.linspace(0, 365, 13) -day_of_year_trop = (day_of_year_trop_boundaries[1:] + day_of_year_trop_boundaries[:-1]) / 2 +day_of_year_trop = ( + day_of_year_trop_boundaries[1:] + day_of_year_trop_boundaries[:-1] +) / 2 tropopause_altitude_km = np.genfromtxt( root / "datasets" / "winds_and_tropopause_global" / "strat-height-monthly.csv", - delimiter="," + delimiter=",", ) # Extend boundaries extend_bounds = 3 -day_of_year_trop = np.hstack(( - day_of_year_trop[-extend_bounds:] - 365, - day_of_year_trop, - day_of_year_trop[:extend_bounds] + 365 -)) -tropopause_altitude_km = np.hstack(( - tropopause_altitude_km[:, -extend_bounds:], - tropopause_altitude_km, - tropopause_altitude_km[:, :extend_bounds] -)) +day_of_year_trop = np.hstack( + ( + day_of_year_trop[-extend_bounds:] - 365, + day_of_year_trop, + day_of_year_trop[:extend_bounds] + 365, + ) +) +tropopause_altitude_km = np.hstack( + ( + tropopause_altitude_km[:, -extend_bounds:], + tropopause_altitude_km, + tropopause_altitude_km[:, :extend_bounds], + ) +) # Make the model tropopause_altitude_model = InterpolatedModel( - x_data_coordinates={ - "latitude" : latitudes_trop, - "day of year": day_of_year_trop - }, - y_data_structured=tropopause_altitude_km * 1e3 + x_data_coordinates={"latitude": latitudes_trop, "day of year": day_of_year_trop}, + y_data_structured=tropopause_altitude_km * 1e3, ) -def tropopause_altitude( - latitude, - day_of_year -): +def tropopause_altitude(latitude, day_of_year): """ Gives the altitude of the tropopause (as determined by the altitude where lapse rate >= 2 C/km) as a function of latitude and day of year. @@ -178,17 +200,13 @@ def tropopause_altitude( Returns: The tropopause altitude, in meters. """ - return tropopause_altitude_model({ - "latitude" : latitude, - "day of year": day_of_year - }) + return tropopause_altitude_model({"latitude": latitude, "day of year": day_of_year}) -if __name__ == '__main__': +if __name__ == "__main__": from aerosandbox.tools.pretty_plots import plt, sns, mpl, show_plot - def plot_winds_at_altitude(altitude=18000): fig, ax = plt.subplots() @@ -202,15 +220,13 @@ def plot_winds_at_altitude(altitude=18000): day_of_year=Day_of_years.flatten(), ).reshape(Latitudes.shape) - args = [ - day_of_years, - latitudes, - winds - ] + args = [day_of_years, latitudes, winds] levels = np.arange(0, 80.1, 5) CS = plt.contour(*args, levels=levels, linewidths=0.5, colors="k", alpha=0.7) - CF = plt.contourf(*args, levels=levels, cmap='viridis_r', alpha=0.7, extend="max") + CF = plt.contourf( + *args, levels=levels, cmap="viridis_r", alpha=0.7, extend="max" + ) cbar = plt.colorbar(label="Wind Speed [m/s]", extendrect=True) ax.clabel(CS, inline=1, fontsize=9, fmt="%.0f m/s") @@ -228,9 +244,9 @@ def plot_winds_at_altitude(altitude=18000): "Sep. 1", "Oct. 1", "Nov. 1", - "Dec. 1" + "Dec. 1", ), - rotation=40 + rotation=40, ) lat_label_vals = np.arange(-80, 80.1, 20) @@ -240,10 +256,7 @@ def plot_winds_at_altitude(altitude=18000): lat_labels.append(f"{lat:.0f}N") else: lat_labels.append(f"{-lat:.0f}S") - plt.yticks( - lat_label_vals, - lat_labels - ) + plt.yticks(lat_label_vals, lat_labels) show_plot( f"95th-Percentile Wind Speeds at {altitude / 1e3:.0f} km Altitude", @@ -251,7 +264,6 @@ def plot_winds_at_altitude(altitude=18000): ylabel="Latitude", ) - def plot_winds_at_day(day_of_year=0): fig, ax = plt.subplots() @@ -265,15 +277,13 @@ def plot_winds_at_day(day_of_year=0): day_of_year=day_of_year * np.ones_like(Altitudes.flatten()), ).reshape(Altitudes.shape) - args = [ - altitudes / 1e3, - latitudes, - winds - ] + args = [altitudes / 1e3, latitudes, winds] levels = np.arange(0, 80.1, 5) CS = plt.contour(*args, levels=levels, linewidths=0.5, colors="k", alpha=0.7) - CF = plt.contourf(*args, levels=levels, cmap='viridis_r', alpha=0.7, extend="max") + CF = plt.contourf( + *args, levels=levels, cmap="viridis_r", alpha=0.7, extend="max" + ) cbar = plt.colorbar(label="Wind Speed [m/s]", extendrect=True) ax.clabel(CS, inline=1, fontsize=9, fmt="%.0f m/s") @@ -284,10 +294,7 @@ def plot_winds_at_day(day_of_year=0): lat_labels.append(f"{lat:.0f}N") else: lat_labels.append(f"{-lat:.0f}S") - plt.yticks( - lat_label_vals, - lat_labels - ) + plt.yticks(lat_label_vals, lat_labels) show_plot( f"95th-Percentile Wind Speeds at Day {day_of_year:.0f}", @@ -295,7 +302,6 @@ def plot_winds_at_day(day_of_year=0): ylabel="Latitude", ) - def plot_tropopause_altitude(): fig, ax = plt.subplots() @@ -304,19 +310,16 @@ def plot_tropopause_altitude(): Day_of_years, Latitudes = np.meshgrid(day_of_years, latitudes) trop_alt = tropopause_altitude( - Latitudes.flatten(), - Day_of_years.flatten() + Latitudes.flatten(), Day_of_years.flatten() ).reshape(Latitudes.shape) - args = [ - day_of_years, - latitudes, - trop_alt / 1e3 - ] + args = [day_of_years, latitudes, trop_alt / 1e3] levels = np.arange(10, 20.1, 1) CS = plt.contour(*args, levels=levels, linewidths=0.5, colors="k", alpha=0.7) - CF = plt.contourf(*args, levels=levels, cmap='viridis_r', alpha=0.7, extend="both") + CF = plt.contourf( + *args, levels=levels, cmap="viridis_r", alpha=0.7, extend="both" + ) cbar = plt.colorbar(label="Tropopause Altitude [km]", extendrect=True) ax.clabel(CS, inline=1, fontsize=9, fmt="%.0f km") @@ -334,9 +337,9 @@ def plot_tropopause_altitude(): "Sep. 1", "Oct. 1", "Nov. 1", - "Dec. 1" + "Dec. 1", ), - rotation=40 + rotation=40, ) lat_label_vals = np.arange(-80, 80.1, 20) @@ -346,10 +349,7 @@ def plot_tropopause_altitude(): lat_labels.append(f"{lat:.0f}N") else: lat_labels.append(f"{-lat:.0f}S") - plt.yticks( - lat_label_vals, - lat_labels - ) + plt.yticks(lat_label_vals, lat_labels) show_plot( f"Tropopause Altitude by Season and Latitude", @@ -357,7 +357,6 @@ def plot_tropopause_altitude(): ylabel="Latitude", ) - def plot_winds_at_tropopause_altitude(): fig, ax = plt.subplots() @@ -371,15 +370,13 @@ def plot_winds_at_tropopause_altitude(): day_of_year=Day_of_years.flatten(), ).reshape(Latitudes.shape) - args = [ - day_of_years, - latitudes, - winds - ] + args = [day_of_years, latitudes, winds] levels = np.arange(0, 80.1, 5) CS = plt.contour(*args, levels=levels, linewidths=0.5, colors="k", alpha=0.7) - CF = plt.contourf(*args, levels=levels, cmap='viridis_r', alpha=0.7, extend="max") + CF = plt.contourf( + *args, levels=levels, cmap="viridis_r", alpha=0.7, extend="max" + ) cbar = plt.colorbar(label="Wind Speed [m/s]", extendrect=True) ax.clabel(CS, inline=1, fontsize=9, fmt="%.0f m/s") @@ -397,9 +394,9 @@ def plot_winds_at_tropopause_altitude(): "Sep. 1", "Oct. 1", "Nov. 1", - "Dec. 1" + "Dec. 1", ), - rotation=40 + rotation=40, ) lat_label_vals = np.arange(-80, 80.1, 20) @@ -409,10 +406,7 @@ def plot_winds_at_tropopause_altitude(): lat_labels.append(f"{lat:.0f}N") else: lat_labels.append(f"{-lat:.0f}S") - plt.yticks( - lat_label_vals, - lat_labels - ) + plt.yticks(lat_label_vals, lat_labels) show_plot( f"95th-Percentile Wind Speeds at Tropopause Altitude", @@ -420,7 +414,6 @@ def plot_winds_at_tropopause_altitude(): ylabel="Latitude", ) - # plot_winds_at_altitude(altitude=18000) # plot_winds_at_day(day_of_year=0) # plot_tropopause_altitude() diff --git a/aerosandbox/modeling/__init__.py b/aerosandbox/modeling/__init__.py index ee965a24e..ae6554db0 100644 --- a/aerosandbox/modeling/__init__.py +++ b/aerosandbox/modeling/__init__.py @@ -1,4 +1,6 @@ from aerosandbox.modeling.fitting import FittedModel from aerosandbox.modeling.interpolation import InterpolatedModel -from aerosandbox.modeling.interpolation_unstructured import UnstructuredInterpolatedModel +from aerosandbox.modeling.interpolation_unstructured import ( + UnstructuredInterpolatedModel, +) from aerosandbox.modeling.black_box import black_box diff --git a/aerosandbox/modeling/black_box.py b/aerosandbox/modeling/black_box.py index 9c718931f..36f67e458 100644 --- a/aerosandbox/modeling/black_box.py +++ b/aerosandbox/modeling/black_box.py @@ -3,12 +3,12 @@ def black_box( - function: Callable[[Any], float], - n_in: int = None, - n_out: int = 1, - fd_method: str ='central', - fd_step: Optional[float] = None, - fd_step_iter: Optional[bool] = None, + function: Callable[[Any], float], + n_in: int = None, + n_out: int = 1, + fd_method: str = "central", + fd_step: Optional[float] = None, + fd_step_iter: Optional[bool] = None, ) -> Callable[[Any], float]: """ Wraps a function as a black box, allowing it to be used in AeroSandbox / CasADi optimization problems. @@ -44,22 +44,24 @@ def black_box( ### Add limitations if n_out > 1: - raise NotImplementedError("Black boxes with multiple outputs are not yet supported.") + raise NotImplementedError( + "Black boxes with multiple outputs are not yet supported." + ) ### Compute finite-differencing options fd_options = {} if fd_step is not None: - fd_options['h'] = fd_step + fd_options["h"] = fd_step if fd_step_iter is not None: - fd_options['h_iter'] = fd_step_iter + fd_options["h_iter"] = fd_step_iter import casadi as cas class BlackBox(cas.Callback): def __init__( - self, + self, ): cas.Callback.__init__(self) self.construct( @@ -68,7 +70,7 @@ def __init__( enable_fd=True, fd_method=fd_method, fd_options=fd_options, - ) + ), ) # Number of inputs and outputs @@ -106,7 +108,9 @@ def wrapped_function_with_kwargs_support(*args, **kwargs): inputs = [] # Check number of positional arguments in the signature - n_positional_args = len(signature.parameters) - len(signature.parameters.values()) + n_positional_args = len(signature.parameters) - len( + signature.parameters.values() + ) n_args = len(signature.parameters) if len(args) < n_positional_args or len(args) > n_args: raise TypeError( @@ -127,9 +131,7 @@ def wrapped_function_with_kwargs_support(*args, **kwargs): else: if parameter.default is parameter.empty: - raise TypeError( - f"Missing required argument '{name}'" - ) + raise TypeError(f"Missing required argument '{name}'") else: input = parameter.default @@ -145,21 +147,19 @@ def wrapped_function_with_kwargs_support(*args, **kwargs): return wrapped_function_with_kwargs_support -if __name__ == '__main__': +if __name__ == "__main__": ### Create a function that's effectively black-box (doesn't use `aerosandbox.numpy`) def my_func( - a1, - a2, - k1=4, - k2=5, - k3=6, + a1, + a2, + k1=4, + k2=5, + k3=6, ): import math - return ( - math.sin(a1) * math.exp(a2) * math.cos(k1 * k2) + k3 - ) + return math.sin(a1) * math.exp(a2) * math.cos(k1 * k2) + k3 ### Now, start an optimization problem import aerosandbox as asb @@ -168,18 +168,14 @@ def my_func( opti = asb.Opti() # Wrap our function such that it can be used in an optimization problem. - my_func_wrapped = black_box( - function=my_func, fd_method="central" - ) + my_func_wrapped = black_box(function=my_func, fd_method="central") # Pick some variables to optimize over m = opti.variable(init_guess=5, lower_bound=3, upper_bound=8) n = opti.variable(init_guess=5, lower_bound=3, upper_bound=8) # Minimize the black-box function - opti.minimize( - my_func_wrapped(m, a2=3, k2=n) - ) + opti.minimize(my_func_wrapped(m, a2=3, k2=n)) # Solve sol = opti.solve() @@ -194,6 +190,8 @@ def my_func( ) fig, ax = plt.subplots() p.contour( - M, N, np.vectorize(my_func)(M, a2=3, k2=N), + M, + N, + np.vectorize(my_func)(M, a2=3, k2=N), ) p.show_plot() diff --git a/aerosandbox/modeling/fitting.py b/aerosandbox/modeling/fitting.py index 070d01040..bef2fd0b9 100644 --- a/aerosandbox/modeling/fitting.py +++ b/aerosandbox/modeling/fitting.py @@ -33,24 +33,21 @@ class FittedModel(SurrogateModel): """ - def __init__(self, - model: Callable[ - [ - Union[np.ndarray, Dict[str, np.ndarray]], - Dict[str, float] - ], - np.ndarray - ], - x_data: Union[np.ndarray, Dict[str, np.ndarray]], - y_data: np.ndarray, - parameter_guesses: Dict[str, float], - parameter_bounds: Dict[str, tuple] = None, - residual_norm_type: str = "L2", - fit_type: str = "best", - weights: np.ndarray = None, - put_residuals_in_logspace: bool = False, - verbose=True, - ): + def __init__( + self, + model: Callable[ + [Union[np.ndarray, Dict[str, np.ndarray]], Dict[str, float]], np.ndarray + ], + x_data: Union[np.ndarray, Dict[str, np.ndarray]], + y_data: np.ndarray, + parameter_guesses: Dict[str, float], + parameter_bounds: Dict[str, tuple] = None, + residual_norm_type: str = "L2", + fit_type: str = "best", + weights: np.ndarray = None, + put_residuals_in_logspace: bool = False, + verbose=True, + ): """ Fits an analytical model to n-dimensional unstructured data using an automatic-differentiable optimization approach. @@ -147,12 +144,11 @@ def flatten(input): return np.array(input).flatten() try: - x_data = { - k: flatten(v) - for k, v in x_data.items() - } + x_data = {k: flatten(v) for k, v in x_data.items()} x_data_is_dict = True - except AttributeError: # If it's not a dict or dict-like, assume it's a 1D ndarray dataset + except ( + AttributeError + ): # If it's not a dict or dict-like, assume it's a 1D ndarray dataset x_data = flatten(x_data) x_data_is_dict = False y_data = flatten(y_data) @@ -167,7 +163,9 @@ def flatten(input): if sum_weights <= 0: raise ValueError("The weights must sum to a positive number!") if np.any(weights < 0): - raise ValueError("No entries of the weights vector are allowed to be negative!") + raise ValueError( + "No entries of the weights vector are allowed to be negative!" + ) weights = weights / np.sum(weights) # Normalize weights so that they sum to 1. ### Check format of parameter_bounds input @@ -176,20 +174,24 @@ def flatten(input): for param_name, v in parameter_bounds.items(): if param_name not in parameter_guesses.keys(): raise ValueError( - f"A parameter name (key = \"{param_name}\") in parameter_bounds was not found in parameter_guesses.") + f'A parameter name (key = "{param_name}") in parameter_bounds was not found in parameter_guesses.' + ) if not np.length(v) == 2: raise ValueError( "Every value in parameter_bounds must be a tuple in the format (lower_bound, upper_bound). " - "For one-sided bounds, use None for the unbounded side.") + "For one-sided bounds, use None for the unbounded side." + ) ### If putting residuals in logspace, check positivity if put_residuals_in_logspace: if not np.all(y_data > 0): - raise ValueError("You can't fit a model with residuals in logspace if y_data is not entirely positive!") + raise ValueError( + "You can't fit a model with residuals in logspace if y_data is not entirely positive!" + ) ### Check dimensionality of inputs to fitting algorithm relevant_inputs = { - "y_data" : y_data, + "y_data": y_data, "weights": weights, } try: @@ -202,7 +204,8 @@ def flatten(input): series_length = np.length(value) if not series_length == n_datapoints: raise ValueError( - f"The supplied data series \"{key}\" has length {series_length}, but y_data has length {n_datapoints}.") + f'The supplied data series "{key}" has length {series_length}, but y_data has length {n_datapoints}.' + ) ##### Formulate and solve the fitting optimization problem @@ -225,58 +228,75 @@ def flatten(input): ### Evaluate the model at the data points you're trying to fit x_data_original = copy.deepcopy( - x_data) # Make a copy of x_data so that you can determine if the model did in-place operations on x and tattle on the user. + x_data + ) # Make a copy of x_data so that you can determine if the model did in-place operations on x and tattle on the user. try: y_model = model(x_data, params) # Evaluate the model except Exception: - raise Exception(""" + raise Exception( + """ There was an error when evaluating the model you supplied with the x_data you supplied. Likely possible causes: * Your model() does not have the call syntax model(x, p), where x is the x_data and p are parameters. * Your model should take in p as a dict of parameters, but it does not. * Your model assumes x is an array-like but you provided x_data as a dict, or vice versa. See the docstring of FittedModel() if you have other usage questions or would like to see examples. - """) + """ + ) try: ### If the model did in-place operations on x_data, throw an error x_data_is_unchanged = np.all(x_data == x_data_original) except ValueError: - x_data_is_unchanged = np.all([ - x_series == x_series_original - for x_series, x_series_original in zip(x_data, x_data_original) - ]) + x_data_is_unchanged = np.all( + [ + x_series == x_series_original + for x_series, x_series_original in zip(x_data, x_data_original) + ] + ) if not x_data_is_unchanged: - raise TypeError("model(x_data, parameter_guesses) did in-place operations on x, which is not allowed!") - if y_model is None: # Make sure that y_model actually returned something sensible - raise TypeError("model(x_data, parameter_guesses) returned None, when it should've returned a 1D ndarray.") + raise TypeError( + "model(x_data, parameter_guesses) did in-place operations on x, which is not allowed!" + ) + if ( + y_model is None + ): # Make sure that y_model actually returned something sensible + raise TypeError( + "model(x_data, parameter_guesses) returned None, when it should've returned a 1D ndarray." + ) ### Compute how far off you are (error) if not put_residuals_in_logspace: error = y_model - y_data else: - y_model = np.fmax(y_model, 1e-300) # Keep y_model very slightly always positive, so that log() doesn't NaN. + y_model = np.fmax( + y_model, 1e-300 + ) # Keep y_model very slightly always positive, so that log() doesn't NaN. error = np.log(y_model) - np.log(y_data) ### Set up the optimization problem to minimize some norm(error), which looks different depending on the norm used: if residual_norm_type.lower() == "l1": # Minimize the L1 norm - abs_error = opti.variable(init_guess=0, - n_vars=np.length(y_data)) # Make the abs() of each error entry an opt. var. - opti.subject_to([ - abs_error >= error, - abs_error >= -error, - ]) + abs_error = opti.variable( + init_guess=0, n_vars=np.length(y_data) + ) # Make the abs() of each error entry an opt. var. + opti.subject_to( + [ + abs_error >= error, + abs_error >= -error, + ] + ) opti.minimize(np.sum(weights * abs_error)) elif residual_norm_type.lower() == "l2": # Minimize the L2 norm - opti.minimize(np.sum(weights * error ** 2)) + opti.minimize(np.sum(weights * error**2)) elif residual_norm_type.lower() == "linf": # Minimize the L-infinity norm - linf_value = opti.variable(init_guess=0) # Make the value of the L-infinity norm an optimization variable - opti.subject_to([ - linf_value >= weights * error, - linf_value >= -weights * error - ]) + linf_value = opti.variable( + init_guess=0 + ) # Make the value of the L-infinity norm an optimization variable + opti.subject_to( + [linf_value >= weights * error, linf_value >= -weights * error] + ) opti.minimize(linf_value) else: @@ -321,9 +341,7 @@ def __call__(self, x): super().__call__(x) return self.model(x, self.parameters) - def goodness_of_fit(self, - type="R^2" - ): + def goodness_of_fit(self, type="R^2"): """ Returns a metric of the goodness of the fit. @@ -349,15 +367,11 @@ def goodness_of_fit(self, y_mean = np.mean(self.y_data) - SS_tot = np.sum( - (self.y_data - y_mean) ** 2 - ) + 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 - ) + SS_res = np.sum((self.y_data - y_model) ** 2) R_squared = 1 - SS_res / SS_tot @@ -375,14 +389,21 @@ def goodness_of_fit(self, else: valid_types = [ "R^2", - "mean_absolute_error", "mae", "L1", - "root_mean_squared_error", "rms", "L2", - "max_absolute_error", "Linf" + "mean_absolute_error", + "mae", + "L1", + "root_mean_squared_error", + "rms", + "L2", + "max_absolute_error", + "Linf", ] valid_types_formatted = [ - f" * \"{valid_type}\"" - for valid_type in valid_types + f' * "{valid_type}"' for valid_type in valid_types ] - raise ValueError("Bad value of `type`! Valid values are:\n" + "\n".join(valid_types_formatted)) + raise ValueError( + "Bad value of `type`! Valid values are:\n" + + "\n".join(valid_types_formatted) + ) diff --git a/aerosandbox/modeling/interpolation.py b/aerosandbox/modeling/interpolation.py index 55f89b947..5cc521097 100644 --- a/aerosandbox/modeling/interpolation.py +++ b/aerosandbox/modeling/interpolation.py @@ -29,12 +29,13 @@ class InterpolatedModel(SurrogateModel): """ - def __init__(self, - x_data_coordinates: Union[np.ndarray, Dict[str, np.ndarray]], - y_data_structured: np.ndarray, - method: str = "bspline", - fill_value=np.nan, # Default behavior: return NaN for all inputs outside data range. - ): + def __init__( + self, + x_data_coordinates: Union[np.ndarray, Dict[str, np.ndarray]], + y_data_structured: np.ndarray, + method: str = "bspline", + fill_value=np.nan, # Default behavior: return NaN for all inputs outside data range. + ): """ Create the interpolator. Note that data must be structured (i.e., gridded on a hypercube) for general N-dimensional interpolation. @@ -91,16 +92,22 @@ def __init__(self, ### Validate inputs for coordinates in x_data_coordinates_values: if len(coordinates.shape) != 1: - raise ValueError(""" + raise ValueError( + """ `x_data_coordinates` must be either: * In the general N-dimensional case, a dict where values are 1D ndarrays defining the coordinates of each axis. * In the 1D case, can also be a 1D ndarray. - """) - implied_y_data_shape = tuple(len(coordinates) for coordinates in x_data_coordinates_values) + """ + ) + implied_y_data_shape = tuple( + len(coordinates) for coordinates in x_data_coordinates_values + ) if not y_data_structured.shape == implied_y_data_shape: - raise ValueError(f""" + raise ValueError( + f""" The shape of `y_data_structured` should be {implied_y_data_shape} - """) + """ + ) ### Store data self.x_data_coordinates = x_data_coordinates @@ -122,6 +129,7 @@ def __init__(self, def __call__(self, x): if isinstance(self.x_data_coordinates, dict): + def get_shape(value): if np.is_casadi_type(value, recursive=False): if value.shape[1] == 1: @@ -132,24 +140,25 @@ def get_shape(value): except AttributeError: return tuple() - shape = np.broadcast_shapes( - *[get_shape(v) for v in x.values()] - ) + shape = np.broadcast_shapes(*[get_shape(v) for v in x.values()]) shape_for_reshaping = (int(np.prod(shape)),) def reshape(value): try: return np.reshape(value, shape_for_reshaping) except ValueError: - if isinstance(value, int) or isinstance(value, float) or value.shape == tuple() or np.prod( - value.shape) == 1: + if ( + isinstance(value, int) + or isinstance(value, float) + or value.shape == tuple() + or np.prod(value.shape) == 1 + ): return value * np.ones(shape_for_reshaping) raise ValueError("Could not reshape value of one of the inputs!") - x = np.stack(tuple( - reshape(x[k]) - for k, v in self.x_data_coordinates.items() - ), axis=1) + x = np.stack( + tuple(reshape(x[k]) for k, v in self.x_data_coordinates.items()), axis=1 + ) output = np.interpn( points=self.x_data_coordinates_values, @@ -157,7 +166,7 @@ def reshape(value): xi=x, method=self.method, bounds_error=False, # Can't be set true if general MX-type inputs are to be expected. - fill_value=self.fill_value + fill_value=self.fill_value, ) try: return np.reshape(output, shape) diff --git a/aerosandbox/modeling/interpolation_unstructured.py b/aerosandbox/modeling/interpolation_unstructured.py index 0b03fe8f3..35b6ada7a 100644 --- a/aerosandbox/modeling/interpolation_unstructured.py +++ b/aerosandbox/modeling/interpolation_unstructured.py @@ -25,15 +25,16 @@ class UnstructuredInterpolatedModel(InterpolatedModel): """ - 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, - ): + 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. @@ -110,21 +111,18 @@ def __init__(self, resampling_interpolator_kwargs = { "kernel": "thin_plate_spline", "degree": 1, - **resampling_interpolator_kwargs + **resampling_interpolator_kwargs, } interpolator = resampling_interpolator( y=np.stack(tuple(x_data.values()), axis=1), d=y_data, - **resampling_interpolator_kwargs + **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() - } + x_data_resample = {k: x_data_resample for k in x_data.keys()} # Now, x_data_resample should be dict-like. Validate this. try: @@ -138,9 +136,7 @@ def __init__(self, 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 + np.min(x_data[k]), np.max(x_data[k]), v ) x_data_coordinates: Dict = x_data_resample @@ -150,21 +146,19 @@ def __init__(self, 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) + 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() - ]) + 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 + **interpolated_model_kwargs, } super().__init__( @@ -177,14 +171,10 @@ def __init__(self, self.y_data_raw = y_data -if __name__ == '__main__': +if __name__ == "__main__": x = np.arange(10) - y = x ** 3 - interp = UnstructuredInterpolatedModel( - x_data=x, - y_data=y - ) - + y = x**3 + interp = UnstructuredInterpolatedModel(x_data=x, y_data=y) def randspace(start, stop, n=50): vals = (stop - start) * np.random.rand(n) + start @@ -192,7 +182,6 @@ def randspace(start, stop, n=50): # vals = np.sort(vals) return vals - np.random.seed(4) X = randspace(-5, 5, 200) Y = randspace(-5, 5, 200) @@ -203,25 +192,26 @@ def randspace(start, stop, n=50): "x": X.flatten(), "y": Y.flatten(), }, - y_data=f.flatten() + y_data=f.flatten(), ) from aerosandbox.tools.pretty_plots import plt, show_plot fig = plt.figure() - ax = fig.add_subplot(projection='3d') + ax = fig.add_subplot(projection="3d") # ax.plot_surface(X, Y, f, color="blue", alpha=0.2) ax.scatter(X.flatten(), Y.flatten(), f.flatten()) X_plot, Y_plot = np.meshgrid( np.linspace(X.min(), X.max(), 500), np.linspace(Y.min(), Y.max(), 500), ) - F_plot = interp({ - "x": X_plot.flatten(), - "y": Y_plot.flatten() - }).reshape(X_plot.shape) + F_plot = interp({"x": X_plot.flatten(), "y": Y_plot.flatten()}).reshape( + X_plot.shape + ) ax.plot_surface( - X_plot, Y_plot, F_plot, + X_plot, + Y_plot, + F_plot, color="red", edgecolors=(1, 1, 1, 0.5), linewidth=0.5, diff --git a/aerosandbox/modeling/splines/bezier.py b/aerosandbox/modeling/splines/bezier.py index 206ddce57..f30c27f65 100644 --- a/aerosandbox/modeling/splines/bezier.py +++ b/aerosandbox/modeling/splines/bezier.py @@ -3,13 +3,13 @@ def quadratic_bezier_patch_from_tangents( - t: Union[float, np.ndarray], - x_a: float, - x_b: float, - y_a: float, - y_b: float, - dydx_a: float, - dydx_b: float, + t: Union[float, np.ndarray], + x_a: float, + x_b: float, + y_a: float, + y_b: float, + dydx_a: float, + dydx_b: float, ) -> Tuple[Union[float, np.ndarray], Union[float, np.ndarray]]: """ Computes sampled points in 2D space from a quadratic Bezier spline defined by endpoints and end-tangents. @@ -52,37 +52,22 @@ def quadratic_bezier_patch_from_tangents( """ ### Compute intercept of tangent lines - x_P1 = ( - (y_b - y_a) + (dydx_a * x_a - dydx_b * x_b) - ) / (dydx_a - dydx_b) + x_P1 = ((y_b - y_a) + (dydx_a * x_a - dydx_b * x_b)) / (dydx_a - dydx_b) y_P1 = y_a + dydx_a * (x_P1 - x_a) - x = ( - (1 - t) ** 2 * x_a + - 2 * (1 - t) * t * x_P1 + - t ** 2 * x_b - ) - y = ( - (1 - t) ** 2 * y_a + - 2 * (1 - t) * t * y_P1 + - t ** 2 * y_b - ) + x = (1 - t) ** 2 * x_a + 2 * (1 - t) * t * x_P1 + t**2 * x_b + y = (1 - t) ** 2 * y_a + 2 * (1 - t) * t * y_P1 + t**2 * y_b return x, y -if __name__ == '__main__': + +if __name__ == "__main__": import matplotlib.pyplot as plt import aerosandbox.tools.pretty_plots as p x, y = quadratic_bezier_patch_from_tangents( - t = np.linspace(0, 1, 11), - x_a=1, - x_b=4, - y_a=2, - y_b=3, - dydx_a=1, - dydx_b=-30 + t=np.linspace(0, 1, 11), x_a=1, x_b=4, y_a=2, y_b=3, dydx_a=1, dydx_b=-30 ) plt.plot(x, y, ".-") - p.show_plot() \ No newline at end of file + p.show_plot() diff --git a/aerosandbox/modeling/splines/hermite.py b/aerosandbox/modeling/splines/hermite.py index d6045f615..c053a781a 100644 --- a/aerosandbox/modeling/splines/hermite.py +++ b/aerosandbox/modeling/splines/hermite.py @@ -3,11 +3,11 @@ def linear_hermite_patch( - x: Union[float, np.ndarray], - x_a: float, - x_b: float, - f_a: float, - f_b: float, + x: Union[float, np.ndarray], + x_a: float, + x_b: float, + f_a: float, + f_b: float, ) -> Union[float, np.ndarray]: """ Computes the linear Hermite polynomial patch that passes through the given endpoints f_a and f_b. @@ -26,14 +26,14 @@ def linear_hermite_patch( def cubic_hermite_patch( - x: Union[float, np.ndarray], - x_a: float, - x_b: float, - f_a: float, - f_b: float, - dfdx_a: float, - dfdx_b: float, - extrapolation: str = 'continue', + x: Union[float, np.ndarray], + x_a: float, + x_b: float, + f_a: float, + f_b: float, + dfdx_a: float, + dfdx_b: float, + extrapolation: str = "continue", ) -> Union[float, np.ndarray]: """ Computes the cubic Hermite polynomial patch that passes through the given endpoints and endpoint derivatives. @@ -55,30 +55,30 @@ def cubic_hermite_patch( """ dx = x_b - x_a t = (x - x_a) / dx # Nondimensional distance along the patch - if extrapolation == 'continue': + if extrapolation == "continue": pass - elif extrapolation == 'clip': + elif extrapolation == "clip": t = np.clip(t, 0, 1) else: raise ValueError("Bad value of `extrapolation`!") return ( - (t ** 3) * (1 * f_b) + - (t ** 2 * (1 - t)) * (3 * f_b - 1 * dfdx_b * dx) + - (t * (1 - t) ** 2) * (3 * f_a + 1 * dfdx_a * dx) + - ((1 - t) ** 3) * (1 * f_a) + (t**3) * (1 * f_b) + + (t**2 * (1 - t)) * (3 * f_b - 1 * dfdx_b * dx) + + (t * (1 - t) ** 2) * (3 * f_a + 1 * dfdx_a * dx) + + ((1 - t) ** 3) * (1 * f_a) ) def cosine_hermite_patch( - x: Union[float, np.ndarray], - x_a: float, - x_b: float, - f_a: float, - f_b: float, - dfdx_a: float, - dfdx_b: float, - extrapolation: str = 'continue', + x: Union[float, np.ndarray], + x_a: float, + x_b: float, + f_a: float, + f_b: float, + dfdx_a: float, + dfdx_b: float, + extrapolation: str = "continue", ) -> Union[float, np.ndarray]: """ Computes a Hermite patch (i.e., values + derivatives at endpoints) that uses a cosine function to blend between @@ -103,9 +103,9 @@ def cosine_hermite_patch( The value of the patch evaluated at the input x. Returns a scalar if x is a scalar, or an array if x is an array. """ t = (x - x_a) / (x_b - x_a) # Nondimensional distance along the patch - if extrapolation == 'continue': + if extrapolation == "continue": pass - elif extrapolation == 'linear': + elif extrapolation == "linear": t = np.clip(t, 0, 1) else: raise ValueError("Bad value of `extrapolation`!") @@ -118,7 +118,7 @@ def cosine_hermite_patch( return b * l1 + (1 - b) * l2 -if __name__ == '__main__': +if __name__ == "__main__": import matplotlib.pyplot as plt import aerosandbox.tools.pretty_plots as p @@ -126,15 +126,8 @@ def cosine_hermite_patch( plt.plot( x, cubic_hermite_patch( - x, - x_a=0, - x_b=1, - f_a=0, - f_b=1, - dfdx_a=-0.5, - dfdx_b=-1, - extrapolation='clip' - ) + x, x_a=0, x_b=1, f_a=0, f_b=1, dfdx_a=-0.5, dfdx_b=-1, extrapolation="clip" + ), ) p.equal() diff --git a/aerosandbox/modeling/surrogate_model.py b/aerosandbox/modeling/surrogate_model.py index b2b8632e9..1bb16b747 100644 --- a/aerosandbox/modeling/surrogate_model.py +++ b/aerosandbox/modeling/surrogate_model.py @@ -37,9 +37,9 @@ def __init__(self): pass @abstractmethod # If you subclass SurrogateModel, you must overwrite __call__ so that it's a callable. - def __call__(self, - x: Union[int, float, np.ndarray, Dict[str, np.ndarray]] - ) -> Union[float, np.ndarray]: + def __call__( + self, x: Union[int, float, np.ndarray, Dict[str, np.ndarray]] + ) -> Union[float, np.ndarray]: """ Evaluates the surrogate model at some given input x. @@ -55,7 +55,8 @@ def __call__(self, if x_data_is_dict and not input_is_dict: raise TypeError( - f"The input to this model should be a dict with: keys {self.input_names()}, values as float or array.") + f"The input to this model should be a dict with: keys {self.input_names()}, values as float or array." + ) if input_is_dict and not x_data_is_dict: raise TypeError("The input to this model should be a float or array.") @@ -65,14 +66,18 @@ def __call__(self, def __repr__(self) -> str: input_names = self.input_names() if input_names is not None: - input_description = f"a dict with: keys {input_names}, values as float or array" + input_description = ( + f"a dict with: keys {input_names}, values as float or array" + ) else: input_description = f"a float or array" - return "\n".join([ - f"SurrogateModel(x) [R^{self.input_dimensionality()} -> R^1]", - f"\tInput: {input_description}", - f"\tOutput: float or array", - ]) + return "\n".join( + [ + f"SurrogateModel(x) [R^{self.input_dimensionality()} -> R^1]", + f"\tInput: {input_description}", + f"\tOutput: float or array", + ] + ) def input_dimensionality(self) -> int: """ diff --git a/aerosandbox/modeling/test_modeling/dataset_temperature.py b/aerosandbox/modeling/test_modeling/dataset_temperature.py index 11957182c..89effba01 100644 --- a/aerosandbox/modeling/test_modeling/dataset_temperature.py +++ b/aerosandbox/modeling/test_modeling/dataset_temperature.py @@ -16,12 +16,9 @@ time = np.hstack((time, 90)) measured_temperature = np.hstack((measured_temperature, 0)) -if __name__ == '__main__': +if __name__ == "__main__": from aerosandbox.tools.pretty_plots import plt, sns, mpl, show_plot fig, ax = plt.subplots() plt.plot(time, measured_temperature, ".") - show_plot( - xlabel="Time", - ylabel="Measured Temperature" - ) + show_plot(xlabel="Time", ylabel="Measured Temperature") diff --git a/aerosandbox/modeling/test_modeling/test_fit_model.py b/aerosandbox/modeling/test_modeling/test_fit_model.py index 642b91568..8b7c0d3e5 100644 --- a/aerosandbox/modeling/test_modeling/test_fit_model.py +++ b/aerosandbox/modeling/test_modeling/test_fit_model.py @@ -15,19 +15,13 @@ def test_single_dimensional_polynomial_fitting(): ### Make some data x = np.linspace(0, 10, 50) noise = 5 * np.random.randn(len(x)) - y = x ** 2 - 8 * x + 5 + noise + y = x**2 - 8 * x + 5 + noise ### Fit data def model(x, p): - return ( - p["a"] * x["x1"] ** 2 + - p["b"] * x["x1"] + - p["c"] - ) + return p["a"] * x["x1"] ** 2 + p["b"] * x["x1"] + p["c"] - x_data = { - "x1": x - } + x_data = {"x1": x} fitted_model = FittedModel( model=model, @@ -72,15 +66,11 @@ def test_multidimensional_power_law_fitting(): y = np.logspace(0, 3) X, Y = np.meshgrid(x, y, indexing="ij") noise = np.random.lognormal(mean=0, sigma=0.05) - Z = 0.5 * X ** 0.75 * Y ** 1.25 * noise + Z = 0.5 * X**0.75 * Y**1.25 * noise ### Fit data def model(x, p): - return ( - p["multiplier"] * - x["X"] ** p["X_power"] * - x["Y"] ** p["Y_power"] - ) + return p["multiplier"] * x["X"] ** p["X_power"] * x["Y"] ** p["Y_power"] x_data = { "X": X.flatten(), @@ -93,15 +83,15 @@ def model(x, p): y_data=Z.flatten(), parameter_guesses={ "multiplier": 1, - "X_power" : 1, - "Y_power" : 1, + "X_power": 1, + "Y_power": 1, }, parameter_bounds={ "multiplier": (None, None), - "X_power" : (None, None), - "Y_power" : (None, None), + "X_power": (None, None), + "Y_power": (None, None), }, - put_residuals_in_logspace=True + put_residuals_in_logspace=True, # Putting residuals in logspace minimizes the norm of log-error instead of absolute error ) @@ -203,18 +193,14 @@ def model(x, p): fitted_model(5) with pytest.raises(TypeError): - fitted_model({ - "temperature": 5 - }) + fitted_model({"temperature": 5}) def model(x, p): return p["m"] * x["hour"] + p["b"] fitted_model = FittedModel( model=model, - x_data={ - "hour": hour - }, + x_data={"hour": hour}, y_data=y_data, parameter_guesses={ "m": 0, @@ -223,13 +209,11 @@ def model(x, p): residual_norm_type="Linf", ) - fitted_model({ - "hour": 5 - }) + fitted_model({"hour": 5}) with pytest.raises(TypeError): fitted_model(5) -if __name__ == '__main__': +if __name__ == "__main__": pytest.main() diff --git a/aerosandbox/modeling/test_modeling/test_fit_model_fit_type.py b/aerosandbox/modeling/test_modeling/test_fit_model_fit_type.py index deca12144..168552fb7 100644 --- a/aerosandbox/modeling/test_modeling/test_fit_model_fit_type.py +++ b/aerosandbox/modeling/test_modeling/test_fit_model_fit_type.py @@ -13,7 +13,7 @@ def test_fit_model_fit_type(plot=False): ### Fit a model def model(x, p): - return p["2"] * x ** 2 + p["1"] * x + p["0"] # Quadratic regression + return p["2"] * x**2 + p["1"] * x + p["0"] # Quadratic regression def fit_model_with_fit_type(fit_type): return FittedModel( @@ -26,7 +26,7 @@ def fit_model_with_fit_type(fit_type): "0": 0, }, fit_type=fit_type, - residual_norm_type="L1" + residual_norm_type="L1", ) best_fit_model = fit_model_with_fit_type("best") @@ -45,11 +45,13 @@ def fit_model_with_fit_type(fit_type): plt.plot(x, lower_bound_model(x), label=r"Lower-Bound Fit") plt.xlabel(r"Time") plt.ylabel(r"Temperature") - plt.title(r"Illustration of Fit Types for Robust Surrogate Modeling (Linear Model)") + plt.title( + r"Illustration of Fit Types for Robust Surrogate Modeling (Linear Model)" + ) plt.tight_layout() plt.legend() plt.show() -if __name__ == '__main__': +if __name__ == "__main__": test_fit_model_fit_type(True) diff --git a/aerosandbox/modeling/test_modeling/test_fit_model_norm_type.py b/aerosandbox/modeling/test_modeling/test_fit_model_norm_type.py index e301cc305..a0794ddea 100644 --- a/aerosandbox/modeling/test_modeling/test_fit_model_norm_type.py +++ b/aerosandbox/modeling/test_modeling/test_fit_model_norm_type.py @@ -24,7 +24,7 @@ def fit_model_with_norm(residual_norm_type): "1": 0, "0": 0, }, - residual_norm_type=residual_norm_type + residual_norm_type=residual_norm_type, ) L1_model = fit_model_with_norm("L1") @@ -43,11 +43,13 @@ def fit_model_with_norm(residual_norm_type): plt.plot(x, LInf_model(x), label=r"$L_\infty$ Fit: $\min (\max |e|)$") plt.xlabel(r"Time") plt.ylabel(r"Temperature") - plt.title(r"Illustration of Various Norm Types for Robust Regression (Quadratic Model)") + plt.title( + r"Illustration of Various Norm Types for Robust Regression (Quadratic Model)" + ) plt.tight_layout() plt.legend() plt.show() -if __name__ == '__main__': +if __name__ == "__main__": test_fit_model_norm_type(plot=True) diff --git a/aerosandbox/modeling/test_modeling/test_fit_model_weighting.py b/aerosandbox/modeling/test_modeling/test_fit_model_weighting.py index 8c8db032f..28e75f78b 100644 --- a/aerosandbox/modeling/test_modeling/test_fit_model_weighting.py +++ b/aerosandbox/modeling/test_modeling/test_fit_model_weighting.py @@ -15,7 +15,7 @@ def test_fit_model_weighting(): "m": 0, "b": 0, }, - weights=None + weights=None, ) # Fit a model with no weighting assert fm(10) != pytest.approx(5, abs=1) # Doesn't give a high value at x = 10 @@ -28,7 +28,7 @@ def test_fit_model_weighting(): "m": 0, "b": 0, }, - weights=(x > 0) & (x < 2) + weights=(x > 0) & (x < 2), ) # Fit a model with weighting assert fm(10) == pytest.approx(5, abs=1) # Gives a high value at x = 10 @@ -36,5 +36,5 @@ def test_fit_model_weighting(): fm.plot() -if __name__ == '__main__': +if __name__ == "__main__": test_fit_model_weighting() diff --git a/aerosandbox/modeling/test_modeling/test_fitted_model.py b/aerosandbox/modeling/test_modeling/test_fitted_model.py index cf8b2aea8..d345926ba 100644 --- a/aerosandbox/modeling/test_modeling/test_fitted_model.py +++ b/aerosandbox/modeling/test_modeling/test_fitted_model.py @@ -26,5 +26,5 @@ def test_plot(get_fitted_model): get_fitted_model.plot() -if __name__ == '__main__': +if __name__ == "__main__": pytest.main() diff --git a/aerosandbox/modeling/test_modeling/test_interpolate_model.py b/aerosandbox/modeling/test_modeling/test_interpolate_model.py index f52b2a031..6fc76aa96 100644 --- a/aerosandbox/modeling/test_modeling/test_interpolate_model.py +++ b/aerosandbox/modeling/test_modeling/test_interpolate_model.py @@ -4,7 +4,7 @@ def underlying_function_1D(x): - return x ** 2 - 8 * x + 5 + return x**2 - 8 * x + 5 def interpolated_model(): @@ -27,8 +27,8 @@ def test_interpolated_model_at_scalar(): def test_interpolated_model_at_vector(): model = interpolated_model() assert np.all( - model(np.array([1.5, 2.5, 3.5])) == - pytest.approx(underlying_function_1D(np.array([1.5, 2.5, 3.5]))) + model(np.array([1.5, 2.5, 3.5])) + == pytest.approx(underlying_function_1D(np.array([1.5, 2.5, 3.5]))) ) @@ -44,6 +44,6 @@ def test_interpolated_model_zeros_patch(): assert f == pytest.approx(0) -if __name__ == '__main__': +if __name__ == "__main__": test_interpolated_model_zeros_patch() pytest.main() diff --git a/aerosandbox/modeling/test_modeling/test_interpolate_model_ND.py b/aerosandbox/modeling/test_modeling/test_interpolate_model_ND.py index 99a0b6004..b157c19c6 100644 --- a/aerosandbox/modeling/test_modeling/test_interpolate_model_ND.py +++ b/aerosandbox/modeling/test_modeling/test_interpolate_model_ND.py @@ -4,7 +4,7 @@ def underlying_function_2D(x1, x2): - return x1 ** 2 + (x2 + 1) ** 2 + return x1**2 + (x2 + 1) ** 2 def interpolated_model(): @@ -41,8 +41,7 @@ def test_interpolated_model_at_vector(): "x2": np.array([2.5, 3.5]), } assert np.all( - model(x_data) == - pytest.approx(underlying_function_2D(*x_data.values())) + model(x_data) == pytest.approx(underlying_function_2D(*x_data.values())) ) @@ -50,7 +49,7 @@ def test_interpolated_model_at_vector(): # interpolated_model.plot() -if __name__ == '__main__': +if __name__ == "__main__": test_interpolated_model_at_scalar() test_interpolated_model_at_vector() # pytest.main() diff --git a/aerosandbox/modeling/test_modeling/test_optimize_through_interpolator.py b/aerosandbox/modeling/test_modeling/test_optimize_through_interpolator.py index 6b3328f1e..9adbac160 100644 --- a/aerosandbox/modeling/test_modeling/test_optimize_through_interpolator.py +++ b/aerosandbox/modeling/test_modeling/test_optimize_through_interpolator.py @@ -14,27 +14,20 @@ def f2(x): def f3(x): return 2 * x - 12 - return np.softmax( - f1(x), - f2(x), - f3(x), - hardness=1 - ) + return np.softmax(f1(x), f2(x), f3(x), hardness=1) def get_interpolated_model(res=51): x_samples = np.linspace(-10, 10, res) f_samples = underlying_function(x_samples) - return InterpolatedModel( - x_data_coordinates=x_samples, - y_data_structured=f_samples - ) + return InterpolatedModel(x_data_coordinates=x_samples, y_data_structured=f_samples) def plot_underlying_function(): import matplotlib.pyplot as plt import seaborn as sns + sns.set(palette=sns.color_palette("husl")) x = np.linspace(-10, 10, 500) f = underlying_function(x) @@ -61,9 +54,7 @@ def test_solve_actual_function(): assert sol(x) == pytest.approx(4.85358, abs=1e-3) -def test_solve_interpolated_unbounded( - interpolated_model=get_interpolated_model() -): +def test_solve_interpolated_unbounded(interpolated_model=get_interpolated_model()): opti = asb.Opti() x = opti.variable(init_guess=-5) @@ -89,7 +80,7 @@ def test_solve_interpolated_unbounded( # assert sol(x) == pytest.approx(4.85358, abs=0.1) -if __name__ == '__main__': +if __name__ == "__main__": # plot_underlying_function() # plot_interpolated_model(interpolated_model()) pytest.main() diff --git a/aerosandbox/numpy/arithmetic_dyadic.py b/aerosandbox/numpy/arithmetic_dyadic.py index 2fad0ece2..ea8922f64 100644 --- a/aerosandbox/numpy/arithmetic_dyadic.py +++ b/aerosandbox/numpy/arithmetic_dyadic.py @@ -16,7 +16,9 @@ def shape_2D(object: Union[float, int, Iterable, _onp.ndarray]) -> Tuple: elif len(shape) == 2: return shape else: - raise ValueError("CasADi can't handle arrays with >2 dimensions, unfortunately.") + raise ValueError( + "CasADi can't handle arrays with >2 dimensions, unfortunately." + ) x1_shape = shape_2D(x1) x2_shape = shape_2D(x2) @@ -36,9 +38,7 @@ def shape_2D(object: Union[float, int, Iterable, _onp.ndarray]) -> Tuple: return x1_tiled, x2_tiled -def add( - x1, x2 -): +def add(x1, x2): if not is_casadi_type(x1) and not is_casadi_type(x2): return _onp.add(x1, x2) else: @@ -46,9 +46,7 @@ def add( return x1 + x2 -def multiply( - x1, x2 -): +def multiply(x1, x2): if not is_casadi_type(x1) and not is_casadi_type(x2): return _onp.multiply(x1, x2) else: @@ -67,11 +65,7 @@ def mod(x1, x2): else: out = _cas.fmod(x1, x2) - out = where( - x1 < 0, - out + x2, - out - ) + out = where(x1 < 0, out + x2, out) return out @@ -83,11 +77,7 @@ def centered_mod(x1, x2): """ if not is_casadi_type(x1) and not is_casadi_type(x2): remainder = _onp.mod(x1, x2) - return where( - remainder > x2 / 2, - remainder - x2, - remainder - ) + return where(remainder > x2 / 2, remainder - x2, remainder) else: return _cas.remainder(x1, x2) diff --git a/aerosandbox/numpy/arithmetic_monadic.py b/aerosandbox/numpy/arithmetic_monadic.py index e693c32b9..cef105ff4 100644 --- a/aerosandbox/numpy/arithmetic_monadic.py +++ b/aerosandbox/numpy/arithmetic_monadic.py @@ -21,7 +21,9 @@ def sum(x, axis: int = None): elif axis is None: return sum(sum(x, axis=0), axis=0) else: - raise ValueError("CasADi types can only be up to 2D, so `axis` must be None, 0, or 1.") + raise ValueError( + "CasADi types can only be up to 2D, so `axis` must be None, 0, or 1." + ) def mean(x, axis: int = None): @@ -41,7 +43,9 @@ def mean(x, axis: int = None): elif axis is None: return mean(mean(x, axis=0), axis=1) else: - raise ValueError("CasADi types can only be up to 2D, so `axis` must be None, 0, or 1.") + raise ValueError( + "CasADi types can only be up to 2D, so `axis` must be None, 0, or 1." + ) def abs(x): @@ -69,6 +73,7 @@ def abs(x): # if axis is None: # return _cas.cumsum(_onp.flatten(x)) + def prod(x, axis: int = None): """ Return the product of array elements over a given axis. diff --git a/aerosandbox/numpy/array.py b/aerosandbox/numpy/array.py index 27f5cf5ba..5da7b042a 100644 --- a/aerosandbox/numpy/array.py +++ b/aerosandbox/numpy/array.py @@ -10,12 +10,13 @@ def array(array_like, dtype=None): See syntax here: https://numpy.org/doc/stable/reference/generated/numpy.array.html """ - if is_casadi_type(array_like, recursive=False): # If you were literally given a CasADi array, just return it + if is_casadi_type( + array_like, recursive=False + ): # If you were literally given a CasADi array, just return it # Handles inputs like cas.DM([1, 2, 3]) return array_like - elif not is_casadi_type(array_like, - recursive=True) or dtype is not None: + elif not is_casadi_type(array_like, recursive=True) or dtype is not None: # If you were given a list of iterables that don't have CasADi types: # Handles inputs like [[1, 2, 3], [4, 5, 6]] return _onp.array(array_like, dtype=dtype) @@ -28,12 +29,7 @@ def make_row(contents: List): except (TypeError, Exception): return contents - return _cas.vertcat( - *[ - make_row(row) - for row in array_like - ] - ) + return _cas.vertcat(*[make_row(row) for row in array_like]) def concatenate(arrays: Sequence, axis: int = 0): @@ -52,7 +48,9 @@ def concatenate(arrays: Sequence, axis: int = 0): elif axis == 1: return _cas.horzcat(*arrays) else: - raise ValueError("CasADi-backend arrays can only be 1D or 2D, so `axis` must be 0 or 1.") + raise ValueError( + "CasADi-backend arrays can only be 1D or 2D, so `axis` must be 0 or 1." + ) def stack(arrays: Sequence, axis: int = 0): @@ -72,14 +70,18 @@ def stack(arrays: Sequence, axis: int = 0): raise ValueError("Can only stack Nx1 CasADi arrays!") else: if not len(array.shape) == 1: - raise ValueError("Can only stack 1D NumPy ndarrays alongside CasADi arrays!") + raise ValueError( + "Can only stack 1D NumPy ndarrays alongside CasADi arrays!" + ) if axis == 0 or axis == -2: return _cas.transpose(_cas.horzcat(*arrays)) elif axis == 1 or axis == -1: return _cas.horzcat(*arrays) else: - raise ValueError("CasADi-backend arrays can only be 1D or 2D, so `axis` must be 0 or 1.") + raise ValueError( + "CasADi-backend arrays can only be 1D or 2D, so `axis` must be 0 or 1." + ) def hstack(arrays): @@ -87,7 +89,8 @@ def hstack(arrays): return _onp.hstack(arrays) else: raise ValueError( - "Use `np.stack()` or `np.concatenate()` instead of `np.hstack()` when dealing with mixed-backend arrays.") + "Use `np.stack()` or `np.concatenate()` instead of `np.hstack()` when dealing with mixed-backend arrays." + ) def vstack(arrays): @@ -95,7 +98,8 @@ def vstack(arrays): return _onp.vstack(arrays) else: raise ValueError( - "Use `np.stack()` or `np.concatenate()` instead of `np.vstack()` when dealing with mixed-backend arrays.") + "Use `np.stack()` or `np.concatenate()` instead of `np.vstack()` when dealing with mixed-backend arrays." + ) def dstack(arrays): @@ -103,7 +107,8 @@ def dstack(arrays): return _onp.dstack(arrays) else: raise ValueError( - "Use `np.stack()` or `np.concatenate()` instead of `np.dstack()` when dealing with mixed-backend arrays.") + "Use `np.stack()` or `np.concatenate()` instead of `np.dstack()` when dealing with mixed-backend arrays." + ) def length(array) -> int: @@ -161,18 +166,14 @@ def diag(v, k=0): n = v.shape[0] if k >= 0: - return array([ - v[i, i + k] - for i in range(n - k) - ]) + return array([v[i, i + k] for i in range(n - k)]) else: - return array([ - v[i - k, i] - for i in range(n + k) - ]) + return array([v[i - k, i] for i in range(n + k)]) else: - raise NotImplementedError("Haven't yet added logic for non-square matrices.") + raise NotImplementedError( + "Haven't yet added logic for non-square matrices." + ) def roll(a, shift, axis: int = None): @@ -257,22 +258,16 @@ def max(a, axis=None): if a.shape[1] == 1: return _cas.mmax(a) else: - return array([ - _cas.mmax(a[:, i]) - for i in range(a.shape[1]) - ]) + return array([_cas.mmax(a[:, i]) for i in range(a.shape[1])]) elif axis == 1: if a.shape[0] == 1: return _cas.mmax(a) else: - return array([ - _cas.mmax(a[i, :]) - for i in range(a.shape[0]) - ]) + return array([_cas.mmax(a[i, :]) for i in range(a.shape[0])]) else: - raise ValueError(f'Invalid axis {axis} for CasADi array.') + raise ValueError(f"Invalid axis {axis} for CasADi array.") def min(a, axis=None): @@ -297,25 +292,19 @@ def min(a, axis=None): if a.shape[1] == 1: return _cas.mmin(a) else: - return array([ - _cas.mmin(a[:, i]) - for i in range(a.shape[1]) - ]) + return array([_cas.mmin(a[:, i]) for i in range(a.shape[1])]) elif axis == 1: if a.shape[0] == 1: return _cas.mmin(a) else: - return array([ - _cas.mmin(a[i, :]) - for i in range(a.shape[0]) - ]) + return array([_cas.mmin(a[i, :]) for i in range(a.shape[0])]) else: - raise ValueError(f'Invalid axis {axis} for CasADi array.') + raise ValueError(f"Invalid axis {axis} for CasADi array.") -def reshape(a, newshape, order='C'): +def reshape(a, newshape, order="C"): """ Gives a new shape to an array without changing its data. @@ -335,7 +324,9 @@ def reshape(a, newshape, order='C'): newshape = tuple(newshape) elif len(newshape) > 2: - raise ValueError("CasADi data types are limited to no more than 2 dimensions.") + raise ValueError( + "CasADi data types are limited to no more than 2 dimensions." + ) if order == "C": return _cas.reshape(a.T, newshape[::-1]).T @@ -345,7 +336,7 @@ def reshape(a, newshape, order='C'): raise NotImplementedError("Only C and F orders are supported.") -def ravel(a, order='C'): +def ravel(a, order="C"): """ Returns a contiguous flattened array. @@ -371,10 +362,12 @@ def tile(A, reps): elif len(reps) == 2: return _cas.repmat(A, reps[0], reps[1]) else: - raise ValueError("Cannot have >2D arrays when using CasADi numeric backend!") + raise ValueError( + "Cannot have >2D arrays when using CasADi numeric backend!" + ) -def zeros_like(a, dtype=None, order='K', subok=True, shape=None): +def zeros_like(a, dtype=None, order="K", subok=True, shape=None): """Return an array of zeros with the same shape and type as a given array.""" if not is_casadi_type(a, recursive=False): return _onp.zeros_like(a, dtype=dtype, order=order, subok=subok, shape=shape) @@ -382,7 +375,7 @@ def zeros_like(a, dtype=None, order='K', subok=True, shape=None): return _onp.zeros(shape=length(a)) -def ones_like(a, dtype=None, order='K', subok=True, shape=None): +def ones_like(a, dtype=None, order="K", subok=True, shape=None): """Return an array of ones with the same shape and type as a given array.""" if not is_casadi_type(a, recursive=False): return _onp.ones_like(a, dtype=dtype, order=order, subok=subok, shape=shape) @@ -390,24 +383,28 @@ def ones_like(a, dtype=None, order='K', subok=True, shape=None): return _onp.ones(shape=length(a)) -def empty_like(prototype, dtype=None, order='K', subok=True, shape=None): +def empty_like(prototype, dtype=None, order="K", subok=True, shape=None): """Return a new array with the same shape and type as a given array.""" if not is_casadi_type(prototype, recursive=False): - return _onp.empty_like(prototype, dtype=dtype, order=order, subok=subok, shape=shape) + return _onp.empty_like( + prototype, dtype=dtype, order=order, subok=subok, shape=shape + ) else: return zeros_like(prototype) -def full_like(a, fill_value, dtype=None, order='K', subok=True, shape=None): +def full_like(a, fill_value, dtype=None, order="K", subok=True, shape=None): """Return a full array with the same shape and type as a given array.""" if not is_casadi_type(a, recursive=False): - return _onp.full_like(a, fill_value, dtype=dtype, order=order, subok=subok, shape=shape) + return _onp.full_like( + a, fill_value, dtype=dtype, order=order, subok=subok, shape=shape + ) else: return fill_value * ones_like(a) def assert_equal_shape( - arrays: Union[List[_onp.ndarray], Dict[str, _onp.ndarray]], + arrays: Union[List[_onp.ndarray], Dict[str, _onp.ndarray]], ) -> None: """ Assert that all of the given arrays are the same shape. If this is not true, raise a ValueError. @@ -443,4 +440,6 @@ def get_shape(array): raise ValueError("The given arrays do not have the same shape!") else: namelist = ", ".join(names) - raise ValueError(f"The given arrays {namelist} do not have the same shape!") + raise ValueError( + f"The given arrays {namelist} do not have the same shape!" + ) diff --git a/aerosandbox/numpy/calculus.py b/aerosandbox/numpy/calculus.py index 0870ae5e9..f6fd0d1ce 100644 --- a/aerosandbox/numpy/calculus.py +++ b/aerosandbox/numpy/calculus.py @@ -19,17 +19,16 @@ def diff(a, n=1, axis=-1, period=None): """ if period is not None: - return _centered_mod( - diff(a, n=n, axis=axis), - period - ) + return _centered_mod(diff(a, n=n, axis=axis), period) if not is_casadi_type(a): return _onp.diff(a, n=n, axis=axis) else: if axis != -1: - raise NotImplementedError("This could be implemented, but haven't had the need yet.") + raise NotImplementedError( + "This could be implemented, but haven't had the need yet." + ) result = a for i in range(n): @@ -38,12 +37,12 @@ def diff(a, n=1, axis=-1, period=None): def gradient( - f, - *varargs, - axis=None, - edge_order=1, - n=1, - period=None, + f, + *varargs, + axis=None, + edge_order=1, + n=1, + period=None, ): """ Return the gradient of an N-dimensional array. @@ -75,38 +74,35 @@ def gradient( """ if ( - not is_casadi_type(f) - and all([not is_casadi_type(vararg) for vararg in varargs]) - and n == 1 - and period is None + not is_casadi_type(f) + and all([not is_casadi_type(vararg) for vararg in varargs]) + and n == 1 + and period is None ): - return _onp.gradient( - f, - *varargs, - axis=axis, - edge_order=edge_order - ) + return _onp.gradient(f, *varargs, axis=axis, edge_order=edge_order) else: f = array(f) shape = f.shape # Handle the varargs argument if len(varargs) == 0: - varargs = (1.,) + varargs = (1.0,) if len(varargs) == 1: varargs = [varargs[0] for i in range(len(shape))] if len(varargs) != len(shape): - raise ValueError("You must specify either 0, 1, or N varargs, where N is the number of dimensions of f.") + raise ValueError( + "You must specify either 0, 1, or N varargs, where N is the number of dimensions of f." + ) else: dxes = [] for i, vararg in enumerate(varargs): - if _onp.prod(array(vararg).shape) == 1: # If it's a scalar, you have dx values - dxes.append( - vararg * _onp.ones(shape[i] - 1) - ) + if ( + _onp.prod(array(vararg).shape) == 1 + ): # If it's a scalar, you have dx values + dxes.append(vararg * _onp.ones(shape[i] - 1)) else: dxes.append( diff( @@ -163,25 +159,15 @@ def get_slice(slice_obj: slice) -> Tuple[slice]: hm = dx[get_slice(slice(None, -1))] hp = dx[get_slice(slice(1, None))] - dfp = ( - f[get_slice(slice(2, None))] - - f[get_slice(slice(1, -1))] - ) - dfm = ( - f[get_slice(slice(1, -1))] - - f[get_slice(slice(None, -2))] - ) + dfp = f[get_slice(slice(2, None))] - f[get_slice(slice(1, -1))] + dfm = f[get_slice(slice(1, -1))] - f[get_slice(slice(None, -2))] if period is not None: dfp = _centered_mod(dfp, period) dfm = _centered_mod(dfm, period) if n == 1: - grad_f = ( - hm ** 2 * dfp + hp ** 2 * dfm - ) / ( - hm * hp * (hm + hp) - ) + grad_f = (hm**2 * dfp + hp**2 * dfm) / (hm * hp * (hm + hp)) if edge_order == 1: # First point @@ -201,10 +187,8 @@ def get_slice(slice_obj: slice) -> Tuple[slice]: hm_f = hm[get_slice(slice(0, 1))] hp_f = hp[get_slice(slice(0, 1))] grad_f_first = ( - 2 * dfm_f * hm_f * hp_f + dfm_f * hp_f ** 2 - dfp_f * hm_f ** 2 - ) / ( - hm_f * hp_f * (hm_f + hp_f) - ) + 2 * dfm_f * hm_f * hp_f + dfm_f * hp_f**2 - dfp_f * hm_f**2 + ) / (hm_f * hp_f * (hm_f + hp_f)) # Last point dfm_l = dfm[get_slice(slice(-1, None))] @@ -212,43 +196,32 @@ def get_slice(slice_obj: slice) -> Tuple[slice]: hm_l = hm[get_slice(slice(-1, None))] hp_l = hp[get_slice(slice(-1, None))] grad_f_last = ( - -dfm_l * hp_l ** 2 + dfp_l * hm_l ** 2 + 2 * dfp_l * hm_l * hp_l - ) / ( - hm_l * hp_l * (hm_l + hp_l) - ) + -dfm_l * hp_l**2 + dfp_l * hm_l**2 + 2 * dfp_l * hm_l * hp_l + ) / (hm_l * hp_l * (hm_l + hp_l)) else: raise ValueError("Invalid edge_order.") - grad_f = concatenate(( - grad_f_first, - grad_f, - grad_f_last - ), axis=axis) + grad_f = concatenate((grad_f_first, grad_f, grad_f_last), axis=axis) return grad_f elif n == 2: - grad_grad_f = ( - 2 / (hm + hp) * ( - dfp / hp - dfm / hm - ) - ) + grad_grad_f = 2 / (hm + hp) * (dfp / hp - dfm / hm) grad_grad_f_first = grad_grad_f[get_slice(slice(0, 1))] grad_grad_f_last = grad_grad_f[get_slice(slice(-1, None))] - grad_grad_f = concatenate(( - grad_grad_f_first, - grad_grad_f, - grad_grad_f_last - ), axis=axis) + grad_grad_f = concatenate( + (grad_grad_f_first, grad_grad_f, grad_grad_f_last), axis=axis + ) return grad_grad_f else: raise ValueError( - "A second-order reconstructor only supports first derivatives (n=1) and second derivatives (n=2).") + "A second-order reconstructor only supports first derivatives (n=1) and second derivatives (n=2)." + ) def trapz(x, modify_endpoints=False): # TODO unify with NumPy trapz, this is different @@ -264,14 +237,14 @@ def trapz(x, modify_endpoints=False): # TODO unify with NumPy trapz, this is di """ import warnings + warnings.warn( "trapz() will eventually be deprecated, since NumPy plans to remove it in the upcoming NumPy 2.0 release (2024). \n" - "For discrete intervals, use asb.numpy.integrate_discrete_intervals(f, method=\"trapz\") instead.", - PendingDeprecationWarning) + 'For discrete intervals, use asb.numpy.integrate_discrete_intervals(f, method="trapz") instead.', + PendingDeprecationWarning, + ) - integral = ( - x[1:] + x[:-1] - ) / 2 + integral = (x[1:] + x[:-1]) / 2 if modify_endpoints: integral[0] = integral[0] + x[0] * 0.5 integral[-1] = integral[-1] + x[-1] * 0.5 @@ -279,7 +252,7 @@ def trapz(x, modify_endpoints=False): # TODO unify with NumPy trapz, this is di return integral -if __name__ == '__main__': +if __name__ == "__main__": import aerosandbox as asb import aerosandbox.numpy as np @@ -287,12 +260,7 @@ def trapz(x, modify_endpoints=False): # TODO unify with NumPy trapz, this is di # print(diff(cas.DM([355, 5]), period=360)) - print( - gradient( - np.linspace(45, 55, 11) % 50, - period=50 - ) - ) + print(gradient(np.linspace(45, 55, 11) % 50, period=50)) # # # a = np.linspace(-500, 500, 21) % 360 - 180 diff --git a/aerosandbox/numpy/conditionals.py b/aerosandbox/numpy/conditionals.py index 840da83e3..6b8d0f87d 100644 --- a/aerosandbox/numpy/conditionals.py +++ b/aerosandbox/numpy/conditionals.py @@ -4,9 +4,9 @@ def where( - condition, - value_if_true, - value_if_false, + condition, + value_if_true, + value_if_false, ): """ Return elements chosen from x or y depending on condition. @@ -14,22 +14,14 @@ def where( See syntax here: https://numpy.org/doc/stable/reference/generated/numpy.where.html """ if not is_casadi_type([condition, value_if_true, value_if_false], recursive=True): - return _onp.where( - condition, - value_if_true, - value_if_false - ) + return _onp.where(condition, value_if_true, value_if_false) else: - return _cas.if_else( - condition, - value_if_true, - value_if_false - ) + return _cas.if_else(condition, value_if_true, value_if_false) def maximum( - x1, - x2, + x1, + x2, ): """ Element-wise maximum of two arrays. @@ -49,8 +41,8 @@ def maximum( def minimum( - x1, - x2, + x1, + x2, ): """ Element-wise minimum of two arrays. diff --git a/aerosandbox/numpy/derivative_discrete_derivations/quadratic.py b/aerosandbox/numpy/derivative_discrete_derivations/quadratic.py index 4cc77f86a..7803614c6 100644 --- a/aerosandbox/numpy/derivative_discrete_derivations/quadratic.py +++ b/aerosandbox/numpy/derivative_discrete_derivations/quadratic.py @@ -1,18 +1,19 @@ import sympy as s from sympy import init_printing + init_printing() # Reconstructs a quadratic interpolant from x1...x3, then gets the derivative at x2 # Define the symbols -x1, x2, x3 = s.symbols('x1 x2 x3', real=True) -f1, f2, f3 = s.symbols('f1 f2 f3', real=True) +x1, x2, x3 = s.symbols("x1 x2 x3", real=True) +f1, f2, f3 = s.symbols("f1 f2 f3", real=True) # hm = x2 - x1 # hp = x3 - x2 hm, hp = s.symbols("hm hp") -q = s.symbols('q') # Normalized space for a Bernstein basis. +q = s.symbols("q") # Normalized space for a Bernstein basis. # Mapping from x-space to q-space has x=x2 -> q=0, x=x3 -> q=1. q1 = 0 @@ -23,9 +24,9 @@ # Define the Bernstein basis polynomials b1 = (1 - q) ** 2 b2 = 2 * q * (1 - q) -b3 = q ** 2 +b3 = q**2 -c1, c2, c3 = s.symbols('c1 c2 c3', real=True) +c1, c2, c3 = s.symbols("c1 c2 c3", real=True) # Can solve for c2 and c3 exactly c1 = f1 @@ -33,7 +34,7 @@ f = c1 * b1 + c2 * b2 + c3 * b3 -f2_quadratic = f.subs(q, q2)#.simplify() +f2_quadratic = f.subs(q, q2) # .simplify() factors = [q2] # factors = [f1, f2, f3, f4] @@ -58,29 +59,33 @@ dfm, dfp = s.symbols("dfm dfp") + def simplify(expr): import copy + original_expr = copy.copy(expr) - expr = expr.subs({ - f3 - f2: dfp, - f2 - f1: dfm, - f3 - f1: dfp + dfm, - x3 - x1: hm + hp, - }) - expr = expr.subs({ - f3 - f2: dfp, - f2 - f1: dfm, - f3 - f1: dfp + dfm, - x3 - x1: hm + hp, - }) - expr = expr.factor([ - hp, - hm - ]).simplify() + expr = expr.subs( + { + f3 - f2: dfp, + f2 - f1: dfm, + f3 - f1: dfp + dfm, + x3 - x1: hm + hp, + } + ) + expr = expr.subs( + { + f3 - f2: dfp, + f2 - f1: dfm, + f3 - f1: dfp + dfm, + x3 - x1: hm + hp, + } + ) + expr = expr.factor([hp, hm]).simplify() if expr != original_expr: expr = simplify(expr) return expr + dfdx_q1 = simplify(dfdx.subs(q, q1)) dfdx_q2 = simplify(dfdx.subs(q, q2)) dfdx_q3 = simplify(dfdx.subs(q, q3)) @@ -89,9 +94,8 @@ def simplify(expr): # integral = (c1 + c2 + c3) / 3 # God I love Bernstein polynomials - # integral = s.simplify(integral) parsimony = len(str(dfdx_q1)) print(s.pretty(dfdx_q1, num_columns=100)) -print(f"Parsimony: {parsimony}") \ No newline at end of file +print(f"Parsimony: {parsimony}") diff --git a/aerosandbox/numpy/derivative_discrete_derivations/quadratic_2nd_derivative.py b/aerosandbox/numpy/derivative_discrete_derivations/quadratic_2nd_derivative.py index 8c8201511..d09b50e65 100644 --- a/aerosandbox/numpy/derivative_discrete_derivations/quadratic_2nd_derivative.py +++ b/aerosandbox/numpy/derivative_discrete_derivations/quadratic_2nd_derivative.py @@ -1,18 +1,19 @@ import sympy as s from sympy import init_printing + init_printing() # Reconstructs a quadratic interpolant from x1...x3, then gets the derivative at x2 # Define the symbols -x1, x2, x3 = s.symbols('x1 x2 x3', real=True) -f1, f2, f3 = s.symbols('f1 f2 f3', real=True) +x1, x2, x3 = s.symbols("x1 x2 x3", real=True) +f1, f2, f3 = s.symbols("f1 f2 f3", real=True) # hm = x2 - x1 # hp = x3 - x2 hm, hp = s.symbols("hm hp") -q = s.symbols('q') # Normalized space for a Bernstein basis. +q = s.symbols("q") # Normalized space for a Bernstein basis. # Mapping from x-space to q-space has x=x2 -> q=0, x=x3 -> q=1. q1 = 0 @@ -23,9 +24,9 @@ # Define the Bernstein basis polynomials b1 = (1 - q) ** 2 b2 = 2 * q * (1 - q) -b3 = q ** 2 +b3 = q**2 -c1, c2, c3 = s.symbols('c1 c2 c3', real=True) +c1, c2, c3 = s.symbols("c1 c2 c3", real=True) # Can solve for c2 and c3 exactly c1 = f1 @@ -33,7 +34,7 @@ f = c1 * b1 + c2 * b2 + c3 * b3 -f2_quadratic = f.subs(q, q2)#.simplify() +f2_quadratic = f.subs(q, q2) # .simplify() factors = [q2] # factors = [f1, f2, f3, f4] @@ -56,36 +57,38 @@ dqdx = 1 / (x3 - x1) dfdx = dfdq * dqdx -df2dx = ( - dfdx.diff(q) / (x3 - x1) -) +df2dx = dfdx.diff(q) / (x3 - x1) dfm, dfp = s.symbols("dfm dfp") + def simplify(expr): import copy + original_expr = copy.copy(expr) - expr = expr.subs({ - f3 - f2: dfp, - f2 - f1: dfm, - f3 - f1: dfp + dfm, - x3 - x1: hm + hp, - }) - expr = expr.subs({ - f3 - f2: dfp, - f2 - f1: dfm, - f3 - f1: dfp + dfm, - x3 - x1: hm + hp, - f1 - 2 * f2 + f3: dfp - dfm, - }) - expr = expr.factor([ - hp, - hm - ]).simplify() + expr = expr.subs( + { + f3 - f2: dfp, + f2 - f1: dfm, + f3 - f1: dfp + dfm, + x3 - x1: hm + hp, + } + ) + expr = expr.subs( + { + f3 - f2: dfp, + f2 - f1: dfm, + f3 - f1: dfp + dfm, + x3 - x1: hm + hp, + f1 - 2 * f2 + f3: dfp - dfm, + } + ) + expr = expr.factor([hp, hm]).simplify() if expr != original_expr: expr = simplify(expr) return expr + dfdx_q1 = simplify(dfdx.subs(q, q1)) dfdx_q2 = simplify(dfdx.subs(q, q2)) dfdx_q3 = simplify(dfdx.subs(q, q3)) @@ -96,9 +99,8 @@ def simplify(expr): # integral = (c1 + c2 + c3) / 3 # God I love Bernstein polynomials - # integral = s.simplify(integral) parsimony = len(str(df2dx)) print(s.pretty(df2dx, num_columns=100)) -print(f"Parsimony: {parsimony}") \ No newline at end of file +print(f"Parsimony: {parsimony}") diff --git a/aerosandbox/numpy/determine_type.py b/aerosandbox/numpy/determine_type.py index d6c41277a..ae676d595 100644 --- a/aerosandbox/numpy/determine_type.py +++ b/aerosandbox/numpy/determine_type.py @@ -3,10 +3,7 @@ from typing import Any -def is_casadi_type( - object: Any, - recursive: bool = True -) -> bool: +def is_casadi_type(object: Any, recursive: bool = True) -> bool: """ Returns a boolean of whether an object is a CasADi data type or not. If the recursive flag is True, iterates recursively, returning True if any subelement (at any depth) is a CasADi type. @@ -24,37 +21,36 @@ def is_casadi_type( t = type(object) # NumPy arrays cannot be or contain CasADi types, unless they are object arrays - if t == _onp.ndarray and object.dtype != 'O': + if t == _onp.ndarray and object.dtype != "O": return False # Skip certain Python types known not to be or contain CasADi types. for type_to_skip in ( - float, int, complex, - bool, str, - range, - type(None), - bytes, bytearray, memoryview + float, + int, + complex, + bool, + str, + range, + type(None), + bytes, + bytearray, + memoryview, ): if t == type_to_skip: return False # If it's directly a CasADi type, we're done. - if ( - t == _cas.MX or - t == _cas.DM or - t == _cas.SX - ): + if t == _cas.MX or t == _cas.DM or t == _cas.SX: return True # At this point, we know it's not a CasADi type, but we don't know if it *contains* a CasADi type (relevant if recursing) if recursive: if ( - issubclass(t, list) or - issubclass(t, tuple) or - issubclass(t, set) or - ( - t == _onp.ndarray and object.dtype == 'O' - ) + issubclass(t, list) + or issubclass(t, tuple) + or issubclass(t, set) + or (t == _onp.ndarray and object.dtype == "O") ): for element in object: if is_casadi_type(element, recursive=True): diff --git a/aerosandbox/numpy/finite_difference_operators.py b/aerosandbox/numpy/finite_difference_operators.py index bc89e62cb..015764d82 100644 --- a/aerosandbox/numpy/finite_difference_operators.py +++ b/aerosandbox/numpy/finite_difference_operators.py @@ -3,9 +3,9 @@ def finite_difference_coefficients( - x: _onp.ndarray, - x0: float = 0, - derivative_degree: int = 1, + x: _onp.ndarray, + x0: float = 0, + derivative_degree: int = 1, ) -> _onp.ndarray: """ Computes the weights (coefficients) in compact finite differece formulas for any order of derivative @@ -56,24 +56,20 @@ def finite_difference_coefficients( return ValueError("The parameter derivative_degree must be an integer >= 1.") expected_order_of_accuracy = length(x) - derivative_degree if expected_order_of_accuracy < 1: - return ValueError("You need to provide at least (derivative_degree+1) grid points in the x vector.") + return ValueError( + "You need to provide at least (derivative_degree+1) grid points in the x vector." + ) ### Implement algorithm; notation from paper in docstring. N = length(x) - 1 - delta = _onp.zeros( - shape=( - derivative_degree + 1, - N + 1, - N + 1 - ), - dtype="O" - ) + delta = _onp.zeros(shape=(derivative_degree + 1, N + 1, N + 1), dtype="O") delta[0, 0, 0] = 1 c1 = 1 - for n in range(1, - N + 1): # TODO make this algorithm more efficient; we only need to store a fraction of this data. + for n in range( + 1, N + 1 + ): # TODO make this algorithm more efficient; we only need to store a fraction of this data. c2 = 1 for v in range(n): c3 = x[n] - x[v] @@ -82,18 +78,23 @@ def finite_difference_coefficients( # d[n, n - 1, v] = 0 for m in range(min(n, derivative_degree) + 1): delta[m, n, v] = ( - (x[n] - x0) * delta[m, n - 1, v] - m * delta[m - 1, n - 1, v] - ) / c3 + (x[n] - x0) * delta[m, n - 1, v] - m * delta[m - 1, n - 1, v] + ) / c3 for m in range(min(n, derivative_degree) + 1): delta[m, n, n] = ( - c1 / c2 * ( - m * delta[m - 1, n - 1, n - 1] - (x[n - 1] - x0) * delta[m, n - 1, n - 1] - ) + c1 + / c2 + * ( + m * delta[m - 1, n - 1, n - 1] + - (x[n - 1] - x0) * delta[m, n - 1, n - 1] + ) ) c1 = c2 coefficients_object_array = delta[derivative_degree, -1, :] - coefficients = array([*coefficients_object_array]) # Reconstructs using aerosandbox.numpy to intelligently type + coefficients = array( + [*coefficients_object_array] + ) # Reconstructs using aerosandbox.numpy to intelligently type return coefficients diff --git a/aerosandbox/numpy/integrate.py b/aerosandbox/numpy/integrate.py index a44f68905..b5d6cf4e0 100644 --- a/aerosandbox/numpy/integrate.py +++ b/aerosandbox/numpy/integrate.py @@ -5,15 +5,12 @@ def quad( - func: Union[Callable, _cas.MX], - a: float, - b: float, - full_output: bool = False, - variable_of_integration: _cas.MX = None, -) -> Union[ - Tuple[float, float], - Tuple[float, float, dict] -]: + func: Union[Callable, _cas.MX], + a: float, + b: float, + full_output: bool = False, + variable_of_integration: _cas.MX = None, +) -> Union[Tuple[float, float], Tuple[float, float, dict]]: if np.is_casadi_type(func): all_vars = _cas.symvar(func) # All variables found in the expression graph @@ -22,28 +19,28 @@ def quad( if not len(all_vars) == 1: raise ValueError( f"`func` must be a function of one variable, or you must specify the `variable_of_integration`.\n" - f"Currently, it is a function of: {all_vars}") + f"Currently, it is a function of: {all_vars}" + ) variable_of_integration = all_vars[0] parameters = [ - var for var in all_vars - if not _cas.is_equal(var, variable_of_integration) + var for var in all_vars if not _cas.is_equal(var, variable_of_integration) ] integrator = _cas.integrator( - 'integrator', - 'cvodes', + "integrator", + "cvodes", { - 'x' : _cas.MX.sym('dummy_variable'), - 'p' : _cas.vertcat(*parameters), - 't' : variable_of_integration, - 'ode': func, + "x": _cas.MX.sym("dummy_variable"), + "p": _cas.vertcat(*parameters), + "t": variable_of_integration, + "ode": func, }, a, # t0 b, # tf { # Options - 'abstol': 1e-8, - 'reltol': 1e-6, + "abstol": 1e-8, + "reltol": 1e-6, }, ) res = integrator( @@ -53,9 +50,9 @@ def quad( tol = 1e-8 if full_output: - return res['xf'], tol, res + return res["xf"], tol, res else: - return res['xf'], tol + return res["xf"], tol else: return integrate.quad( @@ -67,18 +64,18 @@ def quad( def solve_ivp( - fun: Union[Callable, _cas.MX], - t_span: Tuple[float, float], - y0: Union[np.ndarray, _cas.MX], - method: str = 'RK45', - t_eval: Union[np.ndarray, _cas.MX] = None, - dense_output: bool = False, - events: Union[Callable, List[Callable]] = None, - vectorized: bool = False, - args: Optional[Tuple] = None, - t_variable: _cas.MX = None, - y_variables: Union[_cas.MX, Tuple[_cas.MX]] = None, - **options + fun: Union[Callable, _cas.MX], + t_span: Tuple[float, float], + y0: Union[np.ndarray, _cas.MX], + method: str = "RK45", + t_eval: Union[np.ndarray, _cas.MX] = None, + dense_output: bool = False, + events: Union[Callable, List[Callable]] = None, + vectorized: bool = False, + args: Optional[Tuple] = None, + t_variable: _cas.MX = None, + y_variables: Union[_cas.MX, Tuple[_cas.MX]] = None, + **options, ): # Determine which backend to use @@ -93,31 +90,39 @@ def solve_ivp( try: np.asanyarray(f) except ValueError: - raise ValueError("If `fun` is not a Callable, it must be a CasADi expression.") + raise ValueError( + "If `fun` is not a Callable, it must be a CasADi expression." + ) backend = "numpy_func" except TypeError: - raise TypeError("If `fun` is not a Callable, it must be a CasADi expression.") + raise TypeError( + "If `fun` is not a Callable, it must be a CasADi expression." + ) # Do some checks if backend == "casadi_func" or backend == "numpy_func": if t_variable is not None: - raise ValueError("If `fun` is a Callable, `t_variable` must be None (as it's implied).") + raise ValueError( + "If `fun` is a Callable, `t_variable` must be None (as it's implied)." + ) if y_variables is not None: - raise ValueError("If `fun` is a Callable, `y_variables` must be None (as they're implied).") + raise ValueError( + "If `fun` is a Callable, `y_variables` must be None (as they're implied)." + ) if backend == "casadi_expr": if t_variable is None: raise ValueError( - "If `fun` is a CasADi expression, `t_variable` must be specified (and the y_variables are inferred).") + "If `fun` is a CasADi expression, `t_variable` must be specified (and the y_variables are inferred)." + ) all_vars = _cas.symvar(fun) # All variables found in the expression graph # Determine y_variables by selecting all variables that are not t_variable if y_variables is None: - y_variables = np.array([ - var for var in all_vars - if not _cas.is_equal(var, t_variable) - ]) + y_variables = np.array( + [var for var in all_vars if not _cas.is_equal(var, t_variable)] + ) if backend == "numpy_func": return integrate.solve_ivp( @@ -130,17 +135,23 @@ def solve_ivp( events=events, vectorized=vectorized, args=args, - **options + **options, ) elif backend == "casadi_func" or backend == "casadi_expr": # Exception on non-implemented options if dense_output: - raise NotImplementedError("dense_output is not yet implemented for CasADi functions.") + raise NotImplementedError( + "dense_output is not yet implemented for CasADi functions." + ) if events is not None: - raise NotImplementedError("Events are not yet implemented for CasADi functions.") + raise NotImplementedError( + "Events are not yet implemented for CasADi functions." + ) if args: - raise NotImplementedError("args are not yet implemented for CasADi functions.") + raise NotImplementedError( + "args are not yet implemented for CasADi functions." + ) if not np.is_casadi_type(y0, recursive=False): y0 = _cas.vertcat(*y0) @@ -174,35 +185,39 @@ def solve_ivp( def variable_is_t_or_y(var): return ( - _cas.is_equal(var, t_variable) or - _cas.is_equal(var, y_variables) or - any([_cas.is_equal(var, y_variables[i]) for i in range(np.prod(y_variables.shape))]) + _cas.is_equal(var, t_variable) + or _cas.is_equal(var, y_variables) + or any( + [ + _cas.is_equal(var, y_variables[i]) + for i in range(np.prod(y_variables.shape)) + ] + ) ) - parameters = _cas.vertcat(*[ - var for var in all_vars - if not variable_is_t_or_y(var) - ]) + parameters = _cas.vertcat( + *[var for var in all_vars if not variable_is_t_or_y(var)] + ) simtime_eval = np.linspace(0, 1, 100) # Define the integrator integrator = _cas.integrator( - 'integrator', - 'cvodes', + "integrator", + "cvodes", # 'idas', { - 'x' : y_variables, - 'p' : parameters, - 't' : t_variable, - 'ode' : ode, - 'quad': 1, + "x": y_variables, + "p": parameters, + "t": t_variable, + "ode": ode, + "quad": 1, }, 0, simtime_eval, { # Options - 'abstol': 1e-8, - 'reltol': 1e-6, + "abstol": 1e-8, + "reltol": 1e-6, }, ) res = integrator( @@ -212,7 +227,7 @@ def variable_is_t_or_y(var): return integrate._ivp.ivp.OdeResult( t=t0 + (tf - t0) * res["qf"], - y=res['xf'], + y=res["xf"], t_events=None, y_events=None, nfev=0, @@ -228,7 +243,7 @@ def variable_is_t_or_y(var): raise ValueError(f"Invalid backend: {backend}") -if __name__ == '__main__': +if __name__ == "__main__": # t = cas.MX.sym("t") # print( # quad( @@ -245,7 +260,6 @@ def lotkavolterra_func(t, z): y = z[1] return [a * x - b * x * y, -c * y + d * x * y] - t_eval = np.linspace(0, 15, 3000) tf = _cas.MX.sym("tf") # t_eval = np.linspace(0, tf, 100) @@ -264,19 +278,21 @@ def lotkavolterra_func(t, z): _cas.evalf(_cas.substitute(sol.t.T, tf, 15)), _cas.evalf(_cas.substitute(sol.y.T, tf, 15)), ) - plt.xlabel('t') - plt.legend(['x', 'y'], shadow=True) - plt.title('Lotka-Volterra System') + plt.xlabel("t") + plt.legend(["x", "y"], shadow=True) + plt.title("Lotka-Volterra System") plt.show() t = _cas.MX.sym("t") m = _cas.MX.sym("m") n = _cas.MX.sym("n") a, b, c, d = 1.5, 1, 3, 1 - lotkavolterra_expr = np.array([ - a * m - b * m * n, - -c * n + d * m * n, - ]) + lotkavolterra_expr = np.array( + [ + a * m - b * m * n, + -c * n + d * m * n, + ] + ) sol = solve_ivp( lotkavolterra_expr, @@ -291,7 +307,7 @@ def lotkavolterra_func(t, z): _cas.evalf(_cas.substitute(sol.t.T, tf, 15)), _cas.evalf(_cas.substitute(sol.y.T, tf, 15)), ) - plt.xlabel('t') - plt.legend(['x', 'y'], shadow=True) - plt.title('Lotka-Volterra System') + plt.xlabel("t") + plt.legend(["x", "y"], shadow=True) + plt.title("Lotka-Volterra System") plt.show() diff --git a/aerosandbox/numpy/integrate_discrete.py b/aerosandbox/numpy/integrate_discrete.py index 2ada1c6be..f7ddb1586 100644 --- a/aerosandbox/numpy/integrate_discrete.py +++ b/aerosandbox/numpy/integrate_discrete.py @@ -6,11 +6,11 @@ def integrate_discrete_intervals( - f: Union[_onp.ndarray, _cas.MX], - x: Union[_onp.ndarray, _cas.MX] = None, - multiply_by_dx: bool = True, - method: str = "trapezoidal", - method_endpoints: str = "lower_order", + f: Union[_onp.ndarray, _cas.MX], + x: Union[_onp.ndarray, _cas.MX] = None, + multiply_by_dx: bool = True, + method: str = "trapezoidal", + method_endpoints: str = "lower_order", ): """ Given a set of sampled points (x_i, f_i) from a function, computes the integral of that function over each set of @@ -45,7 +45,7 @@ def integrate_discrete_intervals( """ # Determine if an x-array was specified, and calculate dx. - x_is_specified = (x is not None) + x_is_specified = x is not None if not x_is_specified: x = _onp.arange(length(f)) @@ -60,7 +60,13 @@ def integrate_discrete_intervals( degree = 0 # Refers to the highest degree of the polynomial that the method is exact for. remaining_endpoint_intervals = (0, 0) - elif method in ["backward_euler", "backward", "euler_backward", "right", "right_riemann"]: + elif method in [ + "backward_euler", + "backward", + "euler_backward", + "right", + "right_riemann", + ]: avg_f = f[1:] degree = 0 @@ -69,7 +75,8 @@ def integrate_discrete_intervals( elif method in ["trapezoidal", "trapezoid", "trapz", "midpoint"]: if method == "midpoint": raise PendingDeprecationWarning( - "The 'midpoint' method will be deprecated at a future point, since 'trapezoidal' is the more accurate term here.") + "The 'midpoint' method will be deprecated at a future point, since 'trapezoidal' is the more accurate term here." + ) avg_f = (f[1:] + f[:-1]) / 2 @@ -92,13 +99,9 @@ def integrate_discrete_intervals( # q2 = 1 # Integration upper bound q3 = 1 + hp / h - avg_f = ( - f1 - f3 - + 3 * q3 ** 2 * (f1 + f2) - - 2 * q3 * (2 * f1 + f2) - ) / ( - 6 * q3 * (q3 - 1) - ) + avg_f = (f1 - f3 + 3 * q3**2 * (f1 + f2) - 2 * q3 * (2 * f1 + f2)) / ( + 6 * q3 * (q3 - 1) + ) degree = 2 remaining_endpoint_intervals = (0, 1) @@ -119,13 +122,9 @@ def integrate_discrete_intervals( # q2 = 0 # Integration lower bound # q3 = 1 # Integration upper bound - avg_f = ( - f2 - f1 - + 3 * q1 ** 2 * (f2 + f3) - - 2 * q1 * (2 * f2 + f3) - ) / ( - 6 * q1 * (q1 - 1) - ) + avg_f = (f2 - f1 + 3 * q1**2 * (f2 + f3) - 2 * q1 * (2 * f2 + f3)) / ( + 6 * q1 * (q1 - 1) + ) degree = 2 remaining_endpoint_intervals = (1, 0) @@ -151,21 +150,19 @@ def integrate_discrete_intervals( q4 = 1 + hp / h avg_f = ( - 6 * q1 ** 3 * q4 ** 2 * (f2 + f3) - - 4 * q1 ** 3 * q4 * (2 * f2 + f3) - + 2 * q1 ** 3 * (f2 - f4) - - 6 * q1 ** 2 * q4 ** 3 * (f2 + f3) - + 3 * q1 ** 2 * q4 * (3 * f2 + f3) - + 3 * q1 ** 2 * (f4 - f2) - + 4 * q1 * q4 ** 3 * (2 * f2 + f3) - - 3 * q1 * q4 ** 2 * (3 * f2 + f3) - + q1 * (f2 - f4) - + 2 * q4 ** 3 * (f1 - f2) - + 3 * q4 ** 2 * (f2 - f1) - + q4 * (f1 - f2) - ) / ( - 12 * q1 * q4 * (q1 - 1) * (q1 - q4) * (q4 - 1) - ) + 6 * q1**3 * q4**2 * (f2 + f3) + - 4 * q1**3 * q4 * (2 * f2 + f3) + + 2 * q1**3 * (f2 - f4) + - 6 * q1**2 * q4**3 * (f2 + f3) + + 3 * q1**2 * q4 * (3 * f2 + f3) + + 3 * q1**2 * (f4 - f2) + + 4 * q1 * q4**3 * (2 * f2 + f3) + - 3 * q1 * q4**2 * (3 * f2 + f3) + + q1 * (f2 - f4) + + 2 * q4**3 * (f1 - f2) + + 3 * q4**2 * (f2 - f1) + + q4 * (f1 - f2) + ) / (12 * q1 * q4 * (q1 - 1) * (q1 - q4) * (q4 - 1)) degree = 3 remaining_endpoint_intervals = (1, 1) @@ -183,59 +180,57 @@ def integrate_discrete_intervals( if remaining_endpoint_intervals[0] != 0: avg_f_left_intervals = integrate_discrete_intervals( - f=f[:2 + remaining_endpoint_intervals[0]], - x=x[:2 + remaining_endpoint_intervals[0]], + f=f[: 2 + remaining_endpoint_intervals[0]], + x=x[: 2 + remaining_endpoint_intervals[0]], multiply_by_dx=False, method="forward_simpson", method_endpoints="ignore", ) - avg_f = concatenate(( - avg_f_left_intervals, - avg_f - )) + avg_f = concatenate((avg_f_left_intervals, avg_f)) if remaining_endpoint_intervals[1] != 0: avg_f_right_intervals = integrate_discrete_intervals( - f=f[-(2 + remaining_endpoint_intervals[1]):], - x=x[-(2 + remaining_endpoint_intervals[1]):], + f=f[-(2 + remaining_endpoint_intervals[1]) :], + x=x[-(2 + remaining_endpoint_intervals[1]) :], multiply_by_dx=False, method="backward_simpson", method_endpoints="ignore", ) - avg_f = concatenate(( - avg_f, - avg_f_right_intervals, - )) + avg_f = concatenate( + ( + avg_f, + avg_f_right_intervals, + ) + ) elif method_endpoints == "trapezoidal": if remaining_endpoint_intervals[0] != 0: avg_f_left_intervals = integrate_discrete_intervals( - f=f[:1 + remaining_endpoint_intervals[0]], - x=x[:1 + remaining_endpoint_intervals[0]], + f=f[: 1 + remaining_endpoint_intervals[0]], + x=x[: 1 + remaining_endpoint_intervals[0]], multiply_by_dx=False, method="trapezoidal", method_endpoints="ignore", ) - avg_f = concatenate(( - avg_f_left_intervals, - avg_f - )) + avg_f = concatenate((avg_f_left_intervals, avg_f)) if remaining_endpoint_intervals[1] != 0: avg_f_right_intervals = integrate_discrete_intervals( - f=f[-(1 + remaining_endpoint_intervals[1]):], - x=x[-(1 + remaining_endpoint_intervals[1]):], + f=f[-(1 + remaining_endpoint_intervals[1]) :], + x=x[-(1 + remaining_endpoint_intervals[1]) :], multiply_by_dx=False, method="trapezoidal", method_endpoints="ignore", ) - avg_f = concatenate(( - avg_f, - avg_f_right_intervals, - )) + avg_f = concatenate( + ( + avg_f, + avg_f_right_intervals, + ) + ) else: raise ValueError(f"Invalid method_endpoints '{method_endpoints}'.") @@ -259,9 +254,9 @@ def integrate_discrete_intervals( def integrate_discrete_squared_curvature( - f: Union[_onp.ndarray, _cas.MX], - x: Union[_onp.ndarray, _cas.MX] = None, - method: str = "hybrid_simpson_cubic", + f: Union[_onp.ndarray, _cas.MX], + x: Union[_onp.ndarray, _cas.MX] = None, + method: str = "hybrid_simpson_cubic", ): """ Given a set of sampled points (x_i, f_i) from a function f(x), computes the following quantity: @@ -317,7 +312,7 @@ def integrate_discrete_squared_curvature( """ # Determine if an x-array was specified, and calculate dx. - x_is_specified = (x is not None) + x_is_specified = x is not None if not x_is_specified: x = _onp.arange(length(f)) @@ -343,24 +338,24 @@ def integrate_discrete_squared_curvature( ### The following section computes the integral of the squared second derivative of the cubic spline interpolant ### for the "middle" intervals (i.e. not the first or last intervals). ### Code is generated by sympy; here s_i variables represent common subexpressions. - s0 = hm ** 2 - s1 = hp ** 2 + s0 = hm**2 + s1 = hp**2 s2 = h + hm - s3 = h ** 2 - s4 = hp ** 6 - s5 = h ** 6 - s6 = hp ** 5 - s7 = h ** 3 + s3 = h**2 + s4 = hp**6 + s5 = h**6 + s6 = hp**5 + s7 = h**3 s8 = 3 * s7 - s9 = hp ** 4 - s10 = h ** 4 + s9 = hp**4 + s10 = h**4 s11 = 4 * s10 - s12 = hp ** 3 - s13 = 3 * h ** 5 - s14 = hm ** 6 - s15 = hm ** 5 - s16 = hm ** 4 - s17 = hm ** 3 + s12 = hp**3 + s13 = 3 * h**5 + s14 = hm**6 + s15 = hm**5 + s16 = hm**4 + s17 = hm**3 s18 = hm * s10 * s12 s19 = hp * s10 * s17 s20 = s12 * s17 @@ -373,14 +368,57 @@ def integrate_discrete_squared_curvature( s27 = 3 * s3 s28 = -s20 * s27 s29 = 3 * h - middle_intervals = 4 * (df ** 2 * ( - s0 * s27 * s9 + s0 * s29 * s6 + s0 * s4 + s1 * s14 + s1 * s15 * s29 + s1 * s16 * s27 - s12 * s16 * s29 - 2 * s16 * s9 - s17 * s29 * s9 + s23 + s28) + df * dfm * ( - 2 * h * s17 * s9 - hm * s1 * s13 - hm * s24 * s4 - hm * s25 * s6 - hm * s26 * s9 + 3 * s0 * s3 * s9 + s1 * s17 * s7 - 6 * s18 + s21 - s28) + df * dfp * ( - 2 * h * s12 * s16 - hp * s0 * s13 - hp * s14 * s24 - hp * s15 * s25 - hp * s16 * s26 + s0 * s12 * s7 + 3 * s1 * s16 * s3 - 6 * s19 + s22 - s28) + dfm ** 2 * ( - s1 * s5 + s11 * s9 + s12 * s13 + s3 * s4 + s6 * s8) + dfm * dfp * ( - hm * hp * s5 - s18 - s19 - 2 * s20 * s3 - s23) + dfp ** 2 * ( - s0 * s5 + s11 * s16 + s13 * s17 + s14 * s3 + s15 * s8)) / ( - h * s0 * s1 * s2 ** 2 * (h + hp) ** 2 * (hp + s2) ** 2) + middle_intervals = ( + 4 + * ( + df**2 + * ( + s0 * s27 * s9 + + s0 * s29 * s6 + + s0 * s4 + + s1 * s14 + + s1 * s15 * s29 + + s1 * s16 * s27 + - s12 * s16 * s29 + - 2 * s16 * s9 + - s17 * s29 * s9 + + s23 + + s28 + ) + + df + * dfm + * ( + 2 * h * s17 * s9 + - hm * s1 * s13 + - hm * s24 * s4 + - hm * s25 * s6 + - hm * s26 * s9 + + 3 * s0 * s3 * s9 + + s1 * s17 * s7 + - 6 * s18 + + s21 + - s28 + ) + + df + * dfp + * ( + 2 * h * s12 * s16 + - hp * s0 * s13 + - hp * s14 * s24 + - hp * s15 * s25 + - hp * s16 * s26 + + s0 * s12 * s7 + + 3 * s1 * s16 * s3 + - 6 * s19 + + s22 + - s28 + ) + + dfm**2 * (s1 * s5 + s11 * s9 + s12 * s13 + s3 * s4 + s6 * s8) + + dfm * dfp * (hm * hp * s5 - s18 - s19 - 2 * s20 * s3 - s23) + + dfp**2 * (s0 * s5 + s11 * s16 + s13 * s17 + s14 * s3 + s15 * s8) + ) + / (h * s0 * s1 * s2**2 * (h + hp) ** 2 * (hp + s2) ** 2) + ) ### Now we compute the integral for the first interval. h_f = h[slice(0, 1)] @@ -390,33 +428,33 @@ def integrate_discrete_squared_curvature( dfm_f = dfm[slice(0, 1)] dfp_f = dfp[slice(0, 1)] - s0 = h_f ** 2 - s1 = hp_f ** 2 + s0 = h_f**2 + s1 = hp_f**2 s2 = h_f + hm_f - s3 = hp_f ** 6 + s3 = hp_f**6 s4 = df_f * dfm_f - s5 = hm_f ** 6 + s5 = hm_f**6 s6 = 2 * dfp_f s7 = df_f * s6 - s8 = h_f ** 6 + s8 = h_f**6 s9 = dfm_f * dfp_f s10 = 4 * s9 - s11 = df_f ** 2 - s12 = hm_f ** 2 - s13 = dfm_f ** 2 - s14 = dfp_f ** 2 - s15 = hm_f ** 3 - s16 = hp_f ** 5 + s11 = df_f**2 + s12 = hm_f**2 + s13 = dfm_f**2 + s14 = dfp_f**2 + s15 = hm_f**3 + s16 = hp_f**5 s17 = 3 * s11 - s18 = hp_f ** 4 - s19 = hm_f ** 4 + s18 = hp_f**4 + s19 = hm_f**4 s20 = 4 * s19 - s21 = hm_f ** 5 - s22 = hp_f ** 3 - s23 = h_f ** 3 + s21 = hm_f**5 + s22 = hp_f**3 + s23 = h_f**3 s24 = 6 * s13 - s25 = h_f ** 4 - s26 = h_f ** 5 + s25 = h_f**4 + s26 = h_f**5 s27 = 3 * s14 s28 = df_f * dfp_f s29 = -dfm_f @@ -433,22 +471,48 @@ def integrate_discrete_squared_curvature( s40 = -3 * s28 + s9 s41 = 18 * s11 s42 = -s7 - first_interval = 4 * ( - -2 * h_f * hm_f * s3 * s4 - h_f * hp_f * s5 * s7 + hm_f * hp_f * s10 * s8 - hm_f * s0 * s16 * s34 * ( - 4 * df_f + s29) - 9 * hp_f * s0 * s21 * s28 + s0 * s13 * s3 + s0 * s14 * s5 + s0 * s15 * s22 * ( - 27 * s11 - 21 * s4 + s40) + s1 * s11 * s5 + 4 * s1 * s13 * s8 + s1 * s15 * s23 * ( - -12 * s28 - 14 * s4 + s41 + 9 * s9) + s1 * s19 * s39 * ( - s38 - s4 + s40) + 3 * s1 * s21 * s30 * (-dfp_f + s32) + s1 * s25 * s37 * ( - s10 + s13 + s17 - 7 * s4 + s42) + 6 * s1 * s26 * s33 * ( - dfm_f + dfp_f - s32) + s11 * s12 * s3 + s11 * s18 * s20 + s12 * s14 * s8 + 6 * s12 * s16 * s30 * ( - df_f + s29) + s12 * s18 * s39 * (s13 + s38 - 9 * s4) + s12 * s22 * s23 * ( - s24 - 42 * s4 + s41 + s42 + 3 * s9) + 13 * s13 * s18 * s25 + 12 * s13 * s22 * s26 + s14 * s20 * s25 + s15 * s16 * s17 + s15 * s18 * s30 * ( - -7 * dfm_f + s31) - s15 * s25 * s36 * ( - -8 * dfm_f + s31) + s15 * s26 * s27 + s16 * s23 * s24 + s17 * s21 * s22 - 4 * s18 * s23 * s33 * ( - 7 * df_f + s35) - s19 * s22 * s30 * (dfp_f - s31 + s34) - s19 * s23 * s36 * ( - 16 * df_f + s35) + s21 * s23 * s27 + s22 * s25 * s33 * ( - -30 * df_f + 15 * dfm_f + s6) - s26 * s36 * s37 * (s32 + s35)) / ( - hm_f * s0 * s1 * s2 ** 2 * (h_f + hp_f) ** 2 * (hp_f + s2) ** 2) + first_interval = ( + 4 + * ( + -2 * h_f * hm_f * s3 * s4 + - h_f * hp_f * s5 * s7 + + hm_f * hp_f * s10 * s8 + - hm_f * s0 * s16 * s34 * (4 * df_f + s29) + - 9 * hp_f * s0 * s21 * s28 + + s0 * s13 * s3 + + s0 * s14 * s5 + + s0 * s15 * s22 * (27 * s11 - 21 * s4 + s40) + + s1 * s11 * s5 + + 4 * s1 * s13 * s8 + + s1 * s15 * s23 * (-12 * s28 - 14 * s4 + s41 + 9 * s9) + + s1 * s19 * s39 * (s38 - s4 + s40) + + 3 * s1 * s21 * s30 * (-dfp_f + s32) + + s1 * s25 * s37 * (s10 + s13 + s17 - 7 * s4 + s42) + + 6 * s1 * s26 * s33 * (dfm_f + dfp_f - s32) + + s11 * s12 * s3 + + s11 * s18 * s20 + + s12 * s14 * s8 + + 6 * s12 * s16 * s30 * (df_f + s29) + + s12 * s18 * s39 * (s13 + s38 - 9 * s4) + + s12 * s22 * s23 * (s24 - 42 * s4 + s41 + s42 + 3 * s9) + + 13 * s13 * s18 * s25 + + 12 * s13 * s22 * s26 + + s14 * s20 * s25 + + s15 * s16 * s17 + + s15 * s18 * s30 * (-7 * dfm_f + s31) + - s15 * s25 * s36 * (-8 * dfm_f + s31) + + s15 * s26 * s27 + + s16 * s23 * s24 + + s17 * s21 * s22 + - 4 * s18 * s23 * s33 * (7 * df_f + s35) + - s19 * s22 * s30 * (dfp_f - s31 + s34) + - s19 * s23 * s36 * (16 * df_f + s35) + + s21 * s23 * s27 + + s22 * s25 * s33 * (-30 * df_f + 15 * dfm_f + s6) + - s26 * s36 * s37 * (s32 + s35) + ) + / (hm_f * s0 * s1 * s2**2 * (h_f + hp_f) ** 2 * (hp_f + s2) ** 2) + ) ### Now we compute the integral for the last interval. h_l = h[slice(-1, None)] @@ -458,33 +522,33 @@ def integrate_discrete_squared_curvature( dfm_l = dfm[slice(-1, None)] dfp_l = dfp[slice(-1, None)] - s0 = h_l ** 2 - s1 = hm_l ** 2 + s0 = h_l**2 + s1 = hm_l**2 s2 = h_l + hm_l - s3 = hp_l ** 6 + s3 = hp_l**6 s4 = 2 * dfm_l s5 = df_l * s4 - s6 = hm_l ** 6 + s6 = hm_l**6 s7 = df_l * dfp_l - s8 = h_l ** 6 + s8 = h_l**6 s9 = dfm_l * dfp_l s10 = 4 * s9 - s11 = df_l ** 2 - s12 = hp_l ** 2 - s13 = dfm_l ** 2 - s14 = dfp_l ** 2 - s15 = hm_l ** 3 - s16 = hp_l ** 5 + s11 = df_l**2 + s12 = hp_l**2 + s13 = dfm_l**2 + s14 = dfp_l**2 + s15 = hm_l**3 + s16 = hp_l**5 s17 = 3 * s11 - s18 = hm_l ** 4 - s19 = hp_l ** 4 + s18 = hm_l**4 + s19 = hp_l**4 s20 = 4 * s19 - s21 = hm_l ** 5 - s22 = hp_l ** 3 - s23 = h_l ** 3 + s21 = hm_l**5 + s22 = hp_l**3 + s23 = h_l**3 s24 = 3 * s13 - s25 = h_l ** 4 - s26 = h_l ** 5 + s25 = h_l**4 + s26 = h_l**5 s27 = 6 * s14 s28 = df_l * dfm_l s29 = -dfp_l @@ -501,30 +565,57 @@ def integrate_discrete_squared_curvature( s40 = -3 * s28 + s9 s41 = 18 * s11 s42 = -s5 - last_interval = 4 * ( - -h_l * hm_l * s3 * s5 - 2 * h_l * hp_l * s6 * s7 + hm_l * hp_l * s10 * s8 - 9 * hm_l * s0 * s16 * s28 - hp_l * s0 * s21 * s34 * ( - 4 * df_l + s29) + s0 * s13 * s3 + s0 * s14 * s6 + s0 * s15 * s22 * ( - 27 * s11 + s40 - 21 * s7) + s1 * s11 * s3 + 4 * s1 * s14 * s8 + 3 * s1 * s16 * s30 * ( - -dfm_l + s32) + s1 * s19 * s39 * (s38 + s40 - s7) + s1 * s22 * s23 * ( - -12 * s28 + s41 - 14 * s7 + 9 * s9) + s1 * s25 * s37 * ( - s10 + s14 + s17 + s42 - 7 * s7) + 6 * s1 * s26 * s33 * ( - dfm_l + dfp_l - s32) + s11 * s12 * s6 + s11 * s18 * s20 + s12 * s13 * s8 + s12 * s15 * s23 * ( - s27 + s41 + s42 - 42 * s7 + 3 * s9) + s12 * s18 * s39 * ( - s14 + s38 - 9 * s7) + 6 * s12 * s21 * s30 * ( - df_l + s29) + s13 * s20 * s25 + 12 * s14 * s15 * s26 + 13 * s14 * s18 * s25 + s15 * s16 * s17 - s15 * s19 * s30 * ( - dfm_l - s31 + s34) + s15 * s25 * s33 * ( - -30 * df_l + 15 * dfp_l + s4) + s16 * s23 * s24 + s17 * s21 * s22 + s18 * s22 * s30 * ( - -7 * dfp_l + s31) - 4 * s18 * s23 * s33 * (7 * df_l + s35) - s19 * s23 * s36 * ( - 16 * df_l + s35) + s21 * s23 * s27 + s22 * s24 * s26 - s22 * s25 * s36 * ( - -8 * dfp_l + s31) - s26 * s36 * s37 * (s32 + s35)) / ( - hp_l * s0 * s1 * s2 ** 2 * (h_l + hp_l) ** 2 * (hp_l + s2) ** 2) + last_interval = ( + 4 + * ( + -h_l * hm_l * s3 * s5 + - 2 * h_l * hp_l * s6 * s7 + + hm_l * hp_l * s10 * s8 + - 9 * hm_l * s0 * s16 * s28 + - hp_l * s0 * s21 * s34 * (4 * df_l + s29) + + s0 * s13 * s3 + + s0 * s14 * s6 + + s0 * s15 * s22 * (27 * s11 + s40 - 21 * s7) + + s1 * s11 * s3 + + 4 * s1 * s14 * s8 + + 3 * s1 * s16 * s30 * (-dfm_l + s32) + + s1 * s19 * s39 * (s38 + s40 - s7) + + s1 * s22 * s23 * (-12 * s28 + s41 - 14 * s7 + 9 * s9) + + s1 * s25 * s37 * (s10 + s14 + s17 + s42 - 7 * s7) + + 6 * s1 * s26 * s33 * (dfm_l + dfp_l - s32) + + s11 * s12 * s6 + + s11 * s18 * s20 + + s12 * s13 * s8 + + s12 * s15 * s23 * (s27 + s41 + s42 - 42 * s7 + 3 * s9) + + s12 * s18 * s39 * (s14 + s38 - 9 * s7) + + 6 * s12 * s21 * s30 * (df_l + s29) + + s13 * s20 * s25 + + 12 * s14 * s15 * s26 + + 13 * s14 * s18 * s25 + + s15 * s16 * s17 + - s15 * s19 * s30 * (dfm_l - s31 + s34) + + s15 * s25 * s33 * (-30 * df_l + 15 * dfp_l + s4) + + s16 * s23 * s24 + + s17 * s21 * s22 + + s18 * s22 * s30 * (-7 * dfp_l + s31) + - 4 * s18 * s23 * s33 * (7 * df_l + s35) + - s19 * s23 * s36 * (16 * df_l + s35) + + s21 * s23 * s27 + + s22 * s24 * s26 + - s22 * s25 * s36 * (-8 * dfp_l + s31) + - s26 * s36 * s37 * (s32 + s35) + ) + / (hp_l * s0 * s1 * s2**2 * (h_l + hp_l) ** 2 * (hp_l + s2) ** 2) + ) ### Now, we stitch together the intervals. - res = concatenate(( - first_interval, - middle_intervals, - last_interval, - )) + res = concatenate( + ( + first_interval, + middle_intervals, + last_interval, + ) + ) return res @@ -544,7 +635,7 @@ def integrate_discrete_squared_curvature( df = f3 - f2 dfp = f4 - f3 - res_forward_simpson = 4 * (df * hp - dfp * h) ** 2 / (h * hp ** 2 * (h + hp) ** 2) + res_forward_simpson = 4 * (df * hp - dfp * h) ** 2 / (h * hp**2 * (h + hp) ** 2) ### Backward Simpson for intervals 1 to N-1 x1 = x[:-2] @@ -561,7 +652,9 @@ def integrate_discrete_squared_curvature( dfm = f2 - f1 df = f3 - f2 - res_backward_simpson = 4 * (df * hm - dfm * h) ** 2 / (h * hm ** 2 * (h + hm) ** 2) + res_backward_simpson = ( + 4 * (df * hm - dfm * h) ** 2 / (h * hm**2 * (h + hm) ** 2) + ) ### Fuse them together first_interval = res_forward_simpson[slice(0, 1)] @@ -570,44 +663,43 @@ def integrate_discrete_squared_curvature( b = res_forward_simpson[slice(1, None)] # middle_intervals = (a + b) / 2 - middle_intervals = ((a ** 2 + b ** 2) / 2 + 1e-100) ** 0.5 # This is more accurate across all frequencies + middle_intervals = ( + (a**2 + b**2) / 2 + 1e-100 + ) ** 0.5 # This is more accurate across all frequencies last_interval = res_backward_simpson[slice(-1, None)] - res = concatenate(( - first_interval, - middle_intervals, - last_interval, - )) + res = concatenate( + ( + first_interval, + middle_intervals, + last_interval, + ) + ) return res elif method in ["hybrid_simpson_cubic"]: from aerosandbox.numpy.calculus import gradient - dfdx = gradient( - f, - x, - edge_order=2 - ) + + dfdx = gradient(f, x, edge_order=2) h = x[1:] - x[:-1] df = f[1:] - f[:-1] dfdx1 = dfdx[:-1] dfdx2 = dfdx[1:] - res = ( - 4 * (dfdx1 ** 2 + dfdx1 * dfdx2 + dfdx2 ** 2) / h - + 12 * df / h ** 2 * (df / h - dfdx1 - dfdx2) + res = 4 * (dfdx1**2 + dfdx1 * dfdx2 + dfdx2**2) / h + 12 * df / h**2 * ( + df / h - dfdx1 - dfdx2 ) return res - else: raise ValueError(f"Invalid method '{method}'.") -if __name__ == '__main__': +if __name__ == "__main__": import aerosandbox as asb import aerosandbox.numpy as np from scipy import integrate, interpolate @@ -631,12 +723,10 @@ def integrate_discrete_squared_curvature( a = 0 b = 2 - def f(x): sin = np.sin if isinstance(x, np.ndarray) else s.sin return sin(2 * np.pi * x * 1) + 1 - print("\n\nTest 1: Integration") exact = integrate.quad( f, @@ -688,11 +778,7 @@ def f(x): print(f"error: {integral - exact}") approx = integrate_discrete_intervals( - f=np.gradient( - f_vals, - x_vals, - n=2 - ) ** 2, + f=np.gradient(f_vals, x_vals, n=2) ** 2, x=x_vals, ) integral = np.sum(approx) @@ -705,11 +791,7 @@ def f(x): x = np.arange(0, 100 + 1) f = np.cos(np.pi * x / 2) - f_interp = interpolate.InterpolatedUnivariateSpline( - x=x, - y=f, - k=3 - ) + f_interp = interpolate.InterpolatedUnivariateSpline(x=x, y=f, k=3) exact = integrate.quad( lambda x: f_interp.derivative(2)(x) ** 2, x[0], diff --git a/aerosandbox/numpy/integrate_discrete_derivations/cubic_spline_integration.py b/aerosandbox/numpy/integrate_discrete_derivations/cubic_spline_integration.py index 610f1b4d9..b3782acba 100644 --- a/aerosandbox/numpy/integrate_discrete_derivations/cubic_spline_integration.py +++ b/aerosandbox/numpy/integrate_discrete_derivations/cubic_spline_integration.py @@ -1,12 +1,12 @@ def x2_to_x3_integral( - x1, - x2, - x3, - x4, - f1, - f2, - f3, - f4, + x1, + x2, + x3, + x4, + f1, + f2, + f3, + f4, ): h = x3 - x2 hm = x2 - x1 @@ -16,32 +16,25 @@ def x2_to_x3_integral( q4 = 1 + hp / h avg_f = ( - 6 * q1 ** 3 * q4 ** 2 * (f2 + f3) - - 4 * q1 ** 3 * q4 * (2 * f2 + f3) - + 2 * q1 ** 3 * (f2 - f4) - - 6 * q1 ** 2 * q4 ** 3 * (f2 + f3) - + 3 * q1 ** 2 * q4 * (3 * f2 + f3) - + 3 * q1 ** 2 * (-f2 + f4) - + 4 * q1 * q4 ** 3 * (2 * f2 + f3) - - 3 * q1 * q4 ** 2 * (3 * f2 + f3) - + q1 * (f2 - f4) - + 2 * q4 ** 3 * (f1 - f2) - + 3 * q4 ** 2 * (-f1 + f2) - + q4 * (f1 - f2) - ) / ( - 12 * q1 * q4 * (q1 - 1) * (q1 - q4) * (q4 - 1) - ) + 6 * q1**3 * q4**2 * (f2 + f3) + - 4 * q1**3 * q4 * (2 * f2 + f3) + + 2 * q1**3 * (f2 - f4) + - 6 * q1**2 * q4**3 * (f2 + f3) + + 3 * q1**2 * q4 * (3 * f2 + f3) + + 3 * q1**2 * (-f2 + f4) + + 4 * q1 * q4**3 * (2 * f2 + f3) + - 3 * q1 * q4**2 * (3 * f2 + f3) + + q1 * (f2 - f4) + + 2 * q4**3 * (f1 - f2) + + 3 * q4**2 * (-f1 + f2) + + q4 * (f1 - f2) + ) / (12 * q1 * q4 * (q1 - 1) * (q1 - q4) * (q4 - 1)) return avg_f * h def f(x): - return ( - -0.2 * x ** 3 - + x ** 2 - - x - + 1 - ) + return -0.2 * x**3 + x**2 - x + 1 a = 1 diff --git a/aerosandbox/numpy/integrate_discrete_derivations/cubic_spline_interpolant_derivation.py b/aerosandbox/numpy/integrate_discrete_derivations/cubic_spline_interpolant_derivation.py index 3227f4125..0fa2c68b9 100644 --- a/aerosandbox/numpy/integrate_discrete_derivations/cubic_spline_interpolant_derivation.py +++ b/aerosandbox/numpy/integrate_discrete_derivations/cubic_spline_interpolant_derivation.py @@ -1,33 +1,34 @@ import sympy as s from sympy import init_printing + init_printing() # Gets the integral from x2 to x3, looking at the cubic spline interpolant from x1...x4 # Define the symbols -x1, x2, x3, x4 = s.symbols('x1 x2 x3 x4', real=True) -f1, f2, f3, f4 = s.symbols('f1 f2 f3 f4', real=True) +x1, x2, x3, x4 = s.symbols("x1 x2 x3 x4", real=True) +f1, f2, f3, f4 = s.symbols("f1 f2 f3 f4", real=True) h = x3 - x2 hm = x2 - x1 hp = x4 - x3 -q = s.symbols('q') # Normalized space for a Bernstein basis. +q = s.symbols("q") # Normalized space for a Bernstein basis. # Mapping from x-space to q-space has x=x2 -> q=0, x=x3 -> q=1. q2 = 0 q3 = 1 # q1 = q2 - hm / h # q4 = q3 + hp / h -q1, q4 = s.symbols('q1 q4', real=True) +q1, q4 = s.symbols("q1 q4", real=True) # Define the Bernstein basis polynomials b1 = (1 - q) ** 3 b2 = 3 * q * (1 - q) ** 2 -b3 = 3 * q ** 2 * (1 - q) -b4 = q ** 3 +b3 = 3 * q**2 * (1 - q) +b4 = q**3 -c1, c2, c3, c4 = s.symbols('c1 c2 c3 c4', real=True) +c1, c2, c3, c4 = s.symbols("c1 c2 c3 c4", real=True) # Can solve for c2 and c3 exactly c1 = f2 @@ -35,8 +36,8 @@ f = c1 * b1 + c2 * b2 + c3 * b3 + c4 * b4 -f1_cubic = f.subs(q, q1)#.simplify() -f4_cubic = f.subs(q, q4)#.simplify() +f1_cubic = f.subs(q, q1) # .simplify() +f4_cubic = f.subs(q, q4) # .simplify() factors = [q1, q4] # factors = [f1, f2, f3, f4] @@ -56,12 +57,11 @@ c3 = sol[c3].factor(factors).simplify() - -integral = (c1 + c2 + c3 + c4) / 4 # God I love Bernstein polynomials +integral = (c1 + c2 + c3 + c4) / 4 # God I love Bernstein polynomials # integral = s.simplify(integral) integral = integral.factor(factors).simplify() parsimony = len(str(integral)) print(s.pretty(integral, num_columns=100)) -print(f"Parsimony: {parsimony}") \ No newline at end of file +print(f"Parsimony: {parsimony}") diff --git a/aerosandbox/numpy/integrate_discrete_derivations/discrete_squared_curvature_derivation_bernstein.py b/aerosandbox/numpy/integrate_discrete_derivations/discrete_squared_curvature_derivation_bernstein.py index f3dd09d2c..529a8a7b2 100644 --- a/aerosandbox/numpy/integrate_discrete_derivations/discrete_squared_curvature_derivation_bernstein.py +++ b/aerosandbox/numpy/integrate_discrete_derivations/discrete_squared_curvature_derivation_bernstein.py @@ -8,7 +8,7 @@ # Define the symbols hm, h, hp = s.symbols("hm h hp", real=True) -x1, x2, x3, x4 = s.symbols('x1 x2 x3 x4', real=True) +x1, x2, x3, x4 = s.symbols("x1 x2 x3 x4", real=True) # f1, f2, f3, f4 = s.symbols('f1 f2 f3 f4', real=True) dfm, df, dfp = s.symbols("dfm df dfp", real=True) @@ -21,6 +21,7 @@ def simplify(expr, maxdepth=10, _depth=0): import copy + original_expr = copy.copy(expr) print(f"Depth: {_depth} | Parsimony: {len(str(expr))}") maps = { @@ -52,7 +53,7 @@ def simplify(expr, maxdepth=10, _depth=0): return expr -q = s.symbols('q') # Normalized space for a Bernstein basis. +q = s.symbols("q") # Normalized space for a Bernstein basis. # Mapping from x-space to q-space has x=x2 -> q=0, x=x3 -> q=1. q2 = 0 @@ -64,10 +65,10 @@ def simplify(expr, maxdepth=10, _depth=0): # Define the Bernstein basis polynomials b1 = (1 - q) ** 3 b2 = 3 * q * (1 - q) ** 2 -b3 = 3 * q ** 2 * (1 - q) -b4 = q ** 3 +b3 = 3 * q**2 * (1 - q) +b4 = q**3 -c1, c2, c3, c4 = s.symbols('c1 c2 c3 c4', real=True) +c1, c2, c3, c4 = s.symbols("c1 c2 c3 c4", real=True) # Can solve for c2 and c3 exactly c1 = f2 @@ -101,16 +102,13 @@ def simplify(expr, maxdepth=10, _depth=0): dfdx = f.diff(q) / h df2dx = dfdx.diff(q) / h -res = s.integrate(df2dx ** 2, (q, q2, q3)) * h +res = s.integrate(df2dx**2, (q, q2, q3)) * h res = simplify(res.factor(factors)) res = simplify(res) res = simplify(res.subs({c2: c2_sol, c3: c3_sol}).factor(factors)) -cse = s.cse( - res, - symbols=s.numbered_symbols("s"), - list=False) +cse = s.cse(res, symbols=s.numbered_symbols("s"), list=False) parsimony = len(str(res)) # print(s.pretty(res, num_columns=100)) @@ -120,16 +118,14 @@ def simplify(expr, maxdepth=10, _depth=0): print(f"{var} = {expr}") print(f"res = {cse[1]}") -if __name__ == '__main__': - x = s.symbols('x', real=True) +if __name__ == "__main__": + x = s.symbols("x", real=True) a = 0 b = 4 - def example_f(x): - return x ** 3 + 1 - + return x**3 + 1 h_val = b - a hm_val = 1 @@ -141,26 +137,19 @@ def example_f(x): ddfp_val = dfp_val - df_val subs = { - h : h_val, - hm : hm_val, - hp : hp_val, - df : df_val, + h: h_val, + hm: hm_val, + hp: hp_val, + df: df_val, dfm: dfm_val, dfp: dfp_val, # ddfm: ddfm_val, # ddfp: ddfp_val, } - exact = s.N( - s.integrate( - example_f(x).diff(x, 2) ** 2, - (x, a, b) - ) - ) + exact = s.N(s.integrate(example_f(x).diff(x, 2) ** 2, (x, a, b))) - eqn = s.N( - res.subs(subs) - ) + eqn = s.N(res.subs(subs)) print(f"exact: {exact}") print(f"eqn: {eqn}") diff --git a/aerosandbox/numpy/integrate_discrete_derivations/discrete_squared_curvature_derivation_hybrid.py b/aerosandbox/numpy/integrate_discrete_derivations/discrete_squared_curvature_derivation_hybrid.py index 37d063cf2..90406dbd9 100644 --- a/aerosandbox/numpy/integrate_discrete_derivations/discrete_squared_curvature_derivation_hybrid.py +++ b/aerosandbox/numpy/integrate_discrete_derivations/discrete_squared_curvature_derivation_hybrid.py @@ -3,8 +3,10 @@ init_printing() + def simplify(expr, maxdepth=10, _depth=0): import copy + original_expr = copy.copy(expr) print(f"Depth: {_depth} | Parsimony: {len(str(expr))}") expr = expr.simplify() @@ -18,6 +20,8 @@ def simplify(expr, maxdepth=10, _depth=0): return original_expr else: return expr + + ##### Now, a new problem: ##### Do a cubic reconstruction of an interval using values and first derivatives at the endpoints. @@ -39,10 +43,10 @@ def simplify(expr, maxdepth=10, _depth=0): # Define the Bernstein basis polynomials b1 = (1 - q) ** 3 b2 = 3 * q * (1 - q) ** 2 -b3 = 3 * q ** 2 * (1 - q) -b4 = q ** 3 +b3 = 3 * q**2 * (1 - q) +b4 = q**3 -c1, c2, c3, c4 = s.symbols('c1 c2 c3 c4', real=True) +c1, c2, c3, c4 = s.symbols("c1 c2 c3 c4", real=True) # Can solve for c1 and c4 exactly c1 = f1 @@ -69,14 +73,10 @@ def simplify(expr, maxdepth=10, _depth=0): dfdx = f.diff(q) * dqdx df2dx = dfdx.diff(q) * dqdx -res = s.integrate(df2dx ** 2, (q, 0, 1)) * h +res = s.integrate(df2dx**2, (q, 0, 1)) * h res = simplify(res) -cse = s.cse( - res, - symbols=s.numbered_symbols("s"), - list=False -) +cse = s.cse(res, symbols=s.numbered_symbols("s"), list=False) parsimony = len(str(res)) print("\nSquared Curvature:") diff --git a/aerosandbox/numpy/integrate_discrete_derivations/discrete_squared_curvature_derivation_lagrange.py b/aerosandbox/numpy/integrate_discrete_derivations/discrete_squared_curvature_derivation_lagrange.py index d96532b97..86c98f4a3 100644 --- a/aerosandbox/numpy/integrate_discrete_derivations/discrete_squared_curvature_derivation_lagrange.py +++ b/aerosandbox/numpy/integrate_discrete_derivations/discrete_squared_curvature_derivation_lagrange.py @@ -9,14 +9,15 @@ hm, h, hp = s.symbols("hm h hp", real=True) dfm, df, dfp = s.symbols("dfm df dfp", real=True) -x1, x2, x3, x4 = s.symbols('x1 x2 x3 x4', real=True) -f1, f2, f3, f4 = s.symbols('f1 f2 f3 f4', real=True) +x1, x2, x3, x4 = s.symbols("x1 x2 x3 x4", real=True) +f1, f2, f3, f4 = s.symbols("f1 f2 f3 f4", real=True) -x = s.symbols('x', real=True) +x = s.symbols("x", real=True) def simplify(expr, maxdepth=10, _depth=0): import copy + original_expr = copy.copy(expr) print(f"Depth: {_depth} | Parsimony: {len(str(expr))}") maps = { @@ -47,10 +48,10 @@ def simplify(expr, maxdepth=10, _depth=0): f = ( - f1 * (x - x2) * (x - x3) * (x - x4) / ((x1 - x2) * (x1 - x3) * (x1 - x4)) + - f2 * (x - x1) * (x - x3) * (x - x4) / ((x2 - x1) * (x2 - x3) * (x2 - x4)) + - f3 * (x - x1) * (x - x2) * (x - x4) / ((x3 - x1) * (x3 - x2) * (x3 - x4)) + - f4 * (x - x1) * (x - x2) * (x - x3) / ((x4 - x1) * (x4 - x2) * (x4 - x3)) + f1 * (x - x2) * (x - x3) * (x - x4) / ((x1 - x2) * (x1 - x3) * (x1 - x4)) + + f2 * (x - x1) * (x - x3) * (x - x4) / ((x2 - x1) * (x2 - x3) * (x2 - x4)) + + f3 * (x - x1) * (x - x2) * (x - x4) / ((x3 - x1) * (x3 - x2) * (x3 - x4)) + + f4 * (x - x1) * (x - x2) * (x - x3) / ((x4 - x1) * (x4 - x2) * (x4 - x3)) ) f = simplify(f) @@ -60,7 +61,7 @@ def simplify(expr, maxdepth=10, _depth=0): df2dx = f.diff(x, 2) df2dx = simplify(df2dx) -res = s.integrate(df2dx ** 2, (x, x2, x3)) +res = s.integrate(df2dx**2, (x, x2, x3)) res = simplify(res.factor([f1, f2, f3, f4])) parsimony = len(str(res)) diff --git a/aerosandbox/numpy/integrate_discrete_derivations/discrete_squared_curvature_derivation_simpson_bernstein.py b/aerosandbox/numpy/integrate_discrete_derivations/discrete_squared_curvature_derivation_simpson_bernstein.py index c8d86113c..1f31a20d4 100644 --- a/aerosandbox/numpy/integrate_discrete_derivations/discrete_squared_curvature_derivation_simpson_bernstein.py +++ b/aerosandbox/numpy/integrate_discrete_derivations/discrete_squared_curvature_derivation_simpson_bernstein.py @@ -9,7 +9,7 @@ hm, h, hp = s.symbols("hm h hp", real=True) dfm, df, dfp = s.symbols("dfm df dfp", real=True) -x1, x2, x3, x4 = s.symbols('x1 x2 x3 x4', real=True) +x1, x2, x3, x4 = s.symbols("x1 x2 x3 x4", real=True) # f1, f2, f3, f4 = s.symbols('f1 f2 f3 f4', real=True) f1 = 0 @@ -20,6 +20,7 @@ def simplify(expr, maxdepth=10, _depth=0): import copy + original_expr = copy.copy(expr) print(f"Depth: {_depth} | Parsimony: {len(str(expr))}") maps = { @@ -51,7 +52,7 @@ def simplify(expr, maxdepth=10, _depth=0): return expr -q = s.symbols('q') # Normalized space for a Bernstein basis. +q = s.symbols("q") # Normalized space for a Bernstein basis. # Mapping from x-space to q-space has x=x2 -> q=0, x=x3 -> q=1. q2 = 0 @@ -63,9 +64,9 @@ def simplify(expr, maxdepth=10, _depth=0): # Define the Bernstein basis polynomials b1 = (1 - q) ** 2 b2 = 2 * q * (1 - q) -b3 = q ** 2 +b3 = q**2 -c1, c2, c3, c4 = s.symbols('c1 c2 c3 c4', real=True) +c1, c2, c3, c4 = s.symbols("c1 c2 c3 c4", real=True) # Can solve for c2 and c3 exactly c1 = f2 @@ -94,16 +95,13 @@ def simplify(expr, maxdepth=10, _depth=0): dfdx = f.diff(q) / h df2dx = dfdx.diff(q) / h -res = s.integrate(df2dx ** 2, (q, q2, q3)) * h +res = s.integrate(df2dx**2, (q, q2, q3)) * h res = simplify(res.factor([dfm, df, dfp])) res = simplify(res) res = simplify(res.subs({c2: c2_sol}).factor([dfm, df, dfp])) -cse = s.cse( - res, - symbols=s.numbered_symbols("s"), - list=False) +cse = s.cse(res, symbols=s.numbered_symbols("s"), list=False) parsimony = len(str(res)) # print(s.pretty(res, num_columns=100)) @@ -113,14 +111,14 @@ def simplify(expr, maxdepth=10, _depth=0): print(f"{var} = {expr}") print(f"res = {cse[1]}") -if __name__ == '__main__': - x = s.symbols('x', real=True) +if __name__ == "__main__": + x = s.symbols("x", real=True) a = 0 b = 4 def example_f(x): - return x ** 2 + 2 * x + 3 + return x**2 + 2 * x + 3 h_val = b - a hm_val = 1 @@ -130,25 +128,18 @@ def example_f(x): dfp_val = example_f(b + hp_val) - example_f(b) subs = { - h: h_val, - hm: hm_val, - hp: hp_val, - df: df_val, - dfm: dfm_val, - dfp: dfp_val, - } - - exact = s.N( - s.integrate( - example_f(x).diff(x, 2) ** 2, - (x, a, b) - ) - ) - - eqn = s.N( - res.subs(subs) - ) + h: h_val, + hm: hm_val, + hp: hp_val, + df: df_val, + dfm: dfm_val, + dfp: dfp_val, + } + + exact = s.N(s.integrate(example_f(x).diff(x, 2) ** 2, (x, a, b))) + + eqn = s.N(res.subs(subs)) print(f"exact: {exact}") print(f"eqn: {eqn}") - print(f"ratio: {exact/eqn}") \ No newline at end of file + print(f"ratio: {exact/eqn}") diff --git a/aerosandbox/numpy/integrate_discrete_derivations/discrete_squared_curvature_derivation_subgradient_softplus.py b/aerosandbox/numpy/integrate_discrete_derivations/discrete_squared_curvature_derivation_subgradient_softplus.py index a48efdc8f..92b78db5d 100644 --- a/aerosandbox/numpy/integrate_discrete_derivations/discrete_squared_curvature_derivation_subgradient_softplus.py +++ b/aerosandbox/numpy/integrate_discrete_derivations/discrete_squared_curvature_derivation_subgradient_softplus.py @@ -6,29 +6,22 @@ def func_num(x, softness=1): - return np.softmax( - 0, - x, - softness=softness - ) + return np.softmax(0, x, softness=softness) def func_sym(x, softness=1): - return s.log( - 1 + s.exp( - x / softness - ) - ) * softness + return s.log(1 + s.exp(x / softness)) * softness # return s.sqrt(x ** 2 + softness ** 2) - softness = s.Rational(1, 100) -x = np.concatenate([ - np.sinspace(0, 200 * softness, 1000)[::-1][:-1] * -1, - np.sinspace(0, 200 * softness, 1000), -]).astype(float) +x = np.concatenate( + [ + np.sinspace(0, 200 * softness, 1000)[::-1][:-1] * -1, + np.sinspace(0, 200 * softness, 1000), + ] +).astype(float) # Discrete chirp function f = func_num(x, softness=float(softness)) @@ -42,10 +35,7 @@ def func_sym(x, softness=1): x_sym = s.symbols("x") f_sym = func_sym(x_sym, softness=softness) -exact = s.integrate( - f_sym.diff(x_sym, 2) ** 2, - (x_sym, -s.oo, s.oo) -) +exact = s.integrate(f_sym.diff(x_sym, 2) ** 2, (x_sym, -s.oo, s.oo)) print(f"Exact: {exact}") @@ -54,7 +44,7 @@ def func_sym(x, softness=1): subgradients = np.diff(slopes) -discrete = np.sum(subgradients ** 2) +discrete = np.sum(subgradients**2) print(f"Discrete: {discrete}") diff --git a/aerosandbox/numpy/integrate_discrete_derivations/discrete_squared_curvature_display_spectral_convergence.py b/aerosandbox/numpy/integrate_discrete_derivations/discrete_squared_curvature_display_spectral_convergence.py index 6d300415f..bfa27bae6 100644 --- a/aerosandbox/numpy/integrate_discrete_derivations/discrete_squared_curvature_display_spectral_convergence.py +++ b/aerosandbox/numpy/integrate_discrete_derivations/discrete_squared_curvature_display_spectral_convergence.py @@ -7,9 +7,9 @@ x = s.symbols("x", real=True) k = s.symbols("k", positive=True, real=True) -f = s.cos(k * x * 2 * s.pi) / k ** 2 +f = s.cos(k * x * 2 * s.pi) / k**2 d2fdx2 = f.diff(x, 2) -exact = s.simplify(s.integrate(d2fdx2 ** 2, (x, 0, 1))) +exact = s.simplify(s.integrate(d2fdx2**2, (x, 0, 1))) @np.vectorize @@ -20,11 +20,13 @@ def get_approx(period=10, method="cubic"): f.subs(k, (n_samples - 1) / period), )(x_vals) - approx = np.sum(integrate_discrete_squared_curvature( - f=f_vals, - x=x_vals, - method=method, - )) + approx = np.sum( + integrate_discrete_squared_curvature( + f=f_vals, + x=x_vals, + method=method, + ) + ) return approx @@ -48,11 +50,6 @@ def get_approx(period=10, method="cubic"): plt.sca(ax[1]) ax[1].set_ylim(bottom=0) ax[1].plot( - periods, - np.ones_like(periods), - label="Exact", - color="k", - linestyle="--", - alpha=0.5 + periods, np.ones_like(periods), label="Exact", color="k", linestyle="--", alpha=0.5 ) p.show_plot() diff --git a/aerosandbox/numpy/integrate_discrete_derivations/simpson_backward_interpolant_derivation.py b/aerosandbox/numpy/integrate_discrete_derivations/simpson_backward_interpolant_derivation.py index e81467587..772353e7e 100644 --- a/aerosandbox/numpy/integrate_discrete_derivations/simpson_backward_interpolant_derivation.py +++ b/aerosandbox/numpy/integrate_discrete_derivations/simpson_backward_interpolant_derivation.py @@ -1,29 +1,30 @@ import sympy as s from sympy import init_printing + init_printing() # "Backward" -> gets the integral from x2 to x3, by analogy to backward Euler # Define the symbols -x1, x2, x3 = s.symbols('x1 x2 x3', real=True) -f1, f2, f3 = s.symbols('f1 f2 f3', real=True) +x1, x2, x3 = s.symbols("x1 x2 x3", real=True) +f1, f2, f3 = s.symbols("f1 f2 f3", real=True) h = x3 - x2 hm = x2 - x1 -q = s.symbols('q') # Normalized space for a Bernstein basis. +q = s.symbols("q") # Normalized space for a Bernstein basis. # Mapping from x-space to q-space has x=x2 -> q=0, x=x3 -> q=1. -q1 = s.symbols('q1', real=True) +q1 = s.symbols("q1", real=True) q2 = 0 q3 = 1 # Define the Bernstein basis polynomials b1 = (1 - q) ** 2 b2 = 2 * q * (1 - q) -b3 = q ** 2 +b3 = q**2 -c1, c2, c3 = s.symbols('c1 c2 c3', real=True) +c1, c2, c3 = s.symbols("c1 c2 c3", real=True) # Can solve for c2 and c3 exactly c1 = f2 @@ -31,7 +32,7 @@ f = c1 * b1 + c2 * b2 + c3 * b3 -f1_cubic = f.subs(q, q1)#.simplify() +f1_cubic = f.subs(q, q1) # .simplify() factors = [q1] # factors = [f1, f2, f3, f4] @@ -50,11 +51,11 @@ f = c1 * b1 + c2 * b2 + c3 * b3 -integral = (c1 + c2 + c3) / 3 # God I love Bernstein polynomials +integral = (c1 + c2 + c3) / 3 # God I love Bernstein polynomials # integral = s.simplify(integral) integral = integral.factor(factors).simplify() parsimony = len(str(integral)) print(s.pretty(integral, num_columns=100)) -print(f"Parsimony: {parsimony}") \ No newline at end of file +print(f"Parsimony: {parsimony}") diff --git a/aerosandbox/numpy/integrate_discrete_derivations/simpson_forward_interpolant_derivation.py b/aerosandbox/numpy/integrate_discrete_derivations/simpson_forward_interpolant_derivation.py index 8e10c8ecf..41908496f 100644 --- a/aerosandbox/numpy/integrate_discrete_derivations/simpson_forward_interpolant_derivation.py +++ b/aerosandbox/numpy/integrate_discrete_derivations/simpson_forward_interpolant_derivation.py @@ -1,29 +1,30 @@ import sympy as s from sympy import init_printing + init_printing() # "Forward" -> gets the integral from x1 to x2, by analogy to forward Euler # Define the symbols -x1, x2, x3 = s.symbols('x1 x2 x3', real=True) -f1, f2, f3 = s.symbols('f1 f2 f3', real=True) +x1, x2, x3 = s.symbols("x1 x2 x3", real=True) +f1, f2, f3 = s.symbols("f1 f2 f3", real=True) h = x2 - x1 hp = x3 - x2 -q = s.symbols('q') # Normalized space for a Bernstein basis. +q = s.symbols("q") # Normalized space for a Bernstein basis. # Mapping from x-space to q-space has x=x2 -> q=0, x=x3 -> q=1. q1 = 0 q2 = 1 -q3 = s.symbols('q3', real=True) +q3 = s.symbols("q3", real=True) # Define the Bernstein basis polynomials b1 = (1 - q) ** 2 b2 = 2 * q * (1 - q) -b3 = q ** 2 +b3 = q**2 -c1, c2, c3 = s.symbols('c1 c2 c3', real=True) +c1, c2, c3 = s.symbols("c1 c2 c3", real=True) # Can solve for c2 and c3 exactly c1 = f1 @@ -31,7 +32,7 @@ f = c1 * b1 + c2 * b2 + c3 * b3 -f3_cubic = f.subs(q, q3)#.simplify() +f3_cubic = f.subs(q, q3) # .simplify() factors = [q3] # factors = [f1, f2, f3, f4] @@ -50,11 +51,11 @@ f = c1 * b1 + c2 * b2 + c3 * b3 -integral = (c1 + c2 + c3) / 3 # God I love Bernstein polynomials +integral = (c1 + c2 + c3) / 3 # God I love Bernstein polynomials # integral = s.simplify(integral) integral = integral.factor(factors).simplify() parsimony = len(str(integral)) print(s.pretty(integral, num_columns=100)) -print(f"Parsimony: {parsimony}") \ No newline at end of file +print(f"Parsimony: {parsimony}") diff --git a/aerosandbox/numpy/interpolate.py b/aerosandbox/numpy/interpolate.py index e48660fac..d90dc247e 100644 --- a/aerosandbox/numpy/interpolate.py +++ b/aerosandbox/numpy/interpolate.py @@ -20,26 +20,15 @@ def interp(x, xp, fp, left=None, right=None, period=None): Specific notes: xp is assumed to be sorted. """ if not is_casadi_type([x, xp, fp], recursive=True): - return _onp.interp( - x=x, - xp=xp, - fp=fp, - left=left, - right=right, - period=period - ) + return _onp.interp(x=x, xp=xp, fp=fp, left=left, right=right, period=period) else: ### Handle period argument if period is not None: - if any( - logical_or( - xp < 0, - xp > period - ) - ): + if any(logical_or(xp < 0, xp > period)): raise NotImplementedError( - "Haven't yet implemented handling for if xp is outside the period.") # Not easy to implement because casadi doesn't have a sort feature. + "Haven't yet implemented handling for if xp is outside the period." + ) # Not easy to implement because casadi doesn't have a sort feature. x = _cas.fmod(x, period) @@ -56,36 +45,30 @@ def interp(x, xp, fp, left=None, right=None, period=None): ### Make sure xp is an iterable xp = array(xp, dtype=float) - ### Do the interpolation if is_casadi_type([x, xp], recursive=True): - grid = [xp.shape[0]] # size of grid, list is used since can be multi-dimensional - cas_interp = _cas.interpolant('cs_interp','linear',grid,1,{"inline": True}) - f = cas_interp(x,xp,fp) + grid = [ + xp.shape[0] + ] # size of grid, list is used since can be multi-dimensional + cas_interp = _cas.interpolant( + "cs_interp", "linear", grid, 1, {"inline": True} + ) + f = cas_interp(x, xp, fp) else: f = _cas.interp1d(xp, fp, x) ### Handle left/right if left is not None: - f = where( - x < xp[0], - left, - f - ) + f = where(x < xp[0], left, f) if right is not None: - f = where( - x > xp[-1], - right, - f - ) + f = where(x > xp[-1], right, f) ### Return return f def is_data_structured( - x_data_coordinates: Tuple[_onp.ndarray], - y_data_structured: _onp.ndarray + x_data_coordinates: Tuple[_onp.ndarray], y_data_structured: _onp.ndarray ) -> bool: """ Determines if the shapes of a given dataset are consistent with "structured" (i.e. gridded) data. @@ -105,7 +88,9 @@ def is_data_structured( if len(coordinates.shape) != 1: return False - implied_y_data_shape = tuple(len(coordinates) for coordinates in x_data_coordinates) + implied_y_data_shape = tuple( + len(coordinates) for coordinates in x_data_coordinates + ) if not y_data_structured.shape == implied_y_data_shape: return False except TypeError: # if x_data_coordinates is not iterable, for instance @@ -117,12 +102,12 @@ def is_data_structured( def interpn( - points: Tuple[_onp.ndarray], - values: _onp.ndarray, - xi: _onp.ndarray, - method: str = "linear", - bounds_error=True, - fill_value=_onp.nan + points: Tuple[_onp.ndarray], + values: _onp.ndarray, + xi: _onp.ndarray, + method: str = "linear", + bounds_error=True, + fill_value=_onp.nan, ) -> _onp.ndarray: """ Performs multidimensional interpolation on regular grids. Analogue to scipy.interpolate.interpn(). @@ -159,26 +144,30 @@ def interpn( """ ### Check input types for points and values if is_casadi_type([points, values], recursive=True): - raise TypeError("The underlying dataset (points, values) must consist of NumPy arrays.") + raise TypeError( + "The underlying dataset (points, values) must consist of NumPy arrays." + ) ### Check dimensions of points for points_axis in points: points_axis = array(points_axis) if not len(points_axis.shape) == 1: - raise ValueError("`points` must consist of a tuple of 1D ndarrays defining the coordinates of each axis.") + raise ValueError( + "`points` must consist of a tuple of 1D ndarrays defining the coordinates of each axis." + ) ### Check dimensions of values implied_values_shape = tuple(len(points_axis) for points_axis in points) if not values.shape == implied_values_shape: - raise ValueError(f""" + raise ValueError( + f""" The shape of `values` should be {implied_values_shape}. - """) + """ + ) if ( ### NumPy implementation - not is_casadi_type([points, values, xi], recursive=True) - ) and ( - (method == "linear") or (method == "nearest") - ): + not is_casadi_type([points, values, xi], recursive=True) + ) and ((method == "linear") or (method == "nearest")): xi = _onp.array(xi).reshape((-1, len(implied_values_shape))) return _interpolate.interpn( points=points, @@ -186,12 +175,10 @@ def interpn( xi=xi, method=method, bounds_error=bounds_error, - fill_value=fill_value + fill_value=fill_value, ) - elif ( ### CasADi implementation - (method == "linear") or (method == "bspline") - ): + elif (method == "linear") or (method == "bspline"): ### CasADi implementation ### Add handling to patch a specific bug in CasADi that occurs when `values` is all zeros. ### For more information, see: https://github.com/casadi/casadi/issues/2837 if method == "bspline" and all(values == 0): @@ -219,14 +206,8 @@ def interpn( assert xi.shape[1] == n_dimensions ### Calculate the minimum and maximum values along each axis. - axis_values_min = [ - _onp.min(axis_values) - for axis_values in points - ] - axis_values_max = [ - _onp.max(axis_values) - for axis_values in points - ] + axis_values_min = [_onp.min(axis_values) for axis_values in points] + axis_values_max = [_onp.max(axis_values) for axis_values in points] ### If fill_value is None, project the xi back onto the nearest point in the domain. if fill_value is None: @@ -235,38 +216,37 @@ def interpn( xi[:, axis] = where( xi[:, axis] > axis_values_max[axis], axis_values_max[axis], - xi[:, axis] + xi[:, axis], ) xi[:, axis] = where( xi[:, axis] < axis_values_min[axis], axis_values_min[axis], - xi[:, axis] + xi[:, axis], ) ### Check bounds_error if bounds_error: if isinstance(xi, _cas.MX): - raise ValueError("Can't have the `bounds_error` flag as True if `xi` is of cas.MX type.") + raise ValueError( + "Can't have the `bounds_error` flag as True if `xi` is of cas.MX type." + ) for axis in range(n_dimensions): if any( - logical_or( - xi[:, axis] > axis_values_max[axis], - xi[:, axis] < axis_values_min[axis] - ) + logical_or( + xi[:, axis] > axis_values_max[axis], + xi[:, axis] < axis_values_min[axis], + ) ): raise ValueError( f"One of the requested xi is out of bounds in dimension {axis}" ) ### Do the interpolation - values_flattened = _onp.ravel(values, order='F') + values_flattened = _onp.ravel(values, order="F") interpolator = _cas.interpolant( - 'Interpolator', - method, - points, - values_flattened + "Interpolator", method, points, values_flattened ) fi = interpolator(xi.T).T @@ -275,16 +255,8 @@ def interpn( if fill_value is not None: for axis in range(n_dimensions): - fi = where( - xi[:, axis] > axis_values_max[axis], - fill_value, - fi - ) - fi = where( - xi[:, axis] < axis_values_min[axis], - fill_value, - fi - ) + fi = where(xi[:, axis] > axis_values_max[axis], fill_value, fi) + fi = where(xi[:, axis] < axis_values_min[axis], fill_value, fi) ### If DM output (i.e. a numeric value), convert that back to an array if isinstance(fi, _cas.DM): diff --git a/aerosandbox/numpy/linalg.py b/aerosandbox/numpy/linalg.py index 3789640f6..1e9a99576 100644 --- a/aerosandbox/numpy/linalg.py +++ b/aerosandbox/numpy/linalg.py @@ -28,13 +28,7 @@ def outer(x, y, manual=False): See syntax here: https://numpy.org/doc/stable/reference/generated/numpy.outer.html """ if manual: - return [ - [ - xi * yi - for yi in y - ] - for xi in x - ] + return [[xi * yi for yi in y] for xi in x] if not is_casadi_type([x, y], recursive=True): return _onp.outer(x, y) @@ -114,11 +108,7 @@ def norm(x, ord=None, axis=None, keepdims=False): # Figure out which axis, if any, to take a vector norm about. if axis is not None: - if not ( - axis == 0 or - axis == 1 or - axis == -1 - ): + if not (axis == 0 or axis == 1 or axis == -1): raise ValueError("`axis` must be -1, 0, or 1 for CasADi types.") elif x.shape[0] == 1: axis = 1 @@ -129,34 +119,27 @@ def norm(x, ord=None, axis=None, keepdims=False): if axis is not None: ord = 2 else: - ord = 'fro' + ord = "fro" if ord == 1: # norm = _cas.norm_1(x) - norm = sum( - abs(x), - axis=axis - ) + norm = sum(abs(x), axis=axis) elif ord == 2: # norm = _cas.norm_2(x) - norm = sum( - x ** 2, - axis=axis - ) ** 0.5 - elif ord == 'fro' or ord == "frobenius": + norm = sum(x**2, axis=axis) ** 0.5 + elif ord == "fro" or ord == "frobenius": norm = _cas.norm_fro(x) - elif ord == 'inf' or _onp.isinf(ord): + elif ord == "inf" or _onp.isinf(ord): norm = _cas.norm_inf() else: try: - norm = sum( - abs(x) ** ord, - axis=axis - ) ** (1 / ord) + norm = sum(abs(x) ** ord, axis=axis) ** (1 / ord) except Exception as e: print(e) - raise ValueError("Couldn't interpret `ord` sensibly! Tried to interpret it as a floating-point order " - "as a last-ditch effort, but that didn't work.") + raise ValueError( + "Couldn't interpret `ord` sensibly! Tried to interpret it as a floating-point order " + "as a last-ditch effort, but that didn't work." + ) if keepdims: new_shape = list(x.shape) @@ -165,13 +148,14 @@ def norm(x, ord=None, axis=None, keepdims=False): else: return norm + def inv_symmetric_3x3( - m11, - m22, - m33, - m12, - m23, - m13, + m11, + m22, + m33, + m12, + m23, + m13, ): """ Explicitly computes the inverse of a symmetric 3x3 matrix. @@ -191,19 +175,19 @@ def inv_symmetric_3x3( From https://math.stackexchange.com/questions/233378/inverse-of-a-3-x-3-covariance-matrix-or-any-positive-definite-pd-matrix """ det = ( - m11 * (m33 * m22 - m23 ** 2) - - m12 * (m33 * m12 - m23 * m13) + - m13 * (m23 * m12 - m22 * m13) + m11 * (m33 * m22 - m23**2) + - m12 * (m33 * m12 - m23 * m13) + + m13 * (m23 * m12 - m22 * m13) ) inv_det = 1 / det - a11 = m33 * m22 - m23 ** 2 + a11 = m33 * m22 - m23**2 a12 = m13 * m23 - m33 * m12 a13 = m12 * m23 - m13 * m22 - a22 = m33 * m11 - m13 ** 2 + a22 = m33 * m11 - m13**2 a23 = m12 * m13 - m11 * m23 - a33 = m11 * m22 - m12 ** 2 + a33 = m11 * m22 - m12**2 a11 = a11 * inv_det a12 = a12 * inv_det diff --git a/aerosandbox/numpy/linalg_top_level.py b/aerosandbox/numpy/linalg_top_level.py index dcb6412fe..9de99bb20 100644 --- a/aerosandbox/numpy/linalg_top_level.py +++ b/aerosandbox/numpy/linalg_top_level.py @@ -36,11 +36,7 @@ def cross(a, b, axisa=-1, axisb=-1, axisc=-1, axis=None, manual=False): else: if axis is not None: - if not ( - axis == -1 or - axis == 0 or - axis == 1 - ): + if not (axis == -1 or axis == 0 or axis == 1): raise ValueError("`axis` must be -1, 0, or 1.") axisa = axis axisb = axis diff --git a/aerosandbox/numpy/logicals.py b/aerosandbox/numpy/logicals.py index 9e44dd90c..395705589 100644 --- a/aerosandbox/numpy/logicals.py +++ b/aerosandbox/numpy/logicals.py @@ -3,11 +3,7 @@ from aerosandbox.numpy.determine_type import is_casadi_type -def clip( - x, - min, - max -): +def clip(x, min, max): """ Clip a value to a range. Args: diff --git a/aerosandbox/numpy/rotations.py b/aerosandbox/numpy/rotations.py index bc9f992d1..0c0851824 100644 --- a/aerosandbox/numpy/rotations.py +++ b/aerosandbox/numpy/rotations.py @@ -5,8 +5,8 @@ def rotation_matrix_2D( - angle, - as_array: bool = True, + angle, + as_array: bool = True, ): """ Gives the 2D rotation matrix associated with a counterclockwise rotation about an angle. @@ -19,10 +19,7 @@ def rotation_matrix_2D( """ s = sin(angle) c = cos(angle) - rot = [ - [c, -s], - [s, c] - ] + rot = [[c, -s], [s, c]] if as_array: return array(rot) else: @@ -30,10 +27,10 @@ def rotation_matrix_2D( def rotation_matrix_3D( - angle: Union[float, _onp.ndarray], - axis: Union[_onp.ndarray, List, str], - as_array: bool = True, - axis_already_normalized: bool = False + angle: Union[float, _onp.ndarray], + axis: Union[_onp.ndarray, List, str], + as_array: bool = True, + axis_already_normalized: bool = False, ): """ Yields the rotation matrix that corresponds to a rotation by a specified amount about a given axis. @@ -64,23 +61,11 @@ def rotation_matrix_3D( if isinstance(axis, str): if axis.lower() == "x": - rot = [ - [1, 0, 0], - [0, c, -s], - [0, s, c] - ] + rot = [[1, 0, 0], [0, c, -s], [0, s, c]] elif axis.lower() == "y": - rot = [ - [c, 0, s], - [0, 1, 0], - [-s, 0, c] - ] + rot = [[c, 0, s], [0, 1, 0], [-s, 0, c]] elif axis.lower() == "z": - rot = [ - [c, -s, 0], - [s, c, 0], - [0, 0, 1] - ] + rot = [[c, -s, 0], [s, c, 0], [0, 0, 1]] else: raise ValueError("If `axis` is a string, it must be `x`, `y`, or `z`.") else: @@ -89,15 +74,27 @@ def rotation_matrix_3D( uz = axis[2] if not axis_already_normalized: - norm = (ux ** 2 + uy ** 2 + uz ** 2) ** 0.5 + norm = (ux**2 + uy**2 + uz**2) ** 0.5 ux = ux / norm uy = uy / norm uz = uz / norm rot = [ - [c + ux ** 2 * (1 - c), ux * uy * (1 - c) - uz * s, ux * uz * (1 - c) + uy * s], - [uy * ux * (1 - c) + uz * s, c + uy ** 2 * (1 - c), uy * uz * (1 - c) - ux * s], - [uz * ux * (1 - c) - uy * s, uz * uy * (1 - c) + ux * s, c + uz ** 2 * (1 - c)] + [ + c + ux**2 * (1 - c), + ux * uy * (1 - c) - uz * s, + ux * uz * (1 - c) + uy * s, + ], + [ + uy * ux * (1 - c) + uz * s, + c + uy**2 * (1 - c), + uy * uz * (1 - c) - ux * s, + ], + [ + uz * ux * (1 - c) - uy * s, + uz * uy * (1 - c) + ux * s, + c + uz**2 * (1 - c), + ], ] if as_array: @@ -107,10 +104,10 @@ def rotation_matrix_3D( def rotation_matrix_from_euler_angles( - roll_angle: Union[float, _onp.ndarray] = 0, - pitch_angle: Union[float, _onp.ndarray] = 0, - yaw_angle: Union[float, _onp.ndarray] = 0, - as_array: bool = True + roll_angle: Union[float, _onp.ndarray] = 0, + pitch_angle: Union[float, _onp.ndarray] = 0, + yaw_angle: Union[float, _onp.ndarray] = 0, + as_array: bool = True, ): """ Yields the rotation matrix that corresponds to a given Euler angle rotation. @@ -148,7 +145,7 @@ def rotation_matrix_from_euler_angles( rot = [ [ca * cb, ca * sb * sc - sa * cc, ca * sb * cc + sa * sc], [sa * cb, sa * sb * sc + ca * cc, sa * sb * cc - ca * sc], - [-sb, cb * sc, cb * cc] + [-sb, cb * sc, cb * cc], ] if as_array: @@ -157,10 +154,7 @@ def rotation_matrix_from_euler_angles( return rot -def is_valid_rotation_matrix( - a: _onp.ndarray, - tol=1e-9 -) -> bool: +def is_valid_rotation_matrix(a: _onp.ndarray, tol=1e-9) -> bool: """ Returns a boolean of whether the given matrix satisfies the properties of a rotation matrix. @@ -191,7 +185,4 @@ def approx_equal(x, y): if not approx_equal(eye_approx[i, j], eye[i, j]): is_orthogonality_preserving = False - return ( - is_volume_preserving_and_right_handed and - is_orthogonality_preserving - ) + return is_volume_preserving_and_right_handed and is_orthogonality_preserving diff --git a/aerosandbox/numpy/spacing.py b/aerosandbox/numpy/spacing.py index ea1bd56ed..20f66c1ef 100644 --- a/aerosandbox/numpy/spacing.py +++ b/aerosandbox/numpy/spacing.py @@ -3,11 +3,7 @@ from aerosandbox.numpy.determine_type import is_casadi_type -def linspace( - start: float = 0., - stop: float = 1., - num: int = 50 -): +def linspace(start: float = 0.0, stop: float = 1.0, num: int = 50): """ Returns evenly spaced numbers over a specified interval. @@ -19,11 +15,7 @@ def linspace( return _cas.linspace(start, stop, num) -def cosspace( - start: float = 0., - stop: float = 1., - num: int = 50 -): +def cosspace(start: float = 0.0, stop: float = 1.0, num: int = 50): """ Makes a cosine-spaced vector. @@ -39,13 +31,7 @@ def cosspace( mean = (stop + start) / 2 amp = (stop - start) / 2 ones = 0 * start + 1 - spaced_array = mean + amp * _onp.cos( - linspace( - _onp.pi * ones, - 0 * ones, - num - ) - ) + spaced_array = mean + amp * _onp.cos(linspace(_onp.pi * ones, 0 * ones, num)) # Fix the endpoints, which might not be exactly right due to floating-point error. spaced_array[0] = start @@ -55,10 +41,10 @@ def cosspace( def sinspace( - start: float = 0., - stop: float = 1., - num: int = 50, - reverse_spacing: bool = False, + start: float = 0.0, + stop: float = 1.0, + num: int = 50, + reverse_spacing: bool = False, ): """ Makes a sine-spaced vector. By default, bunches points near the start. @@ -84,13 +70,8 @@ def sinspace( if reverse_spacing: return sinspace(stop, start, num)[::-1] ones = 0 * start + 1 - spaced_array = ( - start + (stop - start) * (1 - _onp.cos(linspace( - 0 * ones, - _onp.pi / 2 * ones, - num - )) - ) + spaced_array = start + (stop - start) * ( + 1 - _onp.cos(linspace(0 * ones, _onp.pi / 2 * ones, num)) ) # Fix the endpoints, which might not be exactly right due to floating-point error. spaced_array[0] = start @@ -99,11 +80,7 @@ def sinspace( return spaced_array -def logspace( - start: float = 0., - stop: float = 1., - num: int = 50 -): +def logspace(start: float = 0.0, stop: float = 1.0, num: int = 50): """ Return numbers spaced evenly on a log scale. @@ -115,11 +92,7 @@ def logspace( return 10 ** linspace(start, stop, num) -def geomspace( - start: float = 1., - stop: float = 10., - num: int = 50 -): +def geomspace(start: float = 1.0, stop: float = 10.0, num: int = 50): """ Return numbers spaced evenly on a log scale (a geometric progression). diff --git a/aerosandbox/numpy/surrogate_model_tools.py b/aerosandbox/numpy/surrogate_model_tools.py index 165cf8698..47eda76d0 100644 --- a/aerosandbox/numpy/surrogate_model_tools.py +++ b/aerosandbox/numpy/surrogate_model_tools.py @@ -4,9 +4,9 @@ def softmax( - *args: Union[float, _np.ndarray], - softness: float = None, - hardness: float = None, + *args: Union[float, _np.ndarray], + softness: float = None, + hardness: float = None, ) -> Union[float, _np.ndarray]: """ An element-wise softmax between two or more arrays. Also referred to as the logsumexp() function. @@ -52,8 +52,10 @@ def softmax( raise ValueError("The value of `hardness` must be positive.") if len(args) <= 1: - raise ValueError("You must call softmax with the value of two or more arrays that you'd like to take the " - "element-wise softmax of.") + raise ValueError( + "You must call softmax with the value of two or more arrays that you'd like to take the " + "element-wise softmax of." + ) ### Scale the args by softness args = [arg / softness for arg in args] @@ -65,18 +67,17 @@ def softmax( min = _np.fmin(min, arg) max = _np.fmax(max, arg) - out = max + _np.log(sum( - [_np.exp(_np.maximum(array - max, -500)) for array in args] - ) + out = max + _np.log( + sum([_np.exp(_np.maximum(array - max, -500)) for array in args]) ) out = out * softness return out def softmin( - *args: Union[float, _np.ndarray], - softness: float = None, - hardness: float = None, + *args: Union[float, _np.ndarray], + softness: float = None, + hardness: float = None, ) -> Union[float, _np.ndarray]: """ An element-wise softmin between two or more arrays. Related to the logsumexp() function. @@ -113,29 +114,30 @@ def softmin( def softmax_scalefree( - *args: Union[float, _np.ndarray], - relative_softness: float = None, - relative_hardness: float = None, + *args: Union[float, _np.ndarray], + relative_softness: float = None, + relative_hardness: float = None, ) -> Union[float, _np.ndarray]: - n_specified_arguments = (relative_hardness is not None) + (relative_softness is not None) + n_specified_arguments = (relative_hardness is not None) + ( + relative_softness is not None + ) if n_specified_arguments == 0: relative_softness = 0.01 elif n_specified_arguments == 2: - raise ValueError("You must provide exactly one of `relative_softness` or `relative_hardness.") + raise ValueError( + "You must provide exactly one of `relative_softness` or `relative_hardness." + ) if relative_hardness is not None: relative_softness = 1 / relative_hardness - return softmax( - *args, - softness=relative_softness * _np.linalg.norm(_np.array(args)) - ) + return softmax(*args, softness=relative_softness * _np.linalg.norm(_np.array(args))) def softmin_scalefree( - *args: Union[float, _np.ndarray], - relative_softness: float = None, - relative_hardness: float = None, + *args: Union[float, _np.ndarray], + relative_softness: float = None, + relative_hardness: float = None, ) -> Union[float, _np.ndarray]: return -softmax_scalefree( *[-arg for arg in args], @@ -145,9 +147,9 @@ def softmin_scalefree( def softplus( - x: Union[float, _np.ndarray], - beta=1, - threshold=40, + x: Union[float, _np.ndarray], + beta=1, + threshold=40, ): """ A smooth approximation of the ReLU function, applied elementwise to an array `x`. @@ -166,18 +168,16 @@ def softplus( """ if _np.is_casadi_type(x, recursive=False): return _np.where( - beta * x > threshold, - x, - 1 / beta * _cas.log1p(_cas.exp(beta * x)) + beta * x > threshold, x, 1 / beta * _cas.log1p(_cas.exp(beta * x)) ) else: return 1 / beta * _np.logaddexp(0, beta * x) def sigmoid( - x, - sigmoid_type: str = "tanh", - normalization_range: Tuple[Union[float, int], Union[float, int]] = (0, 1) + x, + sigmoid_type: str = "tanh", + normalization_range: Tuple[Union[float, int], Union[float, int]] = (0, 1), ): """ A sigmoid function. From Wikipedia (https://en.wikipedia.org/wiki/Sigmoid_function): @@ -217,7 +217,7 @@ def sigmoid( elif sigmoid_type == "arctan": s = 2 / _np.pi * _np.arctan(_np.pi / 2 * x) elif sigmoid_type == "polynomial": - s = x / (1 + x ** 2) ** 0.5 + s = x / (1 + x**2) ** 0.5 else: raise ValueError("Bad value of parameter 'type'!") @@ -229,10 +229,7 @@ def sigmoid( return s_normalized -def swish( - x, - beta=1 -): +def swish(x, beta=1): """ A smooth approximation of the ReLU function, applied elementwise to an array `x`. @@ -251,9 +248,9 @@ def swish( def blend( - switch: float, - value_switch_high, - value_switch_low, + switch: float, + value_switch_high, + value_switch_low, ): """ Smoothly blends between two values on the basis of some switch function. @@ -280,15 +277,11 @@ def blend( on the value of the 'switch' parameter. """ - blend_function = lambda x: sigmoid( - x, - normalization_range=(0, 1) - ) + blend_function = lambda x: sigmoid(x, normalization_range=(0, 1)) weight_to_value_switch_high = blend_function(switch) - blend_value = ( - value_switch_high * weight_to_value_switch_high + - value_switch_low * (1 - weight_to_value_switch_high) + blend_value = value_switch_high * weight_to_value_switch_high + value_switch_low * ( + 1 - weight_to_value_switch_high ) return blend_value diff --git a/aerosandbox/numpy/test_numpy/test_all_operations_run.py b/aerosandbox/numpy/test_numpy/test_all_operations_run.py index 7b1fca5eb..f58e5bf8e 100644 --- a/aerosandbox/numpy/test_numpy/test_all_operations_run.py +++ b/aerosandbox/numpy/test_numpy/test_all_operations_run.py @@ -3,13 +3,13 @@ import aerosandbox.numpy as np ### Cause all NumPy warnings to raise exceptions, to make this bulletproof -np.seterr(all='raise') +np.seterr(all="raise") @pytest.fixture def types(): ### Float data types - scalar_float = 1. + scalar_float = 1.0 ### NumPy data types scalar_np = np.array(1) @@ -22,11 +22,15 @@ def types(): vector_cas = opti.variable(n_vars=2, init_guess=1) ### Dynamically-typed data type creation (i.e. type depends on inputs) - vector_dynamic = np.array([scalar_cas, scalar_cas]) # vector as a dynamic-typed array - matrix_dynamic = np.array([ # matrix as an dynamic-typed array - [scalar_cas, scalar_cas], + vector_dynamic = np.array( [scalar_cas, scalar_cas] - ]) + ) # vector as a dynamic-typed array + matrix_dynamic = np.array( + [ # matrix as an dynamic-typed array + [scalar_cas, scalar_cas], + [scalar_cas, scalar_cas], + ] + ) ### Create lists of possible variable types for scalars, vectors, and matrices. scalar_options = [scalar_float, scalar_cas, scalar_np] @@ -37,7 +41,7 @@ def types(): "scalar": scalar_options, "vector": vector_options, "matrix": matrix_options, - "all" : scalar_options + vector_options + matrix_options + "all": scalar_options + vector_options + matrix_options, } @@ -69,7 +73,7 @@ def test_basic_math(types): np.sum(x) # Sum of all entries of array-like object x ### Exponentials & Powers - x ** y + x**y np.power(x, y) np.exp(x) np.log(x) @@ -102,9 +106,9 @@ def test_logic(types): for y in option_set: ### Comparisons """ - Note: if warnings appear here, they're from `np.array(1) == cas.MX(1)` - + Note: if warnings appear here, they're from `np.array(1) == cas.MX(1)` - sensitive to order, as `cas.MX(1) == np.array(1)` is fine. - + However, checking the outputs, these seem to be yielding correct results despite the warning sooo... """ @@ -116,11 +120,7 @@ def test_logic(types): x <= y ### Conditionals - np.where( - x > 1, - x ** 2, - 0 - ) + np.where(x > 1, x**2, 0) ### Elementwise min/max np.fmax(x, y) @@ -167,5 +167,5 @@ def test_rotation_matrices(types): np.rotation_matrix_3D(angle, np.array([axis[0], axis[1], axis[0]])) -if __name__ == '__main__': +if __name__ == "__main__": pytest.main() diff --git a/aerosandbox/numpy/test_numpy/test_arithmetic.py b/aerosandbox/numpy/test_numpy/test_arithmetic.py index 6a4074349..bd824e484 100644 --- a/aerosandbox/numpy/test_numpy/test_arithmetic.py +++ b/aerosandbox/numpy/test_numpy/test_arithmetic.py @@ -28,13 +28,11 @@ def test_cumsum(): n = np.arange(6).reshape((3, 2)) c = cas.DM(n) - assert np.all( - np.cumsum(n) == np.array([0, 1, 3, 6, 10, 15]) - ) + assert np.all(np.cumsum(n) == np.array([0, 1, 3, 6, 10, 15])) # assert np.all( # TODO add casadi testing here # np.cumsum(c) == np.array([0, 1, 3, 6, 10, 15]) # ) -if __name__ == '__main__': +if __name__ == "__main__": pytest.main() diff --git a/aerosandbox/numpy/test_numpy/test_array.py b/aerosandbox/numpy/test_numpy/test_array.py index a006d368c..de2a7f238 100644 --- a/aerosandbox/numpy/test_numpy/test_array.py +++ b/aerosandbox/numpy/test_numpy/test_array.py @@ -5,10 +5,7 @@ def test_array_numpy_equivalency_1D(): - inputs = [ - 1, - 2 - ] + inputs = [1, 2] a = array(inputs) a_np = np.array(inputs) @@ -17,10 +14,7 @@ def test_array_numpy_equivalency_1D(): def test_array_numpy_equivalency_2D(): - inputs = [ - [1, 2], - [3, 4] - ] + inputs = [[1, 2], [3, 4]] a = array(inputs) a_np = np.array(inputs) @@ -42,7 +36,7 @@ def test_can_convert_DM_to_ndarray(): def test_length(): assert length(5) == 1 - assert length(5.) == 1 + assert length(5.0) == 1 assert length([1, 2, 3]) == 3 assert length(np.array(5)) == 1 @@ -89,20 +83,12 @@ def test_diag_onp(): a = np.array([1, 2, 3]) assert np.all(np.diag(a) == np.diag(a, k=0)) assert np.all( - np.diag(a, k=1) == np.array([ - [0, 1, 0, 0], - [0, 0, 2, 0], - [0, 0, 0, 3], - [0, 0, 0, 0] - ]) + np.diag(a, k=1) + == np.array([[0, 1, 0, 0], [0, 0, 2, 0], [0, 0, 0, 3], [0, 0, 0, 0]]) ) assert np.all( - np.diag(a, k=-1) == np.array([ - [0, 0, 0, 0], - [1, 0, 0, 0], - [0, 2, 0, 0], - [0, 0, 3, 0] - ]) + np.diag(a, k=-1) + == np.array([[0, 0, 0, 0], [1, 0, 0, 0], [0, 2, 0, 0], [0, 0, 3, 0]]) ) # Test on 2D square array @@ -123,20 +109,12 @@ def test_diag_casadi(): a = cas.SX(np.array([1, 2, 3])) assert np.all(np.diag(a) == np.diag(a, k=0)) assert np.all( - np.diag(a, k=1) == np.array([ - [0, 1, 0, 0], - [0, 0, 2, 0], - [0, 0, 0, 3], - [0, 0, 0, 0] - ]) + np.diag(a, k=1) + == np.array([[0, 1, 0, 0], [0, 0, 2, 0], [0, 0, 0, 3], [0, 0, 0, 0]]) ) assert np.all( - np.diag(a, k=-1) == np.array([ - [0, 0, 0, 0], - [1, 0, 0, 0], - [0, 2, 0, 0], - [0, 0, 3, 0] - ]) + np.diag(a, k=-1) + == np.array([[0, 0, 0, 0], [1, 0, 0, 0], [0, 2, 0, 0], [0, 0, 3, 0]]) ) # Test on 2D square array @@ -151,6 +129,7 @@ def test_diag_casadi(): # assert np.all(np.diag(c, k=1) == np.array([2, 6])) # assert np.all(np.diag(c, k=-1) == np.array([4])) + def test_roll_onp(): # Test on 1D array a = np.arange(1, 101) # Large array @@ -192,7 +171,9 @@ def test_roll_casadi_2d(): assert np.all(cas.DM(np.roll(a, 2, axis=0)) == np.roll(a_np, 2, axis=0)) # Shift along both axes - assert np.all(cas.DM(np.roll(a, (2, 3), axis=(0, 1))) == np.roll(a_np, (2, 3), axis=(0, 1))) + assert np.all( + cas.DM(np.roll(a, (2, 3), axis=(0, 1))) == np.roll(a_np, (2, 3), axis=(0, 1)) + ) # Test on non-square 2D array a_np = np.reshape(np.arange(1, 201), (10, 20)) # non-square 2D array @@ -205,7 +186,9 @@ def test_roll_casadi_2d(): assert np.all(cas.DM(np.roll(a, 2, axis=0)) == np.roll(a_np, 2, axis=0)) # Shift along both axes - assert np.all(cas.DM(np.roll(a, (2, 3), axis=(0, 1))) == np.roll(a_np, (2, 3), axis=(0, 1))) + assert np.all( + cas.DM(np.roll(a, (2, 3), axis=(0, 1))) == np.roll(a_np, (2, 3), axis=(0, 1)) + ) def test_max(): @@ -304,12 +287,14 @@ def test_reshape_2D_vec_wide(): def test_reshape_2D(): - a_np = np.array([ - [1, 2, 3], - [4, 5, 6], - [7, 8, 9], - [10, 11, 12], - ]) + a_np = np.array( + [ + [1, 2, 3], + [4, 5, 6], + [7, 8, 9], + [10, 11, 12], + ] + ) a_cas = cas.DM(a_np) test_inputs = [ @@ -346,31 +331,22 @@ def test_assert_equal_shape(): a = np.array([1, 2, 3]) b = cas.DM(a) - np.assert_equal_shape([ - a, - a - ]) - np.assert_equal_shape({ - "thing1": a, - "thing2": a, - }) + np.assert_equal_shape([a, a]) + np.assert_equal_shape( + { + "thing1": a, + "thing2": a, + } + ) with pytest.raises(ValueError): - np.assert_equal_shape([ - np.array([1, 2, 3]), - np.array([1, 2, 3, 4]) - ]) - np.assert_equal_shape({ - "thing1": np.array([1, 2, 3]), - "thing2": np.array([1, 2, 3, 4]) - }) - np.assert_equal_shape([ - 2, - 3, - 4 - ]) - - -if __name__ == '__main__': + np.assert_equal_shape([np.array([1, 2, 3]), np.array([1, 2, 3, 4])]) + np.assert_equal_shape( + {"thing1": np.array([1, 2, 3]), "thing2": np.array([1, 2, 3, 4])} + ) + np.assert_equal_shape([2, 3, 4]) + + +if __name__ == "__main__": # # Test on 1D array # a_np = np.arange(1, 101) # a = cas.SX(a_np) diff --git a/aerosandbox/numpy/test_numpy/test_calculus.py b/aerosandbox/numpy/test_numpy/test_calculus.py index 0ca81d0a2..896f00d70 100644 --- a/aerosandbox/numpy/test_numpy/test_calculus.py +++ b/aerosandbox/numpy/test_numpy/test_calculus.py @@ -5,9 +5,7 @@ def test_diff(): a = np.arange(100) - assert np.all( - np.diff(a) == pytest.approx(1) - ) + assert np.all(np.diff(a) == pytest.approx(1)) def test_trapz(): @@ -19,10 +17,8 @@ def test_trapz(): def test_invertability_of_diff_trapz(): a = np.sin(np.arange(10)) - assert np.all( - np.trapz(np.diff(a)) == pytest.approx(np.diff(np.trapz(a))) - ) + assert np.all(np.trapz(np.diff(a)) == pytest.approx(np.diff(np.trapz(a)))) -if __name__ == '__main__': +if __name__ == "__main__": pytest.main() diff --git a/aerosandbox/numpy/test_numpy/test_conditionals.py b/aerosandbox/numpy/test_numpy/test_conditionals.py index 0ac2fe378..19bc25826 100644 --- a/aerosandbox/numpy/test_numpy/test_conditionals.py +++ b/aerosandbox/numpy/test_numpy/test_conditionals.py @@ -7,34 +7,22 @@ def test_where_numpy(): a = np.ones(4) b = 2 * np.ones(4) - c = np.where( - np.array([True, False, True, False]), - a, - b - ) + c = np.where(np.array([True, False, True, False]), a, b) - assert np.all( - c == np.array([1, 2, 1, 2]) - ) + assert np.all(c == np.array([1, 2, 1, 2])) def test_where_casadi(): a = cas.DM(np.ones(4)) b = 2 * cas.DM(np.ones(4)) - c = np.where( - cas.DM([1, 0, 1, 0]), - a, - b - ) + c = np.where(cas.DM([1, 0, 1, 0]), a, b) - assert np.all( - c == cas.DM([1, 2, 1, 2]) - ) + assert np.all(c == cas.DM([1, 2, 1, 2])) # def test_if_else_mixed(): # TODO write this -if __name__ == '__main__': +if __name__ == "__main__": pytest.main() diff --git a/aerosandbox/numpy/test_numpy/test_determine_type.py b/aerosandbox/numpy/test_numpy/test_determine_type.py index 314fffad6..cc9a7845e 100644 --- a/aerosandbox/numpy/test_numpy/test_determine_type.py +++ b/aerosandbox/numpy/test_numpy/test_determine_type.py @@ -10,63 +10,38 @@ def test_int(): def test_float(): - assert is_casadi_type(5., recursive=True) == False - assert is_casadi_type(5., recursive=False) == False + assert is_casadi_type(5.0, recursive=True) == False + assert is_casadi_type(5.0, recursive=False) == False def test_numpy(): - assert is_casadi_type( - np.array([1, 2, 3]), - recursive=True - ) == False - assert is_casadi_type( - np.array([1, 2, 3]), - recursive=False - ) == False + assert is_casadi_type(np.array([1, 2, 3]), recursive=True) == False + assert is_casadi_type(np.array([1, 2, 3]), recursive=False) == False def test_casadi(): - assert is_casadi_type( - cas.MX(np.ones(5)), - recursive=False - ) == True - assert is_casadi_type( - cas.MX(np.ones(5)), - recursive=True - ) == True + assert is_casadi_type(cas.MX(np.ones(5)), recursive=False) == True + assert is_casadi_type(cas.MX(np.ones(5)), recursive=True) == True def test_numpy_list(): - assert is_casadi_type( - [np.array(5), np.array(7)], - recursive=False - ) == False - assert is_casadi_type( - [np.array(5), np.array(7)], - recursive=True - ) == False + assert is_casadi_type([np.array(5), np.array(7)], recursive=False) == False + assert is_casadi_type([np.array(5), np.array(7)], recursive=True) == False def test_casadi_list(): - assert is_casadi_type( - [cas.MX(np.ones(5)), cas.MX(np.ones(5))], - recursive=False - ) == False - assert is_casadi_type( - [cas.MX(np.ones(5)), cas.MX(np.ones(5))], - recursive=True - ) == True + assert ( + is_casadi_type([cas.MX(np.ones(5)), cas.MX(np.ones(5))], recursive=False) + == False + ) + assert ( + is_casadi_type([cas.MX(np.ones(5)), cas.MX(np.ones(5))], recursive=True) == True + ) def test_mixed_list(): - assert is_casadi_type( - [np.array(5), cas.MX(np.ones(5))], - recursive=False - ) == False - assert is_casadi_type( - [np.array(5), cas.MX(np.ones(5))], - recursive=True - ) == True + assert is_casadi_type([np.array(5), cas.MX(np.ones(5))], recursive=False) == False + assert is_casadi_type([np.array(5), cas.MX(np.ones(5))], recursive=True) == True def test_multi_level_contaminated_list(): @@ -86,5 +61,5 @@ def test_multi_level_contaminated_list(): assert is_casadi_type(a, recursive=True) == False -if __name__ == '__main__': +if __name__ == "__main__": pytest.main([__file__]) diff --git a/aerosandbox/numpy/test_numpy/test_finite_difference_operators.py b/aerosandbox/numpy/test_numpy/test_finite_difference_operators.py index 8a155b58f..1ba89be01 100644 --- a/aerosandbox/numpy/test_numpy/test_finite_difference_operators.py +++ b/aerosandbox/numpy/test_numpy/test_finite_difference_operators.py @@ -5,99 +5,38 @@ def test_uniform_forward_difference_first_degree(): assert np.finite_difference_coefficients( - x=np.arange(2), - x0=0, - derivative_degree=1 - ) == pytest.approx( - np.array([ - -1, 1 - ]) - ) + x=np.arange(2), x0=0, derivative_degree=1 + ) == pytest.approx(np.array([-1, 1])) assert np.finite_difference_coefficients( - x=np.arange(9), - x0=0, - derivative_degree=1 + x=np.arange(9), x0=0, derivative_degree=1 ) == pytest.approx( - np.array([ - -761 / 280, - 8, - -14, - 56 / 3, - -35 / 2, - 56 / 5, - -14 / 3, - 8 / 7, - -1 / 8 - ]) + np.array([-761 / 280, 8, -14, 56 / 3, -35 / 2, 56 / 5, -14 / 3, 8 / 7, -1 / 8]) ) def test_uniform_forward_difference_higher_order(): assert np.finite_difference_coefficients( - x=np.arange(5), - x0=0, - derivative_degree=3 - ) == pytest.approx( - np.array([ - -5 / 2, - 9, - -12, - 7, - -3 / 2 - ]) - ) + x=np.arange(5), x0=0, derivative_degree=3 + ) == pytest.approx(np.array([-5 / 2, 9, -12, 7, -3 / 2])) def test_uniform_central_difference(): assert np.finite_difference_coefficients( - x=[-1, 0, 1], - x0=0, - derivative_degree=1 - ) == pytest.approx( - np.array([ - -0.5, - 0, - 0.5 - ]) - ) + x=[-1, 0, 1], x0=0, derivative_degree=1 + ) == pytest.approx(np.array([-0.5, 0, 0.5])) assert np.finite_difference_coefficients( - x=[-1, 0, 1], - x0=0, - derivative_degree=2 - ) == pytest.approx( - np.array([ - 1, - -2, - 1 - ]) - ) + x=[-1, 0, 1], x0=0, derivative_degree=2 + ) == pytest.approx(np.array([1, -2, 1])) assert np.finite_difference_coefficients( - x=[-2, -1, 0, 1, 2], - x0=0, - derivative_degree=2 - ) == pytest.approx( - np.array([ - -1 / 12, - 4 / 3, - -5 / 2, - 4 / 3, - -1 / 12 - ]) - ) + x=[-2, -1, 0, 1, 2], x0=0, derivative_degree=2 + ) == pytest.approx(np.array([-1 / 12, 4 / 3, -5 / 2, 4 / 3, -1 / 12])) def test_nonuniform_difference(): assert np.finite_difference_coefficients( - x=[-1, 2], - x0=0, - derivative_degree=1 - ) == pytest.approx( - np.array([ - -1 / 3, - 1 / 3 - ]) - ) + x=[-1, 2], x0=0, derivative_degree=1 + ) == pytest.approx(np.array([-1 / 3, 1 / 3])) -if __name__ == '__main__': +if __name__ == "__main__": pytest.main() diff --git a/aerosandbox/numpy/test_numpy/test_interpolate.py b/aerosandbox/numpy/test_numpy/test_interpolate.py index 0b029fb86..84c9ccc55 100644 --- a/aerosandbox/numpy/test_numpy/test_interpolate.py +++ b/aerosandbox/numpy/test_numpy/test_interpolate.py @@ -8,10 +8,7 @@ def test_interp(): x_np = np.arange(5) y_np = x_np + 10 - for x, y in zip( - [x_np, cas.DM(x_np)], - [y_np, cas.DM(y_np)] - ): + for x, y in zip([x_np, cas.DM(x_np)], [y_np, cas.DM(y_np)]): assert np.interp(0, x, y) == pytest.approx(10) assert np.interp(4, x, y) == pytest.approx(14) @@ -37,16 +34,12 @@ def value_func_3d(x, y, z): values = value_func_3d(*np.meshgrid(*points, indexing="ij")) point = np.array([2.21, 3.12, 1.15]) - value = np.interpn( - points, values, point - ) + value = np.interpn(points, values, point) assert value == pytest.approx(value_func_3d(*point)) ### CasADi test point = cas.DM(point) - value = np.interpn( - points, values, point - ) + value = np.interpn(points, values, point) assert value == pytest.approx(float(value_func_3d(point[0], point[1], point[2]))) @@ -62,33 +55,19 @@ def value_func_3d(x, y, z): points = (x, y, z) values = value_func_3d(*np.meshgrid(*points, indexing="ij")) - point = np.array([ - [2.21, 3.12, 1.15], - [3.42, 0.81, 2.43] - ]) - value = np.interpn( - points, values, point - ) + point = np.array([[2.21, 3.12, 1.15], [3.42, 0.81, 2.43]]) + value = np.interpn(points, values, point) assert np.all( - value == pytest.approx( - value_func_3d( - *[ - point[:, i] for i in range(point.shape[1]) - ] - ) - ) + value + == pytest.approx(value_func_3d(*[point[:, i] for i in range(point.shape[1])])) ) assert len(value) == 2 ### CasADi test point = cas.DM(point) - value = np.interpn( - points, values, point - ) + value = np.interpn(points, values, point) value_actual = value_func_3d( - *[ - np.array(point[:, i]) for i in range(point.shape[1]) - ] + *[np.array(point[:, i]) for i in range(point.shape[1])] ) for i in range(len(value)): assert value[i] == pytest.approx(float(value_actual[i])) @@ -101,20 +80,16 @@ def test_interpn_bspline_casadi(): """ def func(x, y, z): # Sphere function - return x ** 3 + y ** 3 + z ** 3 + return x**3 + y**3 + z**3 x = np.linspace(-5, 5, 10) y = np.linspace(-5, 5, 20) z = np.linspace(-5, 5, 30) points = (x, y, z) - values = func( - *np.meshgrid(*points, indexing="ij") - ) + values = func(*np.meshgrid(*points, indexing="ij")) point = np.array([0.4, 0.5, 0.6]) - value = np.interpn( - points, values, point, method="bspline" - ) + value = np.interpn(points, values, point, method="bspline") assert value == pytest.approx(func(*point)) @@ -131,16 +106,12 @@ def value_func_3d(x, y, z): point = np.array([5.21, 3.12, 1.15]) with pytest.raises(ValueError): - value = np.interpn( - points, values, point - ) + value = np.interpn(points, values, point) ### CasADi test point = cas.DM(point) with pytest.raises(ValueError): - value = np.interpn( - points, values, point - ) + value = np.interpn(points, values, point) def test_interpn_bounds_error_multiple_samples(): @@ -153,21 +124,14 @@ def value_func_3d(x, y, z): points = (x, y, z) values = value_func_3d(*np.meshgrid(*points, indexing="ij")) - point = np.array([ - [2.21, 3.12, 1.15], - [3.42, 5.81, 2.43] - ]) + point = np.array([[2.21, 3.12, 1.15], [3.42, 5.81, 2.43]]) with pytest.raises(ValueError): - value = np.interpn( - points, values, point - ) + value = np.interpn(points, values, point) ### CasADi test point = cas.DM(point) with pytest.raises(ValueError): - value = np.interpn( - points, values, point - ) + value = np.interpn(points, values, point) def test_interpn_fill_value(): @@ -183,28 +147,24 @@ def value_func_3d(x, y, z): point = np.array([5.21, 3.12, 1.15]) value = np.interpn( - points, values, point, - method="bspline", - bounds_error=False, - fill_value=-17 + points, values, point, method="bspline", bounds_error=False, fill_value=-17 ) assert value == pytest.approx(-17) value = np.interpn( - points, values, point, + points, + values, + point, method="bspline", bounds_error=False, ) assert np.isnan(value) value = np.interpn( - points, values, point, - method="bspline", - bounds_error=None, - fill_value=None + points, values, point, method="bspline", bounds_error=None, fill_value=None ) assert value == pytest.approx(value_func_3d(5, 3.12, 1.15)) -if __name__ == '__main__': +if __name__ == "__main__": pytest.main() diff --git a/aerosandbox/numpy/test_numpy/test_linalg.py b/aerosandbox/numpy/test_numpy/test_linalg.py index f10057d3d..ef360c52c 100644 --- a/aerosandbox/numpy/test_numpy/test_linalg.py +++ b/aerosandbox/numpy/test_numpy/test_linalg.py @@ -16,16 +16,10 @@ def test_norm_2D(): assert np.linalg.norm(cas_a) == np.linalg.norm(a) - assert np.all( - np.linalg.norm(cas_a, axis=0) == - np.linalg.norm(a, axis=0) - ) + assert np.all(np.linalg.norm(cas_a, axis=0) == np.linalg.norm(a, axis=0)) - assert np.all( - np.linalg.norm(cas_a, axis=1) == - np.linalg.norm(a, axis=1) - ) + assert np.all(np.linalg.norm(cas_a, axis=1) == np.linalg.norm(a, axis=1)) -if __name__ == '__main__': +if __name__ == "__main__": pytest.main() diff --git a/aerosandbox/numpy/test_numpy/test_linalg_top_level.py b/aerosandbox/numpy/test_numpy/test_linalg_top_level.py index 06e5b3429..c9f39b3b3 100644 --- a/aerosandbox/numpy/test_numpy/test_linalg_top_level.py +++ b/aerosandbox/numpy/test_numpy/test_linalg_top_level.py @@ -13,15 +13,9 @@ def test_cross_1D_input(): correct_result = np.cross(a, b) cas_correct_result = cas.DM(correct_result) - assert np.all( - np.cross(a, cas_b) == cas_correct_result - ) - assert np.all( - np.cross(cas_a, b) == cas_correct_result - ) - assert np.all( - np.cross(cas_a, cas_b) == cas_correct_result - ) + assert np.all(np.cross(a, cas_b) == cas_correct_result) + assert np.all(np.cross(cas_a, b) == cas_correct_result) + assert np.all(np.cross(cas_a, cas_b) == cas_correct_result) def test_cross_2D_input_last_axis(): @@ -34,15 +28,9 @@ def test_cross_2D_input_last_axis(): correct_result = np.cross(a, b) cas_correct_result = cas.DM(correct_result) - assert np.all( - np.cross(a, cas_b) == cas_correct_result - ) - assert np.all( - np.cross(cas_a, b) == cas_correct_result - ) - assert np.all( - np.cross(cas_a, cas_b) == cas_correct_result - ) + assert np.all(np.cross(a, cas_b) == cas_correct_result) + assert np.all(np.cross(cas_a, b) == cas_correct_result) + assert np.all(np.cross(cas_a, cas_b) == cas_correct_result) def test_cross_2D_input_first_axis(): @@ -55,16 +43,10 @@ def test_cross_2D_input_first_axis(): correct_result = np.cross(a, b, axis=0) cas_correct_result = cas.DM(correct_result) - assert np.all( - np.cross(a, cas_b, axis=0) == cas_correct_result - ) - assert np.all( - np.cross(cas_a, b, axis=0) == cas_correct_result - ) - assert np.all( - np.cross(cas_a, cas_b, axis=0) == cas_correct_result - ) + assert np.all(np.cross(a, cas_b, axis=0) == cas_correct_result) + assert np.all(np.cross(cas_a, b, axis=0) == cas_correct_result) + assert np.all(np.cross(cas_a, cas_b, axis=0) == cas_correct_result) -if __name__ == '__main__': +if __name__ == "__main__": pytest.main() diff --git a/aerosandbox/numpy/test_numpy/test_logicals.py b/aerosandbox/numpy/test_numpy/test_logicals.py index eb9fee0e8..30c97eede 100644 --- a/aerosandbox/numpy/test_numpy/test_logicals.py +++ b/aerosandbox/numpy/test_numpy/test_logicals.py @@ -6,10 +6,8 @@ def test_basic_logicals_numpy(): a = np.array([True, True, False, False]) b = np.array([True, False, True, False]) - assert np.all( - a & b == np.array([True, False, False, False]) - ) + assert np.all(a & b == np.array([True, False, False, False])) -if __name__ == '__main__': +if __name__ == "__main__": pytest.main() diff --git a/aerosandbox/numpy/test_numpy/test_rotations.py b/aerosandbox/numpy/test_numpy/test_rotations.py index 75ffa2dcf..8a9c6d196 100644 --- a/aerosandbox/numpy/test_numpy/test_rotations.py +++ b/aerosandbox/numpy/test_numpy/test_rotations.py @@ -9,26 +9,21 @@ def test_euler_angles_equivalence_to_general_3D(): rot_euler = np.rotation_matrix_from_euler_angles(phi, theta, psi) rot_manual = ( - np.rotation_matrix_3D(psi, np.array([0, 0, 1])) @ - np.rotation_matrix_3D(theta, np.array([0, 1, 0])) @ - np.rotation_matrix_3D(phi, np.array([1, 0, 0])) + np.rotation_matrix_3D(psi, np.array([0, 0, 1])) + @ np.rotation_matrix_3D(theta, np.array([0, 1, 0])) + @ np.rotation_matrix_3D(phi, np.array([1, 0, 0])) ) assert rot_euler == pytest.approx(rot_manual) def test_validity_of_euler_angles(): - rot = np.rotation_matrix_from_euler_angles( - 2, 4, 6 - ) + rot = np.rotation_matrix_from_euler_angles(2, 4, 6) assert np.is_valid_rotation_matrix(rot) def test_validity_of_general_3D(): - rot = np.rotation_matrix_3D( - angle=1, - axis=[2, 3, 4] - ) + rot = np.rotation_matrix_3D(angle=1, axis=[2, 3, 4]) assert np.is_valid_rotation_matrix(rot) @@ -57,5 +52,5 @@ def test_general_3D_shorthands(): assert pytest.approx(rotz) == np.rotation_matrix_3D(1, "z") -if __name__ == '__main__': +if __name__ == "__main__": pytest.main() diff --git a/aerosandbox/numpy/test_numpy/test_surrogate_model_tools.py b/aerosandbox/numpy/test_numpy/test_surrogate_model_tools.py index ccc2a1d0d..0d9505cb9 100644 --- a/aerosandbox/numpy/test_numpy/test_surrogate_model_tools.py +++ b/aerosandbox/numpy/test_numpy/test_surrogate_model_tools.py @@ -31,5 +31,5 @@ def test_softmax(plot=False): plt.show() -if __name__ == '__main__': +if __name__ == "__main__": pytest.main() diff --git a/aerosandbox/numpy/trig.py b/aerosandbox/numpy/trig.py index 58ae492bb..b0cce7112 100644 --- a/aerosandbox/numpy/trig.py +++ b/aerosandbox/numpy/trig.py @@ -1,8 +1,8 @@ import numpy as _onp from numpy import pi as _pi -_deg2rad = 180. / _pi -_rad2deg = _pi / 180. +_deg2rad = 180.0 / _pi +_rad2deg = _pi / 180.0 def degrees(x): diff --git a/aerosandbox/optimization/__init__.py b/aerosandbox/optimization/__init__.py index e963b4b11..26d1ae353 100644 --- a/aerosandbox/optimization/__init__.py +++ b/aerosandbox/optimization/__init__.py @@ -1 +1 @@ -from aerosandbox.optimization.opti import Opti, OptiSol \ No newline at end of file +from aerosandbox.optimization.opti import Opti, OptiSol diff --git a/aerosandbox/optimization/opti.py b/aerosandbox/optimization/opti.py index c33920976..f6de59fa2 100644 --- a/aerosandbox/optimization/opti.py +++ b/aerosandbox/optimization/opti.py @@ -26,14 +26,15 @@ class Opti(cas.Opti): >>> print(sol(x)) # Prints the value of x at the optimum. """ - def __init__(self, - variable_categories_to_freeze: Union[List[str], str] = None, - cache_filename: str = None, - load_frozen_variables_from_cache: bool = False, - save_to_cache_on_solve: bool = False, - ignore_violated_parametric_constraints: bool = False, - freeze_style: str = "parameter", - ): # TODO document + def __init__( + self, + variable_categories_to_freeze: Union[List[str], str] = None, + cache_filename: str = None, + load_frozen_variables_from_cache: bool = False, + save_to_cache_on_solve: bool = False, + ignore_violated_parametric_constraints: bool = False, + freeze_style: str = "parameter", + ): # TODO document # Default arguments if variable_categories_to_freeze is None: @@ -45,33 +46,44 @@ def __init__(self, # Initialize class variables self.variable_categories_to_freeze = variable_categories_to_freeze self.cache_filename = cache_filename - self.load_frozen_variables_from_cache = load_frozen_variables_from_cache # TODO load and start tracking + self.load_frozen_variables_from_cache = ( + load_frozen_variables_from_cache # TODO load and start tracking + ) self.save_to_cache_on_solve = save_to_cache_on_solve - self.ignore_violated_parametric_constraints = ignore_violated_parametric_constraints + self.ignore_violated_parametric_constraints = ( + ignore_violated_parametric_constraints + ) self.freeze_style = freeze_style # Start tracking variables and categorize them. - self.variables_categorized = {} # category name [str] : list of variables [list] + self.variables_categorized = ( + {} + ) # category name [str] : list of variables [list] # Track variable declaration locations, useful for debugging - self._variable_declarations = SortedDict() # first index in super().x : (filename, lineno, code_context, n_vars) - self._constraint_declarations = SortedDict() # first index in super().g : (filename, lineno, code_context, n_cons) + self._variable_declarations = ( + SortedDict() + ) # first index in super().x : (filename, lineno, code_context, n_vars) + self._constraint_declarations = ( + SortedDict() + ) # first index in super().g : (filename, lineno, code_context, n_cons) self._variable_index_counter = 0 self._constraint_index_counter = 0 ### Primary Methods - def variable(self, - init_guess: Union[float, np.ndarray] = None, - n_vars: int = None, - scale: float = None, - freeze: bool = False, - log_transform: bool = False, - category: str = "Uncategorized", - lower_bound: float = None, - upper_bound: float = None, - _stacklevel: int = 1, - ) -> cas.MX: + def variable( + self, + init_guess: Union[float, np.ndarray] = None, + n_vars: int = None, + scale: float = None, + freeze: bool = False, + log_transform: bool = False, + category: str = "Uncategorized", + lower_bound: float = None, + upper_bound: float = None, + _stacklevel: int = 1, + ) -> cas.MX: """ Initializes a new decision variable (or vector of decision variables). You should pass an initial guess ( `init_guess`) upon defining a new variable. Dimensionality is inferred from this initial guess, but it can be @@ -218,22 +230,31 @@ def variable(self, ### Set defaults if init_guess is None: import warnings + if log_transform: init_guess = 1 - warnings.warn("No initial guess set for Opti.variable(). Defaulting to 1 (log-transformed variable).", - stacklevel=2) + warnings.warn( + "No initial guess set for Opti.variable(). Defaulting to 1 (log-transformed variable).", + stacklevel=2, + ) else: init_guess = 0 - warnings.warn("No initial guess set for Opti.variable(). Defaulting to 0.", stacklevel=2) + warnings.warn( + "No initial guess set for Opti.variable(). Defaulting to 0.", + stacklevel=2, + ) if n_vars is None: # Infer dimensionality from init_guess if it is not provided n_vars = np.length(init_guess) if scale is None: # Infer a scale from init_guess if it is not provided if log_transform: scale = 1 else: - scale = np.mean(np.fabs(init_guess)) # Initialize the scale to a heuristic based on the init_guess - if isinstance(scale, - cas.MX) or scale == 0: # If that heuristic leads to a scale of 0, use a scale of 1 instead. + scale = np.mean( + np.fabs(init_guess) + ) # Initialize the scale to a heuristic based on the init_guess + if ( + isinstance(scale, cas.MX) or scale == 0 + ): # If that heuristic leads to a scale of 0, use a scale of 1 instead. scale = 1 # scale = np.fabs( @@ -246,15 +267,15 @@ def variable(self, length_init_guess = np.length(init_guess) if length_init_guess != 1 and length_init_guess != n_vars: - raise ValueError(f"`init_guess` has length {length_init_guess}, but `n_vars` is {n_vars}!") + raise ValueError( + f"`init_guess` has length {length_init_guess}, but `n_vars` is {n_vars}!" + ) # Try to convert init_guess to a float or np.ndarray if it is an Opti parameter. try: init_guess = self.value(init_guess) except RuntimeError as e: - if not ( - freeze and self.freeze_style == "float" - ): + if not (freeze and self.freeze_style == "float"): raise TypeError( "The `init_guess` for a new Opti variable must not be a function of an existing Opti variable." ) @@ -263,16 +284,17 @@ def variable(self, if log_transform: if np.any(init_guess <= 0): raise ValueError( - "If you are initializing a log-transformed variable, the initial guess(es) must all be positive.") + "If you are initializing a log-transformed variable, the initial guess(es) must all be positive." + ) if np.any(scale <= 0): raise ValueError("The 'scale' argument must be a positive number.") # If the variable is in a category to be frozen, fix the variable at the initial guess. is_manually_frozen = freeze if ( - category in self.variable_categories_to_freeze or - category == self.variable_categories_to_freeze or - self.variable_categories_to_freeze == "all" + category in self.variable_categories_to_freeze + or category == self.variable_categories_to_freeze + or self.variable_categories_to_freeze == "all" ): freeze = True @@ -298,17 +320,21 @@ def variable(self, self.set_initial(log_var, np.log(init_guess)) # Track where this variable was declared in code. - filename, lineno, code_context = inspect_tools.get_caller_source_location(stacklevel=_stacklevel + 1) + filename, lineno, code_context = inspect_tools.get_caller_source_location( + stacklevel=_stacklevel + 1 + ) self._variable_declarations[self._variable_index_counter] = ( filename, lineno, code_context, - n_vars + n_vars, ) self._variable_index_counter += n_vars # Track the category of the variable - if category not in self.variables_categorized: # Add a category if it does not exist + if ( + category not in self.variables_categorized + ): # Add a category if it does not exist self.variables_categorized[category] = [] self.variables_categorized[category].append(var) try: @@ -321,32 +347,31 @@ def variable(self, if (not log_transform) or (freeze): if lower_bound is not None: self.subject_to( - var / scale >= lower_bound / scale, - _stacklevel=_stacklevel + 1 + var / scale >= lower_bound / scale, _stacklevel=_stacklevel + 1 ) if upper_bound is not None: self.subject_to( - var / scale <= upper_bound / scale, - _stacklevel=_stacklevel + 1 + var / scale <= upper_bound / scale, _stacklevel=_stacklevel + 1 ) else: if lower_bound is not None: self.subject_to( log_var / log_scale >= np.log(lower_bound) / log_scale, - _stacklevel=_stacklevel + 1 + _stacklevel=_stacklevel + 1, ) if upper_bound is not None: self.subject_to( log_var / log_scale <= np.log(upper_bound) / log_scale, - _stacklevel=_stacklevel + 1 + _stacklevel=_stacklevel + 1, ) return var - def subject_to(self, - constraint: Union[cas.MX, bool, List], # TODO add scale - _stacklevel: int = 1, - ) -> Union[cas.MX, None, List[cas.MX]]: + def subject_to( + self, + constraint: Union[cas.MX, bool, List], # TODO add scale + _stacklevel: int = 1, + ) -> Union[cas.MX, None, List[cas.MX]]: """ Initialize a new equality or inequality constraint(s). @@ -386,39 +411,50 @@ def subject_to(self, # If the latter, recursively apply them. if type(constraint) in (list, tuple): return [ - self.subject_to(each_constraint, _stacklevel=_stacklevel + 2) # return the dual of each constraint + self.subject_to( + each_constraint, _stacklevel=_stacklevel + 2 + ) # return the dual of each constraint for each_constraint in constraint ] # If it's a proper constraint (MX-type and non-parametric), # pass it into the parent class Opti formulation and be done with it. - if isinstance(constraint, cas.MX) and not self.advanced.is_parametric(constraint): + if isinstance(constraint, cas.MX) and not self.advanced.is_parametric( + constraint + ): # constraint = cas.cse(constraint) super().subject_to(constraint) dual = self.dual(constraint) # Track where this constraint was declared in code. n_cons = np.length(constraint) - filename, lineno, code_context = inspect_tools.get_caller_source_location(stacklevel=_stacklevel + 1) + filename, lineno, code_context = inspect_tools.get_caller_source_location( + stacklevel=_stacklevel + 1 + ) self._constraint_declarations[self._constraint_index_counter] = ( filename, lineno, code_context, - n_cons + n_cons, ) self._constraint_index_counter += np.length(constraint) return dual else: # Constraint is not valid because it is not MX type or is parametric. try: - constraint_satisfied = np.all(self.value(constraint)) # Determine if the constraint is true + constraint_satisfied = np.all( + self.value(constraint) + ) # Determine if the constraint is true except Exception: - raise TypeError(f"""Opti.subject_to could not determine the truthiness of your constraint, and it + raise TypeError( + f"""Opti.subject_to could not determine the truthiness of your constraint, and it doesn't appear to be a symbolic type or a boolean type. You supplied the following constraint: - {constraint}""") + {constraint}""" + ) - if isinstance(constraint, - cas.MX) and not constraint_satisfied: # Determine if the constraint is *almost* true + if ( + isinstance(constraint, cas.MX) and not constraint_satisfied + ): # Determine if the constraint is *almost* true try: LHS = constraint.dep(0) RHS = constraint.dep(1) @@ -426,10 +462,12 @@ def subject_to(self, RHS_value = self.value(RHS) except Exception: raise ValueError( - """Could not evaluate the LHS and RHS of the constraint - are you sure you passed in a comparative expression?""") + """Could not evaluate the LHS and RHS of the constraint - are you sure you passed in a comparative expression?""" + ) - constraint_satisfied = np.allclose(LHS_value, - RHS_value) # Call the constraint satisfied if it is *almost* true. + constraint_satisfied = np.allclose( + LHS_value, RHS_value + ) # Call the constraint satisfied if it is *almost* true. if constraint_satisfied or self.ignore_violated_parametric_constraints: # If the constraint(s) always evaluates True (e.g. if you enter "5 > 3"), skip it. @@ -439,25 +477,30 @@ def subject_to(self, # If any of the constraint(s) are always False (e.g. if you enter "5 < 3"), raise an error. # This indicates that the problem is infeasible as-written, likely because the user has frozen too # many decision variables using the Opti.variable(freeze=True) syntax. - raise RuntimeError(f"""The problem is infeasible due to a constraint that always evaluates False. - This can happen if you've frozen too many decision variables, leading to an overconstrained problem.""") + raise RuntimeError( + f"""The problem is infeasible due to a constraint that always evaluates False. + This can happen if you've frozen too many decision variables, leading to an overconstrained problem.""" + ) - def minimize(self, - f: cas.MX, - ) -> None: + def minimize( + self, + f: cas.MX, + ) -> None: # f = cas.cse(f) super().minimize(f) - def maximize(self, - f: cas.MX, - ) -> None: + def maximize( + self, + f: cas.MX, + ) -> None: # f = cas.cse(f) super().minimize(-1 * f) - def parameter(self, - value: Union[float, np.ndarray] = 0., - n_params: int = None, - ) -> cas.MX: + def parameter( + self, + value: Union[float, np.ndarray] = 0.0, + n_params: int = None, + ) -> cas.MX: """ Initializes a new parameter (or vector of parameters). You must pass a value (`value`) upon defining a new parameter. Dimensionality is inferred from this value, but it can be overridden; see below for syntax. @@ -517,17 +560,18 @@ def parameter(self, return param - def solve(self, - parameter_mapping: Dict[cas.MX, float] = None, - max_iter: int = 1000, - max_runtime: float = 1e20, - callback: Callable[[int], Any] = None, - verbose: bool = True, - jit: bool = False, # TODO document, add unit tests for jit - detect_simple_bounds: bool = False, # TODO document - options: Dict = None, # TODO document - behavior_on_failure: str = "raise", - ) -> "OptiSol": + def solve( + self, + parameter_mapping: Dict[cas.MX, float] = None, + max_iter: int = 1000, + max_runtime: float = 1e20, + callback: Callable[[int], Any] = None, + verbose: bool = True, + jit: bool = False, # TODO document, add unit tests for jit + detect_simple_bounds: bool = False, # TODO document + options: Dict = None, # TODO document + behavior_on_failure: str = "raise", + ) -> "OptiSol": """ Solve the optimization problem using CasADi with IPOPT backend. @@ -595,24 +639,24 @@ def solve(self, category_values = solution_dict[category] if len(category_variables) != len(category_values): - raise RuntimeError("""Problem with loading cached solution: it looks like new variables have been + raise RuntimeError( + """Problem with loading cached solution: it looks like new variables have been defined since the cached solution was saved (or variables were defined in a different order). Because of this, the cache cannot be loaded. - Re-run the original optimization study to regenerate the cached solution.""") + Re-run the original optimization study to regenerate the cached solution.""" + ) for var, val in zip(category_variables, category_values): if not var.is_manually_frozen: - parameter_mapping = { - **parameter_mapping, - var: val - } + parameter_mapping = {**parameter_mapping, var: val} ### Map any parameters to needed values for k, v in parameter_mapping.items(): if not np.is_casadi_type(k, recursive=False): raise TypeError( - f"All keys in `parameter_mapping` must be CasADi parameters; you gave an object of type \'{type(k).__name__}\'.\n" - f"In general, make sure all keys are the result of calling `opti.parameter()`.") + f"All keys in `parameter_mapping` must be CasADi parameters; you gave an object of type '{type(k).__name__}'.\n" + f"In general, make sure all keys are the result of calling `opti.parameter()`." + ) size_k = np.prod(k.shape) try: @@ -620,10 +664,12 @@ def solve(self, except AttributeError: size_v = 1 if size_k != size_v: - raise RuntimeError("""Problem with loading cached solution: it looks like the length of a vectorized + raise RuntimeError( + """Problem with loading cached solution: it looks like the length of a vectorized variable has changed since the cached solution was saved (or variables were defined in a different order). Because of this, the cache cannot be loaded. - Re-run the original optimization study to regenerate the cached solution.""") + Re-run the original optimization study to regenerate the cached solution.""" + ) self.set_value(k, v) @@ -632,12 +678,12 @@ def solve(self, options = {} default_options = { - "ipopt.sb" : 'yes', # Hide the IPOPT banner. - "ipopt.max_iter" : max_iter, - "ipopt.max_cpu_time" : max_runtime, - "ipopt.mu_strategy" : "adaptive", + "ipopt.sb": "yes", # Hide the IPOPT banner. + "ipopt.max_iter": max_iter, + "ipopt.max_cpu_time": max_runtime, + "ipopt.mu_strategy": "adaptive", "ipopt.fast_step_computation": "yes", - "detect_simple_bounds" : detect_simple_bounds, + "detect_simple_bounds": detect_simple_bounds, } if jit: @@ -654,10 +700,13 @@ def solve(self, default_options["print_time"] = False # No time printing default_options["ipopt.print_level"] = 0 # No printing from IPOPT - self.solver('ipopt', { - **default_options, - **options, - }) + self.solver( + "ipopt", + { + **default_options, + **options, + }, + ) # Set the callback if callback is not None: @@ -665,38 +714,31 @@ def solve(self, # Do the actual solve if behavior_on_failure == "raise": - sol = OptiSol( - opti=self, - cas_optisol=super().solve() - ) + sol = OptiSol(opti=self, cas_optisol=super().solve()) elif behavior_on_failure == "return_last": try: - sol = OptiSol( - opti=self, - cas_optisol=super().solve() - ) + sol = OptiSol(opti=self, cas_optisol=super().solve()) except RuntimeError: import warnings + warnings.warn("Optimization failed. Returning last solution.") - sol = OptiSol( - opti=self, - cas_optisol=self.debug - ) + sol = OptiSol(opti=self, cas_optisol=self.debug) if self.save_to_cache_on_solve: self.save_solution() return sol - def solve_sweep(self, - parameter_mapping: Dict[cas.MX, np.ndarray], - update_initial_guesses_between_solves=False, - verbose=True, - solve_kwargs: Dict = None, - return_callable: bool = False, - garbage_collect_between_runs: bool = False, - ) -> Union[np.ndarray, Callable[[cas.MX], np.ndarray]]: + def solve_sweep( + self, + parameter_mapping: Dict[cas.MX, np.ndarray], + update_initial_guesses_between_solves=False, + verbose=True, + solve_kwargs: Dict = None, + return_callable: bool = False, + garbage_collect_between_runs: bool = False, + ) -> Union[np.ndarray, Callable[[cas.MX], np.ndarray]]: # Handle defaults if solve_kwargs is None: @@ -706,7 +748,7 @@ def solve_sweep(self, verbose=False, max_iter=200, ), - **solve_kwargs + **solve_kwargs, } # Split parameter_mappings up so that it can be passed into run() via np.vectorize @@ -724,12 +766,12 @@ def run(*args: Tuple[float]) -> Optional["OptiSol"]: # Collect garbage before each run, to avoid memory issues. if garbage_collect_between_runs: import gc + gc.collect() # Reconstruct parameter mapping on a run-by-run basis by zipping together keys and this run's values. parameter_mappings_for_this_run: [cas.MX, float] = { - k: v - for k, v in zip(keys, args) + k: v for k, v in zip(keys, args) } # Pull in run_number so that we can increment this counter @@ -739,25 +781,22 @@ def run(*args: Tuple[float]) -> Optional["OptiSol"]: if verbose: print( "|".join( - [ - f"Run {run_number}/{n_runs}".ljust(12) - ] + [ - f"{v:10.5g}" - for v in args - ] + [""] + [f"Run {run_number}/{n_runs}".ljust(12)] + + [f"{v:10.5g}" for v in args] + + [""] ), - end='' # Leave the newline off, since we'll complete the line later with a success or fail print. + end="", # Leave the newline off, since we'll complete the line later with a success or fail print. ) run_number += 1 import time + start_time = time.time() try: sol = self.solve( - parameter_mapping=parameter_mappings_for_this_run, - **solve_kwargs + parameter_mapping=parameter_mappings_for_this_run, **solve_kwargs ) if update_initial_guesses_between_solves: @@ -765,7 +804,9 @@ def run(*args: Tuple[float]) -> Optional["OptiSol"]: if verbose: stats = sol.stats() - print(f" Solved in {stats['iter_count']} iterations, {time.time() - start_time:.2f} sec.") + print( + f" Solved in {stats['iter_count']} iterations, {time.time() - start_time:.2f} sec." + ) return sol @@ -773,14 +814,13 @@ def run(*args: Tuple[float]) -> Optional["OptiSol"]: if verbose: sol = OptiSol(opti=self, cas_optisol=self.debug) stats = sol.stats() - print(f" Failed in {stats['iter_count']} iterations, {time.time() - start_time:.2f} sec.") + print( + f" Failed in {stats['iter_count']} iterations, {time.time() - start_time:.2f} sec." + ) return None - run_vectorized = np.vectorize( - run, - otypes='O' # object output - ) + run_vectorized = np.vectorize(run, otypes="O") # object output sols = run_vectorized(*values) @@ -797,11 +837,12 @@ def get_vals(x: cas.MX) -> np.ndarray: return sols ### Debugging Methods - def find_variable_declaration(self, - index: int, - use_full_filename: bool = False, - return_string: bool = False, - ) -> Union[None, str]: + def find_variable_declaration( + self, + index: int, + use_full_filename: bool = False, + return_string: bool = False, + ) -> Union[None, str]: ### Check inputs if index < 0: raise ValueError("Indices must be nonnegative.") @@ -810,9 +851,13 @@ def find_variable_declaration(self, f"The variable index exceeds the number of declared variables ({self._variable_index_counter})!" ) - index_of_first_element = self._variable_declarations.iloc[self._variable_declarations.bisect_right(index) - 1] + index_of_first_element = self._variable_declarations.iloc[ + self._variable_declarations.bisect_right(index) - 1 + ] - filename, lineno, code_context, n_vars = self._variable_declarations[index_of_first_element] + filename, lineno, code_context, n_vars = self._variable_declarations[ + index_of_first_element + ] source = inspect_tools.get_source_code_from_location( filename=filename, lineno=lineno, @@ -822,25 +867,25 @@ def find_variable_declaration(self, title = f"{'Scalar' if is_scalar else 'Vector'} variable" if not is_scalar: title += f" (index {index - index_of_first_element} of {n_vars})" - string = "\n".join([ - "", - f"{title} defined in `{str(filename) if use_full_filename else filename.name}`, line {lineno}:", - "", - "```", - source, - "```" - ]) + string = "\n".join( + [ + "", + f"{title} defined in `{str(filename) if use_full_filename else filename.name}`, line {lineno}:", + "", + "```", + source, + "```", + ] + ) if return_string: return string else: print(string) - def find_constraint_declaration(self, - index: int, - use_full_filename: bool = False, - return_string: bool = False - ) -> Union[None, str]: + def find_constraint_declaration( + self, index: int, use_full_filename: bool = False, return_string: bool = False + ) -> Union[None, str]: ### Check inputs if index < 0: raise ValueError("Indices must be nonnegative.") @@ -851,9 +896,11 @@ def find_constraint_declaration(self, index_of_first_element = self._constraint_declarations.iloc[ self._constraint_declarations.bisect_right(index) - 1 - ] + ] - filename, lineno, code_context, n_cons = self._constraint_declarations[index_of_first_element] + filename, lineno, code_context, n_cons = self._constraint_declarations[ + index_of_first_element + ] source = inspect_tools.get_source_code_from_location( filename=filename, lineno=lineno, @@ -863,14 +910,16 @@ def find_constraint_declaration(self, title = f"{'Scalar' if is_scalar else 'Vector'} constraint" if not is_scalar: title += f" (index {index - index_of_first_element} of {n_cons})" - string = "\n".join([ - "", - f"{title} defined in `{str(filename) if use_full_filename else filename.name}`, line {lineno}:", - "", - "```", - source, - "```" - ]) + string = "\n".join( + [ + "", + f"{title} defined in `{str(filename) if use_full_filename else filename.name}`, line {lineno}:", + "", + "```", + source, + "```", + ] + ) if return_string: return string @@ -879,11 +928,12 @@ def find_constraint_declaration(self, ### Advanced Methods - def set_initial_from_sol(self, - sol: cas.OptiSol, - initialize_primals=True, - initialize_duals=True, - ) -> None: + def set_initial_from_sol( + self, + sol: cas.OptiSol, + initialize_primals=True, + initialize_duals=True, + ) -> None: """ Sets the initial value of all variables in the Opti object to the solution of another Opti instance. Useful for warm-starting an Opti instance based on the result of another instance. @@ -901,8 +951,10 @@ def set_initial_from_sol(self, def save_solution(self): if self.cache_filename is None: - raise ValueError("""In order to use the save feature, you need to supply a filepath for the cache upon - initialization of this instance of the Opti stack. For example: Opti(cache_filename = "cache.json")""") + raise ValueError( + """In order to use the save feature, you need to supply a filepath for the cache upon + initialization of this instance of the Opti stack. For example: Opti(cache_filename = "cache.json")""" + ) # Write a function that tries to turn an iterable into a JSON-serializable list def try_to_put_in_list(iterable): @@ -923,18 +975,16 @@ def try_to_put_in_list(iterable): # Write the dictionary to file with open(self.cache_filename, "w+") as f: - json.dump( - solution_dict, - fp=f, - indent=4 - ) + json.dump(solution_dict, fp=f, indent=4) return solution_dict def get_solution_dict_from_cache(self): if self.cache_filename is None: - raise ValueError("""In order to use the load feature, you need to supply a filepath for the cache upon - initialization of this instance of the Opti stack. For example: Opti(cache_filename = "cache.json")""") + raise ValueError( + """In order to use the load feature, you need to supply a filepath for the cache upon + initialization of this instance of the Opti stack. For example: Opti(cache_filename = "cache.json")""" + ) with open(self.cache_filename, "r") as f: solution_dict = json.load(fp=f) @@ -948,15 +998,16 @@ def get_solution_dict_from_cache(self): ### Methods for Dynamics and Control Problems - def derivative_of(self, - variable: cas.MX, - with_respect_to: Union[np.ndarray, cas.MX], - derivative_init_guess: Union[float, np.ndarray], # TODO add default - derivative_scale: Union[float, np.ndarray] = None, - method: str = "trapezoidal", - explicit: bool = False, # TODO implement explicit - _stacklevel: int = 1, - ) -> cas.MX: + def derivative_of( + self, + variable: cas.MX, + with_respect_to: Union[np.ndarray, cas.MX], + derivative_init_guess: Union[float, np.ndarray], # TODO add default + derivative_scale: Union[float, np.ndarray] = None, + method: str = "trapezoidal", + explicit: bool = False, # TODO implement explicit + _stacklevel: int = 1, + ) -> cas.MX: """ Returns a quantity that is either defined or constrained to be a derivative of an existing variable. @@ -1047,7 +1098,9 @@ def derivative_of(self, ### Check inputs N = np.length(variable) if not np.length(with_respect_to) == N: - raise ValueError("The inputs `variable` and `with_respect_to` must be vectors of the same length!") + raise ValueError( + "The inputs `variable` and `with_respect_to` must be vectors of the same length!" + ) ### Clean inputs method = method.lower() @@ -1073,21 +1126,24 @@ def derivative_of(self, ), with_respect_to=with_respect_to, method=method, - _stacklevel=_stacklevel + 1 + _stacklevel=_stacklevel + 1, ) else: - raise NotImplementedError("Haven't yet implemented explicit derivatives! Use implicit ones for now...") + raise NotImplementedError( + "Haven't yet implemented explicit derivatives! Use implicit ones for now..." + ) return derivative - def constrain_derivative(self, - derivative: cas.MX, - variable: cas.MX, - with_respect_to: Union[np.ndarray, cas.MX], - method: str = "trapezoidal", - _stacklevel: int = 1, - ) -> None: + def constrain_derivative( + self, + derivative: cas.MX, + variable: cas.MX, + with_respect_to: Union[np.ndarray, cas.MX], + method: str = "trapezoidal", + _stacklevel: int = 1, + ) -> None: """ Adds a constraint to the optimization problem such that: @@ -1161,24 +1217,17 @@ def constrain_derivative(self, from aerosandbox.numpy.integrate_discrete import integrate_discrete_intervals integrals = integrate_discrete_intervals( - f=derivative, - x=with_respect_to, - multiply_by_dx=True, - method=method + f=derivative, x=with_respect_to, multiply_by_dx=True, method=method ) duals = self.subject_to( - np.diff(variable) == integrals, - _stacklevel=_stacklevel + 1 + np.diff(variable) == integrals, _stacklevel=_stacklevel + 1 ) return duals class OptiSol: - def __init__(self, - opti: Opti, - cas_optisol: cas.OptiSol - ): + def __init__(self, opti: Opti, cas_optisol: cas.OptiSol): """ An OptiSol object represents a solution to an optimization problem. This class is a wrapper around CasADi's `OptiSol` class that provides convenient solution query utilities for various Python data types. @@ -1213,7 +1262,9 @@ def __init__(self, self.opti = opti self._sol = cas_optisol - def __call__(self, x: Union[cas.MX, np.ndarray, float, int, List, Tuple, Set, Dict, Any]) -> Any: + def __call__( + self, x: Union[cas.MX, np.ndarray, float, int, List, Tuple, Set, Dict, Any] + ) -> Any: """ A shorthand alias for `sol.value(x)`. See `OptiSol.value()` documentation for details. @@ -1228,7 +1279,9 @@ def __call__(self, x: Union[cas.MX, np.ndarray, float, int, List, Tuple, Set, Di """ return self.value(x) - def _value_scalar(self, x: Union[cas.MX, np.ndarray, float, int]) -> Union[float, np.ndarray]: + def _value_scalar( + self, x: Union[cas.MX, np.ndarray, float, int] + ) -> Union[float, np.ndarray]: """ Gets the value of a variable at the solution point. For developer use - see following paragraph. @@ -1245,11 +1298,12 @@ def _value_scalar(self, x: Union[cas.MX, np.ndarray, float, int]) -> Union[float """ return self._sol.value(x) - def value(self, - x: Union[cas.MX, np.ndarray, float, int, List, Tuple, Set, Dict, Any], - recursive: bool = True, - warn_on_unknown_types: bool = False - ) -> Any: + def value( + self, + x: Union[cas.MX, np.ndarray, float, int, List, Tuple, Set, Dict, Any], + recursive: bool = True, + warn_on_unknown_types: bool = False, + ) -> Any: """ Gets the value of a variable (or a data structure) at the solution point. This solution point is the optimum, if the optimization process solved successfully. @@ -1300,26 +1354,35 @@ def value(self, if issubclass(t, (set, frozenset)): return {self.value(i) for i in x} if issubclass(t, dict): - return { - self.value(k): self.value(v) - for k, v in x.items() - } + return {self.value(k): self.value(v) for k, v in x.items()} # Skip certain Python types - if issubclass(t, ( - bool, str, - int, float, complex, + if issubclass( + t, + ( + bool, + str, + int, + float, + complex, range, type(None), - bytes, bytearray, memoryview, + bytes, + bytearray, + memoryview, type, - )): + ), + ): return x # Skip certain CasADi types - if issubclass(t, ( - cas.Opti, cas.OptiSol, - )): + if issubclass( + t, + ( + cas.Opti, + cas.OptiSol, + ), + ): return x # If it's any other type, try converting its attribute dictionary, if it has one: @@ -1344,8 +1407,12 @@ def value(self, # item, then hope for the best. if warn_on_unknown_types: import warnings - warnings.warn(f"In solution substitution, could not convert an object of type {t}.\n" - f"Returning it and hoping for the best.", UserWarning) + + warnings.warn( + f"In solution substitution, could not convert an object of type {t}.\n" + f"Returning it and hoping for the best.", + UserWarning, + ) return x @@ -1374,10 +1441,7 @@ def show_infeasibilities(self, tol: float = 1e-3) -> None: g = self(self.opti.g) - constraint_violated = np.logical_or( - g + tol < lbg, - g - tol > ubg - ) + constraint_violated = np.logical_or(g + tol < lbg, g - tol > ubg) lbg_isfinite = np.isfinite(lbg) ubg_isfinite = np.isfinite(ubg) @@ -1388,21 +1452,26 @@ def show_infeasibilities(self, tol: float = 1e-3) -> None: if lbg_isfinite[i] and ubg_isfinite[i]: if lbg[i] == ubg[i]: - print(f"{lbg[i]} == {g[i]} (violation: {np.abs(g[i] - lbg[i])})") + print( + f"{lbg[i]} == {g[i]} (violation: {np.abs(g[i] - lbg[i])})" + ) else: - print(f"{lbg[i]} < {g[i]} < {ubg[i]} (violation: {np.maximum(lbg[i] - g[i], g[i] - ubg[i])})") + print( + f"{lbg[i]} < {g[i]} < {ubg[i]} (violation: {np.maximum(lbg[i] - g[i], g[i] - ubg[i])})" + ) elif lbg_isfinite[i] and not ubg_isfinite[i]: print(f"{lbg[i]} < {g[i]} (violation: {lbg[i] - g[i]})") elif not lbg_isfinite[i] and ubg_isfinite[i]: print(f"{g[i]} < {ubg[i]} (violation: {g[i] - ubg[i]})") else: raise ValueError( - "Contact the AeroSandbox developers if you see this message; it should be impossible.") + "Contact the AeroSandbox developers if you see this message; it should be impossible." + ) self.opti.find_constraint_declaration(index=i) -if __name__ == '__main__': +if __name__ == "__main__": import pytest # pytest.main() @@ -1417,12 +1486,10 @@ def show_infeasibilities(self, tol: float = 1e-3) -> None: y = opti.variable(init_guess=0) # Define objective - f = (a - x) ** 2 + b * (y - x ** 2) ** 2 + f = (a - x) ** 2 + b * (y - x**2) ** 2 opti.minimize(f) - opti.subject_to([ - x ** 2 + y ** 2 <= 1 - ]) + opti.subject_to([x**2 + y**2 <= 1]) # Optimize sol = opti.solve() diff --git a/aerosandbox/optimization/test_optimization/test_opti_analytical_wing_aerostructures.py b/aerosandbox/optimization/test_optimization/test_opti_analytical_wing_aerostructures.py index affd5de4e..d23528422 100644 --- a/aerosandbox/optimization/test_optimization/test_opti_analytical_wing_aerostructures.py +++ b/aerosandbox/optimization/test_optimization/test_opti_analytical_wing_aerostructures.py @@ -51,32 +51,40 @@ def test_gpkit_style_solve(): V = opti.variable(init_guess=1e2, log_transform=True) # cruising speed [m/s] W = opti.variable(init_guess=8e3, log_transform=True) # total aircraft weight [N] Re = opti.variable(init_guess=5e6, log_transform=True) # Reynolds number [-] - C_D = opti.variable(init_guess=3e-2, log_transform=True) # Drag coefficient of wing [-] - C_L = opti.variable(init_guess=1, log_transform=True) # Lift coefficient of wing [-] - C_f = opti.variable(init_guess=1e-2, log_transform=True) # Skin friction coefficient [-] + C_D = opti.variable( + init_guess=3e-2, log_transform=True + ) # Drag coefficient of wing [-] + C_L = opti.variable( + init_guess=1, log_transform=True + ) # Lift coefficient of wing [-] + C_f = opti.variable( + init_guess=1e-2, log_transform=True + ) # Skin friction coefficient [-] W_w = opti.variable(init_guess=3e3, log_transform=True) # Wing weight [N] ### Constraints # Drag model C_D_fuse = CDA0 / S C_D_wpar = k * C_f * S_wetratio - C_D_ind = C_L ** 2 / (pi * A * e) + C_D_ind = C_L**2 / (pi * A * e) opti.subject_to(np.log(C_D) >= np.log(C_D_fuse + C_D_wpar + C_D_ind)) # Wing weight model - W_w_strc = W_W_coeff1 * (N_ult * A ** 1.5 * (W_0 * W * S) ** 0.5) / tau + W_w_strc = W_W_coeff1 * (N_ult * A**1.5 * (W_0 * W * S) ** 0.5) / tau W_w_surf = W_W_coeff2 * S opti.subject_to(np.log(W_w) >= np.log(W_w_surf + W_w_strc)) # Other models - opti.subject_to([ - np.log(D) >= np.log(0.5 * rho * S * C_D * V ** 2), - np.log(Re) <= np.log((rho / mu) * V * (S / A) ** 0.5), - np.log(C_f) >= np.log(0.074 / Re ** 0.2), - np.log(W) <= np.log(0.5 * rho * S * C_L * V ** 2), - np.log(W) <= np.log(0.5 * rho * S * C_Lmax * V_min ** 2), - np.log(W) >= np.log(W_0 + W_w), - ]) + opti.subject_to( + [ + np.log(D) >= np.log(0.5 * rho * S * C_D * V**2), + np.log(Re) <= np.log((rho / mu) * V * (S / A) ** 0.5), + np.log(C_f) >= np.log(0.074 / Re**0.2), + np.log(W) <= np.log(0.5 * rho * S * C_L * V**2), + np.log(W) <= np.log(0.5 * rho * S * C_Lmax * V_min**2), + np.log(W) >= np.log(W_0 + W_w), + ] + ) # Objective opti.minimize(np.log(D)) @@ -100,32 +108,30 @@ def test_geometric_program_solve(): S = opti.variable(init_guess=100, log_transform=True) # total wing area [m^2] V = opti.variable(init_guess=100, log_transform=True) # cruising speed [m/s] W = opti.variable(init_guess=8e3, log_transform=True) # total aircraft weight [N] - C_L = opti.variable(init_guess=1, log_transform=True) # Lift coefficient of wing [-] + C_L = opti.variable( + init_guess=1, log_transform=True + ) # Lift coefficient of wing [-] ### Constraints # Aerodynamics model C_D_fuse = CDA0 / S Re = (rho / mu) * V * (S / A) ** 0.5 - C_f = 0.074 / Re ** 0.2 + C_f = 0.074 / Re**0.2 C_D_wpar = k * C_f * S_wetratio - C_D_ind = C_L ** 2 / (pi * A * e) + C_D_ind = C_L**2 / (pi * A * e) C_D = C_D_fuse + C_D_wpar + C_D_ind - q = 0.5 * rho * V ** 2 + q = 0.5 * rho * V**2 D = q * S * C_D L_cruise = q * S * C_L - L_takeoff = 0.5 * rho * S * C_Lmax * V_min ** 2 + L_takeoff = 0.5 * rho * S * C_Lmax * V_min**2 # Wing weight model - W_w_strc = W_W_coeff1 * (N_ult * A ** 1.5 * (W_0 * W * S) ** 0.5) / tau + W_w_strc = W_W_coeff1 * (N_ult * A**1.5 * (W_0 * W * S) ** 0.5) / tau W_w_surf = W_W_coeff2 * S W_w = W_w_surf + W_w_strc # Other constraints - opti.subject_to([ - W <= L_cruise, - W <= L_takeoff, - W >= W_0 + W_w - ]) + opti.subject_to([W <= L_cruise, W <= L_takeoff, W >= W_0 + W_w]) # Objective opti.minimize(D) @@ -160,26 +166,22 @@ def test_non_log_transformed_solve(): # Aerodynamics model C_D_fuse = CDA0 / S Re = (rho / mu) * V * (S / A) ** 0.5 - C_f = 0.074 / Re ** 0.2 + C_f = 0.074 / Re**0.2 C_D_wpar = k * C_f * S_wetratio - C_D_ind = C_L ** 2 / (pi * A * e) + C_D_ind = C_L**2 / (pi * A * e) C_D = C_D_fuse + C_D_wpar + C_D_ind - q = 0.5 * rho * V ** 2 + q = 0.5 * rho * V**2 D = q * S * C_D L_cruise = q * S * C_L - L_takeoff = 0.5 * rho * S * C_Lmax * V_min ** 2 + L_takeoff = 0.5 * rho * S * C_Lmax * V_min**2 # Wing weight model - W_w_strc = W_W_coeff1 * (N_ult * A ** 1.5 * (W_0 * W * S) ** 0.5) / tau + W_w_strc = W_W_coeff1 * (N_ult * A**1.5 * (W_0 * W * S) ** 0.5) / tau W_w_surf = W_W_coeff2 * S W_w = W_w_surf + W_w_strc # Other constraints - opti.subject_to([ - W <= L_cruise, - W <= L_takeoff, - W == W_0 + W_w - ]) + opti.subject_to([W <= L_cruise, W <= L_takeoff, W == W_0 + W_w]) # Objective opti.minimize(D) @@ -189,5 +191,5 @@ def test_non_log_transformed_solve(): assert sol(D) == pytest.approx(303.1, abs=0.1) -if __name__ == '__main__': +if __name__ == "__main__": pytest.main() diff --git a/aerosandbox/optimization/test_optimization/test_opti_analytical_wing_drag_minimization.py b/aerosandbox/optimization/test_optimization/test_opti_analytical_wing_drag_minimization.py index 89f4d2fed..c7bb06229 100644 --- a/aerosandbox/optimization/test_optimization/test_opti_analytical_wing_drag_minimization.py +++ b/aerosandbox/optimization/test_optimization/test_opti_analytical_wing_drag_minimization.py @@ -20,8 +20,8 @@ def test_normal_problem(): AR = span / chord Re = density * velocity * chord / viscosity - CD_p = 1.328 * Re ** -0.5 - CD_i = CL ** 2 / (pi * AR) + CD_p = 1.328 * Re**-0.5 + CD_i = CL**2 / (pi * AR) opti.subject_to(chord * span == 1) opti.minimize(CD_p + CD_i) @@ -43,8 +43,8 @@ def test_log_transformed_problem(): AR = span / chord Re = density * velocity * chord / viscosity - CD_p = 1.328 * Re ** -0.5 - CD_i = CL ** 2 / (pi * AR) + CD_p = 1.328 * Re**-0.5 + CD_i = CL**2 / (pi * AR) opti.subject_to(chord * span == 1) opti.minimize(CD_p + CD_i) @@ -73,8 +73,8 @@ def test_fixed_variable(): AR = span / chord Re = density * velocity * chord / viscosity - CD_p = 1.328 * Re ** -0.5 - CD_i = CL ** 2 / (pi * AR) + CD_p = 1.328 * Re**-0.5 + CD_i = CL**2 / (pi * AR) opti.subject_to(chord * span == 1) opti.minimize(CD_p + CD_i) @@ -96,8 +96,8 @@ def test_fully_fixed_problem(): AR = span / chord Re = density * velocity * chord / viscosity - CD_p = 1.328 * Re ** -0.5 - CD_i = CL ** 2 / (pi * AR) + CD_p = 1.328 * Re**-0.5 + CD_i = CL**2 / (pi * AR) opti.subject_to(chord * span == 1) opti.minimize(CD_p + CD_i) @@ -119,8 +119,8 @@ def test_overconstrained_fully_fixed_problem(): AR = span / chord Re = density * velocity * chord / viscosity - CD_p = 1.328 * Re ** -0.5 - CD_i = CL ** 2 / (pi * AR) + CD_p = 1.328 * Re**-0.5 + CD_i = CL**2 / (pi * AR) with pytest.raises(RuntimeError): opti.subject_to(chord * span == 1) @@ -135,5 +135,5 @@ def test_overconstrained_fully_fixed_problem(): # assert sol(span) == pytest.approx(1) -if __name__ == '__main__': +if __name__ == "__main__": pytest.main() diff --git a/aerosandbox/optimization/test_optimization/test_opti_bounds.py b/aerosandbox/optimization/test_optimization/test_opti_bounds.py index c9aea1b7b..2767914f2 100644 --- a/aerosandbox/optimization/test_optimization/test_opti_bounds.py +++ b/aerosandbox/optimization/test_optimization/test_opti_bounds.py @@ -6,12 +6,12 @@ def test_bounds(): opti = asb.Opti() x = opti.variable(init_guess=5, lower_bound=3) - opti.minimize(x ** 2) + opti.minimize(x**2) sol = opti.solve() assert sol(x) == pytest.approx(3) -if __name__ == '__main__': +if __name__ == "__main__": pytest.main() diff --git a/aerosandbox/optimization/test_optimization/test_opti_hanging_chain.py b/aerosandbox/optimization/test_optimization/test_opti_hanging_chain.py index a52f901b2..68a5f5f80 100644 --- a/aerosandbox/optimization/test_optimization/test_opti_hanging_chain.py +++ b/aerosandbox/optimization/test_optimization/test_opti_hanging_chain.py @@ -25,9 +25,7 @@ def test_opti_hanging_chain_with_callback(plot=False): opti = asb.Opti() - x = opti.variable( - init_guess=np.linspace(-2, 2, N) - ) + x = opti.variable(init_guess=np.linspace(-2, 2, N)) y = opti.variable( init_guess=1, n_vars=N, @@ -44,28 +42,22 @@ def test_opti_hanging_chain_with_callback(plot=False): opti.minimize(potential_energy) # Add end point constraints - opti.subject_to([ - x[0] == -2, - y[0] == 1, - x[-1] == 2, - y[-1] == 1 - ]) + opti.subject_to([x[0] == -2, y[0] == 1, x[-1] == 2, y[-1] == 1]) # Add a ground constraint - opti.subject_to( - y >= np.cos(0.1 * x) - 0.5 - ) + opti.subject_to(y >= np.cos(0.1 * x) - 0.5) # Add a callback if plot: + def my_callback(iter: int): plt.plot( opti.debug.value(x), opti.debug.value(y), ".-", label=f"Iter {iter}", - zorder=3 + iter + zorder=3 + iter, ) fig, ax = plt.subplots(1, 1, figsize=(6.4, 4.8), dpi=200) @@ -74,14 +66,13 @@ def my_callback(iter: int): plt.plot(x_ground, y_ground, "--k", zorder=2) else: + def my_callback(iter: int): print(f"Iter {iter}") print(f"\tx = {opti.debug.value(x)}") print(f"\ty = {opti.debug.value(y)}") - sol = opti.solve( - callback=my_callback - ) + sol = opti.solve(callback=my_callback) assert sol(potential_energy) == pytest.approx(626.462, abs=1e-3) @@ -89,5 +80,5 @@ def my_callback(iter: int): plt.show() -if __name__ == '__main__': +if __name__ == "__main__": pytest.main() diff --git a/aerosandbox/optimization/test_optimization/test_opti_log_transform.py b/aerosandbox/optimization/test_optimization/test_opti_log_transform.py index 354363abd..6efe8fde2 100644 --- a/aerosandbox/optimization/test_optimization/test_opti_log_transform.py +++ b/aerosandbox/optimization/test_optimization/test_opti_log_transform.py @@ -12,5 +12,5 @@ def test_bounds(): assert sol(x) == pytest.approx(7) -if __name__ == '__main__': +if __name__ == "__main__": pytest.main() diff --git a/aerosandbox/optimization/test_optimization/test_opti_optimal_control_manual_integration.py b/aerosandbox/optimization/test_optimization/test_opti_optimal_control_manual_integration.py index 1a1d3c866..15a293f33 100644 --- a/aerosandbox/optimization/test_optimization/test_opti_optimal_control_manual_integration.py +++ b/aerosandbox/optimization/test_optimization/test_opti_optimal_control_manual_integration.py @@ -37,6 +37,7 @@ # TODO make it second order + def test_rocket_control_problem(plot=False): ### Constants T = 100 @@ -54,27 +55,31 @@ def test_rocket_control_problem(plot=False): gamma = opti.variable(init_guess=0, n_vars=T) # instantaneous fuel consumption a_max = opti.variable(init_guess=0) # maximum acceleration - opti.subject_to([ - np.diff(x) == v[:-1], # physics - np.diff(v) == a[:-1], # physics - x[0] == 0, # boundary condition - v[0] == 0, # boundary condition - x[-1] == d, # boundary condition - v[-1] == 0, # boundary condition - gamma >= c * a, # lower bound on instantaneous fuel consumption - gamma >= -c * a, # lower bound on instantaneous fuel consumption - np.sum(gamma) <= f, # fuel consumption limit - np.diff(a) <= delta, # jerk limits - np.diff(a) >= -delta, # jerk limits - a_max >= a, # lower bound on maximum acceleration - a_max >= -a, # lower bound on maximum acceleration - ]) + opti.subject_to( + [ + np.diff(x) == v[:-1], # physics + np.diff(v) == a[:-1], # physics + x[0] == 0, # boundary condition + v[0] == 0, # boundary condition + x[-1] == d, # boundary condition + v[-1] == 0, # boundary condition + gamma >= c * a, # lower bound on instantaneous fuel consumption + gamma >= -c * a, # lower bound on instantaneous fuel consumption + np.sum(gamma) <= f, # fuel consumption limit + np.diff(a) <= delta, # jerk limits + np.diff(a) >= -delta, # jerk limits + a_max >= a, # lower bound on maximum acceleration + a_max >= -a, # lower bound on maximum acceleration + ] + ) opti.minimize(a_max) # minimize the peak acceleration sol = opti.solve() # solve - assert sol(a_max) == pytest.approx(0.02181991952, rel=1e-3) # solved externally with Julia JuMP + assert sol(a_max) == pytest.approx( + 0.02181991952, rel=1e-3 + ) # solved externally with Julia JuMP if plot: import matplotlib.pyplot as plt @@ -93,5 +98,5 @@ def test_rocket_control_problem(plot=False): plt.show() -if __name__ == '__main__': +if __name__ == "__main__": pytest.main() diff --git a/aerosandbox/optimization/test_optimization/test_opti_parametric_sensitivites.py b/aerosandbox/optimization/test_optimization/test_opti_parametric_sensitivites.py index 1f3f8aa64..5e2d3c391 100644 --- a/aerosandbox/optimization/test_optimization/test_opti_parametric_sensitivites.py +++ b/aerosandbox/optimization/test_optimization/test_opti_parametric_sensitivites.py @@ -25,25 +25,16 @@ def test_rosenbrock_constrained(plot=False): y = opti.variable(init_guess=0) r = opti.parameter() - f = (1 - x) ** 2 + (y - x ** 2) ** 2 + f = (1 - x) ** 2 + (y - x**2) ** 2 opti.minimize(f) - con = x ** 2 + y ** 2 <= r + con = x**2 + y**2 <= r dual = opti.subject_to(con) r_values = np.linspace(1, 3) - sols = [ - opti.solve({r: r_value}) - for r_value in r_values - ] - fs = [ - sol(f) - for sol in sols - ] - duals = [ - sol(dual) # Ensure the dual can be evaluated - for sol in sols - ] + sols = [opti.solve({r: r_value}) for r_value in r_values] + fs = [sol(f) for sol in sols] + duals = [sol(dual) for sol in sols] # Ensure the dual can be evaluated if plot: fig, ax = plt.subplots(1, 1, figsize=(6.4, 4.8), dpi=200) @@ -58,5 +49,5 @@ def test_rosenbrock_constrained(plot=False): assert duals[0] == pytest.approx(0.10898760051521068, abs=1e-6) -if __name__ == '__main__': +if __name__ == "__main__": pytest.main() diff --git a/aerosandbox/optimization/test_optimization/test_opti_poorly_scaled.py b/aerosandbox/optimization/test_optimization/test_opti_poorly_scaled.py index 9998a365b..6d43f3367 100644 --- a/aerosandbox/optimization/test_optimization/test_opti_poorly_scaled.py +++ b/aerosandbox/optimization/test_optimization/test_opti_poorly_scaled.py @@ -18,13 +18,10 @@ def test_opti_poorly_scaled_constraints(constraint_jacobian_condition_number=1e1 c = np.sqrt(constraint_jacobian_condition_number) # Define constraints - opti.subject_to([ - x * c <= 0.9 * c, - y / c <= 0.9 / c - ]) + opti.subject_to([x * c <= 0.9 * c, y / c <= 0.9 / c]) # Define objective - f = (a - x) ** 2 + b * (y - x ** 2) ** 2 + f = (a - x) ** 2 + b * (y - x**2) ** 2 opti.minimize(f) # Optimize @@ -44,7 +41,7 @@ def test_opti_poorly_scaled_objective(objective_hessian_condition_number=1e10): c = np.sqrt(objective_hessian_condition_number) # Define objective - f = x ** 4 * c + y ** 4 / c + f = x**4 * c + y**4 / c opti.minimize(f) # Optimize @@ -56,5 +53,5 @@ def test_opti_poorly_scaled_objective(objective_hessian_condition_number=1e10): assert sol(f) == pytest.approx(0, abs=1e-4) -if __name__ == '__main__': +if __name__ == "__main__": pytest.main() diff --git a/aerosandbox/optimization/test_optimization/test_opti_rosenbrock.py b/aerosandbox/optimization/test_optimization/test_opti_rosenbrock.py index 02e0876ba..a414633b5 100644 --- a/aerosandbox/optimization/test_optimization/test_opti_rosenbrock.py +++ b/aerosandbox/optimization/test_optimization/test_opti_rosenbrock.py @@ -30,7 +30,7 @@ def test_2D_rosenbrock(): # 2-dimensional rosenbrock y = opti.variable(init_guess=0) # Define objective - f = (a - x) ** 2 + b * (y - x ** 2) ** 2 + f = (a - x) ** 2 + b * (y - x**2) ** 2 opti.minimize(f) # Optimize @@ -48,10 +48,10 @@ def test_2D_rosenbrock_circle_constrained(): # 2-dimensional rosenbrock, constr y = opti.variable(init_guess=0) # Define constraints - opti.subject_to(x ** 2 + y ** 2 <= 1) + opti.subject_to(x**2 + y**2 <= 1) # Define objective - f = (a - x) ** 2 + b * (y - x ** 2) ** 2 + f = (a - x) ** 2 + b * (y - x**2) ** 2 opti.minimize(f) # Optimize @@ -92,7 +92,7 @@ def test_2D_rosenbrock_frozen(): y = opti.variable(init_guess=0, freeze=True) # Define objective - f = (a - x) ** 2 + b * (y - x ** 2) ** 2 + f = (a - x) ** 2 + b * (y - x**2) ** 2 opti.minimize(f) # Optimize @@ -103,5 +103,5 @@ def test_2D_rosenbrock_frozen(): assert sol(f) == pytest.approx(0.771, abs=1e-3) -if __name__ == '__main__': +if __name__ == "__main__": pytest.main() diff --git a/aerosandbox/optimization/test_optimization/test_opti_save_load.py b/aerosandbox/optimization/test_optimization/test_opti_save_load.py index ca835ee01..e31ddfb71 100644 --- a/aerosandbox/optimization/test_optimization/test_opti_save_load.py +++ b/aerosandbox/optimization/test_optimization/test_opti_save_load.py @@ -10,7 +10,7 @@ def sumsqr(x): - return np.sum(x ** 2) + return np.sum(x**2) def test_opti(): @@ -80,7 +80,6 @@ def test_save_and_load_opti(tmp_path): cache_filename=temp_filename, variable_categories_to_freeze=["Cat 1"], load_frozen_variables_from_cache=True, - ) x = opti.variable(init_guess=0, category="Cat 1") y = opti.variable(init_guess=0, category="Cat 2") @@ -128,7 +127,6 @@ def test_save_and_load_opti_uncategorized(tmp_path): cache_filename=temp_filename, variable_categories_to_freeze=["Uncategorized"], load_frozen_variables_from_cache=True, - ) x = opti.variable(init_guess=0) y = opti.variable(init_guess=0) @@ -177,7 +175,6 @@ def test_save_and_load_opti_vectorized(tmp_path): cache_filename=temp_filename, variable_categories_to_freeze=["Cat 1"], load_frozen_variables_from_cache=True, - ) x = opti.variable(init_guess=0, n_vars=3, category="Cat 1") y = opti.variable(init_guess=0, n_vars=3, category="Cat 2") @@ -226,7 +223,6 @@ def test_save_and_load_opti_freeze_override(tmp_path): cache_filename=temp_filename, variable_categories_to_freeze=["Cat 1"], load_frozen_variables_from_cache=True, - ) x = opti.variable(init_guess=3, category="Cat 1", freeze=True) y = opti.variable(init_guess=0, category="Cat 2") @@ -244,7 +240,7 @@ def test_save_and_load_opti_freeze_override(tmp_path): assert sol(f) == pytest.approx(1) -if __name__ == '__main__': +if __name__ == "__main__": # from pathlib import Path # tmp_path = Path.home() / "Downloads" / "test" # test_save_and_load_opti(tmp_path) diff --git a/aerosandbox/optimization/test_optimization/test_opti_warm_start.py b/aerosandbox/optimization/test_optimization/test_opti_warm_start.py index 4580cecc6..1ec9db4be 100644 --- a/aerosandbox/optimization/test_optimization/test_opti_warm_start.py +++ b/aerosandbox/optimization/test_optimization/test_opti_warm_start.py @@ -10,14 +10,12 @@ def test_warm_start(): x = opti.variable(init_guess=10) - opti.minimize( - x ** 4 - ) + opti.minimize(x**4) ### Now, solve the optimization problem and print out how many iterations were needed to solve it. sol = opti.solve(verbose=False) - n_iterations_1 = sol.stats()['iter_count'] + n_iterations_1 = sol.stats()["iter_count"] print(f"First run: Solved in {n_iterations_1} iterations.") ### Then, set the initial guess of the Opti problem to the solution that was found. @@ -25,12 +23,12 @@ def test_warm_start(): ### Then, re-solve the problem and print how many iterations were now needed. sol = opti.solve(verbose=False) - n_iterations_2 = sol.stats()['iter_count'] + n_iterations_2 = sol.stats()["iter_count"] print(f"Second run: Solved in {n_iterations_2} iterations.") ### Assert that fewer iterations were needed after a warm-start. assert n_iterations_2 < n_iterations_1 -if __name__ == '__main__': +if __name__ == "__main__": pytest.main() diff --git a/aerosandbox/optimization/test_optimization/test_racecar.py b/aerosandbox/optimization/test_optimization/test_racecar.py index b5c75a402..224aad582 100644 --- a/aerosandbox/optimization/test_optimization/test_racecar.py +++ b/aerosandbox/optimization/test_optimization/test_racecar.py @@ -3,10 +3,7 @@ import pytest -def test_racecar( - N=100, - plot=False -): +def test_racecar(N=100, plot=False): opti = asb.Opti() # Optimization problem t_final = opti.variable(init_guess=1, lower_bound=0) @@ -14,42 +11,31 @@ def test_racecar( x = opti.variable(init_guess=np.linspace(0, 1, N)) v = opti.derivative_of( - x, with_respect_to=t, - derivative_init_guess=1, - method="cubic" + x, with_respect_to=t, derivative_init_guess=1, method="cubic" ) u = opti.variable(init_guess=np.ones(N), lower_bound=0, upper_bound=1) - opti.constrain_derivative( - u - v, - variable=v, with_respect_to=t, - method="cubic" - ) + opti.constrain_derivative(u - v, variable=v, with_respect_to=t, method="cubic") from aerosandbox.numpy.integrate_discrete import ( integrate_discrete_intervals, - integrate_discrete_squared_curvature + integrate_discrete_squared_curvature, ) effort = 0 - effort = 1e-6 * np.sum( - integrate_discrete_squared_curvature( - f=u, - x=t - ) - ) + effort = 1e-6 * np.sum(integrate_discrete_squared_curvature(f=u, x=t)) opti.minimize(t_final + effort) - opti.subject_to([ - v <= 1 - np.sin(2 * np.pi * x) / 2, - x[0] == 0, - v[0] == 0, - x[-1] == 1, - ]) - - sol = opti.solve( - behavior_on_failure="return_last" + opti.subject_to( + [ + v <= 1 - np.sin(2 * np.pi * x) / 2, + x[0] == 0, + v[0] == 0, + x[-1] == 1, + ] ) + + sol = opti.solve(behavior_on_failure="return_last") print(f"t_final: {sol(t_final)}") print(f"error: {np.abs(1.9065661561917042 - sol(t_final))}") assert sol(t_final) == pytest.approx(1.9065661561917042, rel=1e-3) @@ -67,7 +53,7 @@ def test_racecar( p.show_plot() -if __name__ == '__main__': +if __name__ == "__main__": N = 100 # number of control intervals @@ -78,42 +64,31 @@ def test_racecar( x = opti.variable(init_guess=np.linspace(0, 1, N)) v = opti.derivative_of( - x, with_respect_to=t, - derivative_init_guess=1, - method="cubic" + x, with_respect_to=t, derivative_init_guess=1, method="cubic" ) u = opti.variable(init_guess=np.ones(N), lower_bound=0, upper_bound=1) - opti.constrain_derivative( - u - v, - variable=v, with_respect_to=t, - method="cubic" - ) + opti.constrain_derivative(u - v, variable=v, with_respect_to=t, method="cubic") from aerosandbox.numpy.integrate_discrete import ( integrate_discrete_intervals, - integrate_discrete_squared_curvature + integrate_discrete_squared_curvature, ) effort = 0 - effort = 1e-6 * np.sum( - integrate_discrete_squared_curvature( - f=u, - x=t - ) - ) + effort = 1e-6 * np.sum(integrate_discrete_squared_curvature(f=u, x=t)) opti.minimize(t_final + effort) - opti.subject_to([ - v <= 1 - np.sin(2 * np.pi * x) / 2, - x[0] == 0, - v[0] == 0, - x[-1] == 1, - ]) - - sol = opti.solve( - behavior_on_failure="return_last" + opti.subject_to( + [ + v <= 1 - np.sin(2 * np.pi * x) / 2, + x[0] == 0, + v[0] == 0, + x[-1] == 1, + ] ) + + sol = opti.solve(behavior_on_failure="return_last") print(f"t_final: {sol(t_final)}") print(f"error: {np.abs(1.9065661561917042 - sol(t_final))}") diff --git a/aerosandbox/optimization/test_optimization/test_racecar_adaptive_ode.py b/aerosandbox/optimization/test_optimization/test_racecar_adaptive_ode.py index 7bed9c25c..7e2393651 100644 --- a/aerosandbox/optimization/test_optimization/test_racecar_adaptive_ode.py +++ b/aerosandbox/optimization/test_optimization/test_racecar_adaptive_ode.py @@ -25,13 +25,14 @@ def u(t): # Bernstein from scipy.special import comb + tn = t / t_final for i in range(np.length(u_amp)): u += ( - u_amp[i] - * comb(np.length(u_amp) - 1, i) - * tn ** i - * (1 - tn) ** (np.length(u_amp) - 1 - i) + u_amp[i] + * comb(np.length(u_amp) - 1, i) + * tn**i + * (1 - tn) ** (np.length(u_amp) - 1 - i) ) # # # Step @@ -49,10 +50,12 @@ def u(t): return u def func(t, y): - return np.array([ - y[1], - u(t) - y[1], - ]) + return np.array( + [ + y[1], + u(t) - y[1], + ] + ) res = solve_ivp( fun=func, @@ -65,10 +68,12 @@ def func(t, y): opti.minimize(t_final) - opti.subject_to([ - v <= 1 - np.sin(2 * np.pi * x) / 2, - x[-1] >= 1, - ]) + opti.subject_to( + [ + v <= 1 - np.sin(2 * np.pi * x) / 2, + x[-1] >= 1, + ] + ) def callback(i): soli = asb.OptiSol(opti, opti.debug) @@ -78,10 +83,13 @@ def callback(i): if i % 5 == 0: import matplotlib.pyplot as plt import aerosandbox.tools.pretty_plots as p + fig, ax = plt.subplots() ax.plot(soli(t), soli(v), label="speed") ax.plot(soli(t), soli(x), label="position") - ax.plot(soli(t), soli(1 - np.sin(2 * np.pi * x) / 2), "r--", label="speed limit") + ax.plot( + soli(t), soli(1 - np.sin(2 * np.pi * x) / 2), "r--", label="speed limit" + ) t_plot = np.linspace(0, soli(t_final), 1000) ax.plot(t_plot, soli(u(t_plot)), "k", label="throttle") plt.ylim(-0.1, 1.6) @@ -107,6 +115,4 @@ def callback(i): ax.plot(t_plot, sol(u(t_plot)), "k", label="throttle") plt.xlabel("Time [s]") plt.ylabel("Position [m] / Speed [m/s] / Throttle [-]") - p.show_plot( - rotate_axis_labels=False - ) + p.show_plot(rotate_axis_labels=False) diff --git a/aerosandbox/performance/operating_point.py b/aerosandbox/performance/operating_point.py index bdec4f34a..44aa0c210 100644 --- a/aerosandbox/performance/operating_point.py +++ b/aerosandbox/performance/operating_point.py @@ -7,15 +7,16 @@ class OperatingPoint(AeroSandboxObject): - def __init__(self, - atmosphere: Atmosphere = Atmosphere(altitude=0), - velocity: float = 1., - alpha: float = 0., - beta: float = 0., - p: float = 0., - q: float = 0., - r: float = 0., - ): + def __init__( + self, + atmosphere: Atmosphere = Atmosphere(altitude=0), + velocity: float = 1.0, + alpha: float = 0.0, + beta: float = 0.0, + p: float = 0.0, + q: float = 0.0, + r: float = 0.0, + ): """ An object that represents the instantaneous aerodynamic flight conditions of an aircraft. @@ -55,20 +56,20 @@ def state(self) -> Dict[str, Union[float, np.ndarray]]: """ return { "atmosphere": self.atmosphere, - "velocity" : self.velocity, - "alpha" : self.alpha, - "beta" : self.beta, - "p" : self.p, - "q" : self.q, - "r" : self.r, + "velocity": self.velocity, + "alpha": self.alpha, + "beta": self.beta, + "p": self.p, + "q": self.q, + "r": self.r, } - def get_new_instance_with_state(self, - new_state: Union[ - Dict[str, Union[float, np.ndarray]], - List, Tuple, np.ndarray - ] = None - ): + def get_new_instance_with_state( + self, + new_state: Union[ + Dict[str, Union[float, np.ndarray]], List, Tuple, np.ndarray + ] = None, + ): """ Creates a new instance of the OperatingPoint class from the given state. @@ -84,10 +85,9 @@ def get_new_instance_with_state(self, init_args = list(init_signature.parameters.keys())[1:] # Ignore 'self' ### Create a new instance, and give the constructor all the inputs it wants to see (based on values in this instance) - new_op_point: __class__ = self.__class__(**{ - k: getattr(self, k) - for k in init_args - }) + new_op_point: __class__ = self.__class__( + **{k: getattr(self, k) for k in init_args} + ) ### Overwrite the state variables in the new instance with those from the input new_op_point._set_state(new_state=new_state) @@ -95,12 +95,12 @@ def get_new_instance_with_state(self, ### Return the new instance return new_op_point - def _set_state(self, - new_state: Union[ - Dict[str, Union[float, np.ndarray]], - List, Tuple, np.ndarray - ] = None - ): + def _set_state( + self, + new_state: Union[ + Dict[str, Union[float, np.ndarray]], List, Tuple, np.ndarray + ] = None, + ): """ Force-overwrites all state variables with a new set (either partial or complete) of state variables. @@ -116,16 +116,19 @@ def _set_state(self, new_state = {} try: # Assume `value` is a dict-like, with keys - for key in new_state.keys(): # Overwrite each of the specified state variables + for ( + key + ) in new_state.keys(): # Overwrite each of the specified state variables setattr(self, key, new_state[key]) except AttributeError: # Assume it's an iterable that has been sorted. self._set_state( - self.pack_state(new_state)) # Pack the iterable into a dict-like, then do the same thing as above. + self.pack_state(new_state) + ) # Pack the iterable into a dict-like, then do the same thing as above. - def unpack_state(self, - dict_like_state: Dict[str, Union[float, np.ndarray]] = None - ) -> Tuple[Union[float, np.ndarray]]: + def unpack_state( + self, dict_like_state: Dict[str, Union[float, np.ndarray]] = None + ) -> Tuple[Union[float, np.ndarray]]: """ 'Unpacks' a Dict-like state into an array-like that represents the state of the OperatingPoint. @@ -139,9 +142,9 @@ def unpack_state(self, dict_like_state = self.state return tuple(dict_like_state.values()) - def pack_state(self, - array_like_state: Union[List, Tuple, np.ndarray] = None - ) -> Dict[str, Union[float, np.ndarray]]: + def pack_state( + self, array_like_state: Union[List, Tuple, np.ndarray] = None + ) -> Dict[str, Union[float, np.ndarray]]: """ 'Packs' an array into a Dict that represents the state of the OperatingPoint. @@ -155,14 +158,9 @@ def pack_state(self, return self.state if not len(self.state.keys()) == len(array_like_state): raise ValueError( - "There are a differing number of elements in the `state` variable and the `array_like` you're trying to pack!") - return { - k: v - for k, v in zip( - self.state.keys(), - array_like_state + "There are a differing number of elements in the `state` variable and the `array_like` you're trying to pack!" ) - } + return {k: v for k, v in zip(self.state.keys(), array_like_state)} def __repr__(self) -> str: @@ -178,16 +176,17 @@ def makeline(k, v): state_variables_title = "\tState variables:" - state_variables = "\n".join([ - "\t\t" + makeline(k, v) - for k, v in self.state.items() - ]) + state_variables = "\n".join( + ["\t\t" + makeline(k, v) for k, v in self.state.items()] + ) - return "\n".join([ - title, - state_variables_title, - state_variables, - ]) + return "\n".join( + [ + title, + state_variables_title, + state_variables, + ] + ) def __getitem__(self, index: Union[int, slice]): """ @@ -213,8 +212,10 @@ def get_item_of_attribute(a): try: return a[index] except IndexError as e: - raise IndexError(f"A state variable could not be indexed; it has length {len(a)} while the" - f"parent has length {l}.") + raise IndexError( + f"A state variable could not be indexed; it has length {len(a)} while the" + f"parent has length {l}." + ) else: return a @@ -235,7 +236,9 @@ def __len__(self): elif length == lv: pass else: - raise ValueError("State variables are appear vectorized, but of different lengths!") + raise ValueError( + "State variables are appear vectorized, but of different lengths!" + ) return length def __array__(self, dtype="O"): @@ -250,7 +253,7 @@ def dynamic_pressure(self): Returns: float: Dynamic pressure of the working fluid. [Pa] """ - return 0.5 * self.atmosphere.density() * self.velocity ** 2 + return 0.5 * self.atmosphere.density() * self.velocity**2 def total_pressure(self): """ @@ -265,10 +268,8 @@ def total_pressure(self): """ gamma = self.atmosphere.ratio_of_specific_heats() return self.atmosphere.pressure() * ( - 1 + (gamma - 1) / 2 * self.mach() ** 2 - ) ** ( - gamma / (gamma - 1) - ) + 1 + (gamma - 1) / 2 * self.mach() ** 2 + ) ** (gamma / (gamma - 1)) def total_temperature(self): """ @@ -285,9 +286,7 @@ def total_temperature(self): # ) ** ( # (gamma - 1) / gamma # ) - return self.atmosphere.temperature() * ( - 1 + (gamma - 1) / 2 * self.mach() ** 2 - ) + return self.atmosphere.temperature() * (1 + (gamma - 1) / 2 * self.mach() ** 2) def reynolds(self, reference_length): """ @@ -311,7 +310,8 @@ def indicated_airspeed(self): Returns the indicated airspeed associated with the current flight condition, in meters per second. """ return np.sqrt( - 2 * (self.total_pressure() - self.atmosphere.pressure()) + 2 + * (self.total_pressure() - self.atmosphere.pressure()) / Atmosphere(altitude=0, method="isa").density() ) @@ -331,15 +331,16 @@ def energy_altitude(self): + gravitational potential) as the aircraft at the current flight condition. """ - return self.atmosphere.altitude + 1 / (2 * 9.81) * self.velocity ** 2 + return self.atmosphere.altitude + 1 / (2 * 9.81) * self.velocity**2 - def convert_axes(self, - x_from: Union[float, np.ndarray], - y_from: Union[float, np.ndarray], - z_from: Union[float, np.ndarray], - from_axes: str, - to_axes: str, - ) -> Tuple[float, float, float]: + def convert_axes( + self, + x_from: Union[float, np.ndarray], + y_from: Union[float, np.ndarray], + z_from: Union[float, np.ndarray], + from_axes: str, + to_axes: str, + ) -> Tuple[float, float, float]: """ Converts a vector [x_from, y_from, z_from], as given in the `from_axes` frame, to an equivalent vector [x_to, y_to, z_to], as given in the `to_axes` frame. @@ -444,7 +445,9 @@ def compute_rotation_matrix_wind_to_geometry(self) -> np.ndarray: # Since in geometry axes, X is downstream by convention, while in wind axes, X is upstream by convention. # Same with Z being up/down respectively. - r = axes_flip @ alpha_rotation @ beta_rotation # where "@" is the matrix multiplication operator + r = ( + axes_flip @ alpha_rotation @ beta_rotation + ) # where "@" is the matrix multiplication operator return r @@ -460,25 +463,28 @@ def compute_rotation_velocity_geometry_axes(self, points): # Computes the effective velocity-due-to-rotation at a set of points. # Input: a Nx3 array of points # Output: a Nx3 array of effective velocities - angular_velocity_vector_geometry_axes = np.array([ - -self.p, - self.q, - -self.r - ]) # signs convert from body axes to geometry axes + angular_velocity_vector_geometry_axes = np.array( + [-self.p, self.q, -self.r] + ) # signs convert from body axes to geometry axes a = angular_velocity_vector_geometry_axes b = points - rotation_velocity_geometry_axes = np.stack([ - a[1] * b[:, 2] - a[2] * b[:, 1], - a[2] * b[:, 0] - a[0] * b[:, 2], - a[0] * b[:, 1] - a[1] * b[:, 0] - ], axis=1) + rotation_velocity_geometry_axes = np.stack( + [ + a[1] * b[:, 2] - a[2] * b[:, 1], + a[2] * b[:, 0] - a[0] * b[:, 2], + a[0] * b[:, 1] - a[1] * b[:, 0], + ], + axis=1, + ) - rotation_velocity_geometry_axes = -rotation_velocity_geometry_axes # negative sign, since we care about the velocity the WING SEES, not the velocity of the wing. + rotation_velocity_geometry_axes = ( + -rotation_velocity_geometry_axes + ) # negative sign, since we care about the velocity the WING SEES, not the velocity of the wing. return rotation_velocity_geometry_axes -if __name__ == '__main__': +if __name__ == "__main__": op_point = OperatingPoint() diff --git a/aerosandbox/performance/test_performance/test_operating_point_convert_axes_chain.py b/aerosandbox/performance/test_performance/test_operating_point_convert_axes_chain.py index ba8546c4c..479a46e8a 100644 --- a/aerosandbox/performance/test_performance/test_operating_point_convert_axes_chain.py +++ b/aerosandbox/performance/test_performance/test_operating_point_convert_axes_chain.py @@ -8,20 +8,14 @@ op_point = asb.OperatingPoint(alpha=10, beta=5) -def chain_conversion( - axes: List[str] = None -): +def chain_conversion(axes: List[str] = None): if axes is None: axes = ["geometry", "body", "geometry"] x, y, z = copy.deepcopy(vector) for from_axes, to_axes in zip(axes, axes[1:]): x, y, z = op_point.convert_axes( - x_from=x, - y_from=y, - z_from=z, - from_axes=from_axes, - to_axes=to_axes + x_from=x, y_from=y, z_from=z, from_axes=from_axes, to_axes=to_axes ) return np.array(vector) == pytest.approx(np.array([x, y, z])) @@ -43,24 +37,26 @@ def test_wind(): def test_cycle(): - assert chain_conversion([ - "body", - "geometry", - "stability", - "wind", - "body", - "wind", - "stability", - "geometry", - "body", - "geometry", - "wind", - "geometry", - "stability", - "body", - ]) - - -if __name__ == '__main__': + assert chain_conversion( + [ + "body", + "geometry", + "stability", + "wind", + "body", + "wind", + "stability", + "geometry", + "body", + "geometry", + "wind", + "geometry", + "stability", + "body", + ] + ) + + +if __name__ == "__main__": pytest.main() chain_conversion() diff --git a/aerosandbox/performance/test_performance/test_operating_point_convert_axes_specific_rotations.py b/aerosandbox/performance/test_performance/test_operating_point_convert_axes_specific_rotations.py index 4382d457a..86c0e6a28 100644 --- a/aerosandbox/performance/test_performance/test_operating_point_convert_axes_specific_rotations.py +++ b/aerosandbox/performance/test_performance/test_operating_point_convert_axes_specific_rotations.py @@ -6,75 +6,40 @@ def test_alpha_wind(): - op_point = asb.OperatingPoint( - alpha=90, - beta=0 - ) - x, y, z = op_point.convert_axes( - 0, 0, 1, - "geometry", - "wind" - ) + op_point = asb.OperatingPoint(alpha=90, beta=0) + x, y, z = op_point.convert_axes(0, 0, 1, "geometry", "wind") assert x == pytest.approx(-1) assert y == pytest.approx(0) assert z == pytest.approx(0) def test_beta_wind(): - op_point = asb.OperatingPoint( - alpha=0, - beta=90 - ) - x, y, z = op_point.convert_axes( - 0, 1, 0, - "geometry", - "wind" - ) + op_point = asb.OperatingPoint(alpha=0, beta=90) + x, y, z = op_point.convert_axes(0, 1, 0, "geometry", "wind") assert x == pytest.approx(1) assert y == pytest.approx(0) assert z == pytest.approx(0) def test_beta_wind_body(): - op_point = asb.OperatingPoint( - alpha=0, - beta=90 - ) - x, y, z = op_point.convert_axes( - 0, 1, 0, - "body", - "wind" - ) + op_point = asb.OperatingPoint(alpha=0, beta=90) + x, y, z = op_point.convert_axes(0, 1, 0, "body", "wind") assert x == pytest.approx(1) assert y == pytest.approx(0) assert z == pytest.approx(0) def test_alpha_stability_body(): - op_point = asb.OperatingPoint( - alpha=90, - beta=0 - ) - x, y, z = op_point.convert_axes( - 0, 0, 1, - "body", - "stability" - ) + op_point = asb.OperatingPoint(alpha=90, beta=0) + x, y, z = op_point.convert_axes(0, 0, 1, "body", "stability") assert x == pytest.approx(1) assert y == pytest.approx(0) assert z == pytest.approx(0) def test_beta_stability_body(): - op_point = asb.OperatingPoint( - alpha=0, - beta=90 - ) - x, y, z = op_point.convert_axes( - 0, 1, 0, - "body", - "stability" - ) + op_point = asb.OperatingPoint(alpha=0, beta=90) + x, y, z = op_point.convert_axes(0, 1, 0, "body", "stability") assert x == pytest.approx(0) assert y == pytest.approx(1) assert z == pytest.approx(0) @@ -85,15 +50,11 @@ def test_order_wind_body(): alpha=90, beta=90, ) - x, y, z = op_point.convert_axes( - 0, 1, 0, - "body", - "wind" - ) + x, y, z = op_point.convert_axes(0, 1, 0, "body", "wind") assert x == pytest.approx(1) assert y == pytest.approx(0) assert z == pytest.approx(0) -if __name__ == '__main__': +if __name__ == "__main__": pytest.main() diff --git a/aerosandbox/performance/test_performance/test_operating_point_indexing.py b/aerosandbox/performance/test_performance/test_operating_point_indexing.py index 333e8f58e..9ac12c6fd 100644 --- a/aerosandbox/performance/test_performance/test_operating_point_indexing.py +++ b/aerosandbox/performance/test_performance/test_operating_point_indexing.py @@ -64,5 +64,5 @@ def test_op_indexing(): # assert all(dslice.u_e == [4, 6, 8]) -if __name__ == '__main__': +if __name__ == "__main__": test_op_indexing() diff --git a/aerosandbox/propulsion/ignore/propeller_model.py b/aerosandbox/propulsion/ignore/propeller_model.py index c840d0481..b0ccdf0bc 100644 --- a/aerosandbox/propulsion/ignore/propeller_model.py +++ b/aerosandbox/propulsion/ignore/propeller_model.py @@ -14,7 +14,7 @@ # mu = atmo.get_viscosity_from_temperature(atmo.get_temperature_at_altitude(altitude)) # speed_of_sound = 343 air_density = 1.225 -mu = 0.178E-04 +mu = 0.178e-04 speed_of_sound = 340 ## Prop Specs from CAM 6X3 for QPROP Validation @@ -22,10 +22,14 @@ # give value in inches for some number of radial locations from root to tip # tip radial location is propeller radius radial_locations_in = np.array([0.75, 1, 1.5, 2, 2.5, 2.875, 3]) -radial_locations_m = np.array([0.01905, 0.0254, 0.0381, 0.0508, 0.0635, 0.073025, 0.0762]) +radial_locations_m = np.array( + [0.01905, 0.0254, 0.0381, 0.0508, 0.0635, 0.073025, 0.0762] +) # # give value of blade chord in inches for each station blade_chord_in = np.array([0.66, 0.69, 0.63, 0.55, 0.44, 0.30, 0.19]) -blade_chord_m = np.array([0.016764, 0.017526, 0.016002, 0.01397, 0.011176, 0.00762, 0.004826]) +blade_chord_m = np.array( + [0.016764, 0.017526, 0.016002, 0.01397, 0.011176, 0.00762, 0.004826] +) # # give value of blade beta in degrees for each station blade_beta_deg = np.array([27.5, 22, 15.2, 10.2, 6.5, 4.6, 4.2]) # # variable pitch angle @@ -70,6 +74,7 @@ # Cl = 2 * pi * alpha_rad # return Cl + # Interpolation function def interpolate(radial_locations, blade_chords, blade_betas, div): radial_locations_new = np.array([]) @@ -99,7 +104,7 @@ def interpolate(radial_locations, blade_chords, blade_betas, div): # QPROP CL function def airfoil_CL(alpha, Re, Ma): alpha_rad = alpha * pi / 180 - beta = (1 - Ma ** 2) ** 0.5 + beta = (1 - Ma**2) ** 0.5 cl_0 = 0.5 cl_alpha = 5.8 cl_min = -0.3 @@ -123,6 +128,7 @@ def airfoil_CL(alpha, Re, Ma): ## QPROP CDp model + def airfoil_CDp(alpha, Re, Ma, Cl): alpha_rad = alpha * pi / 180 Re_exp = -0.7 @@ -146,8 +152,9 @@ def airfoil_CDp(alpha, Re, Ma, Cl): return cd -radial_locations_m, blade_chord_m, blade_beta_deg = interpolate(radial_locations_m, blade_chord_m, blade_beta_deg, - divisions) +radial_locations_m, blade_chord_m, blade_beta_deg = interpolate( + radial_locations_m, blade_chord_m, blade_beta_deg, divisions +) n_stations = len(radial_locations_m) - 1 tip_radius = radial_locations_m[n_stations] # use tip radial location as prop radius omega = rpm * 2 * pi / 60 # radians per second @@ -175,7 +182,7 @@ def airfoil_CDp(alpha, Re, Ma, Cl): for station in range(n_stations): # TODO undo this # for station in [22]: radial_loc = (radial_locations_m[station] + radial_locations_m[station + 1]) / 2 - blade_section = (radial_locations_m[station + 1] - radial_locations_m[station]) + blade_section = radial_locations_m[station + 1] - radial_locations_m[station] chord_local = (blade_chord_m[station] + blade_chord_m[station + 1]) / 2 twist_local_rad = (blade_twist_rad[station] + blade_twist_rad[station + 1]) / 2 @@ -192,15 +199,15 @@ def airfoil_CDp(alpha, Re, Ma, Cl): ### Define velocity triangle components U_a = airspeed # + u_a # Axial velocity w/o induced eff. assuming u_a = 0 U_t = omega * radial_loc # Tangential velocity w/o induced eff. - U = (U_a ** 2 + U_t ** 2) ** 0.5 # Velocity magnitude + U = (U_a**2 + U_t**2) ** 0.5 # Velocity magnitude W_a = 0.5 * U_a + 0.5 * U * np.sin(Psi) # Axial velocity w/ induced eff. W_t = 0.5 * U_t + 0.5 * U * np.cos(Psi) # Tangential velocity w/ induced eff. v_a = W_a - U_a # Axial induced velocity v_t = U_t - W_t # Tangential induced velocity - W = (W_a ** 2 + W_t ** 2) ** 0.5 + W = (W_a**2 + W_t**2) ** 0.5 Re = air_density * W * chord_local / mu Ma = W / speed_of_sound - v = (v_a ** 2 + v_t ** 2) ** 0.5 + v = (v_a**2 + v_t**2) ** 0.5 loc_wake_adv_ratio = (radial_loc / tip_radius) * (W_a / W_t) f = (n_blades / 2) * (1 - radial_loc / tip_radius) * 1 / loc_wake_adv_ratio F = 2 / pi * np.arccos(np.exp(-f)) @@ -217,25 +224,34 @@ def airfoil_CDp(alpha, Re, Ma, Cl): gamma = 0.5 * W * chord_local * cl ### Add governing equations - opti.subject_to([ - # 0.5 * v == 0.5 * U * cas.sin(Psi / 4), - # v_a == v_t * W_t / W_a, - # U ** 2 == v ** 2 + W ** 2, - # gamma == -0.0145, - # gamma == (4 * pi * radial_loc / n_blades) * F * ( - # 1 + ((4 * loc_wake_adv_ratio * tip_radius) / (pi * n_blades * radial_loc)) ** 2) ** 0.5, - - gamma == v_t * (4 * pi * radial_loc / n_blades) * F * ( - 1 + ((4 * loc_wake_adv_ratio * tip_radius) / (pi * n_blades * radial_loc)) ** 2) ** 0.5, - # vt**2*F**2*(1.+(4.*lam_w*R/(pi*B*r))**2) >= (B*G/(4.*pi*r))**2, - # f + (radial_loc / tip_radius) * n_blades / (2 * loc_wake_adv_ratio) <= (n_blades / 2) * (1 / loc_wake_adv_ratio), - # blade_twist_deg * pi / 180 == alpha_rad + 1 / h_ati, - # h_ati ** 1.83442 == 0.966692 * (W_a / W_t) ** -1.84391 + 0.596688 * (W_a / W_t) ** -0.0973781, - # v_t ** 2 * F ** 2 * (1 + (4 * loc_wake_adv_ratio * tip_radius/(pi * n_blades * radial_loc)) ** 2) >= (n_blades * gamma /(4 * pi * radial_loc)) ** 2, - # alpha_deg >= -45 - # v_a >= 0, - # v_t >= 0 - ]) + opti.subject_to( + [ + # 0.5 * v == 0.5 * U * cas.sin(Psi / 4), + # v_a == v_t * W_t / W_a, + # U ** 2 == v ** 2 + W ** 2, + # gamma == -0.0145, + # gamma == (4 * pi * radial_loc / n_blades) * F * ( + # 1 + ((4 * loc_wake_adv_ratio * tip_radius) / (pi * n_blades * radial_loc)) ** 2) ** 0.5, + gamma + == v_t + * (4 * pi * radial_loc / n_blades) + * F + * ( + 1 + + ((4 * loc_wake_adv_ratio * tip_radius) / (pi * n_blades * radial_loc)) + ** 2 + ) + ** 0.5, + # vt**2*F**2*(1.+(4.*lam_w*R/(pi*B*r))**2) >= (B*G/(4.*pi*r))**2, + # f + (radial_loc / tip_radius) * n_blades / (2 * loc_wake_adv_ratio) <= (n_blades / 2) * (1 / loc_wake_adv_ratio), + # blade_twist_deg * pi / 180 == alpha_rad + 1 / h_ati, + # h_ati ** 1.83442 == 0.966692 * (W_a / W_t) ** -1.84391 + 0.596688 * (W_a / W_t) ** -0.0973781, + # v_t ** 2 * F ** 2 * (1 + (4 * loc_wake_adv_ratio * tip_radius/(pi * n_blades * radial_loc)) ** 2) >= (n_blades * gamma /(4 * pi * radial_loc)) ** 2, + # alpha_deg >= -45 + # v_a >= 0, + # v_t >= 0 + ] + ) ### Solve sol = opti.solve() @@ -250,14 +266,15 @@ def airfoil_CDp(alpha, Re, Ma, Cl): # cd * chord_local * blade_section # ) dThrust = sol( - air_density * n_blades * gamma * ( - W_t - W_a * cd / cl - ) * blade_section + air_density * n_blades * gamma * (W_t - W_a * cd / cl) * blade_section ) dTorque = sol( - air_density * n_blades * gamma * ( - W_a + W_t * cd / cl - ) * radial_loc * blade_section + air_density + * n_blades + * gamma + * (W_a + W_t * cd / cl) + * radial_loc + * blade_section ) # if sol(alpha_deg) <= 0: # break @@ -284,14 +301,29 @@ def airfoil_CDp(alpha, Re, Ma, Cl): # debugging section: outputs printed in qprop print( - "radius chord beta Cl Cd Re Mach effi effp Wa Aswirl adv_wake alpha Wt") + "radius chord beta Cl Cd Re Mach effi effp Wa Aswirl adv_wake alpha Wt" +) for i in range(0, len(radius)): # print(f'{radius[i]} {chord[i]} {beta[i]} {Cl[i]} {Cd[i]} {Re[i]} {Mach[i]} {effi[i]} {effp[i]} {Wa[i]} {a_swirl[i]} {adv_wake[i]}') - print('%.4f %.4f %.3f %.4f %.5f %d %.3f %.4f %.4f %.2f %.3f %.4f %.4f %.2f' - % ( - radius[i], chord[i], beta[i], Cl[i], Cd[i], RE[i], Mach[i], effi[i], effp[i], Wa[i], a_swirl[i], - adv_wake[i], - alpha[i], Wt[i])) + print( + "%.4f %.4f %.3f %.4f %.5f %d %.3f %.4f %.4f %.2f %.3f %.4f %.4f %.2f" + % ( + radius[i], + chord[i], + beta[i], + Cl[i], + Cd[i], + RE[i], + Mach[i], + effi[i], + effp[i], + Wa[i], + a_swirl[i], + adv_wake[i], + alpha[i], + Wt[i], + ) + ) print(f"Thrust Total: {Thrust}") print(f"Torque Total: {Torque}") # return Torque, Thrust diff --git a/aerosandbox/structures/buckling.py b/aerosandbox/structures/buckling.py index 089ef6946..a5831f1b9 100644 --- a/aerosandbox/structures/buckling.py +++ b/aerosandbox/structures/buckling.py @@ -2,11 +2,11 @@ def column_buckling_critical_load( - elastic_modulus: float, - moment_of_inertia: float, - length: float, - boundary_condition_type: str = "pin-pin", - use_recommended_design_values: bool = True, + elastic_modulus: float, + moment_of_inertia: float, + length: float, + boundary_condition_type: str = "pin-pin", + use_recommended_design_values: bool = True, ): """ Computes the critical load (in N) for a column or tube in compression to buckle via primary buckling. Uses Euler's classical critical @@ -40,26 +40,28 @@ def column_buckling_critical_load( """ if boundary_condition_type == "pin-pin": K = 1.00 if use_recommended_design_values else 1.00 - elif boundary_condition_type == "pin-clamp" or boundary_condition_type == "clamp-pin": + elif ( + boundary_condition_type == "pin-clamp" or boundary_condition_type == "clamp-pin" + ): K = 0.80 if use_recommended_design_values else 0.70 elif boundary_condition_type == "clamp-clamp": K = 0.65 if use_recommended_design_values else 0.50 - elif boundary_condition_type == "clamp-free" or boundary_condition_type == "free-clamp": + elif ( + boundary_condition_type == "clamp-free" + or boundary_condition_type == "free-clamp" + ): K = 2.10 if use_recommended_design_values else 2.00 else: raise ValueError("Invalid `boundary_condition_type`.") - return ( - np.pi ** 2 * elastic_modulus * moment_of_inertia - / (K * length) ** 2 - ) + return np.pi**2 * elastic_modulus * moment_of_inertia / (K * length) ** 2 def thin_walled_tube_crippling_buckling_critical_load( - elastic_modulus: float, - wall_thickness: float, - radius: float, - use_recommended_design_values: bool = True, + elastic_modulus: float, + wall_thickness: float, + radius: float, + use_recommended_design_values: bool = True, ): """ Computes the critical load for a thin-walled tube in compression to fail in the crippling mode. (Note: you should also check for @@ -122,12 +124,12 @@ def thin_walled_tube_crippling_buckling_critical_load( def plate_buckling_critical_load( - length: float, - width: float, - wall_thickness: float, - elastic_modulus: float, - poissons_ratio: float = 0.33, - side_boundary_condition_type: str = "clamp-clamp", + length: float, + width: float, + wall_thickness: float, + elastic_modulus: float, + poissons_ratio: float = 0.33, + side_boundary_condition_type: str = "clamp-clamp", ): """ Computes the critical compressive load (in N) for a plate to buckle via plate buckling. @@ -175,9 +177,12 @@ def plate_buckling_critical_load( raise ValueError("Invalid `side_boundary_condition_type`.") critical_buckling_load = ( - K * np.pi ** 2 * elastic_modulus / - (12 * (1 - poissons_ratio ** 2)) * - wall_thickness ** 3 / width + K + * np.pi**2 + * elastic_modulus + / (12 * (1 - poissons_ratio**2)) + * wall_thickness**3 + / width ) return critical_buckling_load diff --git a/aerosandbox/structures/ignore/wing_structure_generator.py b/aerosandbox/structures/ignore/wing_structure_generator.py index 9d077694b..4705ffdad 100644 --- a/aerosandbox/structures/ignore/wing_structure_generator.py +++ b/aerosandbox/structures/ignore/wing_structure_generator.py @@ -6,28 +6,26 @@ from sortedcontainers import SortedDict -class WingStructureGenerator(): - def __init__(self, - wing: asb.Wing, - default_rib_thickness=3e-3, - minimum_airfoil_TE_thickness_rel: float = 0.001 - ): +class WingStructureGenerator: + def __init__( + self, + wing: asb.Wing, + default_rib_thickness=3e-3, + minimum_airfoil_TE_thickness_rel: float = 0.001, + ): self.wing = wing self.default_rib_thickness = default_rib_thickness self.minimum_airfoil_TE_thickness_rel = minimum_airfoil_TE_thickness_rel ### Compute some span properties which are used for locating ribs self._sectional_spans: List[float] = wing.span(_sectional=True) - self._cumulative_spans_up_to_section = np.concatenate(( - [0], - np.cumsum(self._sectional_spans) - )) + self._cumulative_spans_up_to_section = np.concatenate( + ([0], np.cumsum(self._sectional_spans)) + ) self._total_span = sum(self._sectional_spans) ### Generate the OML geometry - self.oml = asb.Airplane( - wings=[wing] - ).generate_cadquery_geometry( + self.oml = asb.Airplane(wings=[wing]).generate_cadquery_geometry( minimum_airfoil_TE_thickness=minimum_airfoil_TE_thickness_rel ) @@ -41,10 +39,10 @@ def __repr__(self): # def open_interactive(self): def add_ribs_from_section_span_fractions( - self, - section_index: int, - section_span_fractions: Union[float, int, List[float], np.ndarray], - rib_thickness: float = None, + self, + section_index: int, + section_span_fractions: Union[float, int, List[float], np.ndarray], + rib_thickness: float = None, ): if rib_thickness is None: rib_thickness = self.default_rib_thickness @@ -59,46 +57,33 @@ def add_ribs_from_section_span_fractions( for s in section_span_fractions: af = xsec_a.airfoil.blend_with_another_airfoil( - airfoil=xsec_b.airfoil, - blend_fraction=s + airfoil=xsec_b.airfoil, blend_fraction=s ) - chord = ( - (1 - s) * xsec_a.chord + s * xsec_b.chord - ) + chord = (1 - s) * xsec_a.chord + s * xsec_b.chord csys = wing._compute_frame_of_section(section_index) span = ( - self._cumulative_spans_up_to_section[section_index] - + s * self._sectional_spans[section_index] + self._cumulative_spans_up_to_section[section_index] + + s * self._sectional_spans[section_index] ) self.ribs[span] = ( cq.Workplane( inPlane=cq.Plane( - origin=tuple( - (1 - s) * xsec_a.xyz_le + s * xsec_b.xyz_le - ), + origin=tuple((1 - s) * xsec_a.xyz_le + s * xsec_b.xyz_le), xDir=tuple(csys[0]), - normal=tuple(-csys[1]) + normal=tuple(-csys[1]), ) - ).spline( - listOfXYTuple=[ - tuple(xy * chord) - for xy in af.coordinates - ] - ).close().extrude( - rib_thickness / 2, - combine=False, - both=True ) + .spline(listOfXYTuple=[tuple(xy * chord) for xy in af.coordinates]) + .close() + .extrude(rib_thickness / 2, combine=False, both=True) ) def add_ribs_from_xsecs( - self, - indexes: List[int] = None, - rib_thickness: float = None + self, indexes: List[int] = None, rib_thickness: float = None ): if rib_thickness is None: rib_thickness = self.default_rib_thickness @@ -123,24 +108,18 @@ def add_ribs_from_xsecs( inPlane=cq.Plane( origin=tuple(xsec.xyz_le), xDir=tuple(csys[0]), - normal=tuple(-csys[1]) + normal=tuple(-csys[1]), ) - ).spline( - listOfXYTuple=[ - tuple(xy * xsec.chord) - for xy in af.coordinates - ] - ).close().extrude( - rib_thickness / 2, - combine=False, - both=True ) + .spline(listOfXYTuple=[tuple(xy * xsec.chord) for xy in af.coordinates]) + .close() + .extrude(rib_thickness / 2, combine=False, both=True) ) def add_ribs_from_span_fractions( - self, - span_fractions: Union[float, List[float], np.ndarray] = np.linspace(0, 1, 10), - rib_thickness: float = None, + self, + span_fractions: Union[float, List[float], np.ndarray] = np.linspace(0, 1, 10), + rib_thickness: float = None, ): ### Handle span_fractions if it's not an iterable try: @@ -160,32 +139,36 @@ def add_ribs_from_span_fractions( "All values of `span_fractions` must be between 0 and 1!" ) else: - section_index = np.argwhere( - self._cumulative_spans_up_to_section > self._total_span * s - )[0][0] - 1 + section_index = ( + np.argwhere( + self._cumulative_spans_up_to_section > self._total_span * s + )[0][0] + - 1 + ) section_span_fraction = ( - s * self._total_span - - self._cumulative_spans_up_to_section[section_index] - ) / self._sectional_spans[section_index] + s * self._total_span + - self._cumulative_spans_up_to_section[section_index] + ) / self._sectional_spans[section_index] self.add_ribs_from_section_span_fractions( section_index=section_index, section_span_fractions=section_span_fraction, - rib_thickness=rib_thickness + rib_thickness=rib_thickness, ) - def add_tube_spar(self, - span_location_root: float, - span_location_tip: float, - diameter_root, - x_over_c_location_root=0.25, - y_over_c_location_root=None, - x_over_c_location_tip=None, - y_over_c_location_tip=None, - diameter_tip: float = None, - cut_ribs: bool = True, - ): + def add_tube_spar( + self, + span_location_root: float, + span_location_tip: float, + diameter_root, + x_over_c_location_root=0.25, + y_over_c_location_root=None, + x_over_c_location_tip=None, + y_over_c_location_tip=None, + diameter_tip: float = None, + cut_ribs: bool = True, + ): if diameter_tip is None: diameter_tip = diameter_root if x_over_c_location_tip is None: @@ -200,79 +183,69 @@ def add_tube_spar(self, # TODO change behavior so that default is a 90 degree spar ### Figure out where the spar root is - section_index = np.argwhere( - self._cumulative_spans_up_to_section > span_location_root - )[0][0] - 1 + section_index = ( + np.argwhere(self._cumulative_spans_up_to_section > span_location_root)[0][0] + - 1 + ) section_span_fraction = ( - span_location_root - - self._cumulative_spans_up_to_section[section_index] - ) / self._sectional_spans[section_index] + span_location_root - self._cumulative_spans_up_to_section[section_index] + ) / self._sectional_spans[section_index] root_csys = self.wing._compute_frame_of_section(section_index) - root_le_point = ( - (1 - section_span_fraction) * wing.xsecs[section_index].xyz_le - + section_span_fraction * wing.xsecs[section_index + 1].xyz_le - ) - root_chord = ( - (1 - section_span_fraction) * wing.xsecs[section_index].chord - + section_span_fraction * wing.xsecs[section_index + 1].chord - ) + root_le_point = (1 - section_span_fraction) * wing.xsecs[ + section_index + ].xyz_le + section_span_fraction * wing.xsecs[section_index + 1].xyz_le + root_chord = (1 - section_span_fraction) * wing.xsecs[ + section_index + ].chord + section_span_fraction * wing.xsecs[section_index + 1].chord root_point = ( - root_le_point + - x_over_c_location_root * root_csys[0] * root_chord + - y_over_c_location_root * root_csys[2] * root_chord + root_le_point + + x_over_c_location_root * root_csys[0] * root_chord + + y_over_c_location_root * root_csys[2] * root_chord ) ### Figure out where the spar tip is - section_index = np.argwhere( - self._cumulative_spans_up_to_section > span_location_tip - )[0][0] - 1 + section_index = ( + np.argwhere(self._cumulative_spans_up_to_section > span_location_tip)[0][0] + - 1 + ) section_span_fraction = ( - span_location_tip - - self._cumulative_spans_up_to_section[section_index] - ) / self._sectional_spans[section_index] + span_location_tip - self._cumulative_spans_up_to_section[section_index] + ) / self._sectional_spans[section_index] tip_csys = self.wing._compute_frame_of_section(section_index) - tip_le_point = ( - (1 - section_span_fraction) * wing.xsecs[section_index].xyz_le - + section_span_fraction * wing.xsecs[section_index + 1].xyz_le - ) - tip_chord = ( - (1 - section_span_fraction) * wing.xsecs[section_index].chord - + section_span_fraction * wing.xsecs[section_index + 1].chord - ) + tip_le_point = (1 - section_span_fraction) * wing.xsecs[ + section_index + ].xyz_le + section_span_fraction * wing.xsecs[section_index + 1].xyz_le + tip_chord = (1 - section_span_fraction) * wing.xsecs[ + section_index + ].chord + section_span_fraction * wing.xsecs[section_index + 1].chord tip_point = ( - tip_le_point + - x_over_c_location_tip * tip_csys[0] * tip_chord + - y_over_c_location_tip * tip_csys[2] * tip_chord + tip_le_point + + x_over_c_location_tip * tip_csys[0] * tip_chord + + y_over_c_location_tip * tip_csys[2] * tip_chord ) normal = tip_point - root_point root_plane = cq.Plane( - origin=tuple(root_point), - xDir=tuple(root_csys[0]), - normal=tuple(normal) + origin=tuple(root_point), xDir=tuple(root_csys[0]), normal=tuple(normal) ) tip_plane = cq.Plane( - origin=tuple(tip_point), - xDir=tuple(tip_csys[0]), - normal=tuple(normal) + origin=tuple(tip_point), xDir=tuple(tip_csys[0]), normal=tuple(normal) ) ### Make the spar - root_wire = cq.Workplane( - inPlane=root_plane - ).circle(radius=2 * diameter_root / 2) - tip_wire = cq.Workplane( - inPlane=tip_plane - ).circle(radius=2 * diameter_tip / 2) + root_wire = cq.Workplane(inPlane=root_plane).circle( + radius=2 * diameter_root / 2 + ) + tip_wire = cq.Workplane(inPlane=tip_plane).circle(radius=2 * diameter_tip / 2) wire_collection = root_wire wire_collection.ctx.pendingWires.extend(tip_wire.ctx.pendingWires) @@ -295,27 +268,13 @@ def add_tube_spar(self, name="Wing", symmetric=True, xsecs=[ - asb.WingXSec( - chord=0.2, - airfoil=af - ), - asb.WingXSec( - xyz_le=[0.05, 0.5, 0], - chord=0.15, - airfoil=af - ), - asb.WingXSec( - xyz_le=[0.1, 0.8, 0.1], - chord=0.1, - airfoil=af - ) - ] + asb.WingXSec(chord=0.2, airfoil=af), + asb.WingXSec(xyz_le=[0.05, 0.5, 0], chord=0.15, airfoil=af), + asb.WingXSec(xyz_le=[0.1, 0.8, 0.1], chord=0.1, airfoil=af), + ], ) -s = WingStructureGenerator( - wing, - default_rib_thickness=1 / 16 * u.inch -) +s = WingStructureGenerator(wing, default_rib_thickness=1 / 16 * u.inch) # s.add_ribs_from_section_span_fractions(0, np.linspace(0, 1, 10)[1:-1]) # s.add_ribs_from_section_span_fractions(1, np.linspace(0, 1, 8)[1:-1]) s.add_ribs_from_xsecs() @@ -327,7 +286,7 @@ def add_tube_spar(self, diameter_root=8e-3, span_location_root=-1 / 16 * u.inch, span_location_tip=s.ribs.keys()[1], - diameter_tip=4e-3 + diameter_tip=4e-3, ) ### Show all ribs diff --git a/aerosandbox/structures/legacy/beams.py b/aerosandbox/structures/legacy/beams.py index a5c62eb79..4abce0947 100644 --- a/aerosandbox/structures/legacy/beams.py +++ b/aerosandbox/structures/legacy/beams.py @@ -6,21 +6,22 @@ class TubeBeam1(AeroSandboxObject): - def __init__(self, - opti, # type: cas.Opti - length, - points_per_point_load=100, - E=228e9, # Pa - isotropic=True, - poisson_ratio=0.5, - diameter_guess=100, # Make this larger for more computational stability, lower for a bit faster speed - thickness=0.14e-3 * 5, - max_allowable_stress=570e6 / 1.75, - density=1600, - G=None, - bending=True, # Should we consider beam bending? - torsion=True, # Should we consider beam torsion? - ): + def __init__( + self, + opti, # type: cas.Opti + length, + points_per_point_load=100, + E=228e9, # Pa + isotropic=True, + poisson_ratio=0.5, + diameter_guess=100, # Make this larger for more computational stability, lower for a bit faster speed + thickness=0.14e-3 * 5, + max_allowable_stress=570e6 / 1.75, + density=1600, + G=None, + bending=True, # Should we consider beam bending? + torsion=True, # Should we consider beam torsion? + ): """ A beam model (static, linear elasticity) that simulates both bending and torsion. @@ -83,18 +84,20 @@ def __init__(self, pass # TODO find poisson? else: raise ValueError( - "You can't uniquely specify shear modulus and Poisson's ratio on an isotropic material!") + "You can't uniquely specify shear modulus and Poisson's ratio on an isotropic material!" + ) # Create data structures to track loads self.point_loads = [] self.distributed_loads = [] - def add_point_load(self, - location, - force=0, - bending_moment=0, - torsional_moment=0, - ): + def add_point_load( + self, + location, + force=0, + bending_moment=0, + torsional_moment=0, + ): """ Adds a point force and/or moment. :param location: Location of the point force along the beam [m] @@ -105,18 +108,19 @@ def add_point_load(self, """ self.point_loads.append( { - "location" : location, - "force" : force, - "bending_moment" : bending_moment, - "torsional_moment": torsional_moment + "location": location, + "force": force, + "bending_moment": bending_moment, + "torsional_moment": torsional_moment, } ) - def add_uniform_load(self, - force=0, - bending_moment=0, - torsional_moment=0, - ): + def add_uniform_load( + self, + force=0, + bending_moment=0, + torsional_moment=0, + ): """ Adds a uniformly distributed force and/or moment across the entire length of the beam. :param force: Total force applied to beam [N] @@ -126,18 +130,19 @@ def add_uniform_load(self, """ self.distributed_loads.append( { - "type" : "uniform", - "force" : force, - "bending_moment" : bending_moment, - "torsional_moment": torsional_moment + "type": "uniform", + "force": force, + "bending_moment": bending_moment, + "torsional_moment": torsional_moment, } ) - def add_elliptical_load(self, - force=0, - bending_moment=0, - torsional_moment=0, - ): + def add_elliptical_load( + self, + force=0, + bending_moment=0, + torsional_moment=0, + ): """ Adds an elliptically distributed force and/or moment across the entire length of the beam. :param force: Total force applied to beam [N] @@ -147,16 +152,14 @@ def add_elliptical_load(self, """ self.distributed_loads.append( { - "type" : "elliptical", - "force" : force, - "bending_moment" : bending_moment, - "torsional_moment": torsional_moment + "type": "elliptical", + "force": force, + "bending_moment": bending_moment, + "torsional_moment": torsional_moment, } ) - def setup(self, - bending_BC_type="cantilevered" - ): + def setup(self, bending_BC_type="cantilevered"): """ Sets up the problem. Run this last. :return: None (in-place) @@ -167,13 +170,16 @@ def setup(self, point_load_locations = [load["location"] for load in self.point_loads] point_load_locations.insert(0, 0) point_load_locations.append(self.length) - self.x = cas.vertcat(*[ - cas.linspace( - point_load_locations[i], - point_load_locations[i + 1], - self.points_per_point_load) - for i in range(len(point_load_locations) - 1) - ]) + self.x = cas.vertcat( + *[ + cas.linspace( + point_load_locations[i], + point_load_locations[i + 1], + self.points_per_point_load, + ) + for i in range(len(point_load_locations) - 1) + ] + ) # Post-process the discretization self.n = self.x.shape[0] @@ -192,21 +198,23 @@ def setup(self, if load["type"] == "uniform": self.force_per_unit_length += load["force"] / self.length elif load["type"] == "elliptical": - load_to_add = load["force"] / self.length * ( - 4 / cas.pi * cas.sqrt(1 - (self.x / self.length) ** 2) + load_to_add = ( + load["force"] + / self.length + * (4 / cas.pi * cas.sqrt(1 - (self.x / self.length) ** 2)) ) self.force_per_unit_length += load_to_add else: - raise ValueError("Bad value of \"type\" for a load within beam.distributed_loads!") + raise ValueError( + 'Bad value of "type" for a load within beam.distributed_loads!' + ) # Initialize optimization variables log_nominal_diameter = self.opti.variable(self.n) self.opti.set_initial(log_nominal_diameter, cas.log(self.diameter_guess)) self.nominal_diameter = cas.exp(log_nominal_diameter) - self.opti.subject_to([ - log_nominal_diameter > cas.log(self.thickness) - ]) + self.opti.subject_to([log_nominal_diameter > cas.log(self.thickness)]) def trapz(x): out = (x[:-1] + x[1:]) / 2 @@ -216,29 +224,38 @@ def trapz(x): # Mass self.volume = cas.sum1( - cas.pi / 4 * trapz( - (self.nominal_diameter + self.thickness) ** 2 - - (self.nominal_diameter - self.thickness) ** 2 - ) * dx + cas.pi + / 4 + * trapz( + (self.nominal_diameter + self.thickness) ** 2 + - (self.nominal_diameter - self.thickness) ** 2 + ) + * dx ) self.mass = self.volume * self.density # Mass proxy self.volume_proxy = cas.sum1( - cas.pi * trapz( - self.nominal_diameter - ) * dx * self.thickness + cas.pi * trapz(self.nominal_diameter) * dx * self.thickness ) self.mass_proxy = self.volume_proxy * self.density # Find moments of inertia - self.I = cas.pi / 64 * ( # bending - (self.nominal_diameter + self.thickness) ** 4 - - (self.nominal_diameter - self.thickness) ** 4 + self.I = ( + cas.pi + / 64 + * ( # bending + (self.nominal_diameter + self.thickness) ** 4 + - (self.nominal_diameter - self.thickness) ** 4 + ) ) - self.J = cas.pi / 32 * ( # torsion - (self.nominal_diameter + self.thickness) ** 4 - - (self.nominal_diameter - self.thickness) ** 4 + self.J = ( + cas.pi + / 32 + * ( # torsion + (self.nominal_diameter + self.thickness) ** 4 + - (self.nominal_diameter - self.thickness) ** 4 + ) ) if self.bending: @@ -253,26 +270,33 @@ def trapz(x): self.opti.set_initial(self.dEIddu, 0) # Define derivatives - self.opti.subject_to([ - cas.diff(self.u) == trapz(self.du) * dx, - cas.diff(self.du) == trapz(self.ddu) * dx, - cas.diff(self.E * self.I * self.ddu) == trapz(self.dEIddu) * dx, - cas.diff(self.dEIddu) == trapz(self.force_per_unit_length) * dx + self.point_forces, - ]) + self.opti.subject_to( + [ + cas.diff(self.u) == trapz(self.du) * dx, + cas.diff(self.du) == trapz(self.ddu) * dx, + cas.diff(self.E * self.I * self.ddu) == trapz(self.dEIddu) * dx, + cas.diff(self.dEIddu) + == trapz(self.force_per_unit_length) * dx + self.point_forces, + ] + ) # Add BCs if bending_BC_type == "cantilevered": - self.opti.subject_to([ - self.u[0] == 0, - self.du[0] == 0, - self.ddu[-1] == 0, # No tip moment - self.dEIddu[-1] == 0, # No tip higher order stuff - ]) + self.opti.subject_to( + [ + self.u[0] == 0, + self.du[0] == 0, + self.ddu[-1] == 0, # No tip moment + self.dEIddu[-1] == 0, # No tip higher order stuff + ] + ) else: raise ValueError("Bad value of bending_BC_type!") # Stress - self.stress_axial = (self.nominal_diameter + self.thickness) / 2 * self.E * self.ddu + self.stress_axial = ( + (self.nominal_diameter + self.thickness) / 2 * self.E * self.ddu + ) if self.torsion: @@ -284,16 +308,19 @@ def trapz(x): ddphi = -self.moment_per_unit_length / (self.G * self.J) self.stress = self.stress_axial - self.opti.subject_to([ - self.stress / self.max_allowable_stress < 1, - self.stress / self.max_allowable_stress > -1, - ]) - - def draw_bending(self, - show=True, - for_print=False, - equal_scale=True, - ): + self.opti.subject_to( + [ + self.stress / self.max_allowable_stress < 1, + self.stress / self.max_allowable_stress > -1, + ] + ) + + def draw_bending( + self, + show=True, + for_print=False, + equal_scale=True, + ): """ Draws a figure that illustrates some bending properties. Must be called on a solved object (i.e. using the substitute_sol method). :param show: Whether or not to show the figure [boolean] @@ -303,20 +330,18 @@ def draw_bending(self, """ import matplotlib.pyplot as plt import seaborn as sns + sns.set(font_scale=1) fig, ax = plt.subplots( 2 if not for_print else 3, 3 if not for_print else 2, - figsize=( - 10 if not for_print else 6, - 6 if not for_print else 6 - ), - dpi=200 + figsize=(10 if not for_print else 6, 6 if not for_print else 6), + dpi=200, ) plt.subplot(231) if not for_print else plt.subplot(321) - plt.plot(self.x, self.u, '.-') + plt.plot(self.x, self.u, ".-") plt.xlabel(r"$x$ [m]") plt.ylabel(r"$u$ [m]") plt.title("Displacement (Bending)") @@ -324,31 +349,31 @@ def draw_bending(self, plt.axis("equal") plt.subplot(232) if not for_print else plt.subplot(322) - plt.plot(self.x, np.arctan(self.du) * 180 / np.pi, '.-') + plt.plot(self.x, np.arctan(self.du) * 180 / np.pi, ".-") plt.xlabel(r"$x$ [m]") plt.ylabel(r"Local Slope [deg]") plt.title("Slope") plt.subplot(233) if not for_print else plt.subplot(323) - plt.plot(self.x, self.force_per_unit_length, '.-') + plt.plot(self.x, self.force_per_unit_length, ".-") plt.xlabel(r"$x$ [m]") plt.ylabel(r"$q$ [N/m]") plt.title("Local Load per Unit Span") plt.subplot(234) if not for_print else plt.subplot(324) - plt.plot(self.x, self.stress_axial / 1e6, '.-') + plt.plot(self.x, self.stress_axial / 1e6, ".-") plt.xlabel(r"$x$ [m]") plt.ylabel("Axial Stress [MPa]") plt.title("Axial Stress") plt.subplot(235) if not for_print else plt.subplot(325) - plt.plot(self.x, self.dEIddu, '.-') + plt.plot(self.x, self.dEIddu, ".-") plt.xlabel(r"$x$ [m]") plt.ylabel(r"$F$ [N]") plt.title("Shear Force") plt.subplot(236) if not for_print else plt.subplot(326) - plt.plot(self.x, self.nominal_diameter, '.-') + plt.plot(self.x, self.nominal_diameter, ".-") plt.xlabel(r"$x$ [m]") plt.ylabel("Diameter [m]") plt.title("Optimal Spar Diameter") @@ -357,7 +382,7 @@ def draw_bending(self, plt.show() if show else None -if __name__ == '__main__': +if __name__ == "__main__": opti = cas.Opti() beam = TubeBeam1( opti=opti, @@ -365,31 +390,37 @@ def draw_bending(self, points_per_point_load=50, diameter_guess=100, bending=True, - torsion=False + torsion=False, ) lift_force = 9.81 * 103.873 load_location = opti.variable() opti.set_initial(load_location, 15) - opti.subject_to([ - load_location > 2, - load_location < 60 / 2 - 2, - load_location == 18, - ]) + opti.subject_to( + [ + load_location > 2, + load_location < 60 / 2 - 2, + load_location == 18, + ] + ) beam.add_point_load(load_location, -lift_force / 3) beam.add_uniform_load(force=lift_force / 2) beam.setup() # Tip deflection constraint - opti.subject_to([ - # beam.u[-1] < 2, # Source: http://web.mit.edu/drela/Public/web/hpa/hpa_structure.pdf - # beam.u[-1] > -2 # Source: http://web.mit.edu/drela/Public/web/hpa/hpa_structure.pdf - beam.du * 180 / cas.pi < 10, - beam.du * 180 / cas.pi > -10 - ]) - opti.subject_to([ - cas.diff(cas.diff(beam.nominal_diameter)) < 0.001, - cas.diff(cas.diff(beam.nominal_diameter)) > -0.001, - ]) + opti.subject_to( + [ + # beam.u[-1] < 2, # Source: http://web.mit.edu/drela/Public/web/hpa/hpa_structure.pdf + # beam.u[-1] > -2 # Source: http://web.mit.edu/drela/Public/web/hpa/hpa_structure.pdf + beam.du * 180 / cas.pi < 10, + beam.du * 180 / cas.pi > -10, + ] + ) + opti.subject_to( + [ + cas.diff(cas.diff(beam.nominal_diameter)) < 0.001, + cas.diff(cas.diff(beam.nominal_diameter)) > -0.001, + ] + ) # opti.minimize(cas.sqrt(beam.mass)) opti.minimize(beam.mass) @@ -414,7 +445,7 @@ def draw_bending(self, # s_opts["start_with_resto"] = "yes" # s_opts["required_infeasibility_reduction"] = 0.001 # s_opts["evaluate_orig_obj_at_resto_trial"] = "yes" - opti.solver('ipopt', p_opts, s_opts) + opti.solver("ipopt", p_opts, s_opts) try: sol = opti.solve() diff --git a/aerosandbox/structures/legacy/simple_beam_opt.py b/aerosandbox/structures/legacy/simple_beam_opt.py index 3dd13e900..8c5f19700 100644 --- a/aerosandbox/structures/legacy/simple_beam_opt.py +++ b/aerosandbox/structures/legacy/simple_beam_opt.py @@ -26,10 +26,11 @@ * ()' is a derivative w.r.t. x. """ + import aerosandbox.numpy as np import casadi as cas -if __name__ == '__main__': +if __name__ == "__main__": opti = cas.Opti() # Initialize a SAND environment @@ -48,10 +49,11 @@ nominal_diameter = cas.exp(log_nominal_diameter) thickness = 0.14e-3 * 5 - opti.subject_to([ - nominal_diameter > thickness, - ]) - + opti.subject_to( + [ + nominal_diameter > thickness, + ] + ) def trapz(x): out = (x[:-1] + x[1:]) / 2 @@ -59,30 +61,49 @@ def trapz(x): out[-1] += x[-1] / 2 return out - # Mass volume = cas.sum1( - cas.pi / 4 * trapz((nominal_diameter + thickness) ** 2 - (nominal_diameter - thickness) ** 2) * dx + cas.pi + / 4 + * trapz( + (nominal_diameter + thickness) ** 2 - (nominal_diameter - thickness) ** 2 + ) + * dx ) mass = volume * 1600 # Bending loads - I = cas.pi / 64 * ((nominal_diameter + thickness) ** 4 - (nominal_diameter - thickness) ** 4) + I = ( + cas.pi + / 64 + * ((nominal_diameter + thickness) ** 4 - (nominal_diameter - thickness) ** 4) + ) EI = E * I total_lift_force = 9.81 * (mass_total - mass) / 2 # 9.81 * 292 / 2 lift_distribution = "elliptical" if lift_distribution == "rectangular": force_per_unit_length = total_lift_force * cas.GenDM_ones(n) / L elif lift_distribution == "elliptical": - force_per_unit_length = total_lift_force * cas.sqrt(1 - (x / L) ** 2) * (4 / cas.pi) / L + force_per_unit_length = ( + total_lift_force * cas.sqrt(1 - (x / L) ** 2) * (4 / cas.pi) / L + ) # Torsion loads - J = cas.pi / 32 * ((nominal_diameter + thickness) ** 4 - (nominal_diameter - thickness) ** 4) + J = ( + cas.pi + / 32 + * ((nominal_diameter + thickness) ** 4 - (nominal_diameter - thickness) ** 4) + ) airfoil_lift_coefficient = 1 airfoil_moment_coefficient = -0.14 airfoil_chord = 1 # meter - moment_per_unit_length = force_per_unit_length * airfoil_moment_coefficient * airfoil_chord / airfoil_lift_coefficient + moment_per_unit_length = ( + force_per_unit_length + * airfoil_moment_coefficient + * airfoil_chord + / airfoil_lift_coefficient + ) # Derivation of above: # CL = L / q c # CM = M / q c**2 @@ -105,24 +126,28 @@ def trapz(x): ddphi = -moment_per_unit_length / (G * J) # Define derivatives - opti.subject_to([ - cas.diff(u) == trapz(du) * dx, - cas.diff(du) == trapz(ddu) * dx, - cas.diff(EI * ddu) == trapz(dEIddu) * dx, - cas.diff(dEIddu) == trapz(ddEIddu) * dx, - cas.diff(phi) == trapz(dphi) * dx, - cas.diff(dphi) == trapz(ddphi) * dx, - ]) + opti.subject_to( + [ + cas.diff(u) == trapz(du) * dx, + cas.diff(du) == trapz(ddu) * dx, + cas.diff(EI * ddu) == trapz(dEIddu) * dx, + cas.diff(dEIddu) == trapz(ddEIddu) * dx, + cas.diff(phi) == trapz(dphi) * dx, + cas.diff(dphi) == trapz(ddphi) * dx, + ] + ) # Add BCs - opti.subject_to([ - u[0] == 0, - du[0] == 0, - ddu[-1] == 0, # No tip moment - dEIddu[-1] == 0, # No tip higher order stuff - phi[0] == 0, - dphi[-1] == 0, - ]) + opti.subject_to( + [ + u[0] == 0, + du[0] == 0, + ddu[-1] == 0, # No tip moment + dEIddu[-1] == 0, # No tip higher order stuff + phi[0] == 0, + dphi[-1] == 0, + ] + ) # Failure criterion stress_axial = (nominal_diameter + thickness) / 2 * E * ddu @@ -130,30 +155,30 @@ def trapz(x): # stress_axial = cas.fmax(0, stress_axial) # stress_shear = cas.fmax(0, stress_shear) stress_von_mises_squared = cas.sqrt( - stress_axial ** 2 + 0 * stress_shear ** 2) # Source: https://en.wikipedia.org/wiki/Von_Mises_yield_criterion + stress_axial**2 + 0 * stress_shear**2 + ) # Source: https://en.wikipedia.org/wiki/Von_Mises_yield_criterion stress = stress_axial - opti.subject_to([ - stress / max_allowable_stress < 1 - ]) + opti.subject_to([stress / max_allowable_stress < 1]) opti.minimize(mass) # Tip deflection constraint - opti.subject_to([ - # u[-1] < 2 # Source: http://web.mit.edu/drela/Public/web/hpa/hpa_structure.pdf - du[-1] * 180 / cas.pi < 10 - ]) + opti.subject_to( + [ + # u[-1] < 2 # Source: http://web.mit.edu/drela/Public/web/hpa/hpa_structure.pdf + du[-1] * 180 / cas.pi + < 10 + ] + ) # Twist - opti.subject_to([ - phi[-1] * 180 / cas.pi > -5 - ]) + opti.subject_to([phi[-1] * 180 / cas.pi > -5]) p_opts = {} s_opts = {} s_opts["max_iter"] = 500 # If you need to interrupt, just use ctrl+c # s_opts["mu_strategy"] = "adaptive" - opti.solver('ipopt', p_opts, s_opts) + opti.solver("ipopt", p_opts, s_opts) try: sol = opti.solve() @@ -169,7 +194,7 @@ def trapz(x): fig, ax = plt.subplots(2, 3, figsize=(10, 6), dpi=200) plt.subplot(231) - plt.plot(sol(x), sol(u), '.-') + plt.plot(sol(x), sol(u), ".-") plt.xlabel("x [m]") plt.ylabel("u [m]") plt.title("Displacement (Bending)") @@ -182,31 +207,31 @@ def trapz(x): # plt.title("Slope") plt.subplot(232) - plt.plot(sol.value(x), sol.value(phi) * 180 / np.pi, '.-') + plt.plot(sol.value(x), sol.value(phi) * 180 / np.pi, ".-") plt.xlabel("x [m]") plt.ylabel("Twist angle [deg]") plt.title("Twist Angle (Torsion)") plt.subplot(233) - plt.plot(sol.value(x), sol.value(force_per_unit_length), '.-') + plt.plot(sol.value(x), sol.value(force_per_unit_length), ".-") plt.xlabel("x [m]") plt.ylabel(r"$F$ [N/m]") plt.title("Local Load per Unit Span") plt.subplot(234) - plt.plot(sol.value(x), sol.value(stress / 1e6), '.-') + plt.plot(sol.value(x), sol.value(stress / 1e6), ".-") plt.xlabel("x [m]") plt.ylabel("Stress [MPa]") plt.title("Peak Stress at Section") plt.subplot(235) - plt.plot(sol.value(x), sol.value(dEIddu), '.-') + plt.plot(sol.value(x), sol.value(dEIddu), ".-") plt.xlabel("x [m]") plt.ylabel("F [N]") plt.title("Shear Force") plt.subplot(236) - plt.plot(sol.value(x), sol.value(nominal_diameter), '.-') + plt.plot(sol.value(x), sol.value(nominal_diameter), ".-") plt.xlabel("x [m]") plt.ylabel("t [m]") plt.title("Optimal Spar Diameter") diff --git a/aerosandbox/structures/legacy/simple_beam_opt_daedalus_calibration.py b/aerosandbox/structures/legacy/simple_beam_opt_daedalus_calibration.py index 9d03298ab..6ac4fdd54 100644 --- a/aerosandbox/structures/legacy/simple_beam_opt_daedalus_calibration.py +++ b/aerosandbox/structures/legacy/simple_beam_opt_daedalus_calibration.py @@ -26,10 +26,11 @@ * ()' is a derivative w.r.t. x. """ + import aerosandbox.numpy as np import casadi as cas -if __name__ == '__main__': +if __name__ == "__main__": opti = cas.Opti() # Initialize a SAND environment @@ -47,28 +48,45 @@ nominal_diameter = cas.exp(log_nominal_diameter) thickness = 0.14e-3 * 5 - opti.subject_to([ - nominal_diameter > thickness, - ]) + opti.subject_to( + [ + nominal_diameter > thickness, + ] + ) # Bending loads - I = cas.pi / 64 * ((nominal_diameter + thickness) ** 4 - (nominal_diameter - thickness) ** 4) + I = ( + cas.pi + / 64 + * ((nominal_diameter + thickness) ** 4 - (nominal_diameter - thickness) ** 4) + ) EI = E * I total_lift_force = 9.81 * 103.873 / 2 lift_distribution = "elliptical" if lift_distribution == "rectangular": force_per_unit_length = total_lift_force * cas.GenDM_ones(n) / L elif lift_distribution == "elliptical": - force_per_unit_length = total_lift_force * cas.sqrt(1 - (x / L) ** 2) * (4 / cas.pi) / L + force_per_unit_length = ( + total_lift_force * cas.sqrt(1 - (x / L) ** 2) * (4 / cas.pi) / L + ) # Torsion loads - J = cas.pi / 32 * ((nominal_diameter + thickness) ** 4 - (nominal_diameter - thickness) ** 4) + J = ( + cas.pi + / 32 + * ((nominal_diameter + thickness) ** 4 - (nominal_diameter - thickness) ** 4) + ) airfoil_lift_coefficient = 1 airfoil_moment_coefficient = -0.14 airfoil_chord = 1 # meter - moment_per_unit_length = force_per_unit_length * airfoil_moment_coefficient * airfoil_chord / airfoil_lift_coefficient + moment_per_unit_length = ( + force_per_unit_length + * airfoil_moment_coefficient + * airfoil_chord + / airfoil_lift_coefficient + ) # Derivation of above: # CL = L / q c # CM = M / q c**2 @@ -90,7 +108,6 @@ ddEIddu = force_per_unit_length ddphi = -moment_per_unit_length / (G * J) - # Define derivatives def trapz(x): out = (x[:-1] + x[1:]) / 2 @@ -98,25 +115,28 @@ def trapz(x): out[-1] += x[-1] / 2 return out - - opti.subject_to([ - cas.diff(u) == trapz(du) * dx, - cas.diff(du) == trapz(ddu) * dx, - cas.diff(EI * ddu) == trapz(dEIddu) * dx, - cas.diff(dEIddu) == trapz(ddEIddu) * dx, - cas.diff(phi) == trapz(dphi) * dx, - cas.diff(dphi) == trapz(ddphi) * dx, - ]) + opti.subject_to( + [ + cas.diff(u) == trapz(du) * dx, + cas.diff(du) == trapz(ddu) * dx, + cas.diff(EI * ddu) == trapz(dEIddu) * dx, + cas.diff(dEIddu) == trapz(ddEIddu) * dx, + cas.diff(phi) == trapz(dphi) * dx, + cas.diff(dphi) == trapz(ddphi) * dx, + ] + ) # Add BCs - opti.subject_to([ - u[0] == 0, - du[0] == 0, - ddu[-1] == 0, # No tip moment - dEIddu[-1] == 0, # No tip higher order stuff - phi[0] == 0, - dphi[-1] == 0, - ]) + opti.subject_to( + [ + u[0] == 0, + du[0] == 0, + ddu[-1] == 0, # No tip moment + dEIddu[-1] == 0, # No tip higher order stuff + phi[0] == 0, + dphi[-1] == 0, + ] + ) # Failure criterion stress_axial = (nominal_diameter + thickness) / 2 * E * ddu @@ -124,34 +144,36 @@ def trapz(x): # stress_axial = cas.fmax(0, stress_axial) # stress_shear = cas.fmax(0, stress_shear) stress_von_mises_squared = cas.sqrt( - stress_axial ** 2 + 0 * stress_shear ** 2) # Source: https://en.wikipedia.org/wiki/Von_Mises_yield_criterion + stress_axial**2 + 0 * stress_shear**2 + ) # Source: https://en.wikipedia.org/wiki/Von_Mises_yield_criterion stress = stress_axial - opti.subject_to([ - stress / max_allowable_stress < 1 - ]) + opti.subject_to([stress / max_allowable_stress < 1]) # Mass volume = cas.sum1( - cas.pi / 4 * trapz((nominal_diameter + thickness) ** 2 - (nominal_diameter - thickness) ** 2) * dx + cas.pi + / 4 + * trapz( + (nominal_diameter + thickness) ** 2 - (nominal_diameter - thickness) ** 2 + ) + * dx ) mass = volume * 1600 opti.minimize(mass) # Tip deflection constraint - opti.subject_to([ - u[-1] < 2 # Source: http://web.mit.edu/drela/Public/web/hpa/hpa_structure.pdf - ]) + opti.subject_to( + [u[-1] < 2] # Source: http://web.mit.edu/drela/Public/web/hpa/hpa_structure.pdf + ) # Twist - opti.subject_to([ - phi[-1] * 180 / cas.pi > -3 - ]) + opti.subject_to([phi[-1] * 180 / cas.pi > -3]) p_opts = {} s_opts = {} s_opts["max_iter"] = 500 # If you need to interrupt, just use ctrl+c # s_opts["mu_strategy"] = "adaptive" - opti.solver('ipopt', p_opts, s_opts) + opti.solver("ipopt", p_opts, s_opts) try: sol = opti.solve() @@ -167,7 +189,7 @@ def trapz(x): fig, ax = plt.subplots(2, 3, figsize=(10, 6), dpi=200) plt.subplot(231) - plt.plot(sol(x), sol(u), '.-') + plt.plot(sol(x), sol(u), ".-") plt.xlabel("x [m]") plt.ylabel("u [m]") plt.title("Displacement (Bending)") @@ -180,31 +202,31 @@ def trapz(x): # plt.title("Slope") plt.subplot(232) - plt.plot(sol.value(x), sol.value(phi) * 180 / np.pi, '.-') + plt.plot(sol.value(x), sol.value(phi) * 180 / np.pi, ".-") plt.xlabel("x [m]") plt.ylabel("Twist angle [deg]") plt.title("Twist Angle (Torsion)") plt.subplot(233) - plt.plot(sol.value(x), sol.value(force_per_unit_length), '.-') + plt.plot(sol.value(x), sol.value(force_per_unit_length), ".-") plt.xlabel("x [m]") plt.ylabel(r"$F$ [N/m]") plt.title("Local Load per Unit Span") plt.subplot(234) - plt.plot(sol.value(x), sol.value(stress / 1e6), '.-') + plt.plot(sol.value(x), sol.value(stress / 1e6), ".-") plt.xlabel("x [m]") plt.ylabel("Stress [MPa]") plt.title("Peak Stress at Section") plt.subplot(235) - plt.plot(sol.value(x), sol.value(dEIddu), '.-') + plt.plot(sol.value(x), sol.value(dEIddu), ".-") plt.xlabel("x [m]") plt.ylabel("F [N]") plt.title("Shear Force") plt.subplot(236) - plt.plot(sol.value(x), sol.value(nominal_diameter), '.-') + plt.plot(sol.value(x), sol.value(nominal_diameter), ".-") plt.xlabel("x [m]") plt.ylabel("t [m]") plt.title("Optimal Spar Diameter") diff --git a/aerosandbox/structures/tube_spar_bending.py b/aerosandbox/structures/tube_spar_bending.py index 27d5b5a91..c6064e7b9 100644 --- a/aerosandbox/structures/tube_spar_bending.py +++ b/aerosandbox/structures/tube_spar_bending.py @@ -6,17 +6,24 @@ class TubeSparBendingStructure(asb.ImplicitAnalysis): @asb.ImplicitAnalysis.initialize - def __init__(self, - length: float, - diameter_function: Union[float, Callable[[np.ndarray], np.ndarray]] = None, - wall_thickness_function: Union[float, Callable[[np.ndarray], np.ndarray]] = None, - bending_point_forces: Dict[float, float] = None, - bending_distributed_force_function: Union[float, Callable[[np.ndarray], np.ndarray]] = 0., - points_per_point_load: int = 20, - elastic_modulus_function: Union[float, Callable[[np.ndarray], np.ndarray]] = 175e9, # Pa - EI_guess: float = None, - assume_thin_tube=True, - ): + def __init__( + self, + length: float, + diameter_function: Union[float, Callable[[np.ndarray], np.ndarray]] = None, + wall_thickness_function: Union[ + float, Callable[[np.ndarray], np.ndarray] + ] = None, + bending_point_forces: Dict[float, float] = None, + bending_distributed_force_function: Union[ + float, Callable[[np.ndarray], np.ndarray] + ] = 0.0, + points_per_point_load: int = 20, + elastic_modulus_function: Union[ + float, Callable[[np.ndarray], np.ndarray] + ] = 175e9, # Pa + EI_guess: float = None, + assume_thin_tube=True, + ): """ A structural spar model that simulates bending of a cantilever tube spar based on beam theory (static, linear elasticity). This tube spar is assumed to have uniform wall thickness in the azimuthal direction, @@ -188,11 +195,15 @@ def __init__(self, E_guess = 175e9 if assume_thin_tube: - I_guess = np.pi / 8 * diameter_guess ** 3 * wall_thickness_guess + I_guess = np.pi / 8 * diameter_guess**3 * wall_thickness_guess else: - I_guess = np.pi / 64 * ( - (diameter_guess + wall_thickness_guess) ** 4 - - (diameter_guess - wall_thickness_guess) ** 4 + I_guess = ( + np.pi + / 64 + * ( + (diameter_guess + wall_thickness_guess) ** 4 + - (diameter_guess - wall_thickness_guess) ** 4 + ) ) EI_guess = E_guess * I_guess @@ -203,11 +214,7 @@ def __init__(self, self.assume_thin_tube = assume_thin_tube ### Discretize - y = np.linspace( - 0, - length, - points_per_point_load - ) + y = np.linspace(0, length, points_per_point_load) N = np.length(y) dy = np.diff(y) @@ -216,14 +223,16 @@ def __init__(self, if isinstance(diameter_function, Callable): diameter = diameter_function(y) elif diameter_function is None: - diameter = self.opti.variable(init_guess=1, n_vars=N, lower_bound=0.) + diameter = self.opti.variable(init_guess=1, n_vars=N, lower_bound=0.0) else: diameter = diameter_function * np.ones_like(y) if isinstance(wall_thickness_function, Callable): wall_thickness = wall_thickness_function(y) elif wall_thickness_function is None: - wall_thickness = self.opti.variable(init_guess=1e-2, n_vars=N, lower_bound=0, upper_bound=diameter) + wall_thickness = self.opti.variable( + init_guess=1e-2, n_vars=N, lower_bound=0, upper_bound=diameter + ) else: wall_thickness = wall_thickness_function * np.ones_like(y) @@ -239,45 +248,47 @@ def __init__(self, ### Evaluate the beam properties if assume_thin_tube: - I = np.pi / 8 * diameter ** 3 * wall_thickness + I = np.pi / 8 * diameter**3 * wall_thickness else: - I = np.pi / 64 * ( - (diameter + wall_thickness) ** 4 - - (diameter - wall_thickness) ** 4 + I = ( + np.pi + / 64 + * ((diameter + wall_thickness) ** 4 - (diameter - wall_thickness) ** 4) ) EI = elastic_modulus * I ### Compute the initial guess u = self.opti.variable( init_guess=np.zeros_like(y), - scale=np.sum(np.trapz(distributed_force) * dy) * length ** 4 / EI_guess + scale=np.sum(np.trapz(distributed_force) * dy) * length**4 / EI_guess, ) du = self.opti.derivative_of( - u, with_respect_to=y, + u, + with_respect_to=y, derivative_init_guess=np.zeros_like(y), - derivative_scale=np.sum(np.trapz(distributed_force) * dy) * length ** 3 / EI_guess + derivative_scale=np.sum(np.trapz(distributed_force) * dy) + * length**3 + / EI_guess, ) ddu = self.opti.derivative_of( - du, with_respect_to=y, + du, + with_respect_to=y, derivative_init_guess=np.zeros_like(y), - derivative_scale=np.sum(np.trapz(distributed_force) * dy) * length ** 2 / EI_guess + derivative_scale=np.sum(np.trapz(distributed_force) * dy) + * length**2 + / EI_guess, ) dEIddu = self.opti.derivative_of( - EI * ddu, with_respect_to=y, + EI * ddu, + with_respect_to=y, derivative_init_guess=np.zeros_like(y), - derivative_scale=np.sum(np.trapz(distributed_force) * dy) * length + derivative_scale=np.sum(np.trapz(distributed_force) * dy) * length, ) self.opti.constrain_derivative( - variable=dEIddu, with_respect_to=y, - derivative=distributed_force + variable=dEIddu, with_respect_to=y, derivative=distributed_force ) - self.opti.subject_to([ - u[0] == 0, - du[0] == 0, - ddu[-1] == 0, - dEIddu[-1] == 0 - ]) + self.opti.subject_to([u[0] == 0, du[0] == 0, ddu[-1] == 0, dEIddu[-1] == 0]) bending_moment = -EI * ddu shear_force = -dEIddu @@ -301,43 +312,40 @@ def __init__(self, def volume(self): if self.assume_thin_tube: return np.sum( - np.pi * np.trapz( - self.diameter * self.wall_thickness - ) * np.diff(self.y) + np.pi * np.trapz(self.diameter * self.wall_thickness) * np.diff(self.y) ) else: return np.sum( - np.pi / 4 * np.trapz( - (self.diameter + self.wall_thickness) ** 2 - - (self.diameter - self.wall_thickness) ** 2 - ) * np.diff(self.y) + np.pi + / 4 + * np.trapz( + (self.diameter + self.wall_thickness) ** 2 + - (self.diameter - self.wall_thickness) ** 2 + ) + * np.diff(self.y) ) def total_force(self): if len(self.bending_point_forces) != 0: raise NotImplementedError - return np.sum( - np.trapz( - self.distributed_force - ) * np.diff(self.y) - ) + return np.sum(np.trapz(self.distributed_force) * np.diff(self.y)) def draw(self, show=True): import matplotlib.pyplot as plt import aerosandbox.tools.pretty_plots as p plot_quantities = { - "Displacement [m]" : self.u, + "Displacement [m]": self.u, # "Local Slope [deg]": np.arctan2d(self.du, 1), - "Local Load [N/m]" : self.distributed_force, - "Axial Stress [MPa]" : self.stress_axial / 1e6, + "Local Load [N/m]": self.distributed_force, + "Axial Stress [MPa]": self.stress_axial / 1e6, "Bending $EI$ [N $\cdot$ m$^2$]": self.elastic_modulus * self.I, - "Tube Diameter [m]" : self.diameter, - "Wall Thickness [m]" : self.wall_thickness, + "Tube Diameter [m]": self.diameter, + "Wall Thickness [m]": self.wall_thickness, } - fig, ax = plt.subplots(2, 3, figsize=(8, 6), sharex='all') + fig, ax = plt.subplots(2, 3, figsize=(8, 6), sharex="all") for i, (k, v) in enumerate(plot_quantities.items()): plt.sca(ax.flatten()[i]) @@ -359,7 +367,7 @@ def draw(self, show=True): p.show_plot("Tube Spar Bending Structure") -if __name__ == '__main__': +if __name__ == "__main__": import aerosandbox.tools.units as u opti = asb.Opti() @@ -371,19 +379,21 @@ def draw(self, show=True): beam = TubeSparBendingStructure( opti=opti, length=half_span, - diameter_function=3.5 * u.inch, # lambda y: (3.5 * u.inch) - (3.5 - 1.25) * u.inch * (y / half_span), + diameter_function=3.5 + * u.inch, # lambda y: (3.5 * u.inch) - (3.5 - 1.25) * u.inch * (y / half_span), points_per_point_load=100, - bending_distributed_force_function=lambda y: (lift / span) * ( - 4 / np.pi * (1 - (y / half_span) ** 2) ** 0.5 - ), # Elliptical + bending_distributed_force_function=lambda y: (lift / span) + * (4 / np.pi * (1 - (y / half_span) ** 2) ** 0.5), # Elliptical # bending_distributed_force_function=lambda y: lift / span * np.ones_like(y) # Uniform, elastic_modulus_function=228e9, ) - opti.subject_to([ - beam.stress_axial <= 500e6, # Stress constraint - beam.u[-1] <= 2, # Tip displacement constraint - beam.wall_thickness > 0.1e-3 # Gauge constraint - ]) + opti.subject_to( + [ + beam.stress_axial <= 500e6, # Stress constraint + beam.u[-1] <= 2, # Tip displacement constraint + beam.wall_thickness > 0.1e-3, # Gauge constraint + ] + ) mass = beam.volume() * 1600 # Density of carbon fiber [kg/m^3] opti.minimize(mass / (lift / 9.81)) @@ -400,7 +410,6 @@ def draw(self, show=True): vehicle_mass = lift / 9.81 ultimate_load_factor = 2 - cruz_estimated_spar_mass = ( - (span * 1.17e-1 + span ** 2 * 1.10e-2) * - (1 + (ultimate_load_factor * vehicle_mass / 100 - 2) / 4) + cruz_estimated_spar_mass = (span * 1.17e-1 + span**2 * 1.10e-2) * ( + 1 + (ultimate_load_factor * vehicle_mass / 100 - 2) / 4 ) diff --git a/aerosandbox/tools/code_benchmarking.py b/aerosandbox/tools/code_benchmarking.py index 5549626c0..d2c54cc7b 100644 --- a/aerosandbox/tools/code_benchmarking.py +++ b/aerosandbox/tools/code_benchmarking.py @@ -26,19 +26,21 @@ class Timer(object): prints the following console output: [a] Timing... - [b] Timing... - [c] Timing... - [c] Elapsed: 100 msec - [b] Elapsed: 100 msec + [b] Timing... + [c] Timing... + [c] Elapsed: 100 msec + [b] Elapsed: 100 msec [a] Elapsed: 100 msec """ + number_running: int = 0 # The number of Timers currently running - def __init__(self, - name: str = None, - verbose: bool = True, - ): + def __init__( + self, + name: str = None, + verbose: bool = True, + ): self.name: str = name self.verbose: bool = verbose self.runtime: float = np.nan @@ -46,8 +48,8 @@ def __init__(self, def __repr__(self): return f"{self.__class__.__name__}: " + ( "Running..." - if np.isnan(self.runtime) else - f"Finished, elapsed: ({self._format_time(self.runtime)})" + if np.isnan(self.runtime) + else f"Finished, elapsed: ({self._format_time(self.runtime)})" ) @staticmethod @@ -85,10 +87,7 @@ def toc(self) -> float: self.t_end = time.perf_counter_ns() self.__class__.number_running -= 1 self.runtime = (self.t_end - self.t_start) / 1e9 - self._print( - f"Elapsed: {self._format_time(self.runtime)}", - number_running_mod=1 - ) + self._print(f"Elapsed: {self._format_time(self.runtime)}", number_running_mod=1) return self.runtime def __exit__(self, type, value, traceback): @@ -96,11 +95,11 @@ def __exit__(self, type, value, traceback): def time_function( - func: Callable, - repeats: int = None, - desired_runtime: float = None, - runtime_reduction=np.min, - warmup: bool = True, + func: Callable, + repeats: int = None, + desired_runtime: float = None, + runtime_reduction=np.min, + warmup: bool = True, ) -> Tuple[float, Any]: """ Runs a given callable and tells you how long it took to run, in seconds. Also returns the result of the function @@ -135,10 +134,7 @@ def time_function( def time_function_once(): start_ns = time.perf_counter_ns() result = func() - return ( - (time.perf_counter_ns() - start_ns) / 1e9, - result - ) + return ((time.perf_counter_ns() - start_ns) / 1e9, result) runtimes = [] @@ -162,20 +158,16 @@ def time_function_once(): runtimes.append(t) if len(runtimes) == 0: - runtimes = [0.] + runtimes = [0.0] - return ( - runtime_reduction(runtimes), - result - ) + return (runtime_reduction(runtimes), result) -if __name__ == '__main__': +if __name__ == "__main__": def f(): time.sleep(0.1) - print(time_function(f, desired_runtime=1)) with Timer("a") as a: diff --git a/aerosandbox/tools/inspect_tools.py b/aerosandbox/tools/inspect_tools.py index 86514761d..47f936721 100644 --- a/aerosandbox/tools/inspect_tools.py +++ b/aerosandbox/tools/inspect_tools.py @@ -6,7 +6,7 @@ def get_caller_source_location( - stacklevel: int = 1, + stacklevel: int = 1, ) -> (Path, int, str): """ Gets the file location where this function itself (`get_caller_source_location()`) is called. @@ -67,10 +67,10 @@ def get_caller_source_location( def get_source_code_from_location( - filename: Union[Path, str], - lineno: int, - code_context: str = None, - strip_lines: bool = False + filename: Union[Path, str], + lineno: int, + code_context: str = None, + strip_lines: bool = False, ) -> str: """ Gets the source code of the single statement that begins at the file location specified. @@ -119,7 +119,9 @@ def get_source_code_from_location( ### Read the source lines of code surrounding the call try: with open(filename, "r") as f: # Read the file containing the call - for _ in range(lineno - 1): # Skip the first N lines of code, until you get to the call + for _ in range( + lineno - 1 + ): # Skip the first N lines of code, until you get to the call f.readline() # Unfortunately there's no way around this, since you need to find the "\n" encodings in the file parenthesis_level = 0 # Track the number of "(" and ")" characters, so you know when the function call is complete @@ -149,10 +151,12 @@ def add_line() -> None: add_line() except OSError as e: raise FileNotFoundError( - "\n".join([ - "Couldn't retrieve source code at this stack level, because the source code file couldn't be opened for some reason.", - "One common possible reason is that you're referring to an IPython console with a multi-line statement." - ]) + "\n".join( + [ + "Couldn't retrieve source code at this stack level, because the source code file couldn't be opened for some reason.", + "One common possible reason is that you're referring to an IPython console with a multi-line statement.", + ] + ) ) source = "".join(source_lines) @@ -160,10 +164,7 @@ def add_line() -> None: return source -def get_caller_source_code( - stacklevel: int = 1, - strip_lines: bool = False -) -> str: +def get_caller_source_code(stacklevel: int = 1, strip_lines: bool = False) -> str: """ Gets the source code of wherever this function is called. @@ -190,7 +191,7 @@ def get_caller_source_code( filename=filename, lineno=lineno, code_context=code_context, - strip_lines=strip_lines + strip_lines=strip_lines, ) @@ -270,7 +271,9 @@ def get_function_argument_names_from_source_code(source_code: str) -> List[str]: while parenthesis_level != 0: i += 1 if i >= len(source_code_rhs): - raise ValueError("Couldn't match all parentheses, so this doesn't look like valid code!") + raise ValueError( + "Couldn't match all parentheses, so this doesn't look like valid code!" + ) char = source_code_rhs[i] if char == "(": @@ -296,18 +299,16 @@ def get_function_argument_names_from_source_code(source_code: str) -> List[str]: def clean(s: str) -> str: return s.strip() - arg_names = [ - clean(arg) for arg in arg_names - ] + arg_names = [clean(arg) for arg in arg_names] return arg_names def codegen( - x: Any, - indent_str: str = " ", - _required_imports: Optional[Set[str]] = None, - _recursion_depth: int = 0, + x: Any, + indent_str: str = " ", + _required_imports: Optional[Set[str]] = None, + _recursion_depth: int = 0, ) -> Tuple[str, Set[str]]: """ Attempts to generate a string of Python code that, when evaluated, would produce the same value as the input. @@ -352,8 +353,8 @@ def codegen( >>> codegen(dict(my_int=4, my_array=np.array([1, 2, 3]))) ('{ - 'my_int': 4, - 'my_array': np.array([1, 2, 3]), + 'my_int': 4, + 'my_array': np.array([1, 2, 3]), }', {'import numpy as np'}) """ @@ -362,21 +363,29 @@ def codegen( _required_imports = set() import_aliases = { - "aerosandbox" : "asb", + "aerosandbox": "asb", "aerosandbox.numpy": "np", - "numpy" : "np", + "numpy": "np", } indent = indent_str * _recursion_depth next_indent = indent_str * (_recursion_depth + 1) - if isinstance(x, ( - bool, str, - int, float, complex, + if isinstance( + x, + ( + bool, + str, + int, + float, + complex, range, type(None), - bytes, bytearray, memoryview - )): + bytes, + bytearray, + memoryview, + ), + ): code = repr(x) elif isinstance(x, list): @@ -386,7 +395,9 @@ def codegen( lines = [] lines.append("[") for xi in x: - item_code, item_required_imports = codegen(xi, _recursion_depth=_recursion_depth + 1) + item_code, item_required_imports = codegen( + xi, _recursion_depth=_recursion_depth + 1 + ) _required_imports.update(item_required_imports) @@ -401,7 +412,9 @@ def codegen( lines = [] lines.append("(") for xi in x: - item_code, item_required_imports = codegen(xi, _recursion_depth=_recursion_depth + 1) + item_code, item_required_imports = codegen( + xi, _recursion_depth=_recursion_depth + 1 + ) _required_imports.update(item_required_imports) @@ -416,7 +429,9 @@ def codegen( lines = [] lines.append("{") for xi in x: - item_code, item_required_imports = codegen(xi, _recursion_depth=_recursion_depth + 1) + item_code, item_required_imports = codegen( + xi, _recursion_depth=_recursion_depth + 1 + ) _required_imports.update(item_required_imports) @@ -431,8 +446,12 @@ def codegen( lines = [] lines.append("{") for k, v in x.items(): - k_code, k_required_imports = codegen(k, _recursion_depth=_recursion_depth + 1) - v_code, v_required_imports = codegen(v, _recursion_depth=_recursion_depth + 1) + k_code, k_required_imports = codegen( + k, _recursion_depth=_recursion_depth + 1 + ) + v_code, v_required_imports = codegen( + v, _recursion_depth=_recursion_depth + 1 + ) _required_imports.update(k_required_imports) _required_imports.update(v_required_imports) @@ -455,9 +474,7 @@ def codegen( # elif package_name in import_aliases: # pre_string = import_aliases[package_name] + "." else: - _required_imports.add( - f"from {module_name} import {x.__class__.__name__}" - ) + _required_imports.add(f"from {module_name} import {x.__class__.__name__}") lines = [] lines.append(x.__class__.__name__ + "(") @@ -468,7 +485,9 @@ def codegen( if inspect.ismethod(arg_value) or inspect.isfunction(arg_value): continue - arg_code, arg_required_imports = codegen(arg_value, _recursion_depth=_recursion_depth + 1) + arg_code, arg_required_imports = codegen( + arg_value, _recursion_depth=_recursion_depth + 1 + ) _required_imports.update(arg_required_imports) @@ -489,12 +508,12 @@ def codegen( # return code, _required_imports -if __name__ == '__main__': +if __name__ == "__main__": + def dashes(): """A quick macro for drawing some dashes, to make the terminal output clearer to distinguish.""" print("\n" + "-" * 50 + "\n") - dashes() print("Caller location:\n", get_caller_source_location(stacklevel=1)) @@ -505,14 +524,8 @@ def dashes(): dashes() - def my_func(): - print( - get_caller_source_code( - stacklevel=2 - ) - ) - + print(get_caller_source_code(stacklevel=2)) print("Caller source code of a function call:") @@ -522,9 +535,7 @@ def my_func(): print("Arguments of f(a, b):") - print( - get_function_argument_names_from_source_code("f(a, b)") - ) + print(get_function_argument_names_from_source_code("f(a, b)")) location = get_caller_source_location() @@ -532,13 +543,11 @@ def my_func(): print("Codegen test:") - def pc(x): code, imports = codegen(x) print("\n".join(sorted(imports))) print(code + "\n" + "-" * 50) - pc(1) pc([1, 2, 3]) pc([1, 2, [3, 4, 5], 6]) diff --git a/aerosandbox/tools/pretty_plots/__init__.py b/aerosandbox/tools/pretty_plots/__init__.py index c790c44ac..61eab326d 100644 --- a/aerosandbox/tools/pretty_plots/__init__.py +++ b/aerosandbox/tools/pretty_plots/__init__.py @@ -19,4 +19,4 @@ mpl.rcParams["figure.dpi"] = 200 mpl.rcParams["axes.formatter.useoffset"] = False -mpl.rcParams["contour.negative_linestyle"] = 'solid' +mpl.rcParams["contour.negative_linestyle"] = "solid" diff --git a/aerosandbox/tools/pretty_plots/annotation.py b/aerosandbox/tools/pretty_plots/annotation.py index a6b124ba3..6f2549359 100644 --- a/aerosandbox/tools/pretty_plots/annotation.py +++ b/aerosandbox/tools/pretty_plots/annotation.py @@ -1,25 +1,24 @@ import matplotlib.pyplot as plt import matplotlib.transforms as transforms + def hline( - y, - linestyle="--", - color="k", - text: str = None, - text_xloc=0.5, - text_ha="center", - text_va="bottom", - text_kwargs=None, - **kwargs + y, + linestyle="--", + color="k", + text: str = None, + text_xloc=0.5, + text_ha="center", + text_va="bottom", + text_kwargs=None, + **kwargs ): # TODO docs if text_kwargs is None: text_kwargs = {} ax = plt.gca() plt.axhline(y=y, ls=linestyle, color=color, **kwargs) if text is not None: - trans = transforms.blended_transform_factory( - ax.transAxes, ax.transData - ) + trans = transforms.blended_transform_factory(ax.transAxes, ax.transData) plt.annotate( text=text, xy=(text_xloc, y), @@ -34,24 +33,22 @@ def hline( def vline( - x, - linestyle="--", - color="k", - text: str = None, - text_yloc=0.5, - text_ha="right", - text_va="center", - text_kwargs=None, - **kwargs + x, + linestyle="--", + color="k", + text: str = None, + text_yloc=0.5, + text_ha="right", + text_va="center", + text_kwargs=None, + **kwargs ): # TODO docs if text_kwargs is None: text_kwargs = {} ax = plt.gca() plt.axvline(x=x, ls=linestyle, color=color, **kwargs) if text is not None: - trans = transforms.blended_transform_factory( - ax.transData, ax.transAxes - ) + trans = transforms.blended_transform_factory(ax.transData, ax.transAxes) plt.annotate( text=text, xy=(x, text_yloc), @@ -63,4 +60,4 @@ def vline( color=color, rotation=90, **text_kwargs - ) \ No newline at end of file + ) diff --git a/aerosandbox/tools/pretty_plots/colors.py b/aerosandbox/tools/pretty_plots/colors.py index 9d784db5b..fe250f820 100644 --- a/aerosandbox/tools/pretty_plots/colors.py +++ b/aerosandbox/tools/pretty_plots/colors.py @@ -28,15 +28,15 @@ "#73959C", "#7692B1", "#868CBC", - ] + ], } def get_discrete_colors_from_colormap( - cmap: str = "rainbow", - N: int = 8, - lower_bound: float = 0, - upper_bound: float = 1, + cmap: str = "rainbow", + N: int = 8, + lower_bound: float = 0, + upper_bound: float = 1, ): """ Returns uniformly-spaced discrete color samples from a (continuous) colormap. @@ -77,4 +77,6 @@ def get_last_line_color(): line = lines[-1] return line._color except IndexError: - return palettes["categorical"][0] # TODO make this just the first color in the current palette + return palettes["categorical"][ + 0 + ] # TODO make this just the first color in the current palette diff --git a/aerosandbox/tools/pretty_plots/formatting.py b/aerosandbox/tools/pretty_plots/formatting.py index 47fd0381f..2af5735a2 100644 --- a/aerosandbox/tools/pretty_plots/formatting.py +++ b/aerosandbox/tools/pretty_plots/formatting.py @@ -10,23 +10,23 @@ def show_plot( - title: str = None, - xlabel: str = None, - ylabel: str = None, - zlabel: str = None, - dpi: float = None, - savefig: Union[str, List[str]] = None, - savefig_transparent: bool = False, - tight_layout: bool = True, - tight_layout_pad: float = 0.25, - legend: bool = None, - legend_inline: bool = False, - legend_frame: bool = True, - pretty_grids: bool = True, - set_ticks: bool = True, - rotate_axis_labels: bool = True, - rotate_axis_labels_linewidth: int = 14, - show: bool = True, + title: str = None, + xlabel: str = None, + ylabel: str = None, + zlabel: str = None, + dpi: float = None, + savefig: Union[str, List[str]] = None, + savefig_transparent: bool = False, + tight_layout: bool = True, + tight_layout_pad: float = 0.25, + legend: bool = None, + legend_inline: bool = False, + legend_frame: bool = True, + pretty_grids: bool = True, + set_ticks: bool = True, + rotate_axis_labels: bool = True, + rotate_axis_labels_linewidth: int = 14, + show: bool = True, ): """ Makes a matplotlib Figure (and all its constituent Axes) look "nice", then displays it. @@ -109,19 +109,21 @@ def show_plot( if pretty_grids: for ax in axes: - if not ax.get_label() == '': + if not ax.get_label() == "": if not ax_is_3d(ax): if any(line.get_visible() for line in ax.get_xgridlines()): - ax.grid(True, 'major', axis='x', linewidth=1.6) - ax.grid(True, 'minor', axis='x', linewidth=0.7) + ax.grid(True, "major", axis="x", linewidth=1.6) + ax.grid(True, "minor", axis="x", linewidth=0.7) if any(line.get_visible() for line in ax.get_ygridlines()): - ax.grid(True, 'major', axis='y', linewidth=1.6) - ax.grid(True, 'minor', axis='y', linewidth=0.7) + ax.grid(True, "major", axis="y", linewidth=1.6) + ax.grid(True, "minor", axis="y", linewidth=0.7) else: for i_ax in [ax.xaxis, ax.yaxis, ax.zaxis]: - i_ax._axinfo["grid"].update(dict( - linewidth=0.7, - )) + i_ax._axinfo["grid"].update( + dict( + linewidth=0.7, + ) + ) i_ax.set_tick_params(which="minor", color=(0, 0, 0, 0)) if set_ticks: @@ -145,7 +147,7 @@ def show_plot( def linlogfmt(x, pos, ticks=None, default="", base=10): if ticks is None: - ticks = [1.] + ticks = [1.0] if x < 0: sign_string = "-" @@ -154,7 +156,7 @@ def linlogfmt(x, pos, ticks=None, default="", base=10): sign_string = "" exponent = np.floor(np.log(x) / np.log(base)) - coeff = x / base ** exponent + coeff = x / base**exponent ### Fix any floating-point error during the floor function if coeff < 1: @@ -166,14 +168,11 @@ def linlogfmt(x, pos, ticks=None, default="", base=10): for tick in ticks: if np.isclose(coeff, tick): - return r"$\mathdefault{%s%g}$" % ( - sign_string, - x - ) + return r"$\mathdefault{%s%g}$" % (sign_string, x) return default - def logfmt(x, pos, ticks=[1.], default="", base=10): + def logfmt(x, pos, ticks=[1.0], default="", base=10): if x < 0: sign_string = "-" x = -x @@ -181,7 +180,7 @@ def logfmt(x, pos, ticks=[1.], default="", base=10): sign_string = "" exponent = np.floor(np.log(x) / np.log(base)) - coeff = x / base ** exponent + coeff = x / base**exponent ### Fix any floating-point error during the floor function if coeff < 1: @@ -197,7 +196,7 @@ def logfmt(x, pos, ticks=[1.], default="", base=10): return r"$\mathdefault{%s%s^{%d}}$" % ( sign_string, base, - exponent + exponent, ) else: if np.isclose(coeff, tick): @@ -206,7 +205,7 @@ def logfmt(x, pos, ticks=[1.], default="", base=10): sign_string, coeff, base, - exponent + exponent, ) return default @@ -241,9 +240,13 @@ def __call__(self): if self.ndivs is None: - majorstep_no_exponent = 10 ** (np.log10(majorstep) % 1) + majorstep_no_exponent = 10 ** ( + np.log10(majorstep) % 1 + ) - if np.isclose(majorstep_no_exponent, [1.0, 2.5, 5.0, 10.0]).any(): + if np.isclose( + majorstep_no_exponent, [1.0, 2.5, 5.0, 10.0] + ).any(): ndivs = 5 else: ndivs = 4 @@ -266,7 +269,7 @@ def __call__(self): min_loc = LogAutoMinorLocator() min_fmt = mt.NullFormatter() - elif ratio < 10 ** 1.5: + elif ratio < 10**1.5: maj_loc = mt.LogLocator(subs=np.arange(1, 10)) # if i_ax.axis_name == "x": # default = r"$^{^|}$" @@ -275,22 +278,22 @@ def __call__(self): # else: # default = "" - maj_fmt = mt.FuncFormatter( - partial(linlogfmt, ticks=[1, 2, 5]) + maj_fmt = mt.FuncFormatter(partial(linlogfmt, ticks=[1, 2, 5])) + min_loc = mt.LogLocator( + numticks=999, subs=np.arange(10, 100) / 10 ) - min_loc = mt.LogLocator(numticks=999, subs=np.arange(10, 100) / 10) min_fmt = mt.NullFormatter() - elif ratio < 10 ** 2.5: + elif ratio < 10**2.5: maj_loc = mt.LogLocator() maj_fmt = mt.FuncFormatter(partial(logfmt, ticks=[1])) min_loc = mt.LogLocator(numticks=999, subs=np.arange(1, 10)) min_fmt = mt.FuncFormatter(partial(logfmt, ticks=[2, 5])) - elif ratio < 10 ** 8: + elif ratio < 10**8: maj_loc = mt.LogLocator() maj_fmt = mt.FuncFormatter(partial(logfmt, ticks=[1])) min_loc = mt.LogLocator(numticks=999, subs=np.arange(1, 10)) min_fmt = mt.FuncFormatter(partial(logfmt, ticks=[1])) - elif ratio < 10 ** 16: + elif ratio < 10**16: maj_loc = mt.LogLocator() maj_fmt = mt.LogFormatterSciNotation() min_loc = mt.LogLocator(numticks=999, subs=np.arange(1, 10)) @@ -300,7 +303,7 @@ def __call__(self): elif i_ax.get_scale() == "linear": maj_loc = mt.MaxNLocator( - nbins='auto', + nbins="auto", steps=[1, 2, 5, 10], min_n_ticks=3, ) @@ -310,7 +313,9 @@ def __call__(self): else: # For any other scale, just use the default tick locations continue - if len(i_ax.get_major_ticks()) != 0: # Unless the user has manually set the ticks to be empty + if ( + len(i_ax.get_major_ticks()) != 0 + ): # Unless the user has manually set the ticks to be empty if maj_loc is not None: i_ax.set_major_locator(maj_loc) if min_loc is not None: @@ -335,33 +340,36 @@ def __call__(self): # Make axis labels if needed if xlabel is not None: for ax in axes: - if not ax.get_label() == '': + if not ax.get_label() == "": ax.set_xlabel(xlabel) if ylabel is not None: for ax in axes: - if not ax.get_label() == '': + if not ax.get_label() == "": ax.set_ylabel(ylabel) if zlabel is not None: if len(axes_with_3D) == 0: import warnings + warnings.warn( "You specified a `zlabel`, but there are no 3D axes in this figure. Ignoring `zlabel`.", - stacklevel=2 + stacklevel=2, ) for ax in axes_with_3D: - if not ax.get_label() == '': + if not ax.get_label() == "": ax.set_zlabel(zlabel) # Rotate axis labels if needed if rotate_axis_labels: for ax in axes: if not ax_is_3d(ax): - if not ax.get_label() == '': + if not ax.get_label() == "": ylabel = ax.get_ylabel() - if (rotate_axis_labels_linewidth is not None) and ("\n" not in ylabel): + if (rotate_axis_labels_linewidth is not None) and ( + "\n" not in ylabel + ): ylabel = sf.wrap_text_ignoring_mathtext( ylabel, width=rotate_axis_labels_linewidth, @@ -408,12 +416,12 @@ def __call__(self): def set_ticks( - x_major: Union[float, int] = None, - x_minor: Union[float, int] = None, - y_major: Union[float, int] = None, - y_minor: Union[float, int] = None, - z_major: Union[float, int] = None, - z_minor: Union[float, int] = None, + x_major: Union[float, int] = None, + x_minor: Union[float, int] = None, + y_major: Union[float, int] = None, + y_minor: Union[float, int] = None, + z_major: Union[float, int] = None, + z_minor: Union[float, int] = None, ): ax = plt.gca() if x_major is not None: @@ -440,7 +448,7 @@ def equal() -> None: ax = plt.gca() if not ax_is_3d(ax): - ax.set_aspect("equal", adjustable='box') + ax.set_aspect("equal", adjustable="box") else: ax.set_box_aspect((1, 1, 1)) diff --git a/aerosandbox/tools/pretty_plots/labellines/core.py b/aerosandbox/tools/pretty_plots/labellines/core.py index 29245a90b..b9bf27ef5 100644 --- a/aerosandbox/tools/pretty_plots/labellines/core.py +++ b/aerosandbox/tools/pretty_plots/labellines/core.py @@ -6,21 +6,25 @@ from matplotlib.container import ErrorbarContainer from matplotlib.dates import DateConverter, num2date -from aerosandbox.tools.pretty_plots.labellines.utils import ensure_float, maximum_bipartite_matching, always_iterable +from aerosandbox.tools.pretty_plots.labellines.utils import ( + ensure_float, + maximum_bipartite_matching, + always_iterable, +) # Label line with line2D label data def labelLine( - line, - x, - label=None, - align=True, - drop_label=False, - yoffset=0, - yoffset_logspace=False, - outline_color="auto", - outline_width=8, - **kwargs, + line, + x, + label=None, + align=True, + drop_label=False, + yoffset=0, + yoffset_logspace=False, + outline_color="auto", + outline_width=8, + **kwargs, ): """Label a single matplotlib line at position x @@ -83,7 +87,7 @@ def labelLine( if yoffset_logspace: y = ya + (yb - ya) * fraction - y *= 10 ** yoffset + y *= 10**yoffset else: y = ya + (yb - ya) * fraction + yoffset @@ -107,10 +111,9 @@ def labelLine( if align: if ax.get_aspect() == "auto": # Compute the slope and label rotation - screen_dx, screen_dy = ( - ax.transData.transform((xfb, yb)) - - ax.transData.transform((xfa, ya)) - ) + screen_dx, screen_dy = ax.transData.transform( + (xfb, yb) + ) - ax.transData.transform((xfa, ya)) elif isinstance(ax.get_aspect(), (float, int)): screen_dx = xfb - xfa screen_dy = (yb - ya) * ax.get_aspect() @@ -152,15 +155,15 @@ def labelLine( def labelLines( - lines, - align=True, - xvals=None, - drop_label=False, - shrink_factor=0.05, - yoffsets=0, - outline_color="auto", - outline_width=5, - **kwargs, + lines, + align=True, + xvals=None, + drop_label=False, + shrink_factor=0.05, + yoffsets=0, + outline_color="auto", + outline_width=5, + **kwargs, ): """Label all lines with their respective legends. @@ -214,8 +217,8 @@ def labelLines( xscale = ax.get_xscale() if xscale == "log": xvals = np.logspace(np.log10(xmin), np.log10(xmax), len(all_lines) + 2)[ - 1:-1 - ] + 1:-1 + ] else: xvals = np.linspace(xmin, xmax, len(all_lines) + 2)[1:-1] @@ -292,13 +295,13 @@ def labelLines( return txts -if __name__ == '__main__': +if __name__ == "__main__": import matplotlib.pyplot as plt fig, ax = plt.subplots() x_plt = np.linspace(0, 2, 1000) y_plt = 0.1 * np.sin(2 * np.pi * x_plt) - line, = plt.plot(x_plt, y_plt, label="hi") + (line,) = plt.plot(x_plt, y_plt, label="hi") # plt.axis("equal") # print(ax.get_aspect()) @@ -313,4 +316,4 @@ def labelLines( yoffset = 0 yoffset_logspace = False outline_color = "auto" - outline_width = 8 \ No newline at end of file + outline_width = 8 diff --git a/aerosandbox/tools/pretty_plots/labellines/utils.py b/aerosandbox/tools/pretty_plots/labellines/utils.py index 5235e1bf9..f31c91fc0 100644 --- a/aerosandbox/tools/pretty_plots/labellines/utils.py +++ b/aerosandbox/tools/pretty_plots/labellines/utils.py @@ -13,11 +13,11 @@ def ensure_float(value): # https://stackoverflow.com/q/23063362/4549682 # somewhere, the datetime64 with timezone is getting converted to 'O' dtype if ( - isinstance(value, datetime) - or isinstance(value, np.datetime64) - or np.issubdtype(value.dtype, np.datetime64) - or str(value.dtype).startswith("datetime64") - or value.dtype == "O" + isinstance(value, datetime) + or isinstance(value, np.datetime64) + or np.issubdtype(value.dtype, np.datetime64) + or str(value.dtype).startswith("datetime64") + or value.dtype == "O" ): return date2num(value) else: # another numpy dtype like float64 diff --git a/aerosandbox/tools/pretty_plots/plots/contour.py b/aerosandbox/tools/pretty_plots/plots/contour.py index a04da7597..eac633712 100644 --- a/aerosandbox/tools/pretty_plots/plots/contour.py +++ b/aerosandbox/tools/pretty_plots/plots/contour.py @@ -7,31 +7,31 @@ def contour( - *args, - levels: Union[int, List, np.ndarray] = 31, - colorbar: bool = True, - linelabels: bool = True, - cmap=None, - alpha: float = 0.7, - extend: str = "neither", - linecolor="k", - linewidths: float = 0.5, - extendrect: bool = True, - linelabels_format: Union[str, Callable[[float], str]] = eng_string, - linelabels_fontsize: float = 8, - max_side_length_nondim: float = np.inf, - colorbar_label: str = None, - x_log_scale: bool = False, - y_log_scale: bool = False, - z_log_scale: bool = False, - mask: np.ndarray = None, - drop_nans: bool = None, - # smooth: Union[bool, int] = False, # TODO implement - contour_kwargs: Dict = None, - contourf_kwargs: Dict = None, - colorbar_kwargs: Dict = None, - linelabels_kwargs: Dict = None, - **kwargs, + *args, + levels: Union[int, List, np.ndarray] = 31, + colorbar: bool = True, + linelabels: bool = True, + cmap=None, + alpha: float = 0.7, + extend: str = "neither", + linecolor="k", + linewidths: float = 0.5, + extendrect: bool = True, + linelabels_format: Union[str, Callable[[float], str]] = eng_string, + linelabels_fontsize: float = 8, + max_side_length_nondim: float = np.inf, + colorbar_label: str = None, + x_log_scale: bool = False, + y_log_scale: bool = False, + z_log_scale: bool = False, + mask: np.ndarray = None, + drop_nans: bool = None, + # smooth: Union[bool, int] = False, # TODO implement + 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. @@ -87,11 +87,12 @@ def contour( Returns: A tuple of (contour, contourf, colorbar) objects. """ - bad_signature_error = ValueError("Call signature should be one of:\n" - " * `contour(Z, **kwargs)`\n" - " * `contour(X, Y, Z, **kwargs)`\n" - " * `contour(X, Y, Z, levels, **kwargs)`" - ) + bad_signature_error = ValueError( + "Call signature should be one of:\n" + " * `contour(Z, **kwargs)`\n" + " * `contour(X, Y, Z, **kwargs)`\n" + " * `contour(X, Y, Z, levels, **kwargs)`" + ) ### Parse *args if len(args) == 1: @@ -109,26 +110,24 @@ def contour( if Y is None: Y = np.arange(Z.shape[0]) - is_gridded = not ( # Determine if the data is gridded or not (i.e., contour vs. tricontour) - X.ndim == 1 and - Y.ndim == 1 and - Z.ndim == 1 + is_gridded = ( + not ( # Determine if the data is gridded or not (i.e., contour vs. tricontour) + X.ndim == 1 and Y.ndim == 1 and Z.ndim == 1 + ) ) ### Check inputs for sanity for k, v in dict( - X=X, - Y=Y, - Z=Z, + X=X, + Y=Y, + Z=Z, ).items(): if np.all(np.isnan(v)): - raise ValueError( - f"All values of '{k}' are NaN!" - ) + raise ValueError(f"All values of '{k}' are NaN!") ### Set defaults if cmap is None: - cmap = mpl.colormaps.get_cmap('viridis') + cmap = mpl.colormaps.get_cmap("viridis") if contour_kwargs is None: contour_kwargs = {} if contourf_kwargs is None: @@ -169,41 +168,31 @@ def contour( ) shared_kwargs = { - "norm" : mpl.colors.LogNorm(), + "norm": mpl.colors.LogNorm(), "locator": locator, - **shared_kwargs + **shared_kwargs, } - colorbar_kwargs = { - "norm": mpl.colors.LogNorm(), - **colorbar_kwargs - } + colorbar_kwargs = {"norm": mpl.colors.LogNorm(), **colorbar_kwargs} if colorbar_label is not None: colorbar_kwargs["label"] = colorbar_label contour_kwargs = { - "colors" : linecolor, + "colors": linecolor, "linewidths": linewidths, **shared_kwargs, - **contour_kwargs - } - contourf_kwargs = { - "cmap": cmap, - **shared_kwargs, - **contourf_kwargs + **contour_kwargs, } + contourf_kwargs = {"cmap": cmap, **shared_kwargs, **contourf_kwargs} - colorbar_kwargs = { - "extendrect": extendrect, - **colorbar_kwargs - } + colorbar_kwargs = {"extendrect": extendrect, **colorbar_kwargs} linelabels_kwargs = { - "inline" : 1, + "inline": 1, "fontsize": linelabels_fontsize, - "fmt" : linelabels_format, - **linelabels_kwargs + "fmt": linelabels_format, + **linelabels_kwargs, } if drop_nans is None: @@ -222,9 +211,7 @@ def contour( if drop_nans: nanmask = np.logical_not( - np.logical_or.reduce( - [np.isnan(X), np.isnan(Y), np.isnan(Z)] - ) + np.logical_or.reduce([np.isnan(X), np.isnan(Y), np.isnan(Z)]) ) X = X[nanmask] @@ -258,30 +245,20 @@ def contour( ### Filter out extrapolation that's too large # See also: https://stackoverflow.com/questions/42426095/matplotlib-contour-contourf-of-concave-non-gridded-data if x_log_scale: - X_nondim = ( - np.log(X[t]) - np.roll(np.log(X[t]), 1, axis=1) - ) / (np.nanmax(np.log(X)) - np.nanmin(np.log(X))) + X_nondim = (np.log(X[t]) - np.roll(np.log(X[t]), 1, axis=1)) / ( + np.nanmax(np.log(X)) - np.nanmin(np.log(X)) + ) else: - X_nondim = ( - X[t] - np.roll(X[t], 1, axis=1) - ) / (np.nanmax(X) - np.nanmin(X)) + X_nondim = (X[t] - np.roll(X[t], 1, axis=1)) / (np.nanmax(X) - np.nanmin(X)) if y_log_scale: - Y_nondim = ( - np.log(Y[t]) - np.roll(np.log(Y[t]), 1, axis=1) - ) / (np.nanmax(np.log(Y)) - np.nanmin(np.log(Y))) + Y_nondim = (np.log(Y[t]) - np.roll(np.log(Y[t]), 1, axis=1)) / ( + np.nanmax(np.log(Y)) - np.nanmin(np.log(Y)) + ) else: - Y_nondim = ( - Y[t] - np.roll(Y[t], 1, axis=1) - ) / (np.nanmax(Y) - np.nanmin(Y)) - - side_length_nondim = np.max( - np.sqrt( - X_nondim ** 2 + - Y_nondim ** 2 - ), - axis=1 - ) + Y_nondim = (Y[t] - np.roll(Y[t], 1, axis=1)) / (np.nanmax(Y) - np.nanmin(Y)) + + side_length_nondim = np.max(np.sqrt(X_nondim**2 + Y_nondim**2), axis=1) if np.all(side_length_nondim > max_side_length_nondim): raise ValueError( @@ -308,7 +285,7 @@ def contour( norm=contf.norm, cmap=contf.cmap, ), - **colorbar_kwargs + **colorbar_kwargs, ) if z_log_scale: @@ -324,7 +301,10 @@ def contour( cbar_is_horizontal = True else: import warnings - warnings.warn("Somehow the colorbar has both x and y ticks, which should not occur. Attempting to reformat y-ticks...") + + warnings.warn( + "Somehow the colorbar has both x and y ticks, which should not occur. Attempting to reformat y-ticks..." + ) cbar_is_horizontal = False if cbar_is_horizontal: @@ -336,21 +316,31 @@ def contour( if cbar_is_horizontal: pass else: - if Z_ratio >= 10 ** 2.05: + if Z_ratio >= 10**2.05: cbar_ax.set_major_locator(mpl.ticker.LogLocator()) - cbar_ax.set_minor_locator(mpl.ticker.LogLocator(subs=np.arange(1, 10))) + cbar_ax.set_minor_locator( + mpl.ticker.LogLocator(subs=np.arange(1, 10)) + ) cbar_ax.set_major_formatter(mpl.ticker.LogFormatterSciNotation()) cbar_ax.set_minor_formatter(mpl.ticker.NullFormatter()) - elif Z_ratio >= 10 ** 1.5: + elif Z_ratio >= 10**1.5: cbar_ax.set_major_locator(mpl.ticker.LogLocator()) - cbar_ax.set_minor_locator(mpl.ticker.LogLocator(subs=np.arange(1, 10))) + cbar_ax.set_minor_locator( + mpl.ticker.LogLocator(subs=np.arange(1, 10)) + ) cbar_ax.set_major_formatter(mpl.ticker.LogFormatterSciNotation()) - cbar_ax.set_minor_formatter(mpl.ticker.LogFormatterSciNotation( - minor_thresholds=(np.inf, np.inf) - )) + cbar_ax.set_minor_formatter( + mpl.ticker.LogFormatterSciNotation( + minor_thresholds=(np.inf, np.inf) + ) + ) else: - cbar_ax.set_major_locator(mpl.ticker.LogLocator(subs=np.arange(1, 10))) - cbar_ax.set_minor_locator(mpl.ticker.LogLocator(subs=np.arange(10, 100) / 10)) + cbar_ax.set_major_locator( + mpl.ticker.LogLocator(subs=np.arange(1, 10)) + ) + cbar_ax.set_minor_locator( + mpl.ticker.LogLocator(subs=np.arange(10, 100) / 10) + ) cbar_ax.set_major_formatter(mpl.ticker.ScalarFormatter()) cbar_ax.set_minor_formatter(mpl.ticker.NullFormatter()) @@ -363,7 +353,7 @@ def contour( return cont, contf, cbar -if __name__ == '__main__': +if __name__ == "__main__": import matplotlib.pyplot as plt import aerosandbox.tools.pretty_plots as p @@ -373,11 +363,7 @@ def contour( Z_ratio = 1 - Z = 10 ** ( - Z_ratio / 2 * np.cos( - 2 * np.pi * (X ** 4 + Y ** 4) - ) - ) + Z = 10 ** (Z_ratio / 2 * np.cos(2 * np.pi * (X**4 + Y**4))) # Z += 0.1 * np.random.randn(*Z.shape) @@ -394,11 +380,7 @@ def contour( z_log_scale=True, cmap=cmap, levels=20, - colorbar_label="Colorbar label" + colorbar_label="Colorbar label", ) # plt.clim(0.1, 10) - p.show_plot( - "Title", - "X label", - "Y label" - ) + p.show_plot("Title", "X label", "Y label") diff --git a/aerosandbox/tools/pretty_plots/plots/pie.py b/aerosandbox/tools/pretty_plots/plots/pie.py index 37322d8bc..b82d681b2 100644 --- a/aerosandbox/tools/pretty_plots/plots/pie.py +++ b/aerosandbox/tools/pretty_plots/plots/pie.py @@ -8,53 +8,56 @@ def pie( - values: Union[np.ndarray, List[float]], - names: List[str], - colors: Union[np.ndarray, List[str]] = None, - label_format: Callable[[str, float, float], str] = lambda name, value, percentage: name, - sort_by: Union[np.ndarray, List[float], str, None] = None, - startangle: float = 0., - center_text: str = None, - x_labels: float = 1.25, - y_max_labels: float = 1.3, - arm_length=20, - arm_radius=5, + values: Union[np.ndarray, List[float]], + names: List[str], + colors: Union[np.ndarray, List[str]] = None, + label_format: Callable[ + [str, float, float], str + ] = lambda name, value, percentage: name, + sort_by: Union[np.ndarray, List[float], str, None] = None, + startangle: float = 0.0, + center_text: str = None, + x_labels: float = 1.25, + y_max_labels: float = 1.3, + arm_length=20, + arm_radius=5, ): # TODO docs ax = plt.gca() n_wedges = len(values) - ### Check inputs if not len(names) == n_wedges: raise ValueError() # TODO ### Sort by - sort_by_error= ValueError('''Argument `sort_by` must be one of:\n + sort_by_error = ValueError( + """Argument `sort_by` must be one of:\n * a string of "values", "names", or "colors" * an array of numbers corresponding to each pie slice, which will then be used for sorting - ''') + """ + ) if sort_by is None: sort_by = np.arange(n_wedges) elif sort_by == "values": - sort_by=values - elif sort_by=="names": - sort_by=names - elif sort_by=="colors": - sort_by=colors # this might not make sense, depending on + sort_by = values + elif sort_by == "names": + sort_by = names + elif sort_by == "colors": + sort_by = colors # this might not make sense, depending on elif isinstance(sort_by, str): raise sort_by_error order = np.argsort(sort_by) names = np.array(names)[order] - values=np.array(values)[order] + values = np.array(values)[order] if colors is None: # Set default colors colors = sns.color_palette("husl", n_colors=n_wedges) else: - colors=np.array(colors)[order] + colors = np.array(colors)[order] ### Compute percentages values = np.array(values).astype(float) @@ -62,12 +65,7 @@ def pie( percentages = 100 * values / total wedges, texts = ax.pie( - x=values, - colors=colors, - startangle=startangle, - wedgeprops=dict( - width=0.3 - ) + x=values, colors=colors, startangle=startangle, wedgeprops=dict(width=0.3) ) for w in wedges: @@ -106,10 +104,7 @@ def pie( arrowstyle="-", color="k", connectionstyle=f"arc,angleA={180 if w.is_right else 0},angleB={w.theta_mid},armA={arm_length},armB={arm_length},rad={arm_radius}", - relpos=( - 0 if w.is_right else 1, - 0.5 - ) + relpos=(0 if w.is_right else 1, 0.5), ), va="center", ) @@ -125,23 +120,23 @@ def pie( ) -if __name__ == '__main__': +if __name__ == "__main__": import matplotlib.pyplot as plt import aerosandbox.tools.pretty_plots as p data = { - "USA" : 336997624, - "Mexico" : 126705138, - "Canada" : 38115012, - "Guatemala" : 17608483, - "Haiti" : 11447569, - "Cuba" : 11256372, + "USA": 336997624, + "Mexico": 126705138, + "Canada": 38115012, + "Guatemala": 17608483, + "Haiti": 11447569, + "Cuba": 11256372, "Dominican Republic": 11117873, - "Honduras" : 10278345, - "Nicaragua" : 6850540, - "El Salvador" : 6314167, - "Costa Rica" : 5153957, - "Panama" : 4351267, + "Honduras": 10278345, + "Nicaragua": 6850540, + "El Salvador": 6314167, + "Costa Rica": 5153957, + "Panama": 4351267, } data["Other"] = 597678511 - np.sum(np.array(list(data.values()))) @@ -149,13 +144,10 @@ def pie( pie( values=list(data.values()), names=list(data.keys()), - colors=[ - "navy" if s in ["USA"] else "lightgray" - for s in data.keys() - ], + colors=["navy" if s in ["USA"] else "lightgray" for s in data.keys()], label_format=lambda name, value, percentage: f"{name}, {eng_string(value)} ({percentage:.0f}%)", startangle=40, - center_text="Majority of North\nAmerica's Population\nlives in USA" + center_text="Majority of North\nAmerica's Population\nlives in USA", ) p.show_plot() diff --git a/aerosandbox/tools/pretty_plots/plots/plot_color_by_value.py b/aerosandbox/tools/pretty_plots/plots/plot_color_by_value.py index bbe473b9c..d5d688665 100644 --- a/aerosandbox/tools/pretty_plots/plots/plot_color_by_value.py +++ b/aerosandbox/tools/pretty_plots/plots/plot_color_by_value.py @@ -6,15 +6,15 @@ def plot_color_by_value( - x: np.ndarray, - y: np.ndarray, - *args, - c: np.ndarray, - cmap='turbo', - colorbar: bool = False, - colorbar_label: str = None, - clim: Tuple[float, float] = None, - **kwargs + x: np.ndarray, + y: np.ndarray, + *args, + c: np.ndarray, + cmap="turbo", + colorbar: bool = False, + colorbar_label: str = None, + clim: Tuple[float, float] = None, + **kwargs ): """ Uses same syntax as matplotlib.pyplot.plot, except that `c` is now an array-like that maps to a specific color @@ -64,36 +64,33 @@ def plot_color_by_value( lines = [] for i, ( - x1, x2, - y1, y2, - c1, c2, - ) in enumerate(zip( - x[:-1], x[1:], - y[:-1], y[1:], - c[:-1], c[1:], - )): + x1, + x2, + y1, + y2, + c1, + c2, + ) in enumerate( + zip( + x[:-1], + x[1:], + y[:-1], + y[1:], + c[:-1], + c[1:], + ) + ): line = plt.plot( [x1, x2], [y1, y2], *args, - color=cmap( - norm( - (c1 + c2) / 2 - ) if cmin != cmax else 0.5 - ), + color=cmap(norm((c1 + c2) / 2) if cmin != cmax else 0.5), **kwargs ) lines += line if label is not None: - line = plt.plot( - [None], - [None], - *args, - color=cmap(0.5), - label=label, - **kwargs - ) + line = plt.plot([None], [None], *args, color=cmap(0.5), label=label, **kwargs) lines += line sm = plt.cm.ScalarMappable(cmap=cmap, norm=norm) if colorbar: @@ -105,7 +102,8 @@ def plot_color_by_value( cbar = None return lines, sm, cbar -if __name__ == '__main__': + +if __name__ == "__main__": import matplotlib.pyplot as plt import aerosandbox.tools.pretty_plots as p @@ -114,13 +112,10 @@ def plot_color_by_value( y = np.sin(10 * x) c = np.sin((10 * x) ** 2) plot_color_by_value( - x, y, - c=c, - clim=(-1, 1), - colorbar=True, colorbar_label="Colorbar Label" + x, y, c=c, clim=(-1, 1), colorbar=True, colorbar_label="Colorbar Label" ) p.show_plot( "Title", "X Axis", "Y Axis", - ) \ No newline at end of file + ) diff --git a/aerosandbox/tools/pretty_plots/plots/plot_smooth.py b/aerosandbox/tools/pretty_plots/plots/plot_smooth.py index 60b6d5aa4..0f5b1b283 100644 --- a/aerosandbox/tools/pretty_plots/plots/plot_smooth.py +++ b/aerosandbox/tools/pretty_plots/plots/plot_smooth.py @@ -6,13 +6,13 @@ def plot_smooth( - *args, - color=None, - label=None, - function_of: str = None, - resample_resolution: int = 500, - drop_nans: bool = False, - **kwargs, + *args, + color=None, + label=None, + function_of: str = None, + resample_resolution: int = 500, + drop_nans: bool = False, + **kwargs, ) -> Tuple[np.ndarray, np.ndarray]: """ Plots a curve that interpolates a 2D dataset. Same as matplotlib.pyplot.plot(), with the following changes: @@ -79,27 +79,26 @@ def plot_smooth( else: x = argslist.pop(0) y = argslist.pop(0) - fmt = '.-' + fmt = ".-" elif len(args) == 1: x = np.arange(np.length(args[0])) y = argslist.pop(0) - fmt = '.-' + fmt = ".-" elif len(args) == 0: - raise ValueError("Missing plot data. Use syntax `plot_smooth(x, y, fmt, **kwargs)'.") + raise ValueError( + "Missing plot data. Use syntax `plot_smooth(x, y, fmt, **kwargs)'." + ) else: - raise ValueError("Unrecognized syntax. Use syntax `plot_smooth(x, y, fmt, **kwargs)'.") + raise ValueError( + "Unrecognized syntax. Use syntax `plot_smooth(x, y, fmt, **kwargs)'." + ) ### Ensure types are correct (e.g., if a list or Pandas Series is passed in) x = np.array(x) y = np.array(y) if drop_nans: - nanmask = np.logical_not( - np.logical_or( - np.isnan(x), - np.isnan(y) - ) - ) + nanmask = np.logical_not(np.logical_or(np.isnan(x), np.isnan(y))) x = x[nanmask] y = y[nanmask] @@ -124,29 +123,17 @@ def plot_smooth( dx_norm = dx / x_rng dy_norm = dy / y_rng - ds_norm = np.sqrt(dx_norm ** 2 + dy_norm ** 2) + ds_norm = np.sqrt(dx_norm**2 + dy_norm**2) - s_norm = np.concatenate([ - [0], - np.nancumsum(ds_norm) / np.nansum(ds_norm) - ]) + s_norm = np.concatenate([[0], np.nancumsum(ds_norm) / np.nansum(ds_norm)]) - bspline = interpolate.make_interp_spline( - x=s_norm, - y=np.stack( - (x, y), axis=1 - ) - ) + bspline = interpolate.make_interp_spline(x=s_norm, y=np.stack((x, y), axis=1)) result = bspline(np.linspace(0, 1, resample_resolution)) x_resample = result[:, 0] y_resample = result[:, 1] elif function_of == "x": - x_resample = np.linspace( - np.nanmin(x), - np.nanmax(x), - resample_resolution - ) + x_resample = np.linspace(np.nanmin(x), np.nanmax(x), resample_resolution) mask = ~np.isnan(x) & ~np.isnan(y) x = x[mask] @@ -161,11 +148,7 @@ def plot_smooth( elif function_of == "y": - y_resample = np.linspace( - np.nanmin(y), - np.nanmax(y), - resample_resolution - ) + y_resample = np.linspace(np.nanmin(y), np.nanmax(y), resample_resolution) mask = ~np.isnan(x) & ~np.isnan(y) x = x[mask] @@ -182,40 +165,29 @@ def plot_smooth( scatter_kwargs = { **kwargs, - 'linewidth': 0, + "linewidth": 0, } if color is not None: - scatter_kwargs['color'] = color + scatter_kwargs["color"] = color - line, = plt.plot( - x, - y, - fmt, - *argslist, - **scatter_kwargs - ) + (line,) = plt.plot(x, y, fmt, *argslist, **scatter_kwargs) if color is None: color = line.get_color() line_kwargs = { - 'color' : color, - 'label' : label, + "color": color, + "label": label, **kwargs, - 'markersize': 0, + "markersize": 0, } - plt.plot( - x_resample, - y_resample, - fmt, - *argslist, - **line_kwargs - ) + plt.plot(x_resample, y_resample, fmt, *argslist, **line_kwargs) return x_resample, y_resample -if __name__ == '__main__': + +if __name__ == "__main__": import aerosandbox.numpy as np # t = np.linspace(0, 1, 12) # Parametric variable @@ -231,9 +203,11 @@ def plot_smooth( fig, ax = plt.subplots() x = np.linspace(0, 1, 8) plot_smooth( - x, np.exp(-10 * x**0.5), color='goldenrod', + x, + np.exp(-10 * x**0.5), + color="goldenrod", function_of="x", # markersize=0, - resample_resolution=2000 + resample_resolution=2000, ) - plt.show() \ No newline at end of file + plt.show() diff --git a/aerosandbox/tools/pretty_plots/plots/plot_with_bootstrapped_uncertainty.py b/aerosandbox/tools/pretty_plots/plots/plot_with_bootstrapped_uncertainty.py index 4a709830b..549f74a48 100644 --- a/aerosandbox/tools/pretty_plots/plots/plot_with_bootstrapped_uncertainty.py +++ b/aerosandbox/tools/pretty_plots/plots/plot_with_bootstrapped_uncertainty.py @@ -6,24 +6,24 @@ def plot_with_bootstrapped_uncertainty( - x: np.ndarray, - y: np.ndarray, - ci: Optional[Union[float, Iterable[float], np.ndarray]] = 0.95, - x_stdev: Union[None, float] = 0., - y_stdev: Union[None, float] = None, - color: Optional[Union[str, Tuple[float]]] = None, - draw_data: bool = True, - label_line: Union[bool, str] = "Best Estimate", - label_ci: bool = True, - label_data: Union[bool, str] = "Raw Data", - line_alpha: float = 0.9, - ci_to_alpha_mapping: Callable[[float], float] = lambda ci: 0.8 * (1 - ci) ** 0.4, - n_bootstraps=2000, - n_fit_points=500, - spline_degree=3, - normalize: bool=True, - x_log_scale: bool = False, - y_log_scale: bool = False, + x: np.ndarray, + y: np.ndarray, + ci: Optional[Union[float, Iterable[float], np.ndarray]] = 0.95, + x_stdev: Union[None, float] = 0.0, + y_stdev: Union[None, float] = None, + color: Optional[Union[str, Tuple[float]]] = None, + draw_data: bool = True, + label_line: Union[bool, str] = "Best Estimate", + label_ci: bool = True, + label_data: Union[bool, str] = "Raw Data", + line_alpha: float = 0.9, + ci_to_alpha_mapping: Callable[[float], float] = lambda ci: 0.8 * (1 - ci) ** 0.4, + n_bootstraps=2000, + n_fit_points=500, + spline_degree=3, + normalize: bool = True, + x_log_scale: bool = False, + y_log_scale: bool = False, ): x = np.array(x) y = np.array(y) @@ -49,7 +49,9 @@ def plot_with_bootstrapped_uncertainty( ### Make sure `ci` is in bounds if not (np.all(ci > 0) and np.all(ci < 1)): - raise ValueError("Confidence interval values in `ci` should all be in the range of (0, 1).") + raise ValueError( + "Confidence interval values in `ci` should all be in the range of (0, 1)." + ) ### Do the bootstrap fits x_fit, y_bootstrap_fits = tsuq.bootstrap_fits( @@ -72,7 +74,7 @@ def plot_with_bootstrapped_uncertainty( y_bootstrap_fits = np.exp(y_bootstrap_fits) ### Plot the best-estimator line - line, = plt.plot( + (line,) = plt.plot( x_fit, np.nanquantile(y_bootstrap_fits, q=0.5, axis=0), color=color, @@ -84,9 +86,9 @@ def plot_with_bootstrapped_uncertainty( color = line.get_color() if x_log_scale: - plt.xscale('log') + plt.xscale("log") if y_log_scale: - plt.yscale('log') + plt.yscale("log") ### Plot the confidence intervals if len(ci) != 0: @@ -100,24 +102,16 @@ def plot_with_bootstrapped_uncertainty( for i, ci_val in enumerate(ci): settings = dict( - color=color, - alpha=ci_to_alpha_mapping(ci_val), - linewidth=0, - zorder=1.5 + color=color, alpha=ci_to_alpha_mapping(ci_val), linewidth=0, zorder=1.5 ) plt.fill_between( x_fit, lower_ci[i], lower_ci[i + 1], label=f"{ci_val:.0%} CI" if label_ci else None, - **settings - ) - plt.fill_between( - x_fit, - upper_ci[i], - upper_ci[i + 1], - **settings + **settings, ) + plt.fill_between(x_fit, upper_ci[i], upper_ci[i + 1], **settings) ### Plot the data if draw_data: @@ -134,7 +128,7 @@ def plot_with_bootstrapped_uncertainty( return x_fit, y_bootstrap_fits -if __name__ == '__main__': +if __name__ == "__main__": import matplotlib.pyplot as plt import aerosandbox.tools.pretty_plots as p @@ -161,16 +155,11 @@ def plot_with_bootstrapped_uncertainty( ax.plot(x, y_true, "k--", label="True Function (Hidden)", alpha=0.8, zorder=1) plt.legend(ncols=2) - p.show_plot( - "Spline Bootstrapping Test", - r"$x$", - r"$y$", - legend=False - ) + p.show_plot("Spline Bootstrapping Test", r"$x$", r"$y$", legend=False) ### Generate data x = np.geomspace(10, 1000, 1000) - y_true = 3 * x ** 0.5 + y_true = 3 * x**0.5 y_stdev = 0.1 diff --git a/aerosandbox/tools/pretty_plots/quickplot.py b/aerosandbox/tools/pretty_plots/quickplot.py index 98b3b2916..3f42ea9ff 100644 --- a/aerosandbox/tools/pretty_plots/quickplot.py +++ b/aerosandbox/tools/pretty_plots/quickplot.py @@ -5,12 +5,12 @@ def qp( - *args: Tuple[Union[np.ndarray, List]], - backend="plotly", - show=True, - plotly_renderer: Union[str, None] = "browser", - orthographic=True, - stacklevel=1 + *args: Tuple[Union[np.ndarray, List]], + backend="plotly", + show=True, + plotly_renderer: Union[str, None] = "browser", + orthographic=True, + stacklevel=1, ) -> None: """ Quickly plots ("QP") a 1D, 2D, or 3D dataset as a line plot with markers. Useful for exploratory data analysis. @@ -56,16 +56,19 @@ def qp( if n_dimensions >= 3: arg_names += ["z"] if n_dimensions >= 4: - arg_names += [ - f"Dim. {i}" - for i in range(4, n_dimensions + 1) - ] + arg_names += [f"Dim. {i}" for i in range(4, n_dimensions + 1)] title = "QuickPlot" try: ### This is some interesting and tricky code here: retrieves the source code of where qp() was called, as a string. - caller_source_code = inspect_tools.get_caller_source_code(stacklevel=stacklevel + 1) + caller_source_code = inspect_tools.get_caller_source_code( + stacklevel=stacklevel + 1 + ) try: - parsed_arg_names = inspect_tools.get_function_argument_names_from_source_code(caller_source_code) + parsed_arg_names = ( + inspect_tools.get_function_argument_names_from_source_code( + caller_source_code + ) + ) title = "QuickPlot: " + " vs. ".join(parsed_arg_names) if len(parsed_arg_names) == n_dimensions: arg_names = parsed_arg_names @@ -92,39 +95,23 @@ def qp( import plotly.graph_objects as go mode = "markers+lines" - marker_dict = dict( - size=5 if n_dimensions != 3 else 2, - line=dict( - width=0 - ) - ) + marker_dict = dict(size=5 if n_dimensions != 3 else 2, line=dict(width=0)) if n_dimensions == 1: fig = go.Figure( - data=go.Scatter( - y=arg_values[0], - mode=mode, - marker=marker_dict - ) + data=go.Scatter(y=arg_values[0], mode=mode, marker=marker_dict) ) fig.update_layout( - title=title, - xaxis_title="Array index #", - yaxis_title=arg_names[0] + title=title, xaxis_title="Array index #", yaxis_title=arg_names[0] ) elif n_dimensions == 2: fig = go.Figure( data=go.Scatter( - x=arg_values[0], - y=arg_values[1], - mode=mode, - marker=marker_dict + x=arg_values[0], y=arg_values[1], mode=mode, marker=marker_dict ) ) fig.update_layout( - title=title, - xaxis_title=arg_names[0], - yaxis_title=arg_names[1] + title=title, xaxis_title=arg_names[0], yaxis_title=arg_names[1] ) elif n_dimensions == 3: fig = go.Figure( @@ -133,7 +120,7 @@ def qp( y=arg_values[1], z=arg_values[2], mode=mode, - marker=marker_dict + marker=marker_dict, ), ) fig.update_layout( @@ -142,7 +129,7 @@ def qp( xaxis_title=arg_names[0], yaxis_title=arg_names[1], zaxis_title=arg_names[2], - ) + ), ) else: raise ValueError("Too many inputs to plot!") @@ -150,18 +137,16 @@ def qp( if orthographic: fig.layout.scene.camera.projection.type = "orthographic" if show: - fig.show( - renderer=plotly_renderer - ) + fig.show(renderer=plotly_renderer) else: raise ValueError("Bad value of `backend`!") -if __name__ == '__main__': +if __name__ == "__main__": import aerosandbox.numpy as np x = np.linspace(0, 10, 100) - y = x ** 2 + y = x**2 z = np.sin(y) qp(x) qp(x, y) diff --git a/aerosandbox/tools/pretty_plots/threedim.py b/aerosandbox/tools/pretty_plots/threedim.py index e2856f0a1..f7bd455b0 100644 --- a/aerosandbox/tools/pretty_plots/threedim.py +++ b/aerosandbox/tools/pretty_plots/threedim.py @@ -8,29 +8,29 @@ # Given in the form: # * key is the view name # * value is a tuple of three floats: (elev, azim, roll) - 'XY' : (90, -90, 0), - 'XZ' : (0, -90, 0), - 'YZ' : (0, 0, 0), - '-XY' : (-90, 90, 0), - '-XZ' : (0, 90, 0), - '-YZ' : (0, 180, 0), - 'left_isometric' : (np.arctan2d(1, 2 ** 0.5), -135, 0), - 'right_isometric': (np.arctan2d(1, 2 ** 0.5), 135, 0) + "XY": (90, -90, 0), + "XZ": (0, -90, 0), + "YZ": (0, 0, 0), + "-XY": (-90, 90, 0), + "-XZ": (0, 90, 0), + "-YZ": (0, 180, 0), + "left_isometric": (np.arctan2d(1, 2**0.5), -135, 0), + "right_isometric": (np.arctan2d(1, 2**0.5), 135, 0), } -preset_view_angles['front'] = preset_view_angles["-YZ"] -preset_view_angles['top'] = preset_view_angles["XY"] -preset_view_angles['side'] = preset_view_angles["XZ"] +preset_view_angles["front"] = preset_view_angles["-YZ"] +preset_view_angles["top"] = preset_view_angles["XY"] +preset_view_angles["side"] = preset_view_angles["XZ"] def figure3d( - nrows: int = 1, - ncols: int = 1, - orthographic: bool = True, - box_aspect: Tuple[float] = None, - adjust_colors: bool = True, - computed_zorder: bool = True, - ax_kwargs: Dict = None, - **fig_kwargs + nrows: int = 1, + ncols: int = 1, + orthographic: bool = True, + box_aspect: Tuple[float] = None, + adjust_colors: bool = True, + computed_zorder: bool = True, + ax_kwargs: Dict = None, + **fig_kwargs, ) -> Tuple[matplotlib.figure.Figure, mpl_toolkits.mplot3d.axes3d.Axes3D]: """ Creates a new 3D figure. Args and kwargs are passed into matplotlib.pyplot.figure(). @@ -44,8 +44,8 @@ def figure3d( ### Collect the keyword arguments to be used for each 3D axis default_axes_kwargs = dict( - projection='3d', - proj_type='ortho' if orthographic else 'persp', + projection="3d", + proj_type="ortho" if orthographic else "persp", box_aspect=box_aspect, computed_zorder=computed_zorder, ) @@ -56,10 +56,7 @@ def figure3d( ### Generate the 3D axis (or axes) fig, ax = plt.subplots( - nrows=nrows, - ncols=ncols, - subplot_kw=axes_kwargs, - **fig_kwargs + nrows=nrows, ncols=ncols, subplot_kw=axes_kwargs, **fig_kwargs ) if adjust_colors: @@ -83,9 +80,7 @@ def figure3d( return fig, ax -def ax_is_3d( - ax: matplotlib.axes.Axes = None -) -> bool: +def ax_is_3d(ax: matplotlib.axes.Axes = None) -> bool: """ Determines if a Matplotlib axis object is 3D or not. @@ -98,12 +93,10 @@ def ax_is_3d( if ax is None: ax = plt.gca() - return hasattr(ax, 'zaxis') + return hasattr(ax, "zaxis") -def set_preset_3d_view_angle( - preset_view: str -) -> None: +def set_preset_3d_view_angle(preset_view: str) -> None: ax = plt.gca() if not ax_is_3d(ax): @@ -113,8 +106,8 @@ def set_preset_3d_view_angle( elev, azim, roll = preset_view_angles[preset_view] except KeyError: raise ValueError( - f"Input '{preset_view}' is not a valid preset view. Valid presets are:\n" + - "\n".join([f" * '{k}'" for k in preset_view_angles.keys()]) + f"Input '{preset_view}' is not a valid preset view. Valid presets are:\n" + + "\n".join([f" * '{k}'" for k in preset_view_angles.keys()]) ) if roll == 0: @@ -126,14 +119,10 @@ def set_preset_3d_view_angle( azim=azim, ) else: - ax.view_init( - elev=elev, - azim=azim, - roll=roll - ) + ax.view_init(elev=elev, azim=azim, roll=roll) -if __name__ == '__main__': +if __name__ == "__main__": import aerosandbox.numpy as np import matplotlib.pyplot as plt import aerosandbox.tools.pretty_plots as p @@ -141,15 +130,13 @@ def set_preset_3d_view_angle( t = np.linspace(0, 1, 100) x = np.sin(4 * 2 * np.pi * t) - y = t ** 2 + y = t**2 z = 5 * t fig, ax = p.figure3d() - p.set_preset_3d_view_angle('left_isometric') + p.set_preset_3d_view_angle("left_isometric") - ax.plot( - x, y, z, "-" - ) + ax.plot(x, y, z, "-") ax.set_xlabel("x") ax.set_ylabel("y") ax.set_zlabel("z") diff --git a/aerosandbox/tools/pretty_plots/utilities/natural_univariate_spline.py b/aerosandbox/tools/pretty_plots/utilities/natural_univariate_spline.py index b43ab7c25..528f67da0 100644 --- a/aerosandbox/tools/pretty_plots/utilities/natural_univariate_spline.py +++ b/aerosandbox/tools/pretty_plots/utilities/natural_univariate_spline.py @@ -14,16 +14,17 @@ class NaturalUnivariateSpline(interpolate.PPoly): """ - def __init__(self, - x: np.ndarray, - y: np.ndarray, - w: np.ndarray = None, - k: int = 3, - s: float = None, - ext=None, - bbox=None, - check_finite=None - ): + def __init__( + self, + x: np.ndarray, + y: np.ndarray, + w: np.ndarray = None, + k: int = 3, + s: float = None, + ext=None, + bbox=None, + check_finite=None, + ): """ @@ -46,24 +47,26 @@ def __init__(self, """ if s is None: m = len(x) - s = m - (2 * m) ** 0.5 # Identical default to UnivariateSpline's `s` argument. + s = ( + m - (2 * m) ** 0.5 + ) # Identical default to UnivariateSpline's `s` argument. ### Deprecate and warn import warnings + if ext is not None: warnings.warn( "The `ext` argument is deprecated, as a NaturalUnivariateSpline implies extrapolation.", - DeprecationWarning + DeprecationWarning, ) if bbox is not None: warnings.warn( "The `bbox` argument is deprecated, as a NaturalUnivariateSpline implies extrapolation.", - DeprecationWarning + DeprecationWarning, ) if check_finite is not None: warnings.warn( - "The `check_finite` argument is deprecated.", - DeprecationWarning + "The `check_finite` argument is deprecated.", DeprecationWarning ) ### Compute the t, c, and k parameters for a UnivariateSpline @@ -76,28 +79,22 @@ def __init__(self, ) ### Construct the spline, without natural extrapolation - spline = interpolate.PPoly.from_spline( - tck=tck - ) + spline = interpolate.PPoly.from_spline(tck=tck) ### Add spline knots for natural positive extrapolation spline.extend( c=np.array( - [[0]] * (k - 2) + [ - [spline(spline.x[-1], 1)], - [spline(spline.x[-1])] - ]), - x=np.array([np.inf]) + [[0]] * (k - 2) + [[spline(spline.x[-1], 1)], [spline(spline.x[-1])]] + ), + x=np.array([np.inf]), ) ### Add spline knots for natural negative extrapolation spline.extend( c=np.array( - [[0]] * (k - 1) + [ - [spline(spline.x[0], 1)], - [spline(spline.x[0])] - ]), - x=np.array([spline.x[0]]) + [[0]] * (k - 1) + [[spline(spline.x[0], 1)], [spline(spline.x[0])]] + ), + x=np.array([spline.x[0]]), ) ### Construct the Natural Univariate Spline @@ -108,7 +105,7 @@ def __init__(self, ) -if __name__ == '__main__': +if __name__ == "__main__": import matplotlib.pyplot as plt import aerosandbox.tools.pretty_plots as p diff --git a/aerosandbox/tools/python/execution.py b/aerosandbox/tools/python/execution.py index c397ac421..10e90ef5a 100644 --- a/aerosandbox/tools/python/execution.py +++ b/aerosandbox/tools/python/execution.py @@ -29,7 +29,9 @@ def run_all_python_files(path: Path, recursive=True, verbose=True) -> None: # Exclusions: if path == Path(os.path.abspath(__file__)): # Don't run this file return - if "ignore" in str(path).lower(): # Don't run any file or folder with the word "ignore" in the name. + if ( + "ignore" in str(path).lower() + ): # Don't run any file or folder with the word "ignore" in the name. return if path.is_file(): diff --git a/aerosandbox/tools/python/io.py b/aerosandbox/tools/python/io.py index 386cd521b..b737ff95c 100644 --- a/aerosandbox/tools/python/io.py +++ b/aerosandbox/tools/python/io.py @@ -3,8 +3,8 @@ def convert_ipynb_to_py( - input_file: Path, - output_file: Path, + input_file: Path, + output_file: Path, ) -> None: """ Reads an input Jupyter notebook (.ipynb) and converts it to a Python file (.py) @@ -22,7 +22,7 @@ def convert_ipynb_to_py( with open(input_file, "r") as f: ipynb_contents = json.load(f) with open(output_file, "w+") as f: - for cell in ipynb_contents['cells']: - if cell['cell_type'] == "code": - f.writelines(cell['source']) + for cell in ipynb_contents["cells"]: + if cell["cell_type"] == "code": + f.writelines(cell["source"]) f.write("\n") diff --git a/aerosandbox/tools/statistics/time_series_uncertainty_quantification.py b/aerosandbox/tools/statistics/time_series_uncertainty_quantification.py index e5bf93d87..974d99434 100644 --- a/aerosandbox/tools/statistics/time_series_uncertainty_quantification.py +++ b/aerosandbox/tools/statistics/time_series_uncertainty_quantification.py @@ -4,14 +4,16 @@ from tqdm import tqdm import aerosandbox.numpy as np -from aerosandbox.tools.pretty_plots.utilities.natural_univariate_spline import NaturalUnivariateSpline as Spline +from aerosandbox.tools.pretty_plots.utilities.natural_univariate_spline import ( + NaturalUnivariateSpline as Spline, +) from scipy import signal from aerosandbox.tools.code_benchmarking import Timer def estimate_noise_standard_deviation( - data: np.ndarray, - estimator_order: int = None, + data: np.ndarray, + estimator_order: int = None, ) -> float: """ Estimates the standard deviation of the random noise in a time-series dataset. @@ -49,42 +51,43 @@ def estimate_noise_standard_deviation( ##### Noise Variance Reconstruction ##### from scipy.special import gammaln + ln_factorial = lambda x: gammaln(x + 1) ### For speed, pre-compute the log-factorial of integers from 1 to estimator_order # ln_f = ln_factorial(np.arange(estimator_order + 1)) - ln_f = np.cumsum( - np.log( - np.concatenate([ - [1], - np.arange(1, estimator_order + 1) - ]) - ) - ) + ln_f = np.cumsum(np.log(np.concatenate([[1], np.arange(1, estimator_order + 1)]))) ### Create a convolutional kernel to vectorize the summation log_coeffs = ( - 2 * ln_f[estimator_order] - ln_f - ln_f[::-1] - 0.5 * ln_factorial(2 * estimator_order) + 2 * ln_f[estimator_order] + - ln_f + - ln_f[::-1] + - 0.5 * ln_factorial(2 * estimator_order) ) - indices = np.nonzero(log_coeffs >= np.log(1e-20) + log_coeffs[estimator_order // 2])[0] - coefficients = np.exp(log_coeffs[indices[0]:indices[-1] + 1]) + indices = np.nonzero( + log_coeffs >= np.log(1e-20) + log_coeffs[estimator_order // 2] + )[0] + coefficients = np.exp(log_coeffs[indices[0] : indices[-1] + 1]) coefficients[::2] *= -1 # Flip the sign on every other coefficient - coefficients -= np.mean(coefficients) # Remove any bias introduced by floating-point error + coefficients -= np.mean( + coefficients + ) # Remove any bias introduced by floating-point error # sample_stdev = signal.convolve(data, coefficients[::-1], 'valid') - sample_stdev = signal.oaconvolve(data, coefficients[::-1], 'valid') - return np.mean(sample_stdev ** 2) ** 0.5 + sample_stdev = signal.oaconvolve(data, coefficients[::-1], "valid") + return np.mean(sample_stdev**2) ** 0.5 def bootstrap_fits( - x: np.ndarray, - y: np.ndarray, - x_noise_stdev: Union[None, float] = 0., - y_noise_stdev: Union[None, float] = None, - n_bootstraps: int = 2000, - fit_points: Union[int, Iterable[float], None] = 300, - spline_degree: int = 3, - normalize: bool = None, + x: np.ndarray, + y: np.ndarray, + x_noise_stdev: Union[None, float] = 0.0, + y_noise_stdev: Union[None, float] = None, + n_bootstraps: int = 2000, + fit_points: Union[int, Iterable[float], None] = 300, + spline_degree: int = 3, + normalize: bool = None, ) -> Union[Tuple[np.ndarray, np.ndarray], List[Spline]]: """ Bootstraps a time-series dataset and fits splines to each bootstrap resample. @@ -181,7 +184,9 @@ def bootstrap_fits( x_stdev_normalized = x_noise_stdev y_stdev_normalized = y_noise_stdev - with tqdm(total=n_bootstraps, desc="Bootstrapping", unit=" samples") as progress_bar: + with tqdm( + total=n_bootstraps, desc="Bootstrapping", unit=" samples" + ) as progress_bar: splines = [] n_valid_splines = 0 n_attempted_splines = 0 @@ -227,28 +232,25 @@ def bootstrap_fits( if normalize: raise ValueError("If `fit_points` is None, `normalize` must be False.") elif isinstance(fit_points, int): - x_fit = np.linspace( - np.min(x), - np.max(x), - fit_points - ) + x_fit = np.linspace(np.min(x), np.max(x), fit_points) else: x_fit = np.array(fit_points) ### Evaluate the splines at the x-points - y_bootstrap_fits = np.array([ - y_unnormalize(spline(x_normalize(x_fit))) - for spline in splines - ]) + y_bootstrap_fits = np.array( + [y_unnormalize(spline(x_normalize(x_fit))) for spline in splines] + ) ### Throw an error if all of the splines are NaN if np.all(np.isnan(y_bootstrap_fits)): - raise ValueError("All of the splines are NaN. This is likely due to a poor choice of `spline_degree`.") + raise ValueError( + "All of the splines are NaN. This is likely due to a poor choice of `spline_degree`." + ) return x_fit, y_bootstrap_fits -if __name__ == '__main__': +if __name__ == "__main__": np.random.seed(1) N = 1000 f_sample_over_f_signal = 1000 diff --git a/aerosandbox/tools/string_formatting.py b/aerosandbox/tools/string_formatting.py index 8de875e15..fb6ae0395 100644 --- a/aerosandbox/tools/string_formatting.py +++ b/aerosandbox/tools/string_formatting.py @@ -3,13 +3,13 @@ def eng_string( - x: float, - unit: str = "", - format='%.3g', - si=True, - add_space_after_number: bool = None, + x: float, + unit: str = "", + format="%.3g", + si=True, + add_space_after_number: bool = None, ) -> str: - ''' + """ Taken from: https://stackoverflow.com/questions/17973278/python-decimal-engineering-notation-for-mili-10e-3-and-micro-10e-6/40691220 Returns float/int value formatted in a simplified engineering format - @@ -41,12 +41,12 @@ def eng_string( With unit="N" and si=True: 1230.0 -> "1.23 kN" -1230000.0 -> "-1.23 MN" - ''' + """ - sign = '' + sign = "" if x < 0: x = -x - sign = '-' + sign = "-" elif x == 0: return format % 0 elif np.isnan(x): @@ -54,16 +54,16 @@ def eng_string( exp = int(np.floor(np.log10(x))) exp3 = exp - (exp % 3) - x3 = x / (10 ** exp3) + x3 = x / (10**exp3) if si and exp3 >= -24 and exp3 <= 24: if exp3 == 0: suffix = "" else: - suffix = 'yzafpnμm kMGTPEZY'[(exp3 + 24) // 3] + suffix = "yzafpnμm kMGTPEZY"[(exp3 + 24) // 3] if add_space_after_number is None: - add_space_after_number = (unit != "") + add_space_after_number = unit != "" if add_space_after_number: suffix = " " + suffix + unit @@ -71,10 +71,10 @@ def eng_string( suffix = suffix + unit else: - suffix = f'e{exp3}' + suffix = f"e{exp3}" if add_space_after_number: - add_space_after_number = (unit != "") + add_space_after_number = unit != "" if add_space_after_number: suffix = suffix + " " + unit @@ -85,8 +85,8 @@ def eng_string( def latex_sci_notation_string( - x: float, - format='%.2e', + x: float, + format="%.2e", ) -> str: """ Converts a floating-point number to a LaTeX-style formatted string. Does not include the `$$` wrapping to put you in math mode. @@ -112,10 +112,10 @@ def hash_string(string: str) -> int: Usual warnings apply: it's MD5, don't use this for anything intended to be cryptographically secure. """ - md5 = hashlib.md5(string.encode('utf-8')) + md5 = hashlib.md5(string.encode("utf-8")) hash_hex = md5.hexdigest() hash_int = int(hash_hex, 16) - hash_int64 = hash_int % (2 ** 32) + hash_int64 = hash_int % (2**32) return hash_int64 @@ -133,7 +133,7 @@ def trim_string(string: str, length: int = 80) -> str: """ if len(string) > length: - return string[:length - 1] + "…" + return string[: length - 1] + "…" else: return string @@ -171,8 +171,8 @@ def has_balanced_parentheses(string: str, left="(", right=")") -> bool: def wrap_text_ignoring_mathtext( - text: str, - width: int = 70, + text: str, + width: int = 70, ) -> str: r""" Reformat the single paragraph in 'text' to fit in lines of no more @@ -253,19 +253,14 @@ def wrap_text_ignoring_mathtext( return output -if __name__ == '__main__': +if __name__ == "__main__": for input in [ r"$ax^2+bx+c$", r"Photon flux $\phi$", r"Photon flux $\phi$ is given by $\phi = \frac{c}{\lambda}$", r"Earnings for 2022 $M\$/year$", - r"$ax^2+bx+c$ and also $3x$" + r"$ax^2+bx+c$ and also $3x$", ]: print(wrap_text_ignoring_mathtext(input, width=10)) - print( - wrap_text_ignoring_mathtext( - r"Angle of Attack $\alpha$ [deg]", - 14 - ) - ) \ No newline at end of file + print(wrap_text_ignoring_mathtext(r"Angle of Attack $\alpha$ [deg]", 14)) diff --git a/aerosandbox/tools/sympy_interactive.py b/aerosandbox/tools/sympy_interactive.py index f4df8f062..b14a3d88a 100644 --- a/aerosandbox/tools/sympy_interactive.py +++ b/aerosandbox/tools/sympy_interactive.py @@ -9,9 +9,9 @@ def simp(x): def show( - rhs: s.Symbol, - lhs: str = None, - simplify=True, + rhs: s.Symbol, + lhs: str = None, + simplify=True, ): # Display an equation if simplify: rhs = simp(rhs) @@ -19,14 +19,6 @@ def show( if lhs is not None: if simplify: lhs = simp(lhs) - display( - s.Eq( - s.symbols(lhs), - rhs, - evaluate=False - ) - ) + display(s.Eq(s.symbols(lhs), rhs, evaluate=False)) else: - display( - rhs - ) + display(rhs) diff --git a/aerosandbox/tools/test_tools/test_inspect_tools.py b/aerosandbox/tools/test_tools/test_inspect_tools.py index a9b39bc5c..14040f8b8 100644 --- a/aerosandbox/tools/test_tools/test_inspect_tools.py +++ b/aerosandbox/tools/test_tools/test_inspect_tools.py @@ -5,29 +5,29 @@ def test_function_argument_names_from_source_code(): tests = { # Pairs of {input: expected_output} - "f(a, b)" : ['a', 'b'], - "f(a,b)" : ['a', 'b'], - "f(\na,\nb)" : ['a', 'b'], - "g = f(a, b)" : ['a', 'b'], - "g.h = f(a, b)" : ['a', 'b'], - "g.h() = f(a, b)" : ['a', 'b'], - "g.h(i=j) = f(a, b)" : ['a', 'b'], - "f(a, b) + g(h)" : ['a', 'b'], - "f(a: int, b: MyType())": ['a', 'b'], - "f(a, b).g(c, d)" : ['a', 'b'], - "f(a(b), c)" : ['a(b)', 'c'], - "f(a(b,c), d)" : ['a(b,c)', 'd'], - "f({a:b}, c)" : ['{a:b}', 'c'], - "f(a[b], c)" : ['a[b]', 'c'], - "f({a:b, c:d}, e)" : ['{a:b, c:d}', 'e'], - "f({a:b,\nc:d}, e)" : ['{a:b,c:d}', 'e'], - "f(dict(a=b,c=d), e)" : ['dict(a=b,c=d)', 'e'], - "f(a=1, b=2)" : ['a=1', 'b=2'], - "f()" : [''], - "f(a, [i for i in l])" : ['a', '[i for i in l]'], - "f(incomplete, " : ValueError, - "3 + 5" : ValueError, - "" : ValueError, + "f(a, b)": ["a", "b"], + "f(a,b)": ["a", "b"], + "f(\na,\nb)": ["a", "b"], + "g = f(a, b)": ["a", "b"], + "g.h = f(a, b)": ["a", "b"], + "g.h() = f(a, b)": ["a", "b"], + "g.h(i=j) = f(a, b)": ["a", "b"], + "f(a, b) + g(h)": ["a", "b"], + "f(a: int, b: MyType())": ["a", "b"], + "f(a, b).g(c, d)": ["a", "b"], + "f(a(b), c)": ["a(b)", "c"], + "f(a(b,c), d)": ["a(b,c)", "d"], + "f({a:b}, c)": ["{a:b}", "c"], + "f(a[b], c)": ["a[b]", "c"], + "f({a:b, c:d}, e)": ["{a:b, c:d}", "e"], + "f({a:b,\nc:d}, e)": ["{a:b,c:d}", "e"], + "f(dict(a=b,c=d), e)": ["dict(a=b,c=d)", "e"], + "f(a=1, b=2)": ["a=1", "b=2"], + "f()": [""], + "f(a, [i for i in l])": ["a", "[i for i in l]"], + "f(incomplete, ": ValueError, + "3 + 5": ValueError, + "": ValueError, } for input, expected_output in tests.items(): @@ -39,7 +39,9 @@ def test_function_argument_names_from_source_code(): ### If you're expecting a specific output, make sure you get that else: - assert get_function_argument_names_from_source_code(input) == expected_output + assert ( + get_function_argument_names_from_source_code(input) == expected_output + ) def test_codegen_builtins(): @@ -78,6 +80,6 @@ def test_codegen_numpy(): assert (eval(code) == x).all() -if __name__ == '__main__': +if __name__ == "__main__": test_function_argument_names_from_source_code() pytest.main() diff --git a/aerosandbox/tools/units.py b/aerosandbox/tools/units.py index df6aa1cf2..12ade952e 100644 --- a/aerosandbox/tools/units.py +++ b/aerosandbox/tools/units.py @@ -45,7 +45,7 @@ # Volume (equivalents in m^3) liter = 0.001 -gallon_us = 231 * inch ** 3 +gallon_us = 231 * inch**3 gallon_imperial = 4.54609 * liter gallon = gallon_us quart = gallon_us / 4 @@ -54,8 +54,8 @@ pascal = 1 atm = 101325 torr = atm / 760 -psi = lbf / inch ** 2 -psf = lbf / foot ** 2 +psi = lbf / inch**2 +psf = lbf / foot**2 # Power (equivalents in Watts) watt = 1 diff --git a/aerosandbox/tools/webplotdigitizer_reader.py b/aerosandbox/tools/webplotdigitizer_reader.py index 70b94abe2..e39df2581 100644 --- a/aerosandbox/tools/webplotdigitizer_reader.py +++ b/aerosandbox/tools/webplotdigitizer_reader.py @@ -5,6 +5,7 @@ https://github.com/ankitrohatgi/WebPlotDigitizer """ + import numpy as np from typing import Dict from pathlib import Path @@ -26,7 +27,7 @@ def remove_nan_rows(a: np.ndarray) -> np.ndarray: def read_webplotdigitizer_csv( - filename: Union[Path, str], + filename: Union[Path, str], ) -> Dict[str, np.ndarray]: """ Reads a CSV file produced by WebPlotDigitizer (https://automeris.io/WebPlotDigitizer/). @@ -47,10 +48,9 @@ def read_webplotdigitizer_csv( with open(filename, "r") as f: lines = f.readlines() - has_titles = np.any([ - np.isnan(string_to_float(s)) - for s in lines[0].split(delimiter) - ]) + has_titles = np.any( + [np.isnan(string_to_float(s)) for s in lines[0].split(delimiter)] + ) if has_titles: titles = lines[0].split(delimiter)[::2] @@ -59,16 +59,19 @@ def read_webplotdigitizer_csv( titles = ["data"] first_data_row = 0 - all_data = np.array([ - [string_to_float(item) for item in line.split(delimiter)] - for line in lines[first_data_row:] - ], dtype=float) + all_data = np.array( + [ + [string_to_float(item) for item in line.split(delimiter)] + for line in lines[first_data_row:] + ], + dtype=float, + ) output = {} for i, title in enumerate(titles): - series = all_data[:, 2 * i: 2 * i + 2] + series = all_data[:, 2 * i : 2 * i + 2] all_nan_rows = np.all(np.isnan(series), axis=1) series = series[~all_nan_rows, :] diff --git a/aerosandbox/visualization/carpet_plot_utils.py b/aerosandbox/visualization/carpet_plot_utils.py index ec9973e7c..227a86029 100644 --- a/aerosandbox/visualization/carpet_plot_utils.py +++ b/aerosandbox/visualization/carpet_plot_utils.py @@ -38,7 +38,9 @@ def signal_handler(signum, frame): try: signal.signal(signal.SIGALRM, signal_handler) except AttributeError: - raise OSError("signal.SIGALRM could not be found. This is probably because you're not using Linux.") + raise OSError( + "signal.SIGALRM could not be found. This is probably because you're not using Linux." + ) signal.alarm(seconds) try: yield @@ -123,12 +125,18 @@ def item(i, j): if not np.isnan(array[i, j]): continue - neighbors = np.array([ - item(i, j - 1), item(i, j + 1), - item(i - 1, j), item(i + 1, j), - item(i - 1, j + 1), item(i + 1, j - 1), - item(i - 1, j - 1), item(i + 1, j + 1), - ]) + neighbors = np.array( + [ + item(i, j - 1), + item(i, j + 1), + item(i - 1, j), + item(i + 1, j), + item(i - 1, j + 1), + item(i + 1, j - 1), + item(i - 1, j - 1), + item(i + 1, j + 1), + ] + ) valid_neighbors = neighbors[np.logical_not(np.isnan(neighbors))] @@ -144,18 +152,22 @@ def item(i, j): assert last_nanfrac == 0, "Could not patch all NaNs!" # Diffusing - print_title("Diffusing") # TODO Perhaps use skimage gaussian blur kernel or similar instead of "+" stencil? + print_title( + "Diffusing" + ) # TODO Perhaps use skimage gaussian blur kernel or similar instead of "+" stencil? for iter in range(50): print(f"{iter + 1:4}") for i in range(array.shape[0]): for j in range(array.shape[1]): if original_nans[i, j]: - neighbors = np.array([ - item(i, j - 1), - item(i, j + 1), - item(i - 1, j), - item(i + 1, j), - ]) + neighbors = np.array( + [ + item(i, j - 1), + item(i, j + 1), + item(i - 1, j), + item(i + 1, j), + ] + ) valid_neighbors = neighbors[np.logical_not(np.isnan(neighbors))] @@ -164,12 +176,11 @@ def item(i, j): return array -if __name__ == '__main__': +if __name__ == "__main__": import time import numpy as np from numpy import linalg - def complicated_function(): print("Starting...") n = 10000 @@ -177,7 +188,6 @@ def complicated_function(): print("Finished") return True - try: with time_limit(1): complicated_function() diff --git a/aerosandbox/visualization/plotly.py b/aerosandbox/visualization/plotly.py index a0714b49d..647c0ed9e 100644 --- a/aerosandbox/visualization/plotly.py +++ b/aerosandbox/visualization/plotly.py @@ -8,8 +8,8 @@ def spy( - matrix, - show=True, + matrix, + show=True, ): """ Plots the sparsity pattern of a matrix. @@ -24,7 +24,9 @@ def spy( abs_m = np.abs(matrix) sparsity_pattern = abs_m >= 1e-16 matrix[sparsity_pattern] = np.log10(abs_m[sparsity_pattern] + 1e-16) - j_index_map, i_index_map = np.meshgrid(np.arange(matrix.shape[1]), np.arange(matrix.shape[0])) + j_index_map, i_index_map = np.meshgrid( + np.arange(matrix.shape[1]), np.arange(matrix.shape[0]) + ) i_index = i_index_map[sparsity_pattern] j_index = j_index_map[sparsity_pattern] @@ -36,14 +38,20 @@ def spy( x=j_index, z=val, # type='heatmap', - colorscale='RdBu', + colorscale="RdBu", showscale=False, ), ) fig.update_layout( plot_bgcolor="black", xaxis=dict(showgrid=False, zeroline=False), - yaxis=dict(showgrid=False, zeroline=False, autorange="reversed", scaleanchor="x", scaleratio=1), + yaxis=dict( + showgrid=False, + zeroline=False, + autorange="reversed", + scaleanchor="x", + scaleratio=1, + ), width=800, height=800 * (matrix.shape[0] / matrix.shape[1]), ) @@ -53,7 +61,7 @@ def spy( def plot_point_cloud( - p # type: np.ndarray + p, # type: np.ndarray ): """ Plots an Nx3 point cloud with Plotly diff --git a/aerosandbox/visualization/plotly_Figure3D.py b/aerosandbox/visualization/plotly_Figure3D.py index 9cc06604b..200e1ea5e 100644 --- a/aerosandbox/visualization/plotly_Figure3D.py +++ b/aerosandbox/visualization/plotly_Figure3D.py @@ -13,10 +13,14 @@ def reflect_over_XZ_plane(input_vector): return input_vector * np.array([1, -1, 1]) elif len(shape) == 2: if not shape[1] == 3: - raise ValueError("The function expected either a 3-element vector or a Nx3 array!") + raise ValueError( + "The function expected either a 3-element vector or a Nx3 array!" + ) return input_vector * np.array([1, -1, 1]) else: - raise ValueError("The function expected either a 3-element vector or a Nx3 array!") + raise ValueError( + "The function expected either a 3-element vector or a Nx3 array!" + ) class Figure3D: @@ -44,10 +48,11 @@ def __init__(self): self.y_streamline = [] self.z_streamline = [] - def add_line(self, - points, - mirror=False, - ): + def add_line( + self, + points, + mirror=False, + ): """ Adds a line (or series of lines) to draw. :param points: an iterable with an arbitrary number of items. Each item is a 3D point, represented as an iterable of length 3. @@ -65,15 +70,13 @@ def add_line(self, self.z_line.append(None) if mirror: reflected_points = [reflect_over_XZ_plane(point) for point in points] - self.add_line( - points=reflected_points, - mirror=False - ) + self.add_line(points=reflected_points, mirror=False) - def add_streamline(self, - points, - mirror=False, - ): + def add_streamline( + self, + points, + mirror=False, + ): """ Adds a line (or series of lines) to draw. :param points: an iterable with an arbitrary number of items. Each item is a 3D point, represented as an iterable of length 3. @@ -91,17 +94,15 @@ def add_streamline(self, self.z_streamline.append(None) if mirror: reflected_points = [reflect_over_XZ_plane(point) for point in points] - self.add_streamline( - points=reflected_points, - mirror=False - ) - - def add_tri(self, - points, - intensity=0, - outline=False, - mirror=False, - ): + self.add_streamline(points=reflected_points, mirror=False) + + def add_tri( + self, + points, + intensity=0, + outline=False, + mirror=False, + ): """ Adds a triangular face to draw. :param points: an iterable with 3 items. Each item is a 3D point, represented as an iterable of length 3. @@ -131,15 +132,16 @@ def add_tri(self, points=reflected_points, intensity=intensity, outline=outline, - mirror=False + mirror=False, ) - def add_quad(self, - points, - intensity=0, - outline=True, - mirror=False, - ): + def add_quad( + self, + points, + intensity=0, + outline=True, + mirror=False, + ): """ Adds a quadrilateral face to draw. All points should be (approximately) coplanar if you want it to look right. :param points: an iterable with 4 items. Each item is a 3D point, represented as an iterable of length 3. Points should be given in sequential order. @@ -175,15 +177,16 @@ def add_quad(self, points=reflected_points, intensity=intensity, outline=outline, - mirror=False + mirror=False, ) - def draw(self, - show=True, - title="", - colorbar_title="", - colorscale="viridis", - ): + def draw( + self, + show=True, + title="", + colorbar_title="", + colorscale="viridis", + ): # Draw faces self.fig.add_trace( go.Mesh3d( @@ -197,7 +200,7 @@ def draw(self, intensity=self.intensity_face, colorbar=dict(title=colorbar_title), colorscale=colorscale, - showscale=colorbar_title is not None + showscale=colorbar_title is not None, ), ) @@ -207,9 +210,9 @@ def draw(self, x=self.x_line, y=self.y_line, z=self.z_line, - mode='lines', - name='', - line=dict(color='rgb(0,0,0)', width=3), + mode="lines", + name="", + line=dict(color="rgb(0,0,0)", width=3), showlegend=False, ) ) @@ -220,16 +223,16 @@ def draw(self, x=self.x_streamline, y=self.y_streamline, z=self.z_streamline, - mode='lines', - name='', - line=dict(color='rgba(119,0,255,200)', width=1), + mode="lines", + name="", + line=dict(color="rgba(119,0,255,200)", width=1), showlegend=False, ) ) self.fig.update_layout( title=title, - scene=dict(aspectmode='data'), + scene=dict(aspectmode="data"), ) if show: @@ -238,5 +241,5 @@ def draw(self, return self.fig -if __name__ == '__main__': +if __name__ == "__main__": fig = Figure3D() diff --git a/aerosandbox/weights/__init__.py b/aerosandbox/weights/__init__.py index a01fc88ac..3e8478b53 100644 --- a/aerosandbox/weights/__init__.py +++ b/aerosandbox/weights/__init__.py @@ -1,2 +1,4 @@ from aerosandbox.weights.mass_properties import * -from aerosandbox.weights.mass_properties_of_shapes import mass_properties_from_radius_of_gyration +from aerosandbox.weights.mass_properties_of_shapes import ( + mass_properties_from_radius_of_gyration, +) diff --git a/aerosandbox/weights/mass_properties.py b/aerosandbox/weights/mass_properties.py index 11fe936ca..9f227a328 100644 --- a/aerosandbox/weights/mass_properties.py +++ b/aerosandbox/weights/mass_properties.py @@ -30,18 +30,19 @@ class MassProperties(AeroSandboxObject): """ - def __init__(self, - mass: Union[float, np.ndarray] = None, - x_cg: Union[float, np.ndarray] = 0., - y_cg: Union[float, np.ndarray] = 0., - z_cg: Union[float, np.ndarray] = 0., - Ixx: Union[float, np.ndarray] = 0., - Iyy: Union[float, np.ndarray] = 0., - Izz: Union[float, np.ndarray] = 0., - Ixy: Union[float, np.ndarray] = 0., - Iyz: Union[float, np.ndarray] = 0., - Ixz: Union[float, np.ndarray] = 0., - ): + def __init__( + self, + mass: Union[float, np.ndarray] = None, + x_cg: Union[float, np.ndarray] = 0.0, + y_cg: Union[float, np.ndarray] = 0.0, + z_cg: Union[float, np.ndarray] = 0.0, + Ixx: Union[float, np.ndarray] = 0.0, + Iyy: Union[float, np.ndarray] = 0.0, + Izz: Union[float, np.ndarray] = 0.0, + Ixy: Union[float, np.ndarray] = 0.0, + Iyz: Union[float, np.ndarray] = 0.0, + Ixz: Union[float, np.ndarray] = 0.0, + ): """ Initializes a new MassProperties object. @@ -81,9 +82,10 @@ def __init__(self, """ if mass is None: import warnings + warnings.warn( "Defining a MassProperties object with zero mass. This can cause problems (divide-by-zero) in dynamics calculations, if this is not intended.\nTo silence this warning, please explicitly set `mass=0` in the MassProperties constructor.", - stacklevel=2 + stacklevel=2, ) mass = 0 @@ -108,15 +110,17 @@ def fmt(x: Union[float, Any], width=14) -> str: return f"{x:.8g}".rjust(width) return trim_string(str(x).rjust(width), length=40) - return "\n".join([ - "MassProperties instance:", - f" Mass : {fmt(self.mass)}", - f" Center of Gravity : ({fmt(self.x_cg)}, {fmt(self.y_cg)}, {fmt(self.z_cg)})", - f" Inertia Tensor : ", - f" (about CG) [{fmt(self.Ixx)}, {fmt(self.Ixy)}, {fmt(self.Ixz)}]", - f" [{fmt(self.Ixy)}, {fmt(self.Iyy)}, {fmt(self.Iyz)}]", - f" [{fmt(self.Ixz)}, {fmt(self.Iyz)}, {fmt(self.Izz)}]", - ]) + return "\n".join( + [ + "MassProperties instance:", + f" Mass : {fmt(self.mass)}", + f" Center of Gravity : ({fmt(self.x_cg)}, {fmt(self.y_cg)}, {fmt(self.z_cg)})", + f" Inertia Tensor : ", + f" (about CG) [{fmt(self.Ixx)}, {fmt(self.Ixy)}, {fmt(self.Ixz)}]", + f" [{fmt(self.Ixy)}, {fmt(self.Iyy)}, {fmt(self.Iyz)}]", + f" [{fmt(self.Ixz)}, {fmt(self.Iyz)}, {fmt(self.Izz)}]", + ] + ) def __getitem__(self, index) -> "MassProperties": """ @@ -142,8 +146,10 @@ def get_item_of_attribute(a): try: return a[index] except IndexError as e: - raise IndexError(f"A state variable could not be indexed; it has length {len(a)} while the" - f"parent has length {l}.") + raise IndexError( + f"A state variable could not be indexed; it has length {len(a)} while the" + f"parent has length {l}." + ) else: return a @@ -152,19 +158,16 @@ def get_item_of_attribute(a): "x_cg": self.x_cg, "y_cg": self.y_cg, "z_cg": self.z_cg, - "Ixx" : self.Ixx, - "Iyy" : self.Iyy, - "Izz" : self.Izz, - "Ixy" : self.Ixy, - "Iyz" : self.Iyz, - "Ixz" : self.Ixz, + "Ixx": self.Ixx, + "Iyy": self.Iyy, + "Izz": self.Izz, + "Ixy": self.Ixy, + "Iyz": self.Iyz, + "Ixz": self.Ixz, } return self.__class__( - **{ - k: get_item_of_attribute(v) - for k, v in inputs.items() - } + **{k: get_item_of_attribute(v) for k, v in inputs.items()} ) def __len__(self): @@ -188,7 +191,9 @@ def __len__(self): elif length == lv: pass else: - raise ValueError("State variables are appear vectorized, but of different lengths!") + raise ValueError( + "State variables are appear vectorized, but of different lengths!" + ) return length def __array__(self, dtype="O"): @@ -205,30 +210,25 @@ def __add__(self, other: "MassProperties") -> "MassProperties": Combines one MassProperties object with another. """ if not isinstance(other, MassProperties): - raise TypeError("MassProperties objects can only be added to other MassProperties objects.") + raise TypeError( + "MassProperties objects can only be added to other MassProperties objects." + ) total_mass = self.mass + other.mass total_x_cg = (self.mass * self.x_cg + other.mass * other.x_cg) / total_mass total_y_cg = (self.mass * self.y_cg + other.mass * other.y_cg) / total_mass total_z_cg = (self.mass * self.z_cg + other.mass * other.z_cg) / total_mass self_inertia_tensor_elements = self.get_inertia_tensor_about_point( - x=total_x_cg, - y=total_y_cg, - z=total_z_cg, - return_tensor=False + x=total_x_cg, y=total_y_cg, z=total_z_cg, return_tensor=False ) other_inertia_tensor_elements = other.get_inertia_tensor_about_point( - x=total_x_cg, - y=total_y_cg, - z=total_z_cg, - return_tensor=False + x=total_x_cg, y=total_y_cg, z=total_z_cg, return_tensor=False ) total_inertia_tensor_elements = [ I__ + J__ for I__, J__ in zip( - self_inertia_tensor_elements, - other_inertia_tensor_elements + self_inertia_tensor_elements, other_inertia_tensor_elements ) ] @@ -293,33 +293,32 @@ def __truediv__(self, other: float) -> "MassProperties": """ return self.__mul__(1 / other) - def allclose(self, - other: "MassProperties", - rtol=1e-5, - atol=1e-8, - equal_nan=False - ) -> bool: - return all([ - np.allclose( - getattr(self, attribute), - getattr(other, attribute), - rtol=rtol, - atol=atol, - equal_nan=equal_nan - ) - for attribute in [ - "mass", - "x_cg", - "y_cg", - "z_cg", - "Ixx", - "Iyy", - "Izz", - "Ixy", - "Iyz", - "Ixz", + def allclose( + self, other: "MassProperties", rtol=1e-5, atol=1e-8, equal_nan=False + ) -> bool: + return all( + [ + np.allclose( + getattr(self, attribute), + getattr(other, attribute), + rtol=rtol, + atol=atol, + equal_nan=equal_nan, + ) + for attribute in [ + "mass", + "x_cg", + "y_cg", + "z_cg", + "Ixx", + "Iyy", + "Izz", + "Ixy", + "Iyz", + "Ixz", + ] ] - ]) + ) @property def xyz_cg(self): @@ -329,9 +328,11 @@ def xyz_cg(self): def inertia_tensor(self): # Returns the inertia tensor about the component's centroid. return np.array( - [[self.Ixx, self.Ixy, self.Ixz], - [self.Ixy, self.Iyy, self.Iyz], - [self.Ixz, self.Iyz, self.Izz]] + [ + [self.Ixx, self.Ixy, self.Ixz], + [self.Ixy, self.Iyy, self.Iyz], + [self.Ixz, self.Iyz, self.Izz], + ] ) def inv_inertia_tensor(self): @@ -349,18 +350,15 @@ def inv_inertia_tensor(self): m23=self.Iyz, m13=self.Ixz, ) - return np.array( - [[iIxx, iIxy, iIxz], - [iIxy, iIyy, iIyz], - [iIxz, iIyz, iIzz]] - ) - - def get_inertia_tensor_about_point(self, - x: float = 0., - y: float = 0., - z: float = 0., - return_tensor: bool = True, - ): + return np.array([[iIxx, iIxy, iIxz], [iIxy, iIyy, iIyz], [iIxz, iIyz, iIzz]]) + + def get_inertia_tensor_about_point( + self, + x: float = 0.0, + y: float = 0.0, + z: float = 0.0, + return_tensor: bool = True, + ): """ Returns the inertia tensor about an arbitrary point. Using https://en.wikipedia.org/wiki/Parallel_axis_theorem#Tensor_generalization @@ -396,11 +394,13 @@ def get_inertia_tensor_about_point(self, Jxz = self.Ixz - self.mass * R[2] * R[0] if return_tensor: - return np.array([ - [Jxx, Jxy, Jxz], - [Jxy, Jyy, Jyz], - [Jxz, Jyz, Jzz], - ]) + return np.array( + [ + [Jxx, Jxy, Jxz], + [Jxy, Jyy, Jyz], + [Jxz, Jyz, Jzz], + ] + ) else: return Jxx, Jyy, Jzz, Jxy, Jyz, Jxz @@ -435,19 +435,23 @@ def is_physically_possible(self) -> bool: # ## This checks that the inertia tensor is positive definite, which is a necessary but not sufficient # condition for an inertia tensor to be physically possible. - impossible_conditions.extend([ - eigs[0] < 0, - eigs[1] < 0, - eigs[2] < 0, - ]) + impossible_conditions.extend( + [ + eigs[0] < 0, + eigs[1] < 0, + eigs[2] < 0, + ] + ) # ## This checks the triangle inequality, which is a necessary but not sufficient condition for an inertia # tensor to be physically possible. - impossible_conditions.extend([ - eigs[0] + eigs[1] < eigs[2], - eigs[0] + eigs[2] < eigs[1], - eigs[1] + eigs[2] < eigs[0], - ]) + impossible_conditions.extend( + [ + eigs[0] + eigs[1] < eigs[2], + eigs[0] + eigs[2] < eigs[1], + eigs[1] + eigs[2] < eigs[0], + ] + ) return not any(impossible_conditions) @@ -457,10 +461,11 @@ def is_point_mass(self) -> bool: """ return np.allclose(self.inertia_tensor, 0) - def generate_possible_set_of_point_masses(self, - method="optimization", - check_if_already_a_point_mass: bool = True, - ) -> List["MassProperties"]: + def generate_possible_set_of_point_masses( + self, + method="optimization", + check_if_already_a_point_mass: bool = True, + ) -> List["MassProperties"]: """ Generates a set of point masses (represented as MassProperties objects with zero inertia tensors), that, when combined, would yield this MassProperties object. @@ -489,12 +494,17 @@ def generate_possible_set_of_point_masses(self, opti = Opti() - approximate_radius = (self.Ixx + self.Iyy + self.Izz) ** 0.5 / self.mass + 1e-16 + approximate_radius = ( + self.Ixx + self.Iyy + self.Izz + ) ** 0.5 / self.mass + 1e-16 point_masses = [ MassProperties( mass=self.mass / 4, - x_cg=opti.variable(init_guess=self.x_cg - approximate_radius, scale=approximate_radius), + x_cg=opti.variable( + init_guess=self.x_cg - approximate_radius, + scale=approximate_radius, + ), y_cg=opti.variable(init_guess=self.y_cg, scale=approximate_radius), z_cg=opti.variable(init_guess=self.z_cg, scale=approximate_radius), ), @@ -502,18 +512,27 @@ def generate_possible_set_of_point_masses(self, mass=self.mass / 4, x_cg=opti.variable(init_guess=self.x_cg, scale=approximate_radius), y_cg=opti.variable(init_guess=self.y_cg, scale=approximate_radius), - z_cg=opti.variable(init_guess=self.z_cg + approximate_radius, scale=approximate_radius), + z_cg=opti.variable( + init_guess=self.z_cg + approximate_radius, + scale=approximate_radius, + ), ), MassProperties( mass=self.mass / 4, x_cg=opti.variable(init_guess=self.x_cg, scale=approximate_radius), y_cg=opti.variable(init_guess=self.y_cg, scale=approximate_radius), - z_cg=opti.variable(init_guess=self.z_cg - approximate_radius, scale=approximate_radius), + z_cg=opti.variable( + init_guess=self.z_cg - approximate_radius, + scale=approximate_radius, + ), ), MassProperties( mass=self.mass / 4, x_cg=opti.variable(init_guess=self.x_cg, scale=approximate_radius), - y_cg=opti.variable(init_guess=self.y_cg + approximate_radius, scale=approximate_radius), + y_cg=opti.variable( + init_guess=self.y_cg + approximate_radius, + scale=approximate_radius, + ), z_cg=opti.variable(init_guess=self.z_cg, scale=approximate_radius), ), ] @@ -539,7 +558,6 @@ def generate_possible_set_of_point_masses(self, return opti.solve(verbose=False)(point_masses) - elif method == "barbell": raise NotImplementedError("Barbell method not yet implemented!") principle_inertias, principle_axes = np.linalg.eig(self.inertia_tensor) @@ -547,9 +565,10 @@ def generate_possible_set_of_point_masses(self, else: raise ValueError("Bad value of `method` argument!") - def export_AVL_mass_file(self, - filename, - ) -> None: + def export_AVL_mass_file( + self, + filename, + ) -> None: """ Exports this MassProperties object to an AVL mass file. @@ -577,49 +596,52 @@ def export_AVL_mass_file(self, def fmt(x: float) -> str: return f"{x:.8g}".ljust(14) - lines.extend([ - " ".join([ - s.ljust(14) for s in [ - "# mass", - "x_cg", - "y_cg", - "z_cg", - "Ixx", - "Iyy", - "Izz", - "Ixy", - "Ixz", - "Iyz", - ] - ]), - " ".join([ - fmt(x) for x in [ - self.mass, - self.x_cg, - self.y_cg, - self.z_cg, - self.Ixx, - self.Iyy, - self.Izz, - -self.Ixy, - -self.Ixz, - -self.Iyz, - ] - ]) - ]) + lines.extend( + [ + " ".join( + [ + s.ljust(14) + for s in [ + "# mass", + "x_cg", + "y_cg", + "z_cg", + "Ixx", + "Iyy", + "Izz", + "Ixy", + "Ixz", + "Iyz", + ] + ] + ), + " ".join( + [ + fmt(x) + for x in [ + self.mass, + self.x_cg, + self.y_cg, + self.z_cg, + self.Ixx, + self.Iyy, + self.Izz, + -self.Ixy, + -self.Ixz, + -self.Iyz, + ] + ] + ), + ] + ) with open(filename, "w+") as f: f.write("\n".join(lines)) -if __name__ == '__main__': - mp1 = MassProperties( - mass=1 - ) - mp2 = MassProperties( - mass=1, - x_cg=1 - ) +if __name__ == "__main__": + mp1 = MassProperties(mass=1) + mp2 = MassProperties(mass=1, x_cg=1) mps = mp1 + mp2 assert mps.x_cg == 0.5 @@ -631,8 +653,14 @@ def fmt(x: float) -> str: while not valid: mass_props = MassProperties( mass=r(), - x_cg=r(), y_cg=r(), z_cg=r(), - Ixx=r(), Iyy=r(), Izz=r(), - Ixy=r(), Iyz=r(), Ixz=r(), + x_cg=r(), + y_cg=r(), + z_cg=r(), + Ixx=r(), + Iyy=r(), + Izz=r(), + Ixy=r(), + Iyz=r(), + Ixz=r(), ) valid = mass_props.is_physically_possible() # adds a bunch of checks diff --git a/aerosandbox/weights/mass_properties_of_shapes.py b/aerosandbox/weights/mass_properties_of_shapes.py index e02be2f7c..2c6a20fe1 100644 --- a/aerosandbox/weights/mass_properties_of_shapes.py +++ b/aerosandbox/weights/mass_properties_of_shapes.py @@ -8,13 +8,13 @@ def mass_properties_from_radius_of_gyration( - mass: float, - x_cg: float = 0, - y_cg: float = 0, - z_cg: float = 0, - radius_of_gyration_x: float = 0, - radius_of_gyration_y: float = 0, - radius_of_gyration_z: float = 0, + mass: float, + x_cg: float = 0, + y_cg: float = 0, + z_cg: float = 0, + radius_of_gyration_x: float = 0, + radius_of_gyration_y: float = 0, + radius_of_gyration_z: float = 0, ) -> MassProperties: """ Returns the mass properties of an object, given its radius of gyration. @@ -41,9 +41,9 @@ def mass_properties_from_radius_of_gyration( x_cg=x_cg, y_cg=y_cg, z_cg=z_cg, - Ixx=mass * radius_of_gyration_x ** 2, - Iyy=mass * radius_of_gyration_y ** 2, - Izz=mass * radius_of_gyration_z ** 2, + Ixx=mass * radius_of_gyration_x**2, + Iyy=mass * radius_of_gyration_y**2, + Izz=mass * radius_of_gyration_z**2, Ixy=0, Iyz=0, Ixz=0, @@ -51,10 +51,10 @@ def mass_properties_from_radius_of_gyration( def mass_properties_of_ellipsoid( - mass: float, - radius_x: float, - radius_y: float, - radius_z: float, + mass: float, + radius_x: float, + radius_y: float, + radius_z: float, ) -> MassProperties: """ Returns the mass properties of an ellipsoid centered on the origin. @@ -73,9 +73,9 @@ def mass_properties_of_ellipsoid( x_cg=0, y_cg=0, z_cg=0, - Ixx=0.2 * mass * (radius_y ** 2 + radius_z ** 2), - Iyy=0.2 * mass * (radius_z ** 2 + radius_x ** 2), - Izz=0.2 * mass * (radius_x ** 2 + radius_y ** 2), + Ixx=0.2 * mass * (radius_y**2 + radius_z**2), + Iyy=0.2 * mass * (radius_z**2 + radius_x**2), + Izz=0.2 * mass * (radius_x**2 + radius_y**2), Ixy=0, Iyz=0, Ixz=0, @@ -83,8 +83,8 @@ def mass_properties_of_ellipsoid( def mass_properties_of_sphere( - mass: float, - radius: float, + mass: float, + radius: float, ) -> MassProperties: """ Returns the mass properties of a sphere centered on the origin. @@ -97,18 +97,15 @@ def mass_properties_of_sphere( """ return mass_properties_of_ellipsoid( - mass=mass, - radius_x=radius, - radius_y=radius, - radius_z=radius + mass=mass, radius_x=radius, radius_y=radius, radius_z=radius ) def mass_properties_of_rectangular_prism( - mass: float, - length_x: float, - length_y: float, - length_z: float, + mass: float, + length_x: float, + length_y: float, + length_z: float, ) -> MassProperties: """ Returns the mass properties of a rectangular prism centered on the origin. @@ -127,9 +124,9 @@ def mass_properties_of_rectangular_prism( x_cg=0, y_cg=0, z_cg=0, - Ixx=1 / 12 * mass * (length_y ** 2 + length_z ** 2), - Iyy=1 / 12 * mass * (length_z ** 2 + length_x ** 2), - Izz=1 / 12 * mass * (length_x ** 2 + length_y ** 2), + Ixx=1 / 12 * mass * (length_y**2 + length_z**2), + Iyy=1 / 12 * mass * (length_z**2 + length_x**2), + Izz=1 / 12 * mass * (length_x**2 + length_y**2), Ixy=0, Iyz=0, Ixz=0, @@ -137,8 +134,8 @@ def mass_properties_of_rectangular_prism( def mass_properties_of_cube( - mass: float, - side_length: float, + mass: float, + side_length: float, ) -> MassProperties: """ Returns the mass properties of a cube centered on the origin.