def Cd_cylinder(Re_D: float, subcritical_only=False) -> float: """ Returns the drag coefficient of a cylinder in crossflow as a function of its Reynolds number. :param Re_D: Reynolds number, referenced to diameter :param subcritical_only: Determines whether the model models purely subcritical (Re < 300k) cylinder flows. Useful, since this model is now convex and can be more well-behaved. :return: Drag coefficient # TODO rework this function to use tanh blending, which will mitigate overflows """ csigc = 5.5766722118597247 csigh = 23.7460859935990563 csub0 = -0.6989492360435040 csub1 = 1.0465189382830078 csub2 = 0.7044228755898569 csub3 = 0.0846501115443938 csup0 = -0.0823564417206403 csupc = 6.8020230357616764 csuph = 9.9999999999999787 csupscl = -0.4570690347113859 x = np.log10(np.abs(Re_D) + 1e-16) if subcritical_only: Cd = 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))))) Cd = 10**log10_Cd return Cd
def Cf_flat_plate(Re_L: float, method="hybrid-sharpe-convex") -> float: """ Returns the mean skin friction coefficient over a flat plate. Don't forget to double it (two sides) if you want a drag coefficient. Args: Re_L: Reynolds number, normalized to the length of the flat plate. method: The method of computing the skin friction coefficient. One of: * "blasius": Uses the Blasius solution. Citing Cengel and Cimbala, "Fluid Mechanics: Fundamentals and Applications", Table 10-4. Valid approximately for Re_L <= 5e5. * "turbulent": Uses turbulent correlations for smooth plates. Citing Cengel and Cimbala, "Fluid Mechanics: Fundamentals and Applications", Table 10-4. Valid approximately for 5e5 <= Re_L <= 1e7. * "hybrid-cengel": Uses turbulent correlations for smooth plates, but accounts for a non-negligible laminar run at the beginning of the plate. Citing Cengel and Cimbala, "Fluid Mechanics: Fundamentals and Applications", Table 10-4. Returns: Mean skin friction coefficient over a flat plate. Valid approximately for 5e5 <= Re_L <= 1e7. * "hybrid-schlichting": Schlichting's model, that roughly accounts for a non-negligtible laminar run. Citing "Boundary Layer Theory" 7th Ed., pg. 644 * "hybrid-sharpe-convex": A hybrid model that blends the Blasius and Schlichting models. Convex in log-log space; however, it may overlook some truly nonconvex behavior near transitional Reynolds numbers. * "hybrid-sharpe-nonconvex": A hybrid model that blends the Blasius and Cengel models. Nonconvex in log-log-space; however, it may capture some truly nonconvex behavior near transitional Reynolds numbers. You can view all of these functions graphically using `aerosandbox.library.aerodynamics.test_aerodynamics.test_Cf_flat_plate.py` """ Re_L = np.abs(Re_L) if method == "blasius": 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 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) 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)
def Cl_flat_plate(alpha, Re_c): """ Returns the approximate lift coefficient of a flat plate, following thin airfoil theory. :param alpha: Angle of attack [deg] :param Re_c: Reynolds number, normalized to the length of the flat plate. :return: Approximate lift coefficient. """ Re_c = np.abs(Re_c) alpha_rad = alpha * np.pi / 180 return 2 * np.pi * alpha_rad
def test_block_move_minimum_time(): opti = asb.Opti() n_timesteps = 300 time = np.linspace( 0, opti.variable(init_guess=1, lower_bound=0), n_timesteps, ) dyn = asb.DynamicsPointMass1DHorizontal( mass_props=asb.MassProperties(mass=1), x_e=opti.variable(init_guess=np.linspace(0, 1, n_timesteps)), u_e=opti.variable(init_guess=1, n_vars=n_timesteps), ) u = opti.variable(init_guess=np.linspace(1, -1, n_timesteps), lower_bound=-1, upper_bound=1) dyn.add_force( Fx=u ) dyn.constrain_derivatives( opti=opti, time=time ) opti.subject_to([ dyn.x_e[0] == 0, dyn.x_e[-1] == 1, dyn.u_e[0] == 0, dyn.u_e[-1] == 0, ]) opti.minimize( time[-1] ) sol = opti.solve() dyn.substitute_solution(sol) assert dyn.x_e[0] == pytest.approx(0) assert dyn.x_e[-1] == pytest.approx(1) assert dyn.u_e[0] == pytest.approx(0) assert dyn.u_e[-1] == pytest.approx(0) assert np.max(dyn.u_e) == pytest.approx(1, abs=0.01) assert sol.value(u)[0] == pytest.approx(1, abs=0.05) assert sol.value(u)[-1] == pytest.approx(-1, abs=0.05) assert np.mean(np.abs(sol.value(u))) == pytest.approx(1, abs=0.01)
def spy( matrix, show=True, ): """ Plots the sparsity pattern of a matrix. :param matrix: The matrix to plot the sparsity pattern of. [2D ndarray or CasADi array] :param show: Whether or not to show the sparsity plot. [boolean] :return: The figure to be plotted [go.Figure] """ try: matrix = matrix.toarray() except: pass 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])) i_index = i_index_map[sparsity_pattern] j_index = j_index_map[sparsity_pattern] val = matrix[sparsity_pattern] val = np.ones_like(i_index) fig = go.Figure( data=go.Heatmap( y=i_index, x=j_index, z=val, # type='heatmap', 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), width=800, height=800 * (matrix.shape[0] / matrix.shape[1]), ) if show: fig.show() return fig
def steady_state_performance(self, speed, grade=0, headwind=0): throttle = optimize.minimize( fun=lambda throttle: (self.performance(speed=speed, throttle_state=throttle, grade=grade, headwind=headwind)['net acceleration'])**2, x0=np.array([0.5]), bounds=[(0, 1)]).x perf = self.performance( speed=speed, throttle_state=throttle, grade=grade, headwind=headwind, ) if throttle > 1 or throttle < 0 or np.abs( perf['net acceleration']) >= 1e-6: raise ValueError( "Could not satisfy zero-net-acceleration condition!") return perf
def goodness_of_fit(self, type="R^2"): """ Returns a metric of the goodness of the fit. Args: type: Type of metric to use for goodness of fit. One of: * "R^2": The coefficient of determination. Strictly speaking only mathematically rigorous to use this for linear fits. https://en.wikipedia.org/wiki/Coefficient_of_determination * "deviation" or "Linf": The maximum deviation of the fit from any of the data points. Returns: The metric of the goodness of the fit. """ if type == "R^2": y_mean = np.mean(self.y_data) SS_tot = np.sum((self.y_data - y_mean)**2) y_model = self(self.x_data) SS_res = np.sum((self.y_data - y_model)**2) R_squared = 1 - SS_res / SS_tot return R_squared elif type == "deviation" or type == "Linf": return np.max(np.abs(self.y_data - self(self.x_data))) else: raise ValueError("Bad value of `type`!")
def firefly_CLA_and_CDA_fuse_hybrid( # TODO remove fuse_fineness_ratio, fuse_boattail_angle, fuse_TE_diameter, fuse_length, fuse_diameter, alpha, V, mach, rho, mu, ): """ Estimated equiv. lift area and equiv. drag area of the Firefly fuselage, component buildup. :param fuse_fineness_ratio: Fineness ratio of the fuselage nose (length / diameter). 0.5 is hemispherical. :param fuse_boattail_angle: Boattail half-angle [deg] :param fuse_TE_diameter: Diameter of the fuselage's base at the "trailing edge" [m] :param fuse_length: Length of the fuselage [m] :param fuse_diameter: Diameter of the fuselage [m] :param alpha: Angle of attack [deg] :param V: Airspeed [m/s] :param mach: Mach number [unitless] :param rho: Air density [kg/m^3] :param mu: Dynamic viscosity of air [Pa*s] :return: A tuple of (CLA, CDA) [m^2] """ alpha_rad = alpha * np.pi / 180 sin_alpha = np.sind(alpha) cos_alpha = np.cosd(alpha) """ Lift of a truncated fuselage, following slender body theory. """ separation_location = 0.3 # 0 for separation at trailing edge, 1 for separation at start of boat-tail. Calibrate this. diameter_at_separation = ( 1 - separation_location ) * fuse_TE_diameter + separation_location * fuse_diameter fuse_TE_area = np.pi / 4 * diameter_at_separation**2 CLA_slender_body = 2 * fuse_TE_area * alpha_rad # Derived from FVA 6.6.5, Eq. 6.75 """ Crossflow force, following the method of Jorgensen, 1977: "Prediction of Static Aero. Chars. for Slender..." """ V_crossflow = V * sin_alpha Re_crossflow = rho * np.abs(V_crossflow) * fuse_diameter / mu mach_crossflow = mach * np.abs(sin_alpha) eta = 1 # TODO make this a function of overall fineness ratio Cdn = 0.2 # Taken from suggestion for supercritical cylinders in Hoerner's Fluid Dynamic Drag, pg. 3-11 S_planform = fuse_diameter * fuse_length CNA = eta * Cdn * S_planform * sin_alpha * np.abs(sin_alpha) CLA_crossflow = CNA * cos_alpha CDA_crossflow = CNA * sin_alpha CLA = CLA_slender_body + CLA_crossflow r""" Zero-lift_force drag_force Model derived from high-fidelity data at: C:\Projects\GitHub\firefly_aerodynamics\Design_Opt\studies\Circular Firefly Fuse CFD\Zero-Lift Drag """ c = np.array([ 112.57153128951402720758778741583, -7.1720570832587240417410612280946, -0.01765596807595304351679033061373, 0.0026135564778264172743071913629365, 550.8775012129947299399645999074, 3.3166868391027000129156476759817, 11774.081980549422951298765838146, 3073258.204571904614567756652832, 0.0299, ]) # coefficients fuse_Re = rho * np.abs(V) * fuse_length / mu CDA_zero_lift = ( (c[0] * np.exp(c[1] * fuse_fineness_ratio) + c[2] * fuse_boattail_angle + c[3] * fuse_boattail_angle**2 + c[4] * fuse_TE_diameter**2 + c[5]) / c[6] * (fuse_Re / c[7])**(-1 / 7) * (fuse_length * fuse_diameter) / c[8]) r""" Assumes a ring-shaped wake projection into the Trefftz plane with a sinusoidal circulation distribution. This results in a uniform grad(phi) within the ring in the Trefftz plane. Derivation at C:\Projects\GitHub\firefly_aerodynamics\Gists and Ideas\Ring Wing Potential Flow """ fuse_oswalds_efficiency = 0.5 CDiA_slender_body = CLA**2 / ( diameter_at_separation**2 * np.pi * fuse_oswalds_efficiency) / 2 # or equivalently, use the next line # CDiA_slender_body = fuse_TE_area * alpha_rad ** 2 / 2 CDA = CDA_crossflow + CDiA_slender_body + CDA_zero_lift return CLA, CDA
def test_rocket(): ### Environment opti = asb.Opti() ### Time discretization N = 500 # Number of discretization points time_final = 100 # seconds time = np.linspace(0, time_final, N) ### Constants 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 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) w_e=opti.variable(init_guess=-z_e_final / time_final, n_vars=N), # Velocity ) dyn.add_gravity_force(g=g) thrust = opti.variable(init_guess=g * mass_initial, n_vars=N) dyn.add_force(Fz=-thrust) dyn.constrain_derivatives( opti=opti, time=time, ) ### Fuel burn opti.constrain_derivative( derivative=-alpha * thrust, variable=dyn.mass_props.mass, with_respect_to=time, method="midpoint", ) ### 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, ]) ### Path constraints 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 ### Solve sol = opti.solve(verbose=False) print(f"Solved in {sol.stats()['iter_count']} iterations.") dyn.substitute_solution(sol) assert dyn.mass_props.mass[-1] == pytest.approx(290049.81034472014, rel=0.05) assert np.abs(dyn.w_e).max() == pytest.approx(1448, rel=0.05)
def ln(x): return np.log(np.abs(x))
def fuselage_aerodynamics( self, fuselage: Fuselage, ): """ Estimates the aerodynamic forces, moments, and derivatives on a fuselage in isolation. Assumes: * The fuselage is a body of revolution aligned with the x_b axis. * The angle between the nose and the freestream is less than 90 degrees. Moments are given with the reference at Fuselage [0, 0, 0]. Uses methods from Jorgensen, Leland Howard. "Prediction of Static Aerodynamic Characteristics for Slender Bodies Alone and with Lifting Surfaces to Very High Angles of Attack". NASA TR R-474. 1977. Args: fuselage: A Fuselage object that you wish to analyze. Returns: """ ##### Alias a few things for convenience op_point = self.op_point Re = op_point.reynolds(reference_length=fuselage.length()) fuse_options = self.get_options(fuselage) ####### Reference quantities (Set these 1 here, just so we can follow Jorgensen syntax.) # Outputs of this function should be invariant of these quantities, if normalization has been done correctly. S_ref = 1 # m^2 c_ref = 1 # m ####### Fuselage zero-lift drag estimation ### Forebody drag C_f_forebody = aerolib.Cf_flat_plate(Re_L=Re) ### Base Drag C_D_base = 0.029 / np.sqrt(C_f_forebody) * fuselage.area_base() / S_ref ### Skin friction drag C_D_skin = C_f_forebody * fuselage.area_wetted() / S_ref ### Wave drag if self.include_wave_drag: sears_haack_drag = transonic.sears_haack_drag_from_volume( volume=fuselage.volume(), length=fuselage.length()) C_D_wave = transonic.approximate_CD_wave( mach=op_point.mach(), mach_crit=critical_mach( fineness_ratio_nose=fuse_options["nose_fineness_ratio"]), CD_wave_at_fully_supersonic=fuse_options["E_wave_drag"] * sears_haack_drag, ) else: C_D_wave = 0 ### Total zero-lift drag C_D_zero_lift = C_D_skin + C_D_base + C_D_wave ####### Jorgensen model ### First, merge the alpha and beta into a single "generalized alpha", which represents the degrees between the fuselage axis and the freestream. x_w, y_w, z_w = op_point.convert_axes(1, 0, 0, from_axes="body", to_axes="wind") generalized_alpha = np.arccosd(x_w / (1 + 1e-14)) sin_generalized_alpha = np.sind(generalized_alpha) cos_generalized_alpha = x_w # ### Limit generalized alpha to -90 < alpha < 90, for now. # generalized_alpha = np.clip(generalized_alpha, -90, 90) # # TODO make the drag/moment functions not give negative results for alpha > 90. alpha_fractional_component = -z_w / np.sqrt( y_w**2 + z_w**2 + 1e-16 ) # The fraction of any "generalized lift" to be in the direction of alpha beta_fractional_component = y_w / np.sqrt( y_w**2 + z_w**2 + 1e-16 ) # The fraction of any "generalized lift" to be in the direction of beta ### Compute normal quantities ### Note the (N)ormal, (A)ligned coordinate system. (See Jorgensen for definitions.) # M_n = sin_generalized_alpha * op_point.mach() Re_n = sin_generalized_alpha * Re # V_n = sin_generalized_alpha * op_point.velocity q = op_point.dynamic_pressure() x_nose = fuselage.xsecs[0].xyz_c[0] x_m = 0 - x_nose x_c = fuselage.x_centroid_projected() - x_nose ##### Potential flow crossflow model C_N_p = ( # Normal force coefficient due to potential flow. (Jorgensen Eq. 2.12, part 1) fuselage.area_base() / S_ref * np.sind(2 * generalized_alpha) * np.cosd(generalized_alpha / 2)) C_m_p = ((fuselage.volume() - fuselage.area_base() * (fuselage.length() - x_m)) / (S_ref * c_ref) * np.sind(2 * generalized_alpha) * np.cosd(generalized_alpha / 2)) ##### Viscous crossflow model C_d_n = np.where( Re_n != 0, aerolib.Cd_cylinder( Re_D=Re_n ), # Replace with 1.20 from Jorgensen Table 1 if not working well 0) eta = jorgensen_eta(fuselage.fineness_ratio()) C_N_v = ( # Normal force coefficient due to viscous crossflow. (Jorgensen Eq. 2.12, part 2) eta * C_d_n * fuselage.area_projected() / S_ref * sin_generalized_alpha**2) C_m_v = (eta * C_d_n * fuselage.area_projected() / S_ref * (x_m - x_c) / c_ref * sin_generalized_alpha**2) ##### Total C_N model C_N = C_N_p + C_N_v C_m_generalized = C_m_p + C_m_v ##### Total C_A model C_A = C_D_zero_lift * cos_generalized_alpha * np.abs( cos_generalized_alpha) ##### Convert to lift, drag C_L_generalized = C_N * cos_generalized_alpha - C_A * sin_generalized_alpha C_D = C_N * sin_generalized_alpha + C_A * cos_generalized_alpha ### Set proper directions C_L = C_L_generalized * alpha_fractional_component C_Y = -C_L_generalized * beta_fractional_component C_l = 0 C_m = C_m_generalized * alpha_fractional_component C_n = -C_m_generalized * beta_fractional_component ### Un-normalize L = C_L * q * S_ref Y = C_Y * q * S_ref D = C_D * q * S_ref l_w = C_l * q * S_ref * c_ref m_w = C_m * q * S_ref * c_ref n_w = C_n * q * S_ref * c_ref ### Convert to axes coordinates for reporting F_w = (-D, Y, -L) F_b = op_point.convert_axes(*F_w, from_axes="wind", to_axes="body") F_g = op_point.convert_axes(*F_b, from_axes="body", to_axes="geometry") M_w = ( l_w, m_w, n_w, ) M_b = op_point.convert_axes(*M_w, from_axes="wind", to_axes="body") M_g = op_point.convert_axes(*M_b, from_axes="body", to_axes="geometry") return { "F_g": F_g, "F_b": F_b, "F_w": F_w, "M_g": M_g, "M_b": M_b, "M_w": M_w, "L": -F_w[2], "Y": F_w[1], "D": -F_w[0], "l_b": M_b[0], "m_b": M_b[1], "n_b": M_b[2] }
) H = opti.variable( H_0, n_vars=N, ) 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 ) # 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 ) # 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
def Cd_cylinder(Re_D: float, mach: float = 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. Args: Re_D: Reynolds number, referenced to diameter mach: Mach number include_mach_effects: If this is set False, it assumes Mach = 0, which simplifies the computation. subcritical_only: Determines whether the model models purely subcritical (Re < 300k) cylinder flows. Useful, since this model is now convex and can be more well-behaved. Returns: # TODO rework this function to use tanh blending, which will mitigate overflows """ ##### Do the viscous part of the computation csigc = 5.5766722118597247 csigh = 23.7460859935990563 csub0 = -0.6989492360435040 csub1 = 1.0465189382830078 csub2 = 0.7044228755898569 csub3 = 0.0846501115443938 csup0 = -0.0823564417206403 csupc = 6.8020230357616764 csuph = 9.9999999999999787 csupscl = -0.4570690347113859 x = np.log10(np.abs(Re_D) + 1e-16) 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))))) 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 Cd = Cd_mach_0 * Cd_over_Cd_mach_0 else: Cd = Cd_mach_0 return Cd
def performance( self, speed, throttle_state=1, grade=0, headwind=0, ): ##### Figure out electric thrust force wheel_radius = self.wheel_diameter / 2 wheel_rads_per_sec = speed / wheel_radius wheel_rpm = wheel_rads_per_sec * 30 / np.pi motor_rpm = wheel_rpm / self.gear_ratio ### Limit performance by either max voltage or max current perf_via_max_voltage = self.motor.performance( voltage=self.max_voltage, rpm=motor_rpm, ) perf_via_throttle = self.motor.performance( current=self.max_current * throttle_state, rpm=motor_rpm, ) if perf_via_max_voltage['torque'] > perf_via_throttle['torque']: perf = perf_via_throttle else: perf = perf_via_max_voltage motor_torque = perf['torque'] wheel_torque = motor_torque / self.gear_ratio wheel_force = wheel_torque / wheel_radius thrust = wheel_force ##### Gravity gravity_drag = 9.81 * np.sin(np.arctan(grade)) * self.mass ##### Rolling Resistance # Crr = 0.0020 # Concrete Crr = 0.0050 # Asphalt # Crr = 0.0060 # Gravel # Crr = 0.0070 # Grass # Crr = 0.0200 # Off-road # Crr = 0.0300 # Sand rolling_drag = 9.81 * np.cos(np.arctan(grade)) * self.mass * Crr ##### Aerodynamics # CDA = 0.408 # Tops CDA = 0.324 # Hoods # CDA = 0.307 # Drops # CDA = 0.2914 # Aerobars eff_speed = speed + headwind air_drag = 0.5 * atmo.density() * eff_speed * np.abs(eff_speed) * CDA ##### Summation net_force = thrust - gravity_drag - rolling_drag - air_drag net_accel = net_force / self.mass return { "net acceleration": net_accel, "motor state": perf, }
opti = asb.Opti() theta = opti.variable( init_guess=theta_0, n_vars=N, ) H = opti.variable( H_0, n_vars=N, ) 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) # 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) # 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