#!/usr/bin/env python3 import os import time import numpy as np from casadi import SX, vertcat from cereal import log try: from opendbc.car.interfaces import ACCEL_MIN, ACCEL_MAX except Exception: # Generated-code builds run before every opendbc extension is available. ACCEL_MIN = -3.5 ACCEL_MAX = 2.0 from openpilot.common.realtime import DT_MDL from openpilot.common.swaglog import cloudlog from openpilot.selfdrive.modeld.constants import index_function if __name__ == "__main__": from openpilot.third_party.acados.acados_template import AcadosModel, AcadosOcp, AcadosOcpSolver else: from openpilot.selfdrive.controls.lib.longitudinal_mpc_lib.c_generated_code.acados_ocp_solver_pyx import AcadosOcpSolverCython MODEL_NAME = "long" LONG_MPC_DIR = os.path.dirname(os.path.abspath(__file__)) EXPORT_DIR = os.path.join(LONG_MPC_DIR, "c_generated_code") JSON_FILE = os.path.join(LONG_MPC_DIR, "acados_ocp_long.json") SOURCES = ("lead0", "lead1", "cruise") X_DIM = 3 U_DIM = 1 PARAM_DIM = 6 COST_E_DIM = 5 COST_DIM = COST_E_DIM + 1 CONSTR_DIM = 4 X_EGO_OBSTACLE_COST = 3.0 J_EGO_COST = 5.0 A_CHANGE_COST = 200.0 DANGER_ZONE_COST = 100.0 LEAD_DANGER_FACTOR = 0.75 LIMIT_COST = 1e6 CRASH_DISTANCE = 0.25 ACADOS_SOLVER_TYPE = "SQP_RTI" N = 12 MAX_T = 10.0 T_IDXS = np.array([index_function(i, max_val=MAX_T, max_idx=N) for i in range(N + 1)]) T_DIFFS = np.diff(T_IDXS, prepend=[0.0]) FCW_IDXS = T_IDXS < 5.0 COMFORT_BRAKE = 2.5 STOP_DISTANCE = 6.0 CRUISE_MIN_ACCEL = -1.2 CRUISE_MAX_ACCEL = 1.6 LEAD_ACCEL_TAU = 1.5 LEAD_FILTER_TAU = 0.45 LEAD_FILTER_RESET_DISTANCE = 8.0 FCW_MIN_MODEL_PROB = 0.9 FCW_MIN_CLOSING_SPEED = 0.5 FCW_MAX_TTC = 4.0 def _personality_value(aggressive, standard, relaxed, personality): # NB: compare with == rather than a dict lookup. `personality` is a capnp # _DynamicEnum read from a message; it compares equal to the schema enumerants # but does not hash to the same bucket, so a dict[personality] lookup KeyErrors # on otherwise-valid values. if personality == log.LongitudinalPersonality.aggressive: return aggressive if personality == log.LongitudinalPersonality.relaxed: return relaxed return standard def get_jerk_factor(aggressive_accel=0.5, aggressive_danger=0.5, aggressive_speed=0.5, standard_accel=1.0, standard_danger=1.0, standard_speed=1.0, relaxed_accel=1.0, relaxed_danger=1.0, relaxed_speed=1.0, custom_personalities=False, personality=log.LongitudinalPersonality.standard): if not custom_personalities: factor = 0.5 if personality == log.LongitudinalPersonality.aggressive else 1.0 return factor, factor, factor return ( _personality_value(aggressive_accel, standard_accel, relaxed_accel, personality), _personality_value(aggressive_danger, standard_danger, relaxed_danger, personality), _personality_value(aggressive_speed, standard_speed, relaxed_speed, personality), ) def get_T_FOLLOW(aggressive_follow=1.25, standard_follow=1.45, relaxed_follow=1.75, custom_personalities=False, personality=log.LongitudinalPersonality.standard): if custom_personalities: return _personality_value(aggressive_follow, standard_follow, relaxed_follow, personality) return _personality_value(1.25, 1.45, 1.75, personality) def get_stopped_equivalence_factor(v_lead): return (v_lead ** 2) / (2.0 * COMFORT_BRAKE) def get_safe_obstacle_distance(v_ego, t_follow): return (v_ego ** 2) / (2.0 * COMFORT_BRAKE) + t_follow * v_ego + STOP_DISTANCE def desired_follow_distance(v_ego, v_lead, t_follow=None): t_follow = get_T_FOLLOW() if t_follow is None else t_follow return get_safe_obstacle_distance(v_ego, t_follow) - get_stopped_equivalence_factor(v_lead) def should_trigger_planner_fcw(lead, v_ego): if lead is None or not bool(getattr(lead, "status", False)): return False if float(getattr(lead, "modelProb", 0.0)) <= FCW_MIN_MODEL_PROB: return False closing_speed = max(0.0, float(v_ego) - float(getattr(lead, "vLead", 0.0))) ttc = max(0.0, float(getattr(lead, "dRel", 0.0))) / max(closing_speed, 1e-3) return closing_speed >= FCW_MIN_CLOSING_SPEED and ttc < FCW_MAX_TTC def gen_long_model(): model = AcadosModel() model.name = MODEL_NAME x_ego = SX.sym("x_ego") v_ego = SX.sym("v_ego") a_ego = SX.sym("a_ego") model.x = vertcat(x_ego, v_ego, a_ego) j_ego = SX.sym("j_ego") model.u = vertcat(j_ego) x_ego_dot = SX.sym("x_ego_dot") v_ego_dot = SX.sym("v_ego_dot") a_ego_dot = SX.sym("a_ego_dot") model.xdot = vertcat(x_ego_dot, v_ego_dot, a_ego_dot) a_min = SX.sym("a_min") a_max = SX.sym("a_max") x_obstacle = SX.sym("x_obstacle") prev_a = SX.sym("prev_a") lead_t_follow = SX.sym("lead_t_follow") lead_danger_factor = SX.sym("lead_danger_factor") model.p = vertcat(a_min, a_max, x_obstacle, prev_a, lead_t_follow, lead_danger_factor) dynamics = vertcat(v_ego, a_ego, j_ego) model.f_impl_expr = model.xdot - dynamics model.f_expl_expr = dynamics return model def gen_long_ocp(): ocp = AcadosOcp() ocp.model = gen_long_model() ocp.dims.N = N ocp.cost.cost_type = "NONLINEAR_LS" ocp.cost.cost_type_e = "NONLINEAR_LS" ocp.cost.W = np.zeros((COST_DIM, COST_DIM)) ocp.cost.W_e = np.zeros((COST_E_DIM, COST_E_DIM)) x_ego, v_ego, a_ego = ocp.model.x[0], ocp.model.x[1], ocp.model.x[2] j_ego = ocp.model.u[0] a_min = ocp.model.p[0] a_max = ocp.model.p[1] x_obstacle = ocp.model.p[2] prev_a = ocp.model.p[3] t_follow = ocp.model.p[4] danger_factor = ocp.model.p[5] desired_distance = get_safe_obstacle_distance(v_ego, t_follow) costs = [ ((x_obstacle - x_ego) - desired_distance) / (v_ego + 10.0), x_ego, v_ego, a_ego, a_ego - prev_a, j_ego, ] ocp.model.cost_y_expr = vertcat(*costs) ocp.model.cost_y_expr_e = vertcat(*costs[:-1]) ocp.cost.yref = np.zeros(COST_DIM) ocp.cost.yref_e = np.zeros(COST_E_DIM) ocp.model.con_h_expr = vertcat( v_ego, a_ego - a_min, a_max - a_ego, ((x_obstacle - x_ego) - danger_factor * desired_distance) / (v_ego + 10.0), ) ocp.constraints.x0 = np.zeros(X_DIM) ocp.parameter_values = np.array([ CRUISE_MIN_ACCEL, CRUISE_MAX_ACCEL, 0.0, 0.0, get_T_FOLLOW(), LEAD_DANGER_FACTOR, ]) zero_constraints = np.zeros(CONSTR_DIM) ocp.cost.zl = zero_constraints ocp.cost.Zl = zero_constraints ocp.cost.Zu = zero_constraints ocp.cost.zu = zero_constraints ocp.constraints.lh = zero_constraints ocp.constraints.uh = 1e4 * np.ones(CONSTR_DIM) ocp.constraints.idxsh = np.arange(CONSTR_DIM) ocp.solver_options.qp_solver = "PARTIAL_CONDENSING_HPIPM" ocp.solver_options.hessian_approx = "GAUSS_NEWTON" ocp.solver_options.integrator_type = "ERK" ocp.solver_options.nlp_solver_type = ACADOS_SOLVER_TYPE ocp.solver_options.qp_solver_cond_N = 1 ocp.solver_options.qp_solver_iter_max = 10 ocp.solver_options.qp_tol = 1e-3 ocp.solver_options.tf = T_IDXS[-1] ocp.solver_options.shooting_nodes = T_IDXS ocp.code_export_directory = EXPORT_DIR return ocp class LongitudinalMpc: def __init__(self, mode="acc", dt=DT_MDL): self.mode = mode self.dt = dt self.solver = AcadosOcpSolverCython(MODEL_NAME, ACADOS_SOLVER_TYPE, N) self.cruise_min_a = CRUISE_MIN_ACCEL self.max_a = CRUISE_MAX_ACCEL self.lead_filter_tau = LEAD_FILTER_TAU self.source = "cruise" self._lead_filter_state = [None, None] self.reset() def reset(self): self.solver.reset() self.x_sol = np.zeros((N + 1, X_DIM)) self.u_sol = np.zeros((N, U_DIM)) self.v_solution = np.zeros(N + 1) self.a_solution = np.zeros(N + 1) self.j_solution = np.zeros(N) self.prev_a = np.zeros(N + 1) self.yref = np.zeros((N + 1, COST_DIM)) self.params = np.zeros((N + 1, PARAM_DIM)) self.x0 = np.zeros(X_DIM) self.crash_cnt = 0 self.solution_status = 0 self.solve_time = 0.0 self.last_cloudlog_t = 0.0 self._lead_filter_state = [None, None] for i in range(N): self.solver.cost_set(i, "yref", self.yref[i]) self.solver.cost_set(N, "yref", self.yref[N][:COST_E_DIM]) for i in range(N + 1): self.solver.set(i, "x", np.zeros(X_DIM)) self.set_weights() def set_cost_weights(self, cost_weights, constraint_cost_weights): weights = np.asfortranarray(np.diag(cost_weights)) for i in range(N): weights[4, 4] = cost_weights[4] * np.interp(T_IDXS[i], [0.0, 1.0, 2.0], [1.0, 1.0, 0.0]) self.solver.cost_set(i, "W", weights) self.solver.cost_set(N, "W", np.copy(weights[:COST_E_DIM, :COST_E_DIM])) constraints = np.asarray(constraint_cost_weights) for i in range(N): self.solver.cost_set(i, "Zl", constraints) def set_weights(self, acceleration_jerk=1.0, danger_jerk=1.0, speed_jerk=1.0, prev_accel_constraint=True, **_): accel_change_cost = acceleration_jerk * A_CHANGE_COST if prev_accel_constraint else 0.0 costs = [X_EGO_OBSTACLE_COST, 0.0, 0.0, 0.0, accel_change_cost, speed_jerk * J_EGO_COST] constraints = [LIMIT_COST, LIMIT_COST, LIMIT_COST, danger_jerk * DANGER_ZONE_COST] self.set_cost_weights(costs, constraints) def set_cur_state(self, v_ego, a_ego): previous_v = self.x0[1] self.x0[1] = v_ego self.x0[2] = a_ego if abs(previous_v - v_ego) > 2.0: for i in range(N + 1): self.solver.set(i, "x", self.x0) def set_accel_limits(self, min_a, max_a): self.cruise_min_a = float(min_a) self.max_a = float(max_a) @staticmethod def extrapolate_lead(x_lead, v_lead, a_lead, a_lead_tau): a_traj = a_lead * np.exp(-a_lead_tau * (T_IDXS ** 2) / 2.0) v_traj = np.clip(v_lead + np.cumsum(T_DIFFS * a_traj), 0.0, 1e8) x_traj = x_lead + np.cumsum(T_DIFFS * v_traj) return np.column_stack((x_traj, v_traj)) def _filter_lead_motion(self, index, lead): raw_distance = float(lead.dRel) raw_velocity = float(lead.vLead) raw_accel = float(lead.aLeadK) state = self._lead_filter_state[index] reset = state is None or abs(raw_distance - state[2]) > LEAD_FILTER_RESET_DISTANCE if reset: filtered_velocity = raw_velocity filtered_accel = raw_accel else: alpha = self.dt / (self.lead_filter_tau + self.dt) filtered_velocity = state[0] + alpha * (raw_velocity - state[0]) filtered_accel = state[1] + alpha * (raw_accel - state[1]) self._lead_filter_state[index] = (filtered_velocity, filtered_accel, raw_distance) return filtered_velocity, filtered_accel def process_lead(self, lead, index): v_ego = self.x0[1] present = lead is not None and bool(getattr(lead, "status", False)) if present: x_lead = float(lead.dRel) v_lead, a_lead = self._filter_lead_motion(index, lead) a_lead_tau = max(float(getattr(lead, "aLeadTau", LEAD_ACCEL_TAU)), 0.1) else: self._lead_filter_state[index] = None x_lead = 50.0 v_lead = v_ego + 10.0 a_lead = 0.0 a_lead_tau = LEAD_ACCEL_TAU min_x_lead = ((v_ego + v_lead) / 2.0) * (v_ego - v_lead) / (-ACCEL_MIN * 2.0) x_lead = float(np.clip(x_lead, min_x_lead, 1e8)) v_lead = float(np.clip(v_lead, 0.0, 1e8)) a_lead = float(np.clip(a_lead, -10.0, 5.0)) return self.extrapolate_lead(x_lead, v_lead, a_lead, a_lead_tau) def update(self, radarstate, v_cruise, x=None, v=None, a=None, j=None, danger_factor=LEAD_DANGER_FACTOR, t_follow=None, personality=log.LongitudinalPersonality.standard, **_): t_follow = get_T_FOLLOW(personality=personality) if t_follow is None else float(t_follow) v_ego = self.x0[1] lead_trajectories = [ self.process_lead(radarstate.leadOne, 0), self.process_lead(radarstate.leadTwo, 1), ] lead_obstacles = [ trajectory[:, 0] + get_stopped_equivalence_factor(trajectory[:, 1]) for trajectory in lead_trajectories ] v_lower = v_ego + T_IDXS * self.cruise_min_a * 1.05 v_upper = v_ego + T_IDXS * self.max_a * 1.05 v_cruise_clipped = np.clip(np.full(N + 1, v_cruise), v_lower, v_upper) cruise_obstacle = np.cumsum(T_DIFFS * v_cruise_clipped) + get_safe_obstacle_distance(v_cruise_clipped, t_follow) obstacles = np.column_stack((*lead_obstacles, cruise_obstacle)) self.source = SOURCES[int(np.argmin(obstacles[0]))] self.yref.fill(0.0) for i in range(N): self.solver.set(i, "yref", self.yref[i]) self.solver.set(N, "yref", self.yref[N][:COST_E_DIM]) self.params[:, 0] = self.cruise_min_a self.params[:, 1] = max(0.0, self.max_a) self.params[:, 2] = np.min(obstacles, axis=1) self.params[:, 3] = self.prev_a self.params[:, 4] = t_follow self.params[:, 5] = float(danger_factor) self.run() crash_risk = False for lead, trajectory in zip((radarstate.leadOne, radarstate.leadTwo), lead_trajectories, strict=True): crash_risk |= bool( should_trigger_planner_fcw(lead, v_ego) and np.any(trajectory[FCW_IDXS, 0] - self.x_sol[FCW_IDXS, 0] < CRASH_DISTANCE) ) self.crash_cnt = self.crash_cnt + 1 if crash_risk else 0 def run(self): for i in range(N + 1): self.solver.set(i, "p", self.params[i]) self.solver.constraints_set(0, "lbx", self.x0) self.solver.constraints_set(0, "ubx", self.x0) self.solution_status = self.solver.solve() self.solve_time = float(self.solver.get_stats("time_tot")[0]) for i in range(N + 1): self.x_sol[i] = self.solver.get(i, "x") for i in range(N): self.u_sol[i] = self.solver.get(i, "u") self.v_solution = self.x_sol[:, 1] self.a_solution = self.x_sol[:, 2] self.j_solution = self.u_sol[:, 0] self.prev_a = np.interp(T_IDXS + self.dt, T_IDXS, self.a_solution) if self.solution_status != 0: now = time.monotonic() if now > self.last_cloudlog_t + 5.0: self.last_cloudlog_t = now cloudlog.warning(f"Long MPC reset, solution_status: {self.solution_status}") self.reset() if __name__ == "__main__": ocp = gen_long_ocp() AcadosOcpSolver.generate(ocp, json_file=JSON_FILE)