def Cd_cylinder(Re_D, subcritical_only=False): """ 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 """ 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 = cas.log10(Re_D) if subcritical_only: Cd = 10**(csub0 * x + csub1) + csub2 + csub3 * x return Cd else: log10_Cd = ( (cas.log10(10**(csub0 * x + csub1) + csub2 + csub3 * x)) * (1 - 1 / (1 + cas.exp(-csigh * (x - csigc)))) + (csup0 + csupscl / csuph * cas.log(cas.exp(csuph * (csupc - x)) + 1)) * (1 / (1 + cas.exp(-csigh * (x - csigc))))) Cd = 10**log10_Cd return Cd
def Cl_rae2822(alpha, Re_c): # A curve fit I did to a RAE2822 airfoil, 2D XFoil data. Incompressible flow. # Within -2 < alpha < 12 and 10^4 < Re_c < 10^6, has R^2 = 0.9857 # Likely valid from -6 < alpha < 12 and 10^4 < Re_c < 10^6. # See: C:\Projects\GitHub\firefly_aerodynamics\Gists and Ideas\XFoil Drag Fitting\rae2822 Re_c = cas.fmax(Re_c, 1) log10_Re = cas.log10(Re_c) # Coeffs a1l = 5.5686866813855172e-02 a1t = 9.7472055628494134e-02 a4l = -7.2145733312046152e-09 a4t = -3.6886704372829236e-06 atr = 8.3723547264375520e-01 atr2 = -8.3128119739031697e-02 c0l = -4.9103908291438701e-02 c0t = 2.3903424824298553e-01 ctr = 1.3082854754897108e+01 rtr = 2.6963082864300731e+00 a = alpha r = log10_Re Cl = (c0t + a1t * a + a4t * a**4) * 1 / ( 1 + cas.exp(ctr - rtr * r - atr * a - atr2 * a**2)) + ( c0l + a1l * a + a4l * a**4) * ( 1 - 1 / (1 + cas.exp(ctr - rtr * r - atr * a - atr2 * a**2))) return Cl
def Cd_profile_e216(alpha, Re_c): # A curve fit I did to a Eppler 216 (e216) airfoil, 2D XFoil data. Incompressible flow. # Within -2 < alpha < 12 and 10^4 < Re_c < 10^6, has R^2 = 0.9995 # Likely valid from -6 < alpha < 12 and 10^4 < Re_c < 10^6. # see: C:\Projects\GitHub\firefly_aerodynamics\Gists and Ideas\XFoil Drag Fitting\e216 Re_c = cas.fmax(Re_c, 1) log10_Re = cas.log10(Re_c) # Coeffs a1l = 4.7167470806940448e-02 a1t = 7.5663005080888857e-02 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 a = alpha r = log10_Re log10_Cd = (c0t + a1t * a + a4t * a**4) * 1 / ( 1 + cas.exp(ctr - rtr * r - atr * a - rtr2 * r**2)) + ( c0l + a1l * a + a2l * a**2) * ( 1 - 1 / (1 + cas.exp(ctr - rtr * r - atr * a - rtr2 * r**2))) Cd = 10**log10_Cd return Cd
def Cl_e216(alpha, Re_c): # A curve fit I did to a Eppler 216 (e216) airfoil, 2D XFoil data. Incompressible flow. # Within -2 < alpha < 12 and 10^4 < Re_c < 10^6, has R^2 = 0.9994 # Likely valid from -6 < alpha < 12 and 10^4 < Re_c < Inf. # See: C:\Projects\GitHub\firefly_aerodynamics\Gists and Ideas\XFoil Drag Fitting\e216 Re_c = cas.fmax(Re_c, 1) log10_Re = cas.log10(Re_c) # Coeffs a1l = 3.0904412662858878e-02 a1t = 9.6452654383488254e-02 a4t = -2.5633334023068302e-05 asl = 6.4175433185427011e-01 atr = 3.6775107602844948e-01 c0l = -2.5909363461176749e-01 c0t = 8.3824440586718862e-01 ctr = 1.1431810545735890e+02 ksl = 5.3416670116733611e-01 rtr = 3.9713338634462829e+01 rtr2 = -3.3634858542657771e+00 xsl = -1.2220899840236835e-01 a = alpha r = log10_Re Cl = (c0t + a1t * a + a4t * a**4) * 1 / ( 1 + cas.exp(ctr - rtr * r - atr * a - rtr2 * r**2)) + ( c0l + a1l * a + asl / (1 + cas.exp(-ksl * (a - xsl)))) * ( 1 - 1 / (1 + cas.exp(ctr - rtr * r - atr * a - rtr2 * r**2))) return Cl
def mass_motor_electric( max_power, kv_rpm_volt=1000, # This is in rpm/volt, not rads/sec/volt! voltage=20, method="astroflight"): """ Estimates the mass of a brushless DC electric motor. Curve fit to scraped Hobbyking BLDC motor data as of 2/24/2020. Estimated range of validity: 50 < max_power < 10000 :param max_power: maximum power [W] :param kv: Voltage constant of the motor, measured in rpm/volt, not rads/sec/volt! [rpm/volt] :param voltage: Operating voltage of the motor [V] :param method: method to use. "burton", "hobbyking", or "astroflight" (increasing level of detail). Burton source: https://dspace.mit.edu/handle/1721.1/112414 Hobbyking source: C:\Projects\GitHub\MotorScraper, https://github.com/austinstover/MotorScraper Astroflight source: Gates, et. al., "Combined Trajectory, Propulsion, and Battery Mass Optimization for Solar-Regen..." https://scholarsarchive.byu.edu/cgi/viewcontent.cgi?article=3932&context=facpub Validity claimed from 1.5 kW to 15 kW, kv from 32 to 1355. :return: estimated motor mass [kg] """ if method == "burton": return max_power / 4128 # Less sophisticated model. 95% CI (3992, 4263), R^2 = 0.866 elif method == "hobbyking": return 10**(0.8205 * cas.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
def Cd_profile_rae2822(alpha, Re_c): # A curve fit I did to a RAE2822 airfoil, 2D XFoil data. Incompressible flow. # Within -2 < alpha < 12 and 10^4 < Re_c < 10^6, has R^2 = 0.9995 # Likely valid from -6 < alpha < 12 and 10^4 < Re_c < Inf. # see: C:\Projects\GitHub\firefly_aerodynamics\Gists and Ideas\XFoil Drag Fitting\e216 Re_c = cas.fmax(Re_c, 1) log10_Re = cas.log10(Re_c) # Coeffs at = 8.1034027621509015e+00 c0l = -8.4296746456429639e-01 c0t = -1.3700609138855402e+00 kart = -4.1609994062600880e-01 kat = 5.9510959342452441e-01 krt = -7.1938030052506197e-01 r1l = 1.1548628822014631e-01 r1t = -4.9133662875044504e-01 rt = 5.0070459892411696e+00 a = alpha r = log10_Re log10_Cd = (c0t + r1t * (r - 4)) * ( 1 / (1 + cas.exp(kat * (a - at) + krt * (r - rt) + kart * (a - at) * (r - rt)))) + ( c0l + r1l * (r - 4)) * ( 1 - 1 / (1 + cas.exp(kat * (a - at) + krt * (r - rt) + kart * (a - at) * (r - rt)))) Cd = 10 ** log10_Cd return Cd
def power_human( duration, # type: float dataset="Healthy Men" # type: str ): """ Finds the power output that a human can sustain for a given duration. Data was fit for durations in the range of 6 seconds to 60,000 seconds. Fits are modeled at: AeroSandbox/studies/HumanPower Data Source: Bicycling Science by D. Wilson, 2004. Figure 2.4. Wilson is aggregating many data sources here. The raw data pulls from a variety of sources: * NASA SP-3006, 1964 * U.K. amateur trials and time-trials records (Whitt, F.R. 1971 "A note on the estimation of the energy expenditure of sporting cyclists." Ergonomics 14) * Wilsons' own analyses Weight estimates for tests subjects are unfortunately not given. :param duration: Time to sustain power output [seconds] :param dataset: Dataset to pull from. A string that is one of the following: "Healthy Men", "First-Class Athletes", "World-Class Athletes", :return: Sustainable power output for the specified duration [W] """ if dataset == "Healthy Men": a = 373.153360 b0 = -0.173127 b1 = 0.083282 b2 = -0.042785 elif dataset == "First-Class Athletes": a = 502.332185 b0 = -0.179030 b1 = 0.097926 b2 = -0.024855 elif dataset == "World-Class Athletes": a = 869.963370 b0 = -0.234291 b1 = 0.064395 b2 = -0.009197 else: raise ValueError("Bad value of 'dataset'!") duration_mins = duration / 60 log_duration_mins = cas.log10(duration_mins) return a * duration_mins**( b0 + b1 * log_duration_mins + b2 * log_duration_mins**2 ) # essentially, a cubic in log-log space
def mass_gearbox( power, rpm_in, rpm_out, ): """ Estimates the mass of a gearbox. Based on data from NASA/TM-2009-215680, available here: https://ntrs.nasa.gov/archive/nasa/casi.ntrs.nasa.gov/20090042817.pdf R^2 = 0.92 to the data. To quote this document: "The correlation was developed based on actual weight data from over fifty rotorcrafts, tiltrotors, and turboprop aircraft." Data fits in the NASA document were thrown out and refitted to extrapolate more sensibly; see: C:\Projects\GitHub\AeroSandbox\studies\GearboxMassFits :param power: Shaft power through the gearbox [W] :param rpm_in: RPM of the input to the gearbox [rpm] :param rpm_out: RPM of the output of the gearbox [rpm] :return: Estimated mass of the gearbox [kg] """ power_hp = power / 745.7 beta = (power_hp / rpm_out)**0.75 * (rpm_in / rpm_out)**0.15 # Beta is a parametric value that tends to collapse the masses of gearboxes onto a line. # Data fit is considered tightly valid for gearboxes with 1 < beta < 100. Sensible extrapolations are made beyond that. p1 = 1.0445171124733774 p2 = 2.0083615496306910 mass_lb = 10**(p1 * cas.log10(beta) + p2) mass = mass_lb / 2.20462262185 return mass
def fit( model, # type: callable x_data, # type: dict y_data, # type: np.ndarray param_guesses, # type: dict param_bounds=None, # type: dict weights=None, # type: np.ndarray verbose=True, # type: bool scale_problem=True, # type: bool put_residuals_in_logspace=False, # type: bool ): """ Fits a model to data through least-squares minimization. :param model: A callable with syntax f(x, p) where: x is a dict of dependent variables. Same format as x_data [dict of 1D ndarrays of length n]. p is a dict of parameters. Same format as param_guesses [dict of scalars]. Model should use CasADi functions for differentiability. :param x_data: a dict of dependent variables. Same format as model's x. [dict of 1D ndarrays of length n] :param y_data: independent variable. [1D ndarray of length n] :param param_guesses: a dict of fit parameters. Same format as model's p. Keys are parameter names, values are initial guesses. [dict of scalars] :param param_bounds: Optional: a dict of bounds on fit parameters. Keys are parameter names, values are a tuple of (min, max). May contain only a subset of param_guesses if desired. Use None to represent one-sided constraints (i.e. (None, 5)). [dict of tuples] :param weights: Optional: weights for data points. If not supplied, weights are assumed to be uniform. Weights are automatically normalized. [1D ndarray of length n] :param verbose: Whether or not to print information about parameters and goodness of fit. :param scale_problem: Whether or not to attempt to scale variables, constraints, and objective for more robust solve. [boolean] :param put_residuals_in_logspace: Whether to optimize using the logarithmic error as opposed to the absolute error (useful for minimizing percent error). Note: If any model outputs or data are negative, this will fail! :return: Optimal fit parameters [dict] """ opti = cas.Opti() # Handle weighting if weights is None: weights = cas.GenDM_ones(y_data.shape[0]) weights /= cas.sum1(weights) def fit_param(initial_guess, lower_bound=None, upper_bound=None): """ Helper function to create a fit variable :param initial_guess: :param lower_bound: :param upper_bound: :return: """ if scale_problem and np.abs(initial_guess) > 1e-8: var = initial_guess * opti.variable() # scale variables else: var = opti.variable() opti.set_initial(var, initial_guess) if lower_bound is not None: lower_bound_abs = np.abs(lower_bound) if scale_problem and lower_bound_abs > 1e-8: opti.subject_to( var / lower_bound_abs > lower_bound / lower_bound_abs) else: opti.subject_to(var > lower_bound) if upper_bound is not None: upper_bound_abs = np.abs(upper_bound) if scale_problem and upper_bound_abs > 1e-8: opti.subject_to( var / upper_bound_abs < upper_bound / upper_bound_abs) else: opti.subject_to(var < upper_bound) return var if param_bounds is None: params = {k: fit_param(param_guesses[k]) for k in param_guesses} else: params = { k: fit_param(param_guesses[k]) if k not in param_bounds else fit_param(param_guesses[k], param_bounds[k][0], param_bounds[k][1]) for k in param_guesses } if scale_problem: y_model_initial = model(x_data, param_guesses) if not put_residuals_in_logspace: residuals_initial = y_model_initial - y_data else: residuals_initial = cas.log10(y_model_initial) - cas.log10(y_data) SSE_initial = cas.sum1(weights * residuals_initial**2) y_model = model(x_data, params) if not put_residuals_in_logspace: residuals = y_model - y_data else: residuals = cas.log10(y_model) - cas.log10(y_data) SSE = cas.sum1(weights * residuals**2) opti.minimize(SSE / SSE_initial) else: y_model = model(x_data, params) if not put_residuals_in_logspace: residuals = y_model - y_data else: residuals = cas.log10(y_model) - cas.log10(y_data) SSE = cas.sum1(weights * residuals**2) opti.minimize(SSE) # Solve p_opts = {} s_opts = {} s_opts["max_iter"] = 3e3 # If you need to interrupt, just use ctrl+c # s_opts["mu_strategy"] = "adaptive" opti.solver('ipopt', p_opts, s_opts) opti.solver('ipopt') if verbose: sol = opti.solve() else: with stdout_redirected(): sol = opti.solve() params_solved = {} for k in params: try: params_solved[k] = sol.value(params[k]) except: params_solved[k] = np.NaN # printing if verbose: # Print parameters print("\nFit Parameters:") if len(params_solved) <= 20: [print("\t%s = %f" % (k, v)) for k, v in params_solved.items()] else: print("\t%i parameters solved for." % len(params_solved)) print("\nGoodness of Fit:") # Print RMS error weighted_RMS_error = sol.value( cas.sqrt(cas.sum1(weights * residuals**2))) print("\tWeighted RMS error: %f" % weighted_RMS_error) # Print R^2 y_data_mean = cas.sum1(y_data) / y_data.shape[0] SS_tot = cas.sum1(weights * (y_data - y_data_mean)**2) SS_res = cas.sum1(weights * (y_data - y_model)**2) R_squared = sol.value(1 - SS_res / SS_tot) print("\tR^2: %f" % R_squared) return params_solved