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 expansion ratio. Reference: Rocket Propulsion Elements, 8th Edition, Equation 3-25 Arguments: chamber_pressure (scalar): Nozzle stagnation chamber pressure [units: pascal]. exit_pressure (scalar): Nozzle exit static pressure [units: pascal]. gamma (scalar): Exhaust gas ratio of specific heats [units: dimensionless]. Returns: scalar: Expansion ratio :math:`\\epsilon = A_e / A_t` [units: dimensionless] """ chamber_pressure = cas.fmax( chamber_pressure, dubious_min_combustion_pressure(oxamide_fraction)) chamber_pressure = cas.fmax(chamber_pressure, exit_pressure * 1.5) AtAe = ((gamma + 1) / 2) ** (1 / (gamma - 1)) \ * (exit_pressure / chamber_pressure) ** (1 / gamma) \ * cas.sqrt((gamma + 1) / (gamma - 1) * (1 - (exit_pressure / chamber_pressure) ** ((gamma - 1) / gamma))) er = 1 / AtAe return er
def Cd_wave_rae2822(Cl, mach, sweep=0.): """ 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. See: C:\Projects\GitHub\firefly_aerodynamics\MSES Interface\analysis\rae2822 :param Cl: Lift coefficient :param mach: Mach number :param sweep: Sweep angle, in deg :return: Wave drag coefficient. """ mach = cas.fmax(mach, 0) sweep_rad = np.pi / 180 * sweep mach_perpendicular = mach * cas.cos( sweep_rad) # Relation from FVA Eq. 8.176 Cl_perpendicular = Cl / cas.cos( sweep_rad)**2 # Relation from FVA Eq. 8.177 # Coeffs c2 = 4.5776476424519119e+00 mc0 = 9.5623337929607111e-01 mc1 = 2.0552787101770234e-01 mc2 = 1.1259268018737063e+00 mc3 = 1.9538856688443659e-01 m = mach_perpendicular l = Cl_perpendicular Cd_wave = cas.fmax(m - (mc0 - mc1 * cas.sqrt(mc2 + (l - mc3)**2)), 0)**2 * c2 return Cd_wave
def Cd_wave_e216(Cl, mach, sweep=0.): """ 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. See: C:\Projects\GitHub\firefly_aerodynamics\MSES Interface\analysis\e216 :param Cl: Lift coefficient :param mach: Mach number :param sweep: Sweep angle, in deg :return: Wave drag coefficient. """ mach = cas.fmax(mach, 0) sweep_rad = np.pi / 180 * sweep mach_perpendicular = mach * cas.cos( sweep_rad) # Relation from FVA Eq. 8.176 Cl_perpendicular = Cl / cas.cos( sweep_rad)**2 # Relation from FVA Eq. 8.177 # Coeffs c0 = 7.2685945744797997e-01 c1 = -1.5483144040727698e-01 c3 = 2.1305118052118968e-01 c4 = 7.8812272501525316e-01 c5 = 3.3888938102072169e-03 l0 = 1.5298928303149546e+00 l1 = 5.2389999717540392e-01 m = mach_perpendicular l = Cl_perpendicular Cd_wave = (cas.fmax(m - (c0 + c1 * cas.sqrt(c3 + (l - c4)**2) + c5 * l), 0) * (l0 + l1 * l))**2 return Cd_wave
def apply_finish_line_constraints( self, opti, endx, endy, endtheta, finish_line, direction, backwards=False ): # Transform robot geometry to pose # Only need to consider front or back edge, depending on direction p1, p2 = self.GEOMETRY[1] if backwards else self.GEOMETRY[0] x1, y1 = rotate_around_origin(p1, endtheta) x2, y2 = rotate_around_origin(p2, endtheta) x1 += endx y1 += endy x2 += endx y2 += endy # Enforce that at least one corner crosses the line (x1_fin, y1_fin), (x2_fin, y2_fin) = finish_line if direction in ["right", "left"]: assert x1_fin == x2_fin if direction == "right": opti.subject_to(ca.fmax(x1, x2) > x1_fin) else: opti.subject_to(ca.fmin(x1, x2) < x1_fin) elif direction in ["up", "down"]: assert y1_fin == y2_fin if direction == "up": opti.subject_to(ca.fmax(y1, y2) > y1_fin) else: opti.subject_to(ca.fmin(y1, y2) < y1_fin) else: raise Exception(f"Unknown direction '{direction}")
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_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 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 Cd_profile_2412(alpha, Re_c): # A curve fit I did to a NACA 2412 airfoil in incompressible flow. # Within -2 < alpha < 12 and 10^5 < Re_c < 10^7, has R^2 = 0.9713 print( "Warning: Cd_profile_e216() recommended over Cd_profile_2412(); those are MUCH more accurate fits." ) Re_c = cas.fmax(Re_c, 1) log_Re = cas.log(Re_c) CD0 = -5.249 Re0 = 15.61 Re1 = 15.31 alpha0 = 1.049 alpha1 = -4.715 cx = 0.009528 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 CD = cas.exp(log_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 _include_statistics_eqs_of_stochastic_variables( self, exp_phi, ls_factor, model, problem): self.stochastic_variables = vertcat(*self.stochastic_variables) for i in range(self.stochastic_variables.numel()): var = self.stochastic_variables[i] if var.is_symbolic(): name = var.name() else: name = 'stoch_var_' + str(i) _, _, _ = self._include_statistics_of_expression( var, name, exp_phi, ls_factor, model, problem) for i in range(self.socp.n_g_stochastic): var = self.socp.g_stochastic_ineq[i] rhs = self.socp.g_stochastic_rhs[i] name = 'stoch_constr_' + str(i) [stoch_ineq_mean, stoch_ineq_var, _] = self._include_statistics_of_expression( var, name, exp_phi, ls_factor, model, problem) stoch_cosntr_viol_prob = problem.create_optimization_theta( name + '_viol_prob', new_theta_opt_max=0.0) k_viol = sqrt(self.socp.g_stochastic_prob[i] / (1 - self.socp.g_stochastic_prob[i])) problem.include_time_equality( stoch_cosntr_viol_prob - (k_viol * sqrt(fmax(1e-6, stoch_ineq_var)) + stoch_ineq_mean - rhs), when='end')
def vec_max(x): """ Compute (symbolically) the maximum element in a vector Parameters ---------- x : nx1 or array The symbolic input array """ n, _ = x.shape if n == 1: return x[0] c = fmax(x[0], x[1]) if n > 2: for i in range(1, n - 1): c = fmax(c, x[i + 1]) return c
def atmos(h): """Compute press, density and temperature at a given altitude. Args: h (float or ndarray): Altitude (in meters). Returns: (float, float, float) or (ndarray, ndarray, ndarray): Air pressure (Pa), density (kg/m3), and temperature (K). """ T = ca.fmax(288.15 - 0.0065 * h, 216.65) rhotrop = 1.225 * (T / 288.15)**4.256848030018761 dhstrat = ca.fmax(0.0, h - 11000.0) rho = rhotrop * np.exp(-dhstrat / 6341.552161) p = rho * R * T return p, rho, T
def burn_rate_coefficient(oxamide_fraction): """Burn rate vs oxamide content model. Valid from 0% to 15% oxamide. # TODO IMPLEMENT THIS Returns: a: propellant burn rate coefficient [units: pascal**(-n) meter second**-1]. """ oxamide_fraction = cas.fmax(oxamide_fraction, 0) return a_0 * (1 - oxamide_fraction) / (1 + lamb * oxamide_fraction)
def Cf_flat_plate(Re_L): """ Returns the mean skin friction coefficient over a flat plate. Don't forget to double it (two sides) if you want a drag coefficient. :param Re_L: Reynolds number, normalized to the length of the flat plate. :return: Mean skin friction coefficient over a flat plate. """ Re_L = cas.fabs(Re_L) # return 0.074 / Re_L ** 0.2 # Turbulent flat plate # return 0.02666 * Re_L ** -0.139 # Schlichting's model, roughly accounts for laminar part ("Boundary Layer Theory" 7th Ed., pg. 644) # return smoothmax(0.074 / Re_L ** 0.2 - 1742 / Re_L, 1.33 / Re_L ** 0.5, 1000) return cas.fmax(0.074 / Re_L**0.2 - 1742 / Re_L, 1.33 / Re_L**0.5)
def predict(): Xc = cs.SX.sym('control', self.n_control, 1) Xd = cs.SX.sym('disturbance', self.n_disturbance, 1) X1 = cs.vertcat(Xd, Xc).T X2 = cs.DM(self.model.X_train_) RBF_length_scale = self.model.kernel_.get_params( )['k1__k1__k2__length_scale'].reshape(1, -1) RBF_constant = self.model.kernel_.get_params( )['k1__k1__k1__constant_value'] Constant_constant = self.model.kernel_.get_params( )['k1__k2__constant_value'] RQ_constant = self.model.kernel_.get_params( )['k2__k1__constant_value'] RQ_alpha = self.model.kernel_.get_params()['k2__k2__alpha'] RQ_length_scale = self.model.kernel_.get_params( )['k2__k2__length_scale'] # mean K1 = RBF_constant * self.RBFn(X1, RBF_length_scale) K2 = Constant_constant * Constant(self.n_samples) K3 = RQ_constant * self.RationalQuadraticn(X1, RQ_alpha, RQ_length_scale) K = (K1 + K2) * K3 y_mean = cs.mtimes(K, self.model.alpha_) + self.model.y_train_mean y_mean = 0.5 * (y_mean + 1) * ( self.normalizer_y.data_max_ - self.normalizer_y.data_min_) + self.normalizer_y.data_min_ # variance: solve L L^T x = K^T # v = linsolve_lower(self.model.L_, K.T) # x = linsolve_uppper(self.model.L_.T, v) x = cs.mtimes(self.LLinv, K.T) K1 = RBF_constant * self.RBF1(X1, X1, RBF_length_scale) K2 = Constant_constant * Constant(1) K3 = RQ_constant * self.RationalQuadratic1(X1, X1, RQ_alpha, RQ_length_scale) K_ = (K1 + K2) * K3 y_var = K_ - cs.mtimes(K, x) y_var = cs.fmax(0, y_var) y_std = y_var**0.5 y_std = y_std * (self.normalizer_y.data_max_ - self.normalizer_y.data_min_) / 2 predict = cs.Function('predict', [Xc, Xd], [y_mean, y_std]) self.predict_ = predict
def scattering_factor(elevation_angle): """ Calculates a scattering factor (a factor that gives losses due to atmospheric scattering at low elevation angles). Source: AeroSandbox/studies/SolarPanelScattering :param elevation_angle: Angle between the horizon and the sun [degrees] :return: Fraction of the light that is not lost to scattering. """ elevation_angle = cas.fmin(cas.fmax(elevation_angle, 0), 90) theta = 90 - elevation_angle # Angle between panel normal and the sun, in degrees theta_rad = theta * cas.pi / 180 # # Model 1 # c = ( # 0.27891510500505767300438719757949, # -0.015994330894744987481281839336589, # -19.707332432605799255043166340329, # -0.66260979582573353852126274432521 # ) # scattering_factor = c[0] + c[3] * theta_rad + cas.exp( # c[1] * ( # cas.tan(theta_rad) + c[2] * theta_rad # ) # ) # Model 2 c = ( -0.04636, -0.3171 ) scattering_factor = cas.exp( c[0] * ( cas.tan(theta_rad * 0.999) + c[1] * theta_rad ) ) # # Model 3 # p1 = -21.74 # p2 = 282.6 # p3 = -1538 # p4 = 1786 # q1 = -923.2 # q2 = 1456 # x = theta_rad # scattering_factor = ((p1*x**3 + p2*x**2 + p3*x + p4) / # (x**2 + q1*x + q2)) # Keep this: # scattering_factor = cas.fmin(cas.fmax(scattering_factor, 0), 1) return scattering_factor
def find_closest_point_on_line(posex, posey, startx, starty, endx, endy): #A to Position = position - line_start a2px = posex - startx a2py = posey - starty #A to B = line_end - line_start a2bx = endx - startx a2by = endy - starty sq_a2b = ca.fmax(0.001, a2bx**2 + a2by**2) #max, to make sure that we are not deviding by 0 a2p_dot_a2b = a2px * a2bx + a2py * a2by diff = a2p_dot_a2b / sq_a2b diff = ca.if_else(diff < 0, 0, diff) diff = ca.if_else(diff > 1, 1, diff) #diff = ca.fmax(0, ca.fmin(1, a2p_dot_a2b/sq_a2b)) closest_point = ca.vertcat(startx + a2bx * diff, starty + a2by * diff) return closest_point
def Cd_wave_Korn(Cl, t_over_c, mach, sweep=0, kappa_A=0.95): """ Wave drag_force coefficient prediction using the (very) low-fidelity Korn Equation method; derived in "Configuration Aerodynamics" by W.H. Mason, Sect. 7.5.2, pg. 7-18 :param Cl: (2D) lift_force coefficient :param t_over_c: thickness-to-chord ratio :param sweep: sweep angle, in degrees :param kappa_A: Airfoil technology factor (0.95 for supercritical section, 0.87 for NACA 6-series) :return: Wave drag coefficient """ mach = cas.fmax(mach, 0) sweep_rad = np.pi / 180 * sweep Mdd = kappa_A / cas.cos(sweep_rad) - t_over_c / cas.cos(sweep_rad) ** 2 - Cl / (10 * cas.cos(sweep_rad) ** 3) Mcrit = Mdd - (0.1 / 80) ** (1 / 3) # Cd_wave = 20 * cas.ramp(mach - Mcrit) ** 4 Cd_wave = 20 * cas.if_else(mach > Mcrit, mach - Mcrit, 0) ** 4 return Cd_wave
def smoothmax(value1, value2, hardness): """ A smooth maximum between two functions. Useful because it's differentiable and convex! Great writeup by John D Cook here: https://www.johndcook.com/soft_maximum.pdf :param value1: Value of function 1. :param value2: Value of function 2. :param hardness: Hardness parameter. Higher values make this closer to max(x1, x2). :return: Soft maximum of the two supplied values. """ value1 = value1 * hardness value2 = value2 * hardness max = cas.fmax(value1, value2) min = cas.fmin(value1, value2) out = max + cas.log(1 + cas.exp(min - max)) out /= hardness return out
def solar_elevation_angle(latitude, day_of_year, time): """ Elevation angle of the sun [degrees] for a local observer. :param latitude: Latitude [degrees] :param day_of_year: Julian day (1 == Jan. 1, 365 == Dec. 31) :param time: Time after local solar noon [seconds] :return: Solar elevation angle [degrees] (angle between horizon and sun). Returns 0 if the sun is below the horizon. """ # Solar elevation angle (including seasonality, latitude, and time of day) # Source: https://www.pveducation.org/pvcdrom/properties-of-sunlight/elevation-angle declination = declination_angle(day_of_year) solar_elevation_angle = cas.asin( sind(declination) * sind(latitude) + cosd(declination) * cosd(latitude) * cosd(time / 86400 * 360)) * 180 / cas.pi # in degrees solar_elevation_angle = cas.fmax(solar_elevation_angle, 0) return solar_elevation_angle
def calculate_optimal_control(self): dd_h_dudu, d_h_du = hessian(self.problem.H, self.model.u) if is_equal(dd_h_dudu, DM.zeros(self.model.n_u, self.model.n_u)): # TODO: Implement the case where the controls are linear on the Hamiltonian ("Bang-Bang" control) raise Exception( 'The Hamiltonian "H" is not strictly convex with respect to the control "u". ' + 'The obtained hessian d^2 H/du^2 = 0') # if not ddH_dudu.is_constant(): # raise NotImplementedError('The Hessian of the Hamiltonian with respect to "u" is not constant, # this case has not been implemented') u_opt = -mtimes(inv(dd_h_dudu), substitute(d_h_du, self.model.u, 0)) for i in range(self.model.n_u): if not self.problem.u_min[i] == -inf: u_opt[i] = fmax(u_opt[i], self.problem.u_min[i]) if not self.problem.u_max[i] == inf: u_opt[i] = fmin(u_opt[i], self.problem.u_max[i]) return u_opt
def loadGPModel(name, model, xscaler, yscaler, kernel='RBF'): """ GP mean and variance as casadi.SX variable """ X = model.X_train_ x = cs.SX.sym('x', 1, X.shape[1]) # mean if kernel == 'RBF': K1 = CasadiRBF(x, X, model) K2 = CasadiConstant(x, X, model) K = K1 + K2 elif kernel == 'Matern': K = CasadiMatern(x, X, model) else: raise NotImplementedError y_mu = cs.mtimes(K, model.alpha_) + model._y_train_mean y_mu = y_mu * yscaler.scale_ + yscaler.mean_ # variance L_inv = solve_triangular(model.L_.T,np.eye(model.L_.shape[0])) K_inv = L_inv.dot(L_inv.T) if kernel == 'RBF': K1_ = CasadiRBF(x, x, model) K2_ = CasadiConstant(x, x, model) K_ = K1_ + K2_ elif kernel == 'Matern': K_ = CasadiMatern(x, x, model) y_var = cs.diag(K_) - cs.sum2(cs.mtimes(K, K_inv)*K) y_var = cs.fmax(y_var, 0) y_std = cs.sqrt(y_var) y_std *= yscaler.scale_ gpmodel = cs.Function(name, [x], [y_mu, y_std]) return gpmodel
def apply_obstacle_constraints(self, opti, xpos, ypos, theta, obstacles): for obx, oby, obr in obstacles: for p1, p2 in self.GEOMETRY: # Transform robot geometry to pose x1, y1 = rotate_around_origin(p1, theta) x2, y2 = rotate_around_origin(p2, theta) x1 += xpos y1 += ypos x2 += xpos y2 += ypos # Compute the closest distance between a point and a line segment px = x2 - x1 py = y2 - y1 norm = px * px + py * py u = ((obx - x1) * px + (oby - y1) * py) / norm u = ca.fmax(ca.fmin(u, 1), 0) x = x1 + u * px y = y1 + u * py dx = x - obx dy = y - oby dist = np.sqrt((dx * dx + dy * dy)) opti.subject_to(dist > obr + self.OBSTACLE_BUFFER)
def opt_mintime(reftrack: np.ndarray, coeffs_x: np.ndarray, coeffs_y: np.ndarray, normvectors: np.ndarray, pars: dict, tpamap_path: str, tpadata_path: str, export_path: str, print_debug: bool = False, plot_debug: bool = False) -> tuple: """ Created by: Fabian Christ Documentation: The minimum lap time problem is described as an optimal control problem, converted to a nonlinear program using direct orthogonal Gauss-Legendre collocation and then solved by the interior-point method IPOPT. Reduced computing times are achieved using a curvilinear abscissa approach for track description, algorithmic differentiation using the software framework CasADi, and a smoothing of the track input data by approximate spline regression. The vehicles behavior is approximated as a double track model with quasi-steady state tire load simplification and nonlinear tire model. Please refer to our paper for further information: Christ, Wischnewski, Heilmeier, Lohmann Time-Optimal Trajectory Planning for a Race Car Considering Variable Tire-Road Friction Coefficients Inputs: reftrack: track [x_m, y_m, w_tr_right_m, w_tr_left_m] coeffs_x: coefficient matrix of the x splines with size (no_splines x 4) coeffs_y: coefficient matrix of the y splines with size (no_splines x 4) normvectors: array containing normalized normal vectors for every traj. point [x_component, y_component] pars: parameters dictionary tpamap_path: file path to tpa map (required for friction map loading) tpadata_path: file path to tpa data (required for friction map loading) export_path: path to output folder for warm start files and solution files print_debug: determines if debug messages are printed plot_debug: determines if debug plots are shown Outputs: alpha_opt: solution vector of the optimization problem containing the lateral shift in m for every point v_opt: velocity profile for the raceline """ # ------------------------------------------------------------------------------------------------------------------ # PREPARE TRACK INFORMATION ---------------------------------------------------------------------------------------- # ------------------------------------------------------------------------------------------------------------------ # spline lengths spline_lengths_refline = tph.calc_spline_lengths.calc_spline_lengths( coeffs_x=coeffs_x, coeffs_y=coeffs_y) # calculate heading and curvature (numerically) kappa_refline = tph.calc_head_curv_num. \ calc_head_curv_num(path=reftrack[:, :2], el_lengths=spline_lengths_refline, is_closed=True, stepsize_curv_preview=pars["curv_calc_opts"]["d_preview_curv"], stepsize_curv_review=pars["curv_calc_opts"]["d_review_curv"], stepsize_psi_preview=pars["curv_calc_opts"]["d_preview_head"], stepsize_psi_review=pars["curv_calc_opts"]["d_review_head"])[1] # close track kappa_refline_cl = np.append(kappa_refline, kappa_refline[0]) w_tr_left_cl = np.append(reftrack[:, 3], reftrack[0, 3]) w_tr_right_cl = np.append(reftrack[:, 2], reftrack[0, 2]) # step size along the reference line h = pars["stepsize_opts"]["stepsize_reg"] # optimization steps (0, 1, 2 ... end point/start point) steps = [i for i in range(kappa_refline_cl.size)] # number of control intervals N = steps[-1] # station along the reference line s_opt = np.linspace(0.0, N * h, N + 1) # interpolate curvature of reference line in terms of steps kappa_interp = ca.interpolant('kappa_interp', 'linear', [steps], kappa_refline_cl) # interpolate track width (left and right to reference line) in terms of steps w_tr_left_interp = ca.interpolant('w_tr_left_interp', 'linear', [steps], w_tr_left_cl) w_tr_right_interp = ca.interpolant('w_tr_right_interp', 'linear', [steps], w_tr_right_cl) # describe friction coefficients from friction map with linear equations or gaussian basis functions if pars["optim_opts"]["var_friction"] is not None: w_mue_fl, w_mue_fr, w_mue_rl, w_mue_rr, center_dist = opt_mintime_traj.src. \ approx_friction_map.approx_friction_map(reftrack=reftrack, normvectors=normvectors, tpamap_path=tpamap_path, tpadata_path=tpadata_path, pars=pars, dn=pars["optim_opts"]["dn"], n_gauss=pars["optim_opts"]["n_gauss"], print_debug=print_debug, plot_debug=plot_debug) # ------------------------------------------------------------------------------------------------------------------ # DIRECT GAUSS-LEGENDRE COLLOCATION -------------------------------------------------------------------------------- # ------------------------------------------------------------------------------------------------------------------ # degree of interpolating polynomial d = 3 # legendre collocation points tau = np.append(0, ca.collocation_points(d, 'legendre')) # coefficient matrix for formulating the collocation equation C = np.zeros((d + 1, d + 1)) # coefficient matrix for formulating the collocation equation D = np.zeros(d + 1) # coefficient matrix for formulating the collocation equation B = np.zeros(d + 1) # construct polynomial basis for j in range(d + 1): # construct Lagrange polynomials to get the polynomial basis at the collocation point p = np.poly1d([1]) for r in range(d + 1): if r != j: p *= np.poly1d([1, -tau[r]]) / (tau[j] - tau[r]) # evaluate polynomial at the final time to get the coefficients of the continuity equation D[j] = p(1.0) # evaluate time derivative of polynomial at collocation points to get the coefficients of continuity equation p_der = np.polyder(p) for r in range(d + 1): C[j, r] = p_der(tau[r]) # evaluate integral of the polynomial to get the coefficients of the quadrature function pint = np.polyint(p) B[j] = pint(1.0) # ------------------------------------------------------------------------------------------------------------------ # STATE VARIABLES -------------------------------------------------------------------------------------------------- # ------------------------------------------------------------------------------------------------------------------ # number of state variables nx = 5 # velocity [m/s] v_n = ca.SX.sym('v_n') v_s = 50 v = v_s * v_n # side slip angle [rad] beta_n = ca.SX.sym('beta_n') beta_s = 0.5 beta = beta_s * beta_n # yaw rate [rad/s] omega_z_n = ca.SX.sym('omega_z_n') omega_z_s = 1 omega_z = omega_z_s * omega_z_n # lateral distance to reference line (positive = left) [m] n_n = ca.SX.sym('n_n') n_s = 5.0 n = n_s * n_n # relative angle to tangent on reference line [rad] xi_n = ca.SX.sym('xi_n') xi_s = 1.0 xi = xi_s * xi_n # scaling factors for state variables x_s = np.array([v_s, beta_s, omega_z_s, n_s, xi_s]) # put all states together x = ca.vertcat(v_n, beta_n, omega_z_n, n_n, xi_n) # ------------------------------------------------------------------------------------------------------------------ # CONTROL VARIABLES ------------------------------------------------------------------------------------------------ # ------------------------------------------------------------------------------------------------------------------ # number of control variables nu = 4 # steer angle [rad] delta_n = ca.SX.sym('delta_n') delta_s = 0.5 delta = delta_s * delta_n # positive longitudinal force (drive) [N] f_drive_n = ca.SX.sym('f_drive_n') f_drive_s = 7500.0 f_drive = f_drive_s * f_drive_n # negative longitudinal force (brake) [N] f_brake_n = ca.SX.sym('f_brake_n') f_brake_s = 20000.0 f_brake = f_brake_s * f_brake_n # lateral wheel load transfer [N] gamma_y_n = ca.SX.sym('gamma_y_n') gamma_y_s = 5000.0 gamma_y = gamma_y_s * gamma_y_n # scaling factors for control variables u_s = np.array([delta_s, f_drive_s, f_brake_s, gamma_y_s]) # put all controls together u = ca.vertcat(delta_n, f_drive_n, f_brake_n, gamma_y_n) # ------------------------------------------------------------------------------------------------------------------ # MODEL EQUATIONS -------------------------------------------------------------------------------------------------- # ------------------------------------------------------------------------------------------------------------------ # extract vehicle and tire parameters veh = pars["vehicle_params_mintime"] tire = pars["tire_params_mintime"] # general constants g = pars["veh_params"]["g"] rho = pars["veh_params"]["rho_air"] mass = pars["veh_params"]["mass"] # curvature of reference line [rad/m] kappa = ca.SX.sym('kappa') # drag force [N] f_xdrag = pars["veh_params"]["dragcoeff"] * v**2 # rolling resistance forces [N] f_xroll_fl = 0.5 * tire["c_roll"] * mass * g * veh["wheelbase_rear"] / veh[ "wheelbase"] f_xroll_fr = 0.5 * tire["c_roll"] * mass * g * veh["wheelbase_rear"] / veh[ "wheelbase"] f_xroll_rl = 0.5 * tire["c_roll"] * mass * g * veh[ "wheelbase_front"] / veh["wheelbase"] f_xroll_rr = 0.5 * tire["c_roll"] * mass * g * veh[ "wheelbase_front"] / veh["wheelbase"] f_xroll = tire["c_roll"] * mass * g # static normal tire forces [N] f_zstat_fl = 0.5 * mass * g * veh["wheelbase_rear"] / veh["wheelbase"] f_zstat_fr = 0.5 * mass * g * veh["wheelbase_rear"] / veh["wheelbase"] f_zstat_rl = 0.5 * mass * g * veh["wheelbase_front"] / veh["wheelbase"] f_zstat_rr = 0.5 * mass * g * veh["wheelbase_front"] / veh["wheelbase"] # dynamic normal tire forces (aerodynamic downforces) [N] f_zlift_fl = 0.5 * 0.5 * veh["clA_front"] * rho * v**2 f_zlift_fr = 0.5 * 0.5 * veh["clA_front"] * rho * v**2 f_zlift_rl = 0.5 * 0.5 * veh["clA_rear"] * rho * v**2 f_zlift_rr = 0.5 * 0.5 * veh["clA_rear"] * rho * v**2 # dynamic normal tire forces (load transfers) [N] f_zdyn_fl = (-0.5 * veh["cog_z"] / veh["wheelbase"] * (f_drive + f_brake - f_xdrag - f_xroll) - veh["k_roll"] * gamma_y) f_zdyn_fr = (-0.5 * veh["cog_z"] / veh["wheelbase"] * (f_drive + f_brake - f_xdrag - f_xroll) + veh["k_roll"] * gamma_y) f_zdyn_rl = (0.5 * veh["cog_z"] / veh["wheelbase"] * (f_drive + f_brake - f_xdrag - f_xroll) - (1.0 - veh["k_roll"]) * gamma_y) f_zdyn_rr = (0.5 * veh["cog_z"] / veh["wheelbase"] * (f_drive + f_brake - f_xdrag - f_xroll) + (1.0 - veh["k_roll"]) * gamma_y) # sum of all normal tire forces [N] f_z_fl = f_zstat_fl + f_zlift_fl + f_zdyn_fl f_z_fr = f_zstat_fr + f_zlift_fr + f_zdyn_fr f_z_rl = f_zstat_rl + f_zlift_rl + f_zdyn_rl f_z_rr = f_zstat_rr + f_zlift_rr + f_zdyn_rr # slip angles [rad] alpha_fl = delta - ca.atan( (v * ca.sin(beta) + veh["wheelbase_front"] * omega_z) / (v * ca.cos(beta) - 0.5 * veh["track_width_front"] * omega_z)) alpha_fr = delta - ca.atan( (v * ca.sin(beta) + veh["wheelbase_front"] * omega_z) / (v * ca.cos(beta) + 0.5 * veh["track_width_front"] * omega_z)) alpha_rl = ca.atan( (-v * ca.sin(beta) + veh["wheelbase_rear"] * omega_z) / (v * ca.cos(beta) - 0.5 * veh["track_width_rear"] * omega_z)) alpha_rr = ca.atan( (-v * ca.sin(beta) + veh["wheelbase_rear"] * omega_z) / (v * ca.cos(beta) + 0.5 * veh["track_width_rear"] * omega_z)) # lateral tire forces [N] f_y_fl = (pars["optim_opts"]["mue"] * f_z_fl * (1 + tire["eps_front"] * f_z_fl / tire["f_z0"]) * ca.sin(tire["C_front"] * ca.atan(tire["B_front"] * alpha_fl - tire["E_front"] * (tire["B_front"] * alpha_fl - ca.atan(tire["B_front"] * alpha_fl))))) f_y_fr = (pars["optim_opts"]["mue"] * f_z_fr * (1 + tire["eps_front"] * f_z_fr / tire["f_z0"]) * ca.sin(tire["C_front"] * ca.atan(tire["B_front"] * alpha_fr - tire["E_front"] * (tire["B_front"] * alpha_fr - ca.atan(tire["B_front"] * alpha_fr))))) f_y_rl = (pars["optim_opts"]["mue"] * f_z_rl * (1 + tire["eps_rear"] * f_z_rl / tire["f_z0"]) * ca.sin(tire["C_rear"] * ca.atan(tire["B_rear"] * alpha_rl - tire["E_rear"] * (tire["B_rear"] * alpha_rl - ca.atan(tire["B_rear"] * alpha_rl))))) f_y_rr = (pars["optim_opts"]["mue"] * f_z_rr * (1 + tire["eps_rear"] * f_z_rr / tire["f_z0"]) * ca.sin(tire["C_rear"] * ca.atan(tire["B_rear"] * alpha_rr - tire["E_rear"] * (tire["B_rear"] * alpha_rr - ca.atan(tire["B_rear"] * alpha_rr))))) # longitudinal tire forces [N] f_x_fl = 0.5 * f_drive * veh["k_drive_front"] + 0.5 * f_brake * veh[ "k_brake_front"] - f_xroll_fl f_x_fr = 0.5 * f_drive * veh["k_drive_front"] + 0.5 * f_brake * veh[ "k_brake_front"] - f_xroll_fr f_x_rl = 0.5 * f_drive * (1 - veh["k_drive_front"]) + 0.5 * f_brake * ( 1 - veh["k_brake_front"]) - f_xroll_rl f_x_rr = 0.5 * f_drive * (1 - veh["k_drive_front"]) + 0.5 * f_brake * ( 1 - veh["k_brake_front"]) - f_xroll_rr # longitudinal acceleration [m/s²] ax = (f_x_rl + f_x_rr + (f_x_fl + f_x_fr) * ca.cos(delta) - (f_y_fl + f_y_fr) * ca.sin(delta) - pars["veh_params"]["dragcoeff"] * v**2) / mass # lateral acceleration [m/s²] ay = ((f_x_fl + f_x_fr) * ca.sin(delta) + f_y_rl + f_y_rr + (f_y_fl + f_y_fr) * ca.cos(delta)) / mass # time-distance scaling factor (dt/ds) sf = (1.0 - n * kappa) / (v * (ca.cos(xi + beta))) # model equations for two track model (ordinary differential equations) dv = (sf / mass) * ( (f_x_rl + f_x_rr) * ca.cos(beta) + (f_x_fl + f_x_fr) * ca.cos(delta - beta) + (f_y_rl + f_y_rr) * ca.sin(beta) - (f_y_fl + f_y_fr) * ca.sin(delta - beta) - f_xdrag * ca.cos(beta)) dbeta = sf * ( -omega_z + (-(f_x_rl + f_x_rr) * ca.sin(beta) + (f_x_fl + f_x_fr) * ca.sin(delta - beta) + (f_y_rl + f_y_rr) * ca.cos(beta) + (f_y_fl + f_y_fr) * ca.cos(delta - beta) + f_xdrag * ca.sin(beta)) / (mass * v)) domega_z = (sf / veh["I_z"]) * ( (f_x_rr - f_x_rl) * veh["track_width_rear"] / 2 - (f_y_rl + f_y_rr) * veh["wheelbase_rear"] + ((f_x_fr - f_x_fl) * ca.cos(delta) + (f_y_fl - f_y_fr) * ca.sin(delta)) * veh["track_width_front"] / 2 + ((f_y_fl + f_y_fr) * ca.cos(delta) + (f_x_fl + f_x_fr) * ca.sin(delta)) * veh["track_width_front"]) dn = sf * v * ca.sin(xi + beta) dxi = sf * omega_z - kappa # put all ODEs together dx = ca.vertcat(dv, dbeta, domega_z, dn, dxi) / x_s # ------------------------------------------------------------------------------------------------------------------ # CONTROL BOUNDARIES ----------------------------------------------------------------------------------------------- # ------------------------------------------------------------------------------------------------------------------ delta_min = -veh["delta_max"] / delta_s # min. steer angle [rad] delta_max = veh["delta_max"] / delta_s # max. steer angle [rad] f_drive_min = 0.0 # min. longitudinal drive force [N] f_drive_max = veh[ "f_drive_max"] / f_drive_s # max. longitudinal drive force [N] f_brake_min = -veh[ "f_brake_max"] / f_brake_s # min. longitudinal brake force [N] f_brake_max = 0.0 # max. longitudinal brake force [N] gamma_y_min = -np.inf # min. lateral wheel load transfer [N] gamma_y_max = np.inf # max. lateral wheel load transfer [N] # ------------------------------------------------------------------------------------------------------------------ # STATE BOUNDARIES ------------------------------------------------------------------------------------------------- # ------------------------------------------------------------------------------------------------------------------ v_min = 1.0 / v_s # min. velocity [m/s] v_max = pars["optim_opts"]["v_max"] / v_s # max. velocity [m/s] beta_min = -0.5 * np.pi / beta_s # min. side slip angle [rad] beta_max = 0.5 * np.pi / beta_s # max. side slip angle [rad] omega_z_min = -0.5 * np.pi / omega_z_s # min. yaw rate [rad/s] omega_z_max = 0.5 * np.pi / omega_z_s # max. yaw rate [rad/s] xi_min = -0.5 * np.pi / xi_s # min. relative angle to tangent on reference line [rad] xi_max = 0.5 * np.pi / xi_s # max. relative angle to tangent on reference line [rad] # ------------------------------------------------------------------------------------------------------------------ # INITIAL GUESS FOR DECISION VARIABLES ----------------------------------------------------------------------------- # ------------------------------------------------------------------------------------------------------------------ v_guess = 20.0 / v_s # ------------------------------------------------------------------------------------------------------------------ # HELPER FUNCTIONS ------------------------------------------------------------------------------------------------- # ------------------------------------------------------------------------------------------------------------------ # continuous time dynamics f_dyn = ca.Function('f_dyn', [x, u, kappa], [dx, sf], ['x', 'u', 'kappa'], ['dx', 'sf']) # longitudinal tire forces [N] f_fx = ca.Function('f_fx', [x, u], [f_x_fl, f_x_fr, f_x_rl, f_x_rr], ['x', 'u'], ['f_x_fl', 'f_x_fr', 'f_x_rl', 'f_x_rr']) # lateral tire forces [N] f_fy = ca.Function('f_fy', [x, u], [f_y_fl, f_y_fr, f_y_rl, f_y_rr], ['x', 'u'], ['f_y_fl', 'f_y_fr', 'f_y_rl', 'f_y_rr']) # vertical tire forces [N] f_fz = ca.Function('f_fz', [x, u], [f_z_fl, f_z_fr, f_z_rl, f_z_rr], ['x', 'u'], ['f_z_fl', 'f_z_fr', 'f_z_rl', 'f_z_rr']) # longitudinal and lateral acceleration [m/s²] f_a = ca.Function('f_a', [x, u], [ax, ay], ['x', 'u'], ['ax', 'ay']) # ------------------------------------------------------------------------------------------------------------------ # FORMULATE NONLINEAR PROGRAM -------------------------------------------------------------------------------------- # ------------------------------------------------------------------------------------------------------------------ # initialize NLP vectors w = [] w0 = [] lbw = [] ubw = [] J = 0 g = [] lbg = [] ubg = [] # initialize ouput vectors x_opt = [] u_opt = [] dt_opt = [] tf_opt = [] ax_opt = [] ay_opt = [] ec_opt = [] # initialize control vectors (for regularization) delta_p = [] F_p = [] # boundary constraint: lift initial conditions Xk = ca.MX.sym('X0', nx) w.append(Xk) n_min = (-w_tr_right_interp(0) + pars["optim_opts"]["width_opt"] / 2) / n_s n_max = (w_tr_left_interp(0) - pars["optim_opts"]["width_opt"] / 2) / n_s lbw.append([v_min, beta_min, omega_z_min, n_min, xi_min]) ubw.append([v_max, beta_max, omega_z_max, n_max, xi_max]) w0.append([v_guess, 0.0, 0.0, 0.0, 0.0]) x_opt.append(Xk * x_s) # loop along the racetrack and formulate path constraints & system dynamic for k in range(N): # add decision variables for the control Uk = ca.MX.sym('U_' + str(k), nu) w.append(Uk) lbw.append([delta_min, f_drive_min, f_brake_min, gamma_y_min]) ubw.append([delta_max, f_drive_max, f_brake_max, gamma_y_max]) w0.append([0.0] * nu) # add decision variables for the state at collocation points Xc = [] for j in range(d): Xkj = ca.MX.sym('X_' + str(k) + '_' + str(j), nx) Xc.append(Xkj) w.append(Xkj) lbw.append([-np.inf] * nx) ubw.append([np.inf] * nx) w0.append([v_guess, 0.0, 0.0, 0.0, 0.0]) # loop over all collocation points Xk_end = D[0] * Xk sf_opt = [] for j in range(1, d + 1): # calculate the state derivative at the collocation point xp = C[0, j] * Xk for r in range(d): xp = xp + C[r + 1, j] * Xc[r] # interpolate kappa at the collocation point kappa_col = kappa_interp(k + tau[j]) # append collocation equations (system dynamic) fj, qj = f_dyn(Xc[j - 1], Uk, kappa_col) g.append(h * fj - xp) lbg.append([0.0] * nx) ubg.append([0.0] * nx) # add contribution to the end state Xk_end = Xk_end + D[j] * Xc[j - 1] # add contribution to quadrature function J = J + B[j] * qj * h # add contribution to scaling factor (for calculating lap time) sf_opt.append(B[j] * qj * h) # calculate used energy dt_opt.append(sf_opt[0] + sf_opt[1] + sf_opt[2]) ec_opt.append(Xk[0] * v_s * Uk[1] * f_drive_s * dt_opt[-1]) # add new decision variables for state at end of the collocation interval Xk = ca.MX.sym('X_' + str(k + 1), nx) w.append(Xk) n_min = (-w_tr_right_interp(k + 1) + pars["optim_opts"]["width_opt"] / 2.0) / n_s n_max = (w_tr_left_interp(k + 1) - pars["optim_opts"]["width_opt"] / 2.0) / n_s lbw.append([v_min, beta_min, omega_z_min, n_min, xi_min]) ubw.append([v_max, beta_max, omega_z_max, n_max, xi_max]) w0.append([v_guess, 0.0, 0.0, 0.0, 0.0]) # add equality constraint g.append(Xk_end - Xk) lbg.append([0.0] * nx) ubg.append([0.0] * nx) # get tire forces f_x_flk, f_x_frk, f_x_rlk, f_x_rrk = f_fx(Xk, Uk) f_y_flk, f_y_frk, f_y_rlk, f_y_rrk = f_fy(Xk, Uk) f_z_flk, f_z_frk, f_z_rlk, f_z_rrk = f_fz(Xk, Uk) # get accelerations (longitudinal + lateral) axk, ayk = f_a(Xk, Uk) # path constraint: limitied engine power g.append(Xk[0] * Uk[1]) lbg.append([-np.inf]) ubg.append([veh["power_max"] / (f_drive_s * v_s)]) # get constant friction coefficient if pars["optim_opts"]["var_friction"] is None: mue_fl = pars["optim_opts"]["mue"] mue_fr = pars["optim_opts"]["mue"] mue_rl = pars["optim_opts"]["mue"] mue_rr = pars["optim_opts"]["mue"] # calculate variable friction coefficients along the reference line (regression with linear equations) elif pars["optim_opts"]["var_friction"] == "linear": # friction coefficient for each tire mue_fl = w_mue_fl[k + 1, 0] * Xk[3] * n_s + w_mue_fl[k + 1, 1] mue_fr = w_mue_fr[k + 1, 0] * Xk[3] * n_s + w_mue_fr[k + 1, 1] mue_rl = w_mue_rl[k + 1, 0] * Xk[3] * n_s + w_mue_rl[k + 1, 1] mue_rr = w_mue_rr[k + 1, 0] * Xk[3] * n_s + w_mue_rr[k + 1, 1] # calculate variable friction coefficients along the reference line (regression with gaussian basis functions) elif pars["optim_opts"]["var_friction"] == "gauss": # gaussian basis functions sigma = 2.0 * center_dist[k + 1, 0] n_gauss = pars["optim_opts"]["n_gauss"] n_q = np.linspace(-n_gauss, n_gauss, 2 * n_gauss + 1) * center_dist[k + 1, 0] gauss_basis = [] for i in range(2 * n_gauss + 1): gauss_basis.append( ca.exp(-(Xk[3] * n_s - n_q[i])**2 / (2 * (sigma**2)))) gauss_basis = ca.vertcat(*gauss_basis) mue_fl = ca.dot(w_mue_fl[k + 1, :-1], gauss_basis) + w_mue_fl[k + 1, -1] mue_fr = ca.dot(w_mue_fr[k + 1, :-1], gauss_basis) + w_mue_fr[k + 1, -1] mue_rl = ca.dot(w_mue_rl[k + 1, :-1], gauss_basis) + w_mue_rl[k + 1, -1] mue_rr = ca.dot(w_mue_rr[k + 1, :-1], gauss_basis) + w_mue_rr[k + 1, -1] else: raise ValueError("No friction coefficients are available!") # path constraint: Kamm's Circle for each wheel g.append(((f_x_flk / (mue_fl * f_z_flk))**2 + (f_y_flk / (mue_fl * f_z_flk))**2)) g.append(((f_x_frk / (mue_fr * f_z_frk))**2 + (f_y_frk / (mue_fr * f_z_frk))**2)) g.append(((f_x_rlk / (mue_rl * f_z_rlk))**2 + (f_y_rlk / (mue_rl * f_z_rlk))**2)) g.append(((f_x_rrk / (mue_rr * f_z_rrk))**2 + (f_y_rrk / (mue_rr * f_z_rrk))**2)) lbg.append([0.0] * 4) ubg.append([1.0] * 4) # path constraint: lateral wheel load transfer g.append(( (f_y_flk + f_y_frk) * ca.cos(Uk[0] * delta_s) + f_y_rlk + f_y_rrk + (f_x_flk + f_x_frk) * ca.sin(Uk[0] * delta_s)) * veh["cog_z"] / ((veh["track_width_front"] + veh["track_width_rear"]) / 2) - Uk[3] * gamma_y_s) lbg.append([0.0]) ubg.append([0.0]) # path constraint: f_drive * f_brake == 0 (no simultaneous operation of brake and accelerator pedal) g.append(Uk[1] * Uk[2]) lbg.append([-20000.0 / (f_drive_s * f_brake_s)]) ubg.append([0.0]) # path constraint: actor dynamic if k > 0: sigma = (1 - kappa_interp(k) * Xk[3] * n_s) / (Xk[0] * v_s) g.append((Uk - w[1 + (k - 1) * nx]) / (pars["stepsize_opts"]["stepsize_reg"] * sigma)) lbg.append([ delta_min / (veh["t_delta"]), -np.inf, f_brake_min / (veh["t_brake"]), -np.inf ]) ubg.append([ delta_max / (veh["t_delta"]), f_drive_max / (veh["t_drive"]), np.inf, np.inf ]) # path constraint: safe trajectories with acceleration ellipse if pars["optim_opts"]["safe_traj"]: g.append((ca.fmax(axk, 0) / pars["optim_opts"]["ax_pos_safe"])**2 + (ayk / pars["optim_opts"]["ay_safe"])**2) g.append((ca.fmin(axk, 0) / pars["optim_opts"]["ax_neg_safe"])**2 + (ayk / pars["optim_opts"]["ay_safe"])**2) lbg.append([0.0] * 2) ubg.append([1.0] * 2) # append controls (for regularization) delta_p.append(Uk[0] * delta_s) F_p.append(Uk[1] * f_drive_s / 10000.0 + Uk[2] * f_brake_s / 10000.0) # append outputs x_opt.append(Xk * x_s) u_opt.append(Uk * u_s) tf_opt.extend([f_x_flk, f_y_flk, f_z_flk, f_x_frk, f_y_frk, f_z_frk]) tf_opt.extend([f_x_rlk, f_y_rlk, f_z_rlk, f_x_rrk, f_y_rrk, f_z_rrk]) ax_opt.append(axk) ay_opt.append(ayk) # boundary constraint: start states = final states g.append(w[0] - Xk) lbg.append([0.0] * nx) ubg.append([0.0] * nx) # path constraint: limited energy consumption if pars["optim_opts"]["limit_energy"]: g.append(ca.sum1(ca.vertcat(*ec_opt)) / 3600000.0) lbg.append([0]) ubg.append([pars["optim_opts"]["energy_limit"]]) # formulate differentiation matrix (for regularization) diff_matrix = np.eye(N) for i in range(N - 1): diff_matrix[i, i + 1] = -1.0 diff_matrix[N - 1, 0] = -1.0 # regularization (delta) delta_p = ca.vertcat(*delta_p) Jp_delta = ca.mtimes(ca.MX(diff_matrix), delta_p) Jp_delta = ca.dot(Jp_delta, Jp_delta) # regularization (f_drive + f_brake) F_p = ca.vertcat(*F_p) Jp_f = ca.mtimes(ca.MX(diff_matrix), F_p) Jp_f = ca.dot(Jp_f, Jp_f) # formulate objective J = J + pars["optim_opts"]["penalty_F"] * Jp_f + pars["optim_opts"][ "penalty_delta"] * Jp_delta # concatenate NLP vectors w = ca.vertcat(*w) g = ca.vertcat(*g) w0 = np.concatenate(w0) lbw = np.concatenate(lbw) ubw = np.concatenate(ubw) lbg = np.concatenate(lbg) ubg = np.concatenate(ubg) # concatenate output vectors x_opt = ca.vertcat(*x_opt) u_opt = ca.vertcat(*u_opt) tf_opt = ca.vertcat(*tf_opt) dt_opt = ca.vertcat(*dt_opt) ax_opt = ca.vertcat(*ax_opt) ay_opt = ca.vertcat(*ay_opt) ec_opt = ca.vertcat(*ec_opt) # ------------------------------------------------------------------------------------------------------------------ # CREATE NLP SOLVER ------------------------------------------------------------------------------------------------ # ------------------------------------------------------------------------------------------------------------------ # fill nlp structure nlp = {'f': J, 'x': w, 'g': g} # solver options opts = { "expand": True, "verbose": print_debug, "ipopt.max_iter": 2000, "ipopt.tol": 1e-7 } # solver options for warm start if pars["optim_opts"]["warm_start"]: opts_warm_start = { "ipopt.warm_start_init_point": "yes", "ipopt.warm_start_bound_push": 1e-9, "ipopt.warm_start_mult_bound_push": 1e-9, "ipopt.warm_start_slack_bound_push": 1e-9, "ipopt.mu_init": 1e-9 } opts.update(opts_warm_start) # load warm start files if pars["optim_opts"]["warm_start"]: try: w0 = np.loadtxt(os.path.join(export_path, 'w0.csv')) lam_x0 = np.loadtxt(os.path.join(export_path, 'lam_x0.csv')) lam_g0 = np.loadtxt(os.path.join(export_path, 'lam_g0.csv')) except IOError: print('\033[91m' + 'WARNING: Failed to load warm start files!' + '\033[0m') sys.exit(1) # check warm start files if pars["optim_opts"]["warm_start"] and not len(w0) == len(lbw): print( '\033[91m' + 'WARNING: Warm start files do not fit to the dimension of the NLP!' + '\033[0m') sys.exit(1) # create solver instance solver = ca.nlpsol("solver", "ipopt", nlp, opts) # ------------------------------------------------------------------------------------------------------------------ # SOLVE NLP -------------------------------------------------------------------------------------------------------- # ------------------------------------------------------------------------------------------------------------------ # start time measure t0 = time.perf_counter() # solve NLP if pars["optim_opts"]["warm_start"]: sol = solver(x0=w0, lbx=lbw, ubx=ubw, lbg=lbg, ubg=ubg, lam_x0=lam_x0, lam_g0=lam_g0) else: sol = solver(x0=w0, lbx=lbw, ubx=ubw, lbg=lbg, ubg=ubg) # end time measure tend = time.perf_counter() # ------------------------------------------------------------------------------------------------------------------ # EXTRACT SOLUTION ------------------------------------------------------------------------------------------------- # ------------------------------------------------------------------------------------------------------------------ # helper function to extract solution for state variables, control variables, tire forces, time f_sol = ca.Function( 'f_sol', [w], [x_opt, u_opt, tf_opt, dt_opt, ax_opt, ay_opt, ec_opt], ['w'], ['x_opt', 'u_opt', 'tf_opt', 'dt_opt', 'ax_opt', 'ay_opt', 'ec_opt']) # extract solution x_opt, u_opt, tf_opt, dt_opt, ax_opt, ay_opt, ec_opt = f_sol(sol['x']) # solution for state variables x_opt = np.reshape(x_opt, (N + 1, nx)) # solution for control variables u_opt = np.reshape(u_opt, (N, nu)) # solution for tire forces tf_opt = np.append(tf_opt[-12:], tf_opt[:]) tf_opt = np.reshape(tf_opt, (N + 1, 12)) # solution for time t_opt = np.hstack((0.0, np.cumsum(dt_opt))) # solution for acceleration ax_opt = np.append(ax_opt[-1], ax_opt) ay_opt = np.append(ay_opt[-1], ay_opt) atot_opt = np.sqrt(np.power(ax_opt, 2) + np.power(ay_opt, 2)) # solution for energy consumption ec_opt_cum = np.hstack((0.0, np.cumsum(ec_opt))) / 3600.0 # ------------------------------------------------------------------------------------------------------------------ # EXPORT SOLUTION -------------------------------------------------------------------------------------------------- # ------------------------------------------------------------------------------------------------------------------ # export data to CSVs opt_mintime_traj.src.export_mintime_solution.export_mintime_solution( file_path=export_path, s=s_opt, t=t_opt, x=x_opt, u=u_opt, tf=tf_opt, ax=ax_opt, ay=ay_opt, atot=atot_opt, w0=sol["x"], lam_x0=sol["lam_x"], lam_g0=sol["lam_g"]) # ------------------------------------------------------------------------------------------------------------------ # PLOT & PRINT RESULTS --------------------------------------------------------------------------------------------- # ------------------------------------------------------------------------------------------------------------------ if plot_debug: opt_mintime_traj.src.result_plots_mintime.result_plots_mintime( pars=pars, reftrack=reftrack, s=s_opt, t=t_opt, x=x_opt, u=u_opt, ax=ax_opt, ay=ay_opt, atot=atot_opt, tf=tf_opt, ec=ec_opt_cum) if print_debug: print("INFO: Laptime: %.3fs" % t_opt[-1]) print("INFO: NLP solving time: %.3fs" % (tend - t0)) print("INFO: Maximum abs(ay): %.2fm/s2" % np.amax(ay_opt)) print("INFO: Maximum ax: %.2fm/s2" % np.amax(ax_opt)) print("INFO: Minimum ax: %.2fm/s2" % np.amin(ax_opt)) print("INFO: Maximum total acc: %.2fm/s2" % np.amax(atot_opt)) print('INFO: Energy consumption: %.3fWh' % ec_opt_cum[-1]) return -x_opt[:-1, 3], x_opt[:-1, 0]
def _convert(self, symbol, t, y, y_dot, inputs): """ See :meth:`CasadiConverter.convert()`. """ if isinstance( symbol, ( pybamm.Scalar, pybamm.Array, pybamm.Time, pybamm.InputParameter, pybamm.ExternalVariable, ), ): return casadi.MX(symbol.evaluate(t, y, y_dot, inputs)) elif isinstance(symbol, pybamm.StateVector): if y is None: raise ValueError("Must provide a 'y' for converting state vectors") return casadi.vertcat(*[y[y_slice] for y_slice in symbol.y_slices]) elif isinstance(symbol, pybamm.StateVectorDot): if y_dot is None: raise ValueError("Must provide a 'y_dot' for converting state vectors") return casadi.vertcat(*[y_dot[y_slice] for y_slice in symbol.y_slices]) elif isinstance(symbol, pybamm.BinaryOperator): left, right = symbol.children # process children converted_left = self.convert(left, t, y, y_dot, inputs) converted_right = self.convert(right, t, y, y_dot, inputs) if isinstance(symbol, pybamm.Minimum): return casadi.fmin(converted_left, converted_right) if isinstance(symbol, pybamm.Maximum): return casadi.fmax(converted_left, converted_right) # _binary_evaluate defined in derived classes for specific rules return symbol._binary_evaluate(converted_left, converted_right) elif isinstance(symbol, pybamm.UnaryOperator): converted_child = self.convert(symbol.child, t, y, y_dot, inputs) if isinstance(symbol, pybamm.AbsoluteValue): return casadi.fabs(converted_child) return symbol._unary_evaluate(converted_child) elif isinstance(symbol, pybamm.Function): converted_children = [ self.convert(child, t, y, y_dot, inputs) for child in symbol.children ] # Special functions if symbol.function == np.min: return casadi.mmin(*converted_children) elif symbol.function == np.max: return casadi.mmax(*converted_children) elif symbol.function == np.abs: return casadi.fabs(*converted_children) elif symbol.function == np.sqrt: return casadi.sqrt(*converted_children) elif symbol.function == np.sin: return casadi.sin(*converted_children) elif symbol.function == np.arcsinh: return casadi.arcsinh(*converted_children) elif symbol.function == np.arccosh: return casadi.arccosh(*converted_children) elif symbol.function == np.tanh: return casadi.tanh(*converted_children) elif symbol.function == np.cosh: return casadi.cosh(*converted_children) elif symbol.function == np.sinh: return casadi.sinh(*converted_children) elif symbol.function == np.cos: return casadi.cos(*converted_children) elif symbol.function == np.exp: return casadi.exp(*converted_children) elif symbol.function == np.log: return casadi.log(*converted_children) elif symbol.function == np.sign: return casadi.sign(*converted_children) elif isinstance(symbol.function, (PchipInterpolator, CubicSpline)): return casadi.interpolant("LUT", "bspline", [symbol.x], symbol.y)( *converted_children ) elif symbol.function.__name__.startswith("elementwise_grad_of_"): differentiating_child_idx = int(symbol.function.__name__[-1]) # Create dummy symbolic variables in order to differentiate using CasADi dummy_vars = [ casadi.MX.sym("y_" + str(i)) for i in range(len(converted_children)) ] func_diff = casadi.gradient( symbol.differentiated_function(*dummy_vars), dummy_vars[differentiating_child_idx], ) # Create function and evaluate it using the children casadi_func_diff = casadi.Function("func_diff", dummy_vars, [func_diff]) return casadi_func_diff(*converted_children) # Other functions else: return symbol._function_evaluate(converted_children) elif isinstance(symbol, pybamm.Concatenation): converted_children = [ self.convert(child, t, y, y_dot, inputs) for child in symbol.children ] if isinstance(symbol, (pybamm.NumpyConcatenation, pybamm.SparseStack)): return casadi.vertcat(*converted_children) # DomainConcatenation specifies a particular ordering for the concatenation, # which we must follow elif isinstance(symbol, pybamm.DomainConcatenation): slice_starts = [] all_child_vectors = [] for i in range(symbol.secondary_dimensions_npts): child_vectors = [] for child_var, slices in zip( converted_children, symbol._children_slices ): for child_dom, child_slice in slices.items(): slice_starts.append(symbol._slices[child_dom][i].start) child_vectors.append( child_var[child_slice[i].start : child_slice[i].stop] ) all_child_vectors.extend( [v for _, v in sorted(zip(slice_starts, child_vectors))] ) return casadi.vertcat(*all_child_vectors) else: raise TypeError( """ Cannot convert symbol of type '{}' to CasADi. Symbols must all be 'linear algebra' at this stage. """.format( type(symbol) ) )
def f_inv_neg_relu(x): return csd.fmax(0, -x)
def f_inv_relu(x): return csd.fmax(0, x)
if load_dual: with open(dual_location, 'r') as infile: dual_vals = json.load(infile) if len(dual_vals) != opti.ng: raise Exception( "Couldn't load the dual, since your problem has %i cons and the cached problem has %i cons." % (opti.ng, len(dual_vals))) for i in tqdm(range(opti.ng), desc="Loading dual variables:"): opti.set_initial(opti.lam_g[i], dual_vals[i]) sind = lambda theta: cas.sin(theta * cas.pi / 180) cosd = lambda theta: cas.cos(theta * cas.pi / 180) tand = lambda theta: cas.tan(theta * cas.pi / 180) atan2d = lambda y_val, x_val: cas.atan2(y_val, x_val) * 180 / np.pi clip = lambda x, min, max: cas.fmin(cas.fmax(min, x), max) def smoothmax(value1, value2, hardness): """ A smooth maximum between two functions. Useful because it's differentiable and convex! Great writeup by John D Cook here: https://www.johndcook.com/soft_maximum.pdf :param value1: Value of function 1. :param value2: Value of function 2. :param hardness: Hardness parameter. Higher values make this closer to max(x1, x2). :return: Soft maximum of the two supplied values. """ value1 = value1 * hardness value2 = value2 * hardness
def simplify(self, options): if options.get('replace_parameter_expressions', False): logger.info("Replacing parameter expressions") simple_parameters, symbols, values = [], [], [] for p in self.parameters: value = ca.MX(p.value) if value.is_constant(): simple_parameters.append(p) else: symbols.append(p.symbol) values.append(value) self.parameters = simple_parameters if len(values) > 0: # Resolve expressions that include other, non-simple parameter # expressions. converged = False while not converged: new_values = ca.substitute(values, symbols, values) converged = ca.is_equal(ca.veccat(*values), ca.veccat(*new_values), CASADI_COMPARISON_DEPTH) values = new_values if len(self.equations) > 0: self.equations = ca.substitute(self.equations, symbols, values) if len(self.initial_equations) > 0: self.initial_equations = ca.substitute(self.initial_equations, symbols, values) # Replace parameter expressions in metadata for variable in itertools.chain(self.states, self.alg_states, self.inputs, self.parameters, self.constants): for attribute in ast.Symbol.ATTRIBUTES: value = getattr(variable, attribute) if isinstance(value, ca.MX) and not value.is_constant(): [value] = ca.substitute([value], symbols, values) setattr(variable, attribute, value) if options.get('replace_constant_expressions', False): logger.info("Replacing constant expressions") simple_constants, symbols, values = [], [], [] for c in self.constants: value = ca.MX(c.value) if value.is_constant(): simple_constants.append(c) else: symbols.append(c.symbol) values.append(value) self.constants = simple_constants if len(values) > 0: # Resolve expressions that include other, non-simple parameter # expressions. converged = False while not converged: new_values = ca.substitute(values, symbols, values) converged = ca.is_equal(ca.veccat(*values), ca.veccat(*new_values), CASADI_COMPARISON_DEPTH) values = new_values if len(self.equations) > 0: self.equations = ca.substitute(self.equations, symbols, values) if len(self.initial_equations) > 0: self.initial_equations = ca.substitute(self.initial_equations, symbols, values) # Replace constant expressions in metadata for variable in itertools.chain(self.states, self.alg_states, self.inputs, self.parameters, self.constants): for attribute in ast.Symbol.ATTRIBUTES: value = getattr(variable, attribute) if isinstance(value, ca.MX) and not value.is_constant(): [value] = ca.substitute([value], symbols, values) setattr(variable, attribute, value) if options.get('eliminate_constant_assignments', False): logger.info("Elimating constant variable assignments") alg_states = OrderedDict([(s.symbol.name(), s) for s in self.alg_states]) reduced_equations = [] for eq in self.equations: if eq.is_symbolic() and eq.name() in alg_states: constant = alg_states.pop(eq.name()) constant.value = 0.0 self.constants.append(constant) # Skip this equation continue if eq.n_dep() == 2 and (eq.is_op(ca.OP_SUB) or eq.is_op(ca.OP_ADD)): if eq.dep(0).is_symbolic() and eq.dep(0).name() in alg_states and eq.dep(1).is_constant(): variable = eq.dep(0) value = eq.dep(1) elif eq.dep(1).is_symbolic() and eq.dep(1).name() in alg_states and eq.dep(0).is_constant(): variable = eq.dep(1) value = eq.dep(0) else: variable = None value = None if variable is not None: constant = alg_states.pop(variable.name()) if eq.is_op(ca.OP_SUB): constant.value = value else: constant.value = -value self.constants.append(constant) # Skip this equation continue # Keep this equation reduced_equations.append(eq) # Eliminate alias variables self.alg_states = list(alg_states.values()) self.equations = reduced_equations if options.get('replace_parameter_values', False): logger.info("Replacing parameter values") # N.B. Any parameter expression elimination must be done first. unspecified_parameters, symbols, values = [], [], [] for p in self.parameters: if ca.MX(p.value).is_constant() and ca.MX(p.value).is_regular(): symbols.append(p.symbol) values.append(p.value) else: unspecified_parameters.append(p) if len(self.equations) > 0: self.equations = ca.substitute(self.equations, symbols, values) if len(self.initial_equations) > 0: self.initial_equations = ca.substitute(self.initial_equations, symbols, values) self.parameters = unspecified_parameters # Replace parameter values in metadata for variable in itertools.chain(self.states, self.alg_states, self.inputs, self.parameters, self.constants): for attribute in ast.Symbol.ATTRIBUTES: value = getattr(variable, attribute) if isinstance(value, ca.MX) and not value.is_constant(): [value] = ca.substitute([value], symbols, values) setattr(variable, attribute, value) if options.get('replace_constant_values', False): logger.info("Replacing constant values") # N.B. Any parameter expression elimination must be done first. symbols = self._symbols(self.constants) values = [v.value for v in self.constants] if len(self.equations) > 0: self.equations = ca.substitute(self.equations, symbols, values) if len(self.initial_equations) > 0: self.initial_equations = ca.substitute(self.initial_equations, symbols, values) self.constants = [] # Replace constant values in metadata for variable in itertools.chain(self.states, self.alg_states, self.inputs, self.parameters, self.constants): for attribute in ast.Symbol.ATTRIBUTES: value = getattr(variable, attribute) if isinstance(value, ca.MX) and not value.is_constant(): [value] = ca.substitute([value], symbols, values) setattr(variable, attribute, value) if options.get('eliminable_variable_expression', None) is not None: logger.info("Elimating variables that match the regular expression {}".format(options['eliminable_variable_expression'])) p = re.compile(options['eliminable_variable_expression']) alg_states = OrderedDict([(s.symbol.name(), s) for s in self.alg_states]) variables = [] values = [] reduced_equations = [] for eq in self.equations: if eq.is_symbolic() and eq.name() in alg_states and p.match(eq.name()): variables.append(eq) values.append(0.0) del alg_states[eq.name()] # Skip this equation continue if eq.n_dep() == 2 and (eq.is_op(ca.OP_SUB) or eq.is_op(ca.OP_ADD)): if eq.dep(0).is_symbolic() and eq.dep(0).name() in alg_states and p.match(eq.dep(0).name()): variable = eq.dep(0) value = eq.dep(1) elif eq.dep(1).is_symbolic() and eq.dep(1).name() in alg_states and p.match(eq.dep(1).name()): variable = eq.dep(1) value = eq.dep(0) else: variable = None value = None if variable is not None: del alg_states[variable.name()] variables.append(variable) if eq.is_op(ca.OP_SUB): values.append(value) else: values.append(-value) # Skip this equation continue # Keep this equation reduced_equations.append(eq) # Eliminate alias variables self.alg_states = list(alg_states.values()) self.equations = reduced_equations if len(self.equations) > 0: self.equations = ca.substitute(self.equations, variables, values) if len(self.initial_equations) > 0: self.initial_equations = ca.substitute(self.initial_equations, variables, values) if options.get('expand_vectors', False): logger.info("Expanding vectors") symbols = [] values = [] for l in ['states', 'der_states', 'alg_states', 'inputs', 'parameters', 'constants']: old_vars = getattr(self, l) new_vars = [] for old_var in old_vars: if old_var.symbol.numel() > 1: rows = [] for i in range(old_var.symbol.size1()): cols = [] for j in range(old_var.symbol.size2()): if old_var.symbol.size1() > 1 and old_var.symbol.size2() > 1: component_symbol = ca.MX.sym('{}[{},{}]'.format(old_var.symbol.name(), i, j)) elif old_var.symbol.size1() > 1: component_symbol = ca.MX.sym('{}[{}]'.format(old_var.symbol.name(), i)) elif old_var.symbol.size2() > 1: component_symbol = ca.MX.sym('{}[{}]'.format(old_var.symbol.name(), j)) else: raise AssertionError component_var = Variable(component_symbol, old_var.python_type) for attribute in ast.Symbol.ATTRIBUTES: value = ca.MX(getattr(old_var, attribute)) if value.size1() == old_var.symbol.size1() and value.size2() == old_var.symbol.size2(): setattr(component_var, attribute, value[i, j]) elif value.size1() == old_var.symbol.size1(): setattr(component_var, attribute, value[i]) elif value.size2() == old_var.symbol.size2(): setattr(component_var, attribute, value[j]) else: assert value.size1() == 1 assert value.size2() == 1 setattr(component_var, attribute, value) cols.append(component_var) rows.append(cols) symbols.append(old_var.symbol) values.append(ca.vertcat(*[ca.horzcat(*[v.symbol for v in row]) for row in rows])) new_vars.extend(itertools.chain.from_iterable(rows)) else: new_vars.append(old_var) setattr(self, l, new_vars) if len(self.equations) > 0: self.equations = ca.substitute(self.equations, symbols, values) self.equations = list(itertools.chain.from_iterable(ca.vertsplit(ca.vec(eq)) for eq in self.equations)) if len(self.initial_equations) > 0: self.initial_equations = ca.substitute(self.initial_equations, symbols, values) self.initial_equations = list(itertools.chain.from_iterable(ca.vertsplit(ca.vec(eq)) for eq in self.initial_equations)) # Replace values in metadata for variable in itertools.chain(self.states, self.alg_states, self.inputs, self.parameters, self.constants): for attribute in ast.Symbol.ATTRIBUTES: value = getattr(variable, attribute) if isinstance(value, ca.MX) and not value.is_constant(): [value] = ca.substitute([value], symbols, values) setattr(variable, attribute, value) if options.get('detect_aliases', False): logger.info("Detecting aliases") states = OrderedDict([(s.symbol.name(), s) for s in self.states]) der_states = OrderedDict([(s.symbol.name(), s) for s in self.der_states]) alg_states = OrderedDict([(s.symbol.name(), s) for s in self.alg_states]) inputs = OrderedDict([(s.symbol.name(), s) for s in self.inputs]) all_states = OrderedDict() all_states.update(states) all_states.update(der_states) all_states.update(alg_states) all_states.update(inputs) alias_rel = AliasRelation() # For now, we only eliminate algebraic states. do_not_eliminate = set(list(der_states) + list(states) + list(inputs)) reduced_equations = [] for eq in self.equations: if eq.n_dep() == 2 and (eq.is_op(ca.OP_SUB) or eq.is_op(ca.OP_ADD)): if eq.dep(0).is_symbolic() and eq.dep(1).is_symbolic(): if eq.dep(0).name() in alg_states: alg_state = eq.dep(0) other_state = eq.dep(1) elif eq.dep(1).name() in alg_states: alg_state = eq.dep(1) other_state = eq.dep(0) else: alg_state = None other_state = None # If both states are algebraic, we need to decide which to eliminate if eq.dep(0).name() in alg_states and eq.dep(1).name() in alg_states: # Most of the time it does not matter which one we eliminate. # The exception is if alg_state has already been aliased to a # variable in do_not_eliminate. If this is the case, setting the # states in the default order will cause the new canonical variable # to be other_state, unseating (and eliminating) the current # canonical variable (which is in do_not_eliminate). if alias_rel.canonical_signed(alg_state.name())[0] in do_not_eliminate: # swap the states other_state, alg_state = alg_state, other_state if alg_state is not None: # Check to see if we are linking two entries in do_not_eliminate if alias_rel.canonical_signed(alg_state.name())[0] in do_not_eliminate and \ alias_rel.canonical_signed(other_state.name())[0] in do_not_eliminate: # Don't do anything for now, we only eliminate alg_states pass else: # Eliminate alg_state by aliasing it to other_state if eq.is_op(ca.OP_SUB): alias_rel.add(other_state.name(), alg_state.name()) else: alias_rel.add(other_state.name(), '-' + alg_state.name()) # To keep equations balanced, drop this equation continue # Keep this equation reduced_equations.append(eq) # Eliminate alias variables variables, values = [], [] for canonical, aliases in alias_rel: canonical_state = all_states[canonical] python_type = canonical_state.python_type start = canonical_state.start m, M = canonical_state.min, canonical_state.max nominal = canonical_state.nominal fixed = canonical_state.fixed for alias in aliases: if alias[0] == '-': sign = -1 alias = alias[1:] else: sign = 1 alias_state = all_states[alias] variables.append(alias_state.symbol) values.append(sign * canonical_state.symbol) # If any of the aliases has a nonstandard type, apply it to # the canonical state as well if alias_state.python_type != float: python_type = alias_state.python_type # If any of the aliases has a nondefault start value, apply it to # the canonical state as well alias_state_start = ca.MX(alias_state.start) if alias_state_start.is_regular() and not alias_state_start.is_zero(): start = sign * alias_state.start # The intersection of all bound ranges applies m = ca.fmax(m, alias_state.min if sign == 1 else -alias_state.max) M = ca.fmin(M, alias_state.max if sign == 1 else -alias_state.min) # Take the largest nominal of all aliases nominal = ca.fmax(nominal, alias_state.nominal) # If any of the aliases is fixed, the canonical state is as well fixed = ca.fmax(fixed, alias_state.fixed) del all_states[alias] canonical_state.aliases = aliases canonical_state.python_type = python_type canonical_state.start = start canonical_state.min = m canonical_state.max = M canonical_state.nominal = nominal canonical_state.fixed = fixed self.states = [v for k, v in all_states.items() if k in states] self.der_states = [v for k, v in all_states.items() if k in der_states] self.alg_states = [v for k, v in all_states.items() if k in alg_states] self.inputs = [v for k, v in all_states.items() if k in inputs] self.equations = reduced_equations if len(self.equations) > 0: self.equations = ca.substitute(self.equations, variables, values) if len(self.initial_equations) > 0: self.initial_equations = ca.substitute(self.initial_equations, variables, values) if options.get('reduce_affine_expression', False): logger.info("Collapsing model into an affine expression") for equation_list in ['equations', 'initial_equations']: equations = getattr(self, equation_list) if len(equations) > 0: states = ca.veccat(*self._symbols(itertools.chain(self.states, self.der_states, self.alg_states, self.inputs))) constants = ca.veccat(*self._symbols(self.constants)) parameters = ca.veccat(*self._symbols(self.parameters)) equations = ca.veccat(*equations) Af = ca.Function('Af', [states, constants, parameters], [ca.jacobian(equations, states)]) bf = ca.Function('bf', [states, constants, parameters], [equations]) # Work around CasADi issue #172 if len(self.constants) == 0 or not ca.depends_on(equations, constants): constants = 0 else: logger.warning('Not all constants have been eliminated. As a result, the affine DAE expression will use a symbolic matrix, as opposed to a numerical sparse matrix.') if len(self.parameters) == 0 or not ca.depends_on(equations, parameters): parameters = 0 else: logger.warning('Not all parameters have been eliminated. As a result, the affine DAE expression will use a symbolic matrix, as opposed to a numerical sparse matrix.') A = Af(0, constants, parameters) b = bf(0, constants, parameters) # Replace veccat'ed states with brand new state vectors so as to avoid the value copy operations induced by veccat. self._states_vector = ca.MX.sym('states_vector', sum([s.numel() for s in self._symbols(self.states)])) self._der_states_vector = ca.MX.sym('der_states_vector', sum([s.numel() for s in self._symbols(self.der_states)])) self._alg_states_vector = ca.MX.sym('alg_states_vector', sum([s.numel() for s in self._symbols(self.alg_states)])) self._inputs_vector = ca.MX.sym('inputs_vector', sum([s.numel() for s in self._symbols(self.inputs)])) states_vector = ca.vertcat(self._states_vector, self._der_states_vector, self._alg_states_vector, self._inputs_vector) equations = [ca.reshape(ca.mtimes(A, states_vector), equations.shape) + b] setattr(self, equation_list, equations) if options.get('expand_mx', False): logger.info("Expanding MX graph") if len(self.equations) > 0: self.equations = ca.matrix_expand(self.equations) if len(self.initial_equations) > 0: self.initial_equations = ca.matrix_expand(self.initial_equations) logger.info("Finished model simplification")
def trim_and_square(x): return cd.fmax(x, 0)**2