def draw_streamlines(self, res=200, show=True): fig, ax = plt.subplots(1, 1, figsize=(6.4, 4.8), dpi=200) plt.xlim(-0.5, 1.5) plt.ylim(-0.5, 0.5) xrng = np.diff(np.array(ax.get_xlim())) yrng = np.diff(np.array(ax.get_ylim())) x = np.linspace(*ax.get_xlim(), int(np.round(res * xrng / yrng))) y = np.linspace(*ax.get_ylim(), res) X, Y = np.meshgrid(x, y) shape = X.shape X = X.flatten() Y = Y.flatten() U, V = self.calculate_velocity(X, Y) X = X.reshape(shape) Y = Y.reshape(shape) U = U.reshape(shape) V = V.reshape(shape) # NaN out any points inside the airfoil for airfoil in self.airfoils: contains = airfoil.contains_points(X, Y) U[contains] = np.NaN V[contains] = np.NaN speed = (U**2 + V**2)**0.5 Cp = 1 - speed**2 ### Draw the airfoils for airfoil in self.airfoils: plt.fill(airfoil.x(), airfoil.y(), "k", linewidth=0, zorder=4) plt.streamplot( x, y, U, V, color=speed, density=2.5, arrowsize=0, cmap=plt.get_cmap('coolwarm_r'), ) CB = plt.colorbar( orientation="horizontal", shrink=0.8, aspect=40, ) CB.set_label(r"Relative Airspeed ($U/U_\infty$)") plt.clim(0.6, 1.4) plt.gca().set_aspect('equal', adjustable='box') plt.xlabel(r"$x/c$") plt.ylabel(r"$y/c$") plt.title(rf"Inviscid Airfoil: Flow Field") plt.tight_layout() if show: plt.show()
def _enforce_governing_equations(self): for airfoil in self.airfoils: ### Compute normal velocities at the middle of each panel x_midpoints = np.trapz(airfoil.x()) y_midpoints = np.trapz(airfoil.y()) u_midpoints, v_midpoints = self.calculate_velocity( x_field=x_midpoints, y_field=y_midpoints, ) panel_dx = np.diff(airfoil.x()) panel_dy = np.diff(airfoil.y()) 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 yp_hat_x = -xp_hat_y yp_hat_y = xp_hat_x normal_velocities = u_midpoints * yp_hat_x + v_midpoints * yp_hat_y ### Add in flow tangency constraint self.opti.subject_to(normal_velocities == 0) ### Add in Kutta condition self.opti.subject_to(airfoil.gamma[0] + airfoil.gamma[-1] == 0)
def test_opti_hanging_chain_with_callback(plot=False): N = 40 m = 40 / N D = 70 * N g = 9.81 L = 1 opti = asb.Opti() x = opti.variable(init_guess=np.linspace(-2, 2, N)) y = opti.variable( init_guess=1, n_vars=N, ) distance = np.sqrt( # Distance from one node to the next np.diff(x)**2 + np.diff(y)**2) potential_energy_spring = 0.5 * D * np.sum((distance - L / N)**2) potential_energy_gravity = g * m * np.sum(y) potential_energy = potential_energy_spring + potential_energy_gravity opti.minimize(potential_energy) # Add end point constraints 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) # 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) fig, ax = plt.subplots(1, 1, figsize=(6.4, 4.8), dpi=200) x_ground = np.linspace(-2, 2, N) y_ground = np.cos(0.1 * x_ground) - 0.5 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) assert sol.value(potential_energy) == pytest.approx(626.462, abs=1e-3) if plot: plt.show()
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 ### Sum up the vorticity on this airfoil by integrating airfoil.vorticity = np.sum( (airfoil.gamma[1:] + airfoil.gamma[:-1]) / 2 * panel_length) 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
def test_quadcopter_navigation(): opti = asb.Opti() N = 300 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) mass = 0.1 dyn = asb.FreeBodyDynamics( opti_to_add_constraints_to=opti, time=time, xe=opti.variable(init_guess=np.linspace(0, 1, N)), ze=opti.variable(init_guess=np.linspace(0, -1, 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, 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, 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.minimize(effort) sol = opti.solve() dyn.substitute_solution(sol) assert sol.value(effort) == pytest.approx(0.714563, rel=0.01) print(sol.value(effort))
def test_block_move_fixed_time(): opti = asb.Opti() n_timesteps = 300 time = np.linspace(0, 1, n_timesteps) dyn = asb.DynamicsPointMass1DHorizontal( mass_props=asb.MassProperties(mass=1), x_e=opti.variable(init_guess=np.linspace(0, 1, n_timesteps)), u_e=opti.variable(init_guess=1, n_vars=n_timesteps), ) u = opti.variable(init_guess=np.linspace(1, -1, n_timesteps)) dyn.add_force( Fx=u ) dyn.constrain_derivatives( opti=opti, time=time ) opti.subject_to([ dyn.x_e[0] == 0, dyn.x_e[-1] == 1, dyn.u_e[0] == 0, dyn.u_e[-1] == 0, ]) # effort = np.sum( # np.trapz(dyn.X ** 2) * np.diff(time) # ) effort = np.sum( # More sophisticated integral-of-squares integration (closed form correct) np.diff(time) / 3 * (u[:-1] ** 2 + u[:-1] * u[1:] + u[1:] ** 2) ) opti.minimize(effort) sol = opti.solve() dyn.substitute_solution(sol) assert dyn.x_e[0] == pytest.approx(0) assert dyn.x_e[-1] == pytest.approx(1) assert dyn.u_e[0] == pytest.approx(0) assert dyn.u_e[-1] == pytest.approx(0) assert np.max(dyn.u_e) == pytest.approx(1.5, abs=0.01) assert sol.value(u)[0] == pytest.approx(6, abs=0.05) assert sol.value(u)[-1] == pytest.approx(-6, abs=0.05)
def peak_sun_hours_per_day_on_horizontal(latitude, day_of_year, scattering=True): """ How many hours of equivalent peak sun do you get per day? :param latitude: Latitude [degrees] :param day_of_year: Julian day (1 == Jan. 1, 365 == Dec. 31) :param time: Time since (local) solar noon [seconds] :param scattering: Boolean: include scattering effects at very low angles? :return: """ times = np.linspace(0, 86400, 1000) dt = np.diff(times) normalized_fluxes = ( # solar_flux_outside_atmosphere_normal(day_of_year) * incidence_angle_function(latitude, day_of_year, times, scattering) ) sun_hours = np.sum( (normalized_fluxes[1:] + normalized_fluxes[:-1]) / 2 * dt ) / 3600 return sun_hours
data = io.loadmat(str(root / "data" / "wind_data_99.mat")) lats_v = data["lats"].flatten() alts_v = data["alts"].flatten() speeds = data["speeds"].reshape(len(alts_v), len(lats_v)).T.flatten() lats, alts = np.meshgrid(lats_v, alts_v, indexing="ij") lats = lats.flatten() alts = alts.flatten() # %% lats_scaled = (lats - 37.5) / 11.5 alts_scaled = (alts - 24200) / 24200 speeds_scaled = (speeds - 7) / 56 alt_diff = np.diff(alts_v) alt_diff_aug = np.hstack((alt_diff[0], alt_diff, alt_diff[-1])) weights_1d = (alt_diff_aug[:-1] + alt_diff_aug[1:]) / 2 weights_1d = weights_1d / np.mean(weights_1d) # region_of_interest = np.logical_and( # alts_v > 10000, # alts_v < 40000 # ) # true_weights = np.where( # region_of_interest, # 2, # 1 # ) weights = np.tile(weights_1d, (93, 1)).flatten() # %%
def test_trapz(): a = np.arange(100) assert np.diff(np.trapz(a)) == pytest.approx(1)
def constrain_derivative(self, derivative: cas.MX, variable: cas.MX, with_respect_to: Union[np.ndarray, cas.MX], method: str = "midpoint", ) -> None: """ Adds a constraint to the optimization problem such that: d(variable) / d(with_respect_to) == derivative Can be used directly; also called indirectly by opti.derivative_of() for implicit derivative creation. Args: derivative: The derivative that is to be constrained here. variable: The variable or quantity that you are taking the derivative of. The "numerator" of the derivative, in colloquial parlance. with_respect_to: The variable or quantity that you are taking the derivative with respect to. The "denominator" of the derivative, in colloquial parlance. In a typical example case, this `with_respect_to` parameter would be time. Please make sure that the value of this parameter is monotonically increasing, otherwise you may get nonsensical answers. method: The type of integrator to use to define this derivative. Options are: * "forward euler" - a first-order-accurate forward Euler method Citation: https://en.wikipedia.org/wiki/Euler_method * "backwards euler" - a first-order-accurate backwards Euler method Citation: https://en.wikipedia.org/wiki/Backward_Euler_method * "midpoint" or "trapezoid" - a second-order-accurate midpoint method Citation: https://en.wikipedia.org/wiki/Midpoint_method * "simpson" - Simpson's rule for integration Citation: https://en.wikipedia.org/wiki/Simpson%27s_rule * "runge-kutta" or "rk4" - a fourth-order-accurate Runge-Kutta method. I suppose that technically, "forward euler", "backward euler", and "midpoint" are all (lower-order) Runge-Kutta methods... Citation: https://en.wikipedia.org/wiki/Runge%E2%80%93Kutta_methods#The_Runge%E2%80%93Kutta_method * "runge-kutta-3/8" - A modified version of the Runge-Kutta 4 proposed by Kutta in 1901. Also fourth-order-accurate, but all of the error coefficients are smaller than they are in the standard Runge-Kutta 4 method. The downside is that more floating point operations are required per timestep, as the Butcher tableau is more dense (i.e. not banded). Citation: Kutta, Martin (1901), "Beitrag zur näherungsweisen Integration totaler Differentialgleichungen", Zeitschrift für Mathematik und Physik, 46: 435–453 Note that all methods are expressed as integrators rather than differentiators; this prevents singularities from forming in the limit of timestep approaching zero. (For those coming from the PDE world, this is analogous to using finite volume methods rather than finite difference methods to allow shock capturing.) Returns: None (adds constraint in-place). """ d_var = np.diff(variable) d_time = np.diff(with_respect_to) # Calculate the timestep # TODO scale constraints by variable scale? # TODO make if method == "forward euler" or method == "forward" or method == "forwards": # raise NotImplementedError self.subject_to( d_var == derivative[:-1] * d_time ) elif method == "backward euler" or method == "backward" or method == "backwards": # raise NotImplementedError self.subject_to( d_var == derivative[1:] * d_time ) elif method == "midpoint" or method == "trapezoid" or method == "trapezoidal": self.subject_to( d_var == np.trapz(derivative) * d_time, ) elif method == "simpson": raise NotImplementedError elif method == "runge-kutta" or method == "rk4": raise NotImplementedError elif method == "runge-kutta-3/8": raise NotImplementedError else: raise ValueError("Bad value of `method`!")
def repanel( self, n_points_per_side: int = 100, ) -> 'Airfoil': """ Returns a repaneled version of the airfoil with cosine-spaced coordinates on the upper and lower surfaces. :param n_points_per_side: Number of points per side (upper and lower) of the airfoil [int] Notes: The number of points defining the final airfoil will be n_points_per_side*2-1, since one point (the leading edge point) is shared by both the upper and lower surfaces. :return: Returns the new airfoil. """ upper_original_coors = self.upper_coordinates( ) # Note: includes leading edge point, be careful about duplicates lower_original_coors = self.lower_coordinates( ) # Note: includes leading edge point, be careful about duplicates # Find distances between coordinates, assuming linear interpolation upper_distances_between_points = ( (upper_original_coors[:-1, 0] - upper_original_coors[1:, 0])**2 + (upper_original_coors[:-1, 1] - upper_original_coors[1:, 1])** 2)**0.5 lower_distances_between_points = ( (lower_original_coors[:-1, 0] - lower_original_coors[1:, 0])**2 + (lower_original_coors[:-1, 1] - lower_original_coors[1:, 1])** 2)**0.5 upper_distances_from_TE = np.hstack( (0, np.cumsum(upper_distances_between_points))) lower_distances_from_LE = np.hstack( (0, np.cumsum(lower_distances_between_points))) upper_distances_from_TE_normalized = upper_distances_from_TE / upper_distances_from_TE[ -1] lower_distances_from_LE_normalized = lower_distances_from_LE / lower_distances_from_LE[ -1] distances_from_TE_normalized = np.hstack( (upper_distances_from_TE_normalized, 1 + lower_distances_from_LE_normalized[1:])) # Generate a cosine-spaced list of points from 0 to 1 cosspaced_points = np.cosspace(0, 1, n_points_per_side) s = np.hstack(( cosspaced_points, 1 + cosspaced_points[1:], )) # Check that there are no duplicate points in the airfoil. if np.any(np.diff(distances_from_TE_normalized) == 0): raise ValueError( "This airfoil has a duplicated point (i.e. two adjacent points with the same (x, y) coordinates), so you can't repanel it!" ) x = interp1d( distances_from_TE_normalized, self.x(), kind="cubic", )(s) y = interp1d( distances_from_TE_normalized, self.y(), kind="cubic", )(s) return Airfoil(name=self.name, coordinates=stack_coordinates(x, y))
if __name__ == '__main__': mc = 0.7 drag = lambda mach: approximate_CD_wave( mach, mach_crit=mc, CD_wave_at_fully_supersonic=1, ) import matplotlib.pyplot as plt import aerosandbox.tools.pretty_plots as p fig, ax = plt.subplots(1, 3, figsize=(10, 5)) mach = np.linspace(0., 2, 10000) drag = drag(mach) ddragdm = np.gradient(drag, np.diff(mach)[0]) dddragdm = np.gradient(ddragdm, np.diff(mach)[0]) plt.sca(ax[0]) plt.title("$C_D$") plt.ylabel("$C_{D, wave} / C_{D, wave, M=1.2}$") plt.plot(mach, drag) plt.ylim(-0.05, 1.5) # plt.ylim(-0.01, 0.05) plt.sca(ax[1]) plt.title("$d(C_D)/d(M)$") plt.ylabel(r"$\frac{d(C_{D, wave})}{dM}$") plt.plot(mach, ddragdm) plt.ylim(-5, 15)
def draw( self, vehicle_model: Airplane = None, backend: str = "pyvista", draw_axes: bool = True, scale_vehicle_model: Union[float, None] = None, n_vehicles_to_draw: int = 10, cg_axes: str = "geometry", show: bool = True, ): if backend == "pyvista": import pyvista as pv import aerosandbox.tools.pretty_plots as p if vehicle_model is None: default_vehicle_stl = _asb_root / "dynamics/visualization/default_assets/yf23.stl" vehicle_model = pv.read(str(default_vehicle_stl)) elif isinstance(vehicle_model, pv.PolyData): pass elif isinstance(vehicle_model, Airplane): vehicle_model = vehicle_model.draw(backend="pyvista", show=False) vehicle_model.rotate_y( 180) # Rotate from geometry axes to body axes. elif isinstance( vehicle_model, str ): # Interpret the string as a filepath to a .stl or similar try: pv.read(filename=vehicle_model) except: raise ValueError("Could not parse `vehicle_model`!") else: raise TypeError( "`vehicle_model` should be an Airplane or PolyData object." ) x_e = np.array(self.x_e) y_e = np.array(self.y_e) z_e = np.array(self.z_e) if np.length(x_e) == 1: x_e = x_e * np.ones(len(self)) if np.length(y_e) == 1: y_e = y_e * np.ones(len(self)) if np.length(z_e) == 1: z_e = z_e * np.ones(len(self)) if scale_vehicle_model is None: trajectory_bounds = np.array([ [x_e.min(), x_e.max()], [y_e.min(), y_e.max()], [z_e.min(), z_e.max()], ]) trajectory_size = np.max(np.diff(trajectory_bounds, axis=1)) vehicle_bounds = np.array(vehicle_model.bounds).reshape((3, 2)) vehicle_size = np.max(np.diff(vehicle_bounds, axis=1)) scale_vehicle_model = 0.1 * trajectory_size / vehicle_size ### Initialize the plotter plotter = pv.Plotter() # Set the window title title = "ASB Dynamics" addenda = [] if scale_vehicle_model != 1: addenda.append( f"Vehicle drawn at {scale_vehicle_model:.2g}x scale") addenda.append(f"{self.__class__.__name__} Engine") if len(addenda) != 0: title = title + f" ({'; '.join(addenda)})" plotter.title = title # Draw axes and grid plotter.add_axes() plotter.show_grid(color='gray') ### Draw the vehicle for i in np.unique( np.round(np.linspace(0, len(self) - 1, n_vehicles_to_draw))).astype(int): dyn = self[i] try: phi = dyn.phi except AttributeError: phi = dyn.bank try: theta = dyn.theta except AttributeError: theta = dyn.gamma try: psi = dyn.psi except AttributeError: psi = dyn.track x_cg_b, y_cg_b, z_cg_b = dyn.convert_axes(dyn.mass_props.x_cg, dyn.mass_props.y_cg, dyn.mass_props.z_cg, from_axes=cg_axes, to_axes="body") this_vehicle = copy.deepcopy(vehicle_model) this_vehicle.translate([ -x_cg_b, -y_cg_b, -z_cg_b, ], inplace=True) this_vehicle.points *= scale_vehicle_model this_vehicle.rotate_x(np.degrees(phi), inplace=True) this_vehicle.rotate_y(np.degrees(theta), inplace=True) this_vehicle.rotate_z(np.degrees(psi), inplace=True) this_vehicle.translate([ dyn.x_e, dyn.y_e, dyn.z_e, ], inplace=True) plotter.add_mesh(this_vehicle, ) if draw_axes: rot = np.rotation_matrix_from_euler_angles(phi, theta, psi) axes_scale = 0.5 * np.max( np.diff(np.array(this_vehicle.bounds).reshape((3, -1)), axis=1)) origin = np.array([ dyn.x_e, dyn.y_e, dyn.z_e, ]) for i, c in enumerate(["r", "g", "b"]): plotter.add_mesh( pv.Spline( np.array( [origin, origin + rot[:, i] * axes_scale])), color=c, line_width=2.5, ) for i in range(len(self)): ### Draw the trajectory line polyline = pv.Spline(np.array([x_e, y_e, z_e]).T) plotter.add_mesh( polyline, color=p.adjust_lightness(p.palettes["categorical"][0], 1.2), line_width=3, ) ### Finalize the plotter plotter.camera.up = (0, 0, -1) plotter.camera.Azimuth(90) plotter.camera.Elevation(60) if show: plotter.show() return plotter
velocity = opti.derivative_of( position, with_respect_to=time, derivative_init_guess=1, ) force = opti.variable(init_guess=np.linspace(1, -1, n_timesteps), n_vars=n_timesteps) opti.constrain_derivative( variable=velocity, with_respect_to=time, derivative=force / mass_block, ) effort_expended = np.sum(np.trapz(force**2) * np.diff(time)) opti.minimize(effort_expended) ### Boundary conditions opti.subject_to([ position[0] == 0, position[-1] == 1, velocity[0] == 0, velocity[-1] == 0, ]) sol = opti.solve() import matplotlib.pyplot as plt import seaborn as sns
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))))
) # 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 ) # 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 ) ) # From AVF Eq. 6.38 d_theta_dx = np.diff(theta) / np.diff(x) d_ue_dx = np.diff(ue) / np.diff(x) d_H_star_dx = np.diff(H_star) / np.diff(x) def int(x): return (x[1:] + x[:-1]) / 2 def logint(x): # return int(x) logx = np.log(x) return np.exp( (logx[1:] + logx[:-1]) / 2 )
def test_diff(): a = np.arange(100) assert np.all(np.diff(a) == pytest.approx(1))