Files
openpilot/selfdrive/controls/lib/longitudinal_planner.py
Bruce Wayne 580f836249 longitudinal: move cruise speed limit out of the MPC
The cruise speed limit is now applied in the planner instead of as a
fake obstacle inside the MPC. The MPC only plans around lead obstacles,
and the planner applies a rate-limited cruise acceleration limit.

- long_mpc.py: remove v_cruise param, cruise obstacle, CRUISE_*_ACCEL
  constants, and cruise from MPC_SOURCES
- longitudinal_planner.py: apply rate-limited cruise acceleration limit
  outside the MPC and take the min with the MPC output
2026-07-03 08:42:24 +08:00

298 lines
13 KiB
Python
Executable File

#!/usr/bin/env python3
import math
import numpy as np
import cereal.messaging as messaging
from opendbc.car.interfaces import ACCEL_MIN, ACCEL_MAX
from openpilot.common.constants import CV
from openpilot.common.filter_simple import FirstOrderFilter
from openpilot.common.realtime import DT_MDL
from openpilot.selfdrive.modeld.constants import ModelConstants
from openpilot.selfdrive.controls.lib.longcontrol import LongCtrlState
from openpilot.selfdrive.controls.lib.longitudinal_mpc_lib.long_mpc import LongitudinalMpc, LongitudinalPlanSource
from openpilot.selfdrive.controls.lib.longitudinal_mpc_lib.long_mpc import T_IDXS as T_IDXS_MPC
from openpilot.selfdrive.controls.lib.drive_helpers import CONTROL_N, get_accel_from_plan
from openpilot.selfdrive.car.cruise import V_CRUISE_MAX, V_CRUISE_UNSET
from openpilot.common.swaglog import cloudlog
from dragonpilot.selfdrive.controls.lib.acm import ACM
from dragonpilot.selfdrive.controls.lib.aem import AEM
from dragonpilot.selfdrive.controls.lib.apm import APM
A_CRUISE_MAX_VALS = [1.6, 1.2, 0.8, 0.6]
A_CRUISE_MAX_BP = [0., 10.0, 25., 40.]
CONTROL_N_T_IDX = ModelConstants.T_IDXS[:CONTROL_N]
ALLOW_THROTTLE_THRESHOLD = 0.4
MIN_ALLOW_THROTTLE_SPEED = 2.5
# Cruise speed limit (moved out of the MPC)
CRUISE_MAX_ACCEL = 1.6
CRUISE_JERK = 0.5 # m/s^3, max rate of change of cruise limit acceleration
# Lookup table for turns
_A_TOTAL_MAX_V = [1.7, 3.2]
_A_TOTAL_MAX_BP = [20., 40.]
# Vision-based curve slowdown: look ahead 1~3 seconds for max curvature
_CURVE_LOOKAHEAD_START = 1.0 # 最早看未来多少秒
_CURVE_LOOKAHEAD_END = 3.0 # 最远看未来多少秒
_CURVE_SLOWDOWN_THRESHOLD = 5.0 # 横向加速度超过此值开始减速 (m/s²)
def get_future_curvature(position_x, position_y, v_ego, t_idxs, lookahead_start, lookahead_end):
"""从模型轨迹 position.x/y 计算前方指定时间窗口内的最大曲率"""
# 找到对应时间窗口的索引
idxs = [i for i, t in enumerate(t_idxs) if lookahead_start <= t <= lookahead_end]
if len(idxs) < 2:
return 0.0
x = np.array(position_x)[idxs]
y = np.array(position_y)[idxs]
# 计算路径上相邻点之间的弯曲程度
# 用三点法估算曲率: 在 (x_i, y_i) 处的曲率 = |x' y'' - y' x''| / (x'² + y'²)^1.5
curvatures = []
for i in range(1, len(idxs) - 1):
dx = x[i+1] - x[i-1]
dy = y[i+1] - y[i-1]
ddx = x[i+1] - 2*x[i] + x[i-1]
ddy = y[i+1] - 2*y[i] + y[i-1]
denom = (dx*dx + dy*dy) ** 1.5
if denom > 1e-6:
curv = abs(dx * ddy - dy * ddx) / denom
curvatures.append(curv)
return max(curvatures) if curvatures else 0.0
def limit_accel_in_turns_vision(v_ego, position_x, position_y, t_idxs, a_target):
"""用视觉轨迹预估前方弯道,限制最大允许纵向加速度"""
curvature = get_future_curvature(position_x, position_y, v_ego, t_idxs,
_CURVE_LOOKAHEAD_START, _CURVE_LOOKAHEAD_END)
if curvature < 1e-6:
return a_target, 0.0
a_y_predicted = v_ego ** 2 * curvature
if a_y_predicted <= _CURVE_SLOWDOWN_THRESHOLD:
return a_target, a_y_predicted
# 线性减速:横向加速度越大,纵向限制越紧
# 基准:a_y=5时无限制,a_y=8时只允许-2 m/s²(主动减速)
slowdown_ratio = max(0.0, 1.0 - (a_y_predicted - _CURVE_SLOWDOWN_THRESHOLD) / 4.0)
a_x_allowed = np.interp(slowdown_ratio, [0.0, 1.0], [-2.0, a_target[1]])
return [a_target[0], min(a_target[1], a_x_allowed)], a_y_predicted
class DPFlags:
ACM = 1
AEM = 2
APM = 2 ** 2
pass
def get_max_accel(v_ego):
return np.interp(v_ego, A_CRUISE_MAX_BP, A_CRUISE_MAX_VALS)
def get_coast_accel(pitch):
return np.sin(pitch) * -5.65 - 0.3 # fitted from data using xx/projects/allow_throttle/compute_coast_accel.py
def limit_accel_in_turns(v_ego, angle_steers, a_target, CP):
"""
This function returns a limited long acceleration allowed, depending on the existing lateral acceleration
this should avoid accelerating when losing the target in turns
"""
# FIXME: This function to calculate lateral accel is incorrect and should use the VehicleModel
# The lookup table for turns should also be updated if we do this
a_total_max = np.interp(v_ego, _A_TOTAL_MAX_BP, _A_TOTAL_MAX_V)
a_y = v_ego ** 2 * angle_steers * CV.DEG_TO_RAD / (CP.steerRatio * CP.wheelbase)
a_x_allowed = math.sqrt(max(a_total_max ** 2 - a_y ** 2, 0.))
return [a_target[0], min(a_target[1], a_x_allowed)]
class LongitudinalPlanner:
def __init__(self, CP, init_v=0.0, init_a=0.0, dt=DT_MDL):
self.CP = CP
self.mpc = LongitudinalMpc(dt=dt)
self.fcw = False
self.dt = dt
self.allow_throttle = True
self.a_desired = init_a
self.v_desired_filter = FirstOrderFilter(init_v, 2.0, self.dt)
self.a_cruise = 0.0
self.prev_accel_clip = [ACCEL_MIN, ACCEL_MAX]
self.curve_brake_filter = FirstOrderFilter(0.0, 0.5, self.dt) # 弯道减速平滑滤波
self.output_a_target = 0.0
self.output_should_stop = False
self.v_desired_trajectory = np.zeros(CONTROL_N)
self.a_desired_trajectory = np.zeros(CONTROL_N)
self.j_desired_trajectory = np.zeros(CONTROL_N)
self.acm = ACM()
self.aem = AEM()
self.apm = APM()
@staticmethod
def parse_model(model_msg):
if (len(model_msg.position.x) == ModelConstants.IDX_N and
len(model_msg.velocity.x) == ModelConstants.IDX_N and
len(model_msg.acceleration.x) == ModelConstants.IDX_N):
x = np.interp(T_IDXS_MPC, ModelConstants.T_IDXS, model_msg.position.x)
v = np.interp(T_IDXS_MPC, ModelConstants.T_IDXS, model_msg.velocity.x)
a = np.interp(T_IDXS_MPC, ModelConstants.T_IDXS, model_msg.acceleration.x)
j = np.zeros(len(T_IDXS_MPC))
else:
x = np.zeros(len(T_IDXS_MPC))
v = np.zeros(len(T_IDXS_MPC))
a = np.zeros(len(T_IDXS_MPC))
j = np.zeros(len(T_IDXS_MPC))
if len(model_msg.meta.disengagePredictions.gasPressProbs) > 1:
throttle_prob = model_msg.meta.disengagePredictions.gasPressProbs[1]
else:
throttle_prob = 1.0
return x, v, a, j, throttle_prob
def update(self, sm, dp_flags = 0):
if len(sm['carControl'].orientationNED) == 3:
accel_coast = get_coast_accel(sm['carControl'].orientationNED[1])
else:
accel_coast = ACCEL_MAX
v_ego = sm['carState'].vEgo
v_cruise_kph = min(sm['carState'].vCruise, V_CRUISE_MAX)
v_cruise = v_cruise_kph * CV.KPH_TO_MS
v_cruise_initialized = sm['carState'].vCruise != V_CRUISE_UNSET
long_control_off = sm['controlsState'].longControlState == LongCtrlState.off
force_slow_decel = sm['controlsState'].forceDecel
# Reset current state when not engaged, or user is controlling the speed
reset_state = long_control_off if self.CP.openpilotLongitudinalControl else not sm['selfdriveState'].enabled
# PCM cruise speed may be updated a few cycles later, check if initialized
reset_state = reset_state or not v_cruise_initialized
# No change cost when user is controlling the speed, or when standstill
prev_accel_constraint = not (reset_state or sm['carState'].standstill)
accel_clip = [ACCEL_MIN, get_max_accel(v_ego)]
# Vision-based curve slowdown (ahead 1~3s), falls back to steering angle method at low speed
if v_ego > 5.0: # >18 km/h, 视觉轨迹更可靠
accel_clip_curve, a_y_pred = limit_accel_in_turns_vision(v_ego, sm['modelV2'].position.x, sm['modelV2'].position.y,
ModelConstants.T_IDXS, accel_clip)
# 平滑滤波,防止弯道减速突然变化
filtered_clip = self.curve_brake_filter.update(accel_clip_curve[1])
accel_clip[1] = min(accel_clip[1], filtered_clip) if a_y_pred > _CURVE_SLOWDOWN_THRESHOLD else max(accel_clip[1], filtered_clip)
else:
steer_angle_without_offset = sm['carState'].steeringAngleDeg - sm['liveParameters'].angleOffsetDeg
accel_clip = limit_accel_in_turns(v_ego, steer_angle_without_offset, accel_clip, self.CP)
if reset_state:
self.v_desired_filter.x = v_ego
# Clip aEgo to cruise limits to prevent large accelerations when becoming active
self.a_desired = np.clip(sm['carState'].aEgo, accel_clip[0], accel_clip[1])
# Prevent divergence, smooth in current v_ego
self.v_desired_filter.x = max(0.0, self.v_desired_filter.update(v_ego))
_, _, _, _, throttle_prob = self.parse_model(sm['modelV2'])
# Don't clip at low speeds since throttle_prob doesn't account for creep
self.allow_throttle = throttle_prob > ALLOW_THROTTLE_THRESHOLD or v_ego <= MIN_ALLOW_THROTTLE_SPEED
if not self.allow_throttle:
clipped_accel_coast = max(accel_coast, accel_clip[0])
clipped_accel_coast_interp = np.interp(v_ego, [MIN_ALLOW_THROTTLE_SPEED, MIN_ALLOW_THROTTLE_SPEED*2], [accel_clip[1], clipped_accel_coast])
accel_clip[1] = min(accel_clip[1], clipped_accel_coast_interp)
if force_slow_decel:
v_cruise = 0.0
personality = sm['selfdriveState'].personality
if dp_flags & DPFlags.APM:
personality = self.apm.get_personality(v_ego, personality)
self.mpc.set_weights(prev_accel_constraint, personality=personality)
self.mpc.set_cur_state(self.v_desired_filter.x, self.a_desired)
self.mpc.update(sm['radarState'], personality=personality)
self.v_desired_trajectory = np.interp(CONTROL_N_T_IDX, T_IDXS_MPC, self.mpc.v_solution)
self.a_desired_trajectory = np.interp(CONTROL_N_T_IDX, T_IDXS_MPC, self.mpc.a_solution)
# ACM - Adaptive Coasting Module
if dp_flags & DPFlags.ACM:
user_control = long_control_off if self.CP.openpilotLongitudinalControl else not sm['selfdriveState'].enabled
self.acm.update_states(sm['carControl'], sm['radarState'], user_control, v_ego, v_cruise)
self.a_desired_trajectory = self.acm.update_a_desired_trajectory(self.a_desired_trajectory)
self.j_desired_trajectory = np.interp(CONTROL_N_T_IDX, T_IDXS_MPC[:-1], self.mpc.j_solution)
# TODO counter is only needed because radar is glitchy, remove once radar is gone
self.fcw = self.mpc.crash_cnt > 2 and not sm['carState'].standstill
if self.fcw:
cloudlog.info("FCW triggered")
# Interpolate 0.05 seconds and save as starting point for next iteration
a_prev = self.a_desired
self.a_desired = float(np.interp(self.dt, CONTROL_N_T_IDX, self.a_desired_trajectory))
self.v_desired_filter.x = self.v_desired_filter.x + self.dt * (self.a_desired + a_prev) / 2.0
action_t = self.CP.longitudinalActuatorDelay + DT_MDL
output_a_target_mpc, output_should_stop_mpc = get_accel_from_plan(self.v_desired_trajectory, self.a_desired_trajectory, CONTROL_N_T_IDX,
action_t=action_t, vEgoStopping=self.CP.vEgoStopping)
# Apply cruise speed limit outside the MPC
v_err = v_cruise - v_ego
if force_slow_decel:
a_cruise_target = ACCEL_MIN
elif v_err > 0:
a_cruise_target = CRUISE_MAX_ACCEL * np.clip(v_err / (CRUISE_MAX_ACCEL**2 / (2 * CRUISE_JERK)), 0, 1)
else:
a_cruise_target = 0.0
self.a_cruise = float(np.clip(a_cruise_target, self.a_cruise - CRUISE_JERK * self.dt, self.a_cruise + CRUISE_JERK * self.dt))
if self.a_cruise < output_a_target_mpc:
output_a_target_mpc = self.a_cruise
self.mpc.source = LongitudinalPlanSource.cruise
output_a_target_e2e = sm['modelV2'].action.desiredAcceleration
output_should_stop_e2e = sm['modelV2'].action.shouldStop
mode = 'blended' if sm['selfdriveState'].experimentalMode else 'acc'
if dp_flags & DPFlags.AEM:
self.aem.update_states(model_msg=sm['modelV2'], radar_msg=sm['radarState'], v_ego=sm['carState'].vEgo)
mode = self.aem.get_mode(mode)
if mode == 'blended':
output_a_target = min(output_a_target_e2e, output_a_target_mpc)
self.output_should_stop = output_should_stop_e2e or output_should_stop_mpc
if output_a_target < output_a_target_mpc:
self.mpc.source = LongitudinalPlanSource.e2e
else:
output_a_target = output_a_target_mpc
self.output_should_stop = output_should_stop_mpc
for idx in range(2):
accel_clip[idx] = np.clip(accel_clip[idx], self.prev_accel_clip[idx] - 0.05, self.prev_accel_clip[idx] + 0.05)
self.output_a_target = np.clip(output_a_target, accel_clip[0], accel_clip[1])
self.prev_accel_clip = accel_clip
def publish(self, sm, pm):
plan_send = messaging.new_message('longitudinalPlan')
plan_send.valid = sm.all_checks(service_list=['carState', 'controlsState', 'selfdriveState', 'radarState'])
longitudinalPlan = plan_send.longitudinalPlan
longitudinalPlan.modelMonoTime = sm.logMonoTime['modelV2']
longitudinalPlan.processingDelay = (plan_send.logMonoTime / 1e9) - sm.logMonoTime['modelV2']
longitudinalPlan.solverExecutionTime = self.mpc.solve_time
longitudinalPlan.speeds = self.v_desired_trajectory.tolist()
longitudinalPlan.accels = self.a_desired_trajectory.tolist()
longitudinalPlan.jerks = self.j_desired_trajectory.tolist()
longitudinalPlan.hasLead = sm['radarState'].leadOne.status
longitudinalPlan.longitudinalPlanSource = self.mpc.source
longitudinalPlan.fcw = self.fcw
longitudinalPlan.aTarget = float(self.output_a_target)
longitudinalPlan.shouldStop = bool(self.output_should_stop)
longitudinalPlan.allowBrake = True
longitudinalPlan.allowThrottle = bool(self.allow_throttle)
pm.send('longitudinalPlan', plan_send)