mirror of
https://jihulab.com/mr-one/openpilot.git
synced 2026-10-28 05:23:46 +08:00
580f836249
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
298 lines
13 KiB
Python
Executable File
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)
|