Skip to content

Commit d76f756

Browse files
authored
long_mpc: simplify longitudinal planner by removing "modes" (#37014)
1 parent 71a418d commit d76f756

File tree

3 files changed

+47
-117
lines changed

3 files changed

+47
-117
lines changed

selfdrive/controls/lib/longitudinal_mpc_lib/long_mpc.py

Lines changed: 30 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -35,15 +35,14 @@
3535
X_EGO_COST = 0.
3636
V_EGO_COST = 0.
3737
A_EGO_COST = 0.
38-
J_EGO_COST = 5.0
38+
J_EGO_COST = 5.
3939
A_CHANGE_COST = 200.
4040
DANGER_ZONE_COST = 100.
4141
CRASH_DISTANCE = .25
4242
LEAD_DANGER_FACTOR = 0.75
4343
LIMIT_COST = 1e6
4444
ACADOS_SOLVER_TYPE = 'SQP_RTI'
4545

46-
4746
# Fewer timestamps don't hurt performance and lead to
4847
# much better convergence of the MPC with low iterations
4948
N = 12
@@ -57,6 +56,7 @@
5756
STOP_DISTANCE = 6.0
5857
CRUISE_MIN_ACCEL = -1.2
5958
CRUISE_MAX_ACCEL = 1.6
59+
MIN_X_LEAD_FACTOR = 0.5
6060

6161
def get_jerk_factor(personality=log.LongitudinalPersonality.standard):
6262
if personality==log.LongitudinalPersonality.relaxed:
@@ -85,20 +85,12 @@ def get_stopped_equivalence_factor(v_lead):
8585
def get_safe_obstacle_distance(v_ego, t_follow):
8686
return (v_ego**2) / (2 * COMFORT_BRAKE) + t_follow * v_ego + STOP_DISTANCE
8787

88-
def desired_follow_distance(v_ego, v_lead, t_follow=None):
89-
if t_follow is None:
90-
t_follow = get_T_FOLLOW()
91-
return get_safe_obstacle_distance(v_ego, t_follow) - get_stopped_equivalence_factor(v_lead)
92-
93-
9488
def gen_long_model():
9589
model = AcadosModel()
9690
model.name = MODEL_NAME
9791

98-
# set up states & controls
99-
x_ego = SX.sym('x_ego')
100-
v_ego = SX.sym('v_ego')
101-
a_ego = SX.sym('a_ego')
92+
# states
93+
x_ego, v_ego, a_ego = SX.sym('x_ego'), SX.sym('v_ego'), SX.sym('a_ego')
10294
model.x = vertcat(x_ego, v_ego, a_ego)
10395

10496
# controls
@@ -126,7 +118,6 @@ def gen_long_model():
126118
model.f_expl_expr = f_expl
127119
return model
128120

129-
130121
def gen_long_ocp():
131122
ocp = AcadosOcp()
132123
ocp.model = gen_long_model()
@@ -222,30 +213,31 @@ def gen_long_ocp():
222213

223214

224215
class LongitudinalMpc:
225-
def __init__(self, mode='acc', dt=DT_MDL):
226-
self.mode = mode
216+
def __init__(self, dt=DT_MDL):
227217
self.dt = dt
228218
self.solver = AcadosOcpSolverCython(MODEL_NAME, ACADOS_SOLVER_TYPE, N)
229219
self.reset()
230220
self.source = SOURCES[2]
231221

232222
def reset(self):
233-
# self.solver = AcadosOcpSolverCython(MODEL_NAME, ACADOS_SOLVER_TYPE, N)
234223
self.solver.reset()
235-
# self.solver.options_set('print_level', 2)
224+
225+
self.x_sol = np.zeros((N+1, X_DIM))
226+
self.u_sol = np.zeros((N, 1))
236227
self.v_solution = np.zeros(N+1)
237228
self.a_solution = np.zeros(N+1)
238-
self.prev_a = np.array(self.a_solution)
239229
self.j_solution = np.zeros(N)
230+
self.prev_a = np.array(self.a_solution)
240231
self.yref = np.zeros((N+1, COST_DIM))
232+
241233
for i in range(N):
242234
self.solver.cost_set(i, "yref", self.yref[i])
243235
self.solver.cost_set(N, "yref", self.yref[N][:COST_E_DIM])
244-
self.x_sol = np.zeros((N+1, X_DIM))
245-
self.u_sol = np.zeros((N,1))
236+
246237
self.params = np.zeros((N+1, PARAM_DIM))
247238
for i in range(N+1):
248239
self.solver.set(i, 'x', np.zeros(X_DIM))
240+
249241
self.last_cloudlog_t = 0
250242
self.status = False
251243
self.crash_cnt = 0.0
@@ -276,16 +268,9 @@ def set_cost_weights(self, cost_weights, constraint_cost_weights):
276268

277269
def set_weights(self, prev_accel_constraint=True, personality=log.LongitudinalPersonality.standard):
278270
jerk_factor = get_jerk_factor(personality)
279-
if self.mode == 'acc':
280-
a_change_cost = A_CHANGE_COST if prev_accel_constraint else 0
281-
cost_weights = [X_EGO_OBSTACLE_COST, X_EGO_COST, V_EGO_COST, A_EGO_COST, jerk_factor * a_change_cost, jerk_factor * J_EGO_COST]
282-
constraint_cost_weights = [LIMIT_COST, LIMIT_COST, LIMIT_COST, DANGER_ZONE_COST]
283-
elif self.mode == 'blended':
284-
a_change_cost = 40.0 if prev_accel_constraint else 0
285-
cost_weights = [0., 0.1, 0.2, 5.0, a_change_cost, 1.0]
286-
constraint_cost_weights = [LIMIT_COST, LIMIT_COST, LIMIT_COST, DANGER_ZONE_COST]
287-
else:
288-
raise NotImplementedError(f'Planner mode {self.mode} not recognized in planner cost set')
271+
a_change_cost = A_CHANGE_COST if prev_accel_constraint else 0
272+
cost_weights = [X_EGO_OBSTACLE_COST, X_EGO_COST, V_EGO_COST, A_EGO_COST, jerk_factor * a_change_cost, jerk_factor * J_EGO_COST]
273+
constraint_cost_weights = [LIMIT_COST, LIMIT_COST, LIMIT_COST, DANGER_ZONE_COST]
289274
self.set_cost_weights(cost_weights, constraint_cost_weights)
290275

291276
def set_cur_state(self, v, a):
@@ -320,14 +305,14 @@ def process_lead(self, lead):
320305

321306
# MPC will not converge if immediate crash is expected
322307
# Clip lead distance to what is still possible to brake for
323-
min_x_lead = ((v_ego + v_lead)/2) * (v_ego - v_lead) / (-ACCEL_MIN * 2)
308+
min_x_lead = MIN_X_LEAD_FACTOR * (v_ego + v_lead) * (v_ego - v_lead) / (-ACCEL_MIN * 2)
324309
x_lead = np.clip(x_lead, min_x_lead, 1e8)
325310
v_lead = np.clip(v_lead, 0.0, 1e8)
326311
a_lead = np.clip(a_lead, -10., 5.)
327312
lead_xv = self.extrapolate_lead(x_lead, v_lead, a_lead, a_lead_tau)
328313
return lead_xv
329314

330-
def update(self, radarstate, v_cruise, x, v, a, j, personality=log.LongitudinalPersonality.standard):
315+
def update(self, radarstate, v_cruise, personality=log.LongitudinalPersonality.standard):
331316
t_follow = get_T_FOLLOW(personality)
332317
v_ego = self.x0[1]
333318
self.status = radarstate.leadOne.status or radarstate.leadTwo.status
@@ -341,56 +326,28 @@ def update(self, radarstate, v_cruise, x, v, a, j, personality=log.LongitudinalP
341326
lead_0_obstacle = lead_xv_0[:,0] + get_stopped_equivalence_factor(lead_xv_0[:,1])
342327
lead_1_obstacle = lead_xv_1[:,0] + get_stopped_equivalence_factor(lead_xv_1[:,1])
343328

344-
self.params[:,0] = ACCEL_MIN
345-
self.params[:,1] = ACCEL_MAX
346-
347-
# Update in ACC mode or ACC/e2e blend
348-
if self.mode == 'acc':
349-
self.params[:,5] = LEAD_DANGER_FACTOR
350-
351-
# Fake an obstacle for cruise, this ensures smooth acceleration to set speed
352-
# when the leads are no factor.
353-
v_lower = v_ego + (T_IDXS * CRUISE_MIN_ACCEL * 1.05)
354-
# TODO does this make sense when max_a is negative?
355-
v_upper = v_ego + (T_IDXS * CRUISE_MAX_ACCEL * 1.05)
356-
v_cruise_clipped = np.clip(v_cruise * np.ones(N+1),
357-
v_lower,
358-
v_upper)
359-
cruise_obstacle = np.cumsum(T_DIFFS * v_cruise_clipped) + get_safe_obstacle_distance(v_cruise_clipped, t_follow)
360-
x_obstacles = np.column_stack([lead_0_obstacle, lead_1_obstacle, cruise_obstacle])
361-
self.source = SOURCES[np.argmin(x_obstacles[0])]
362-
363-
# These are not used in ACC mode
364-
x[:], v[:], a[:], j[:] = 0.0, 0.0, 0.0, 0.0
365-
366-
elif self.mode == 'blended':
367-
self.params[:,5] = 1.0
368-
369-
x_obstacles = np.column_stack([lead_0_obstacle,
370-
lead_1_obstacle])
371-
cruise_target = T_IDXS * np.clip(v_cruise, v_ego - 2.0, 1e3) + x[0]
372-
xforward = ((v[1:] + v[:-1]) / 2) * (T_IDXS[1:] - T_IDXS[:-1])
373-
x = np.cumsum(np.insert(xforward, 0, x[0]))
374-
375-
x_and_cruise = np.column_stack([x, cruise_target])
376-
x = np.min(x_and_cruise, axis=1)
329+
# Fake an obstacle for cruise, this ensures smooth acceleration to set speed
330+
# when the leads are no factor.
331+
v_lower = v_ego + (T_IDXS * CRUISE_MIN_ACCEL * 1.05)
332+
# TODO does this make sense when max_a is negative?
333+
v_upper = v_ego + (T_IDXS * CRUISE_MAX_ACCEL * 1.05)
334+
v_cruise_clipped = np.clip(v_cruise * np.ones(N+1), v_lower, v_upper)
335+
cruise_obstacle = np.cumsum(T_DIFFS * v_cruise_clipped) + get_safe_obstacle_distance(v_cruise_clipped, t_follow)
377336

378-
self.source = 'e2e' if x_and_cruise[1,0] < x_and_cruise[1,1] else 'cruise'
337+
x_obstacles = np.column_stack([lead_0_obstacle, lead_1_obstacle, cruise_obstacle])
338+
self.source = SOURCES[np.argmin(x_obstacles[0])]
379339

380-
else:
381-
raise NotImplementedError(f'Planner mode {self.mode} not recognized in planner update')
382-
383-
self.yref[:,1] = x
384-
self.yref[:,2] = v
385-
self.yref[:,3] = a
386-
self.yref[:,5] = j
340+
self.yref[:,:] = 0.0
387341
for i in range(N):
388342
self.solver.set(i, "yref", self.yref[i])
389343
self.solver.set(N, "yref", self.yref[N][:COST_E_DIM])
390344

345+
self.params[:,0] = ACCEL_MIN
346+
self.params[:,1] = ACCEL_MAX
391347
self.params[:,2] = np.min(x_obstacles, axis=1)
392348
self.params[:,3] = np.copy(self.prev_a)
393349
self.params[:,4] = t_follow
350+
self.params[:,5] = LEAD_DANGER_FACTOR
394351

395352
self.run()
396353
if (np.any(lead_xv_0[FCW_IDXS,0] - self.x_sol[FCW_IDXS,0] < CRASH_DISTANCE) and
@@ -399,18 +356,7 @@ def update(self, radarstate, v_cruise, x, v, a, j, personality=log.LongitudinalP
399356
else:
400357
self.crash_cnt = 0
401358

402-
# Check if it got within lead comfort range
403-
# TODO This should be done cleaner
404-
if self.mode == 'blended':
405-
if any((lead_0_obstacle - get_safe_obstacle_distance(self.x_sol[:,1], t_follow))- self.x_sol[:,0] < 0.0):
406-
self.source = 'lead0'
407-
if any((lead_1_obstacle - get_safe_obstacle_distance(self.x_sol[:,1], t_follow))- self.x_sol[:,0] < 0.0) and \
408-
(lead_1_obstacle[0] - lead_0_obstacle[0]):
409-
self.source = 'lead1'
410-
411359
def run(self):
412-
# t0 = time.monotonic()
413-
# reset = 0
414360
for i in range(N+1):
415361
self.solver.set(i, 'p', self.params[i])
416362
self.solver.constraints_set(0, "lbx", self.x0)
@@ -422,13 +368,6 @@ def run(self):
422368
self.time_linearization = float(self.solver.get_stats('time_lin')[0])
423369
self.time_integrator = float(self.solver.get_stats('time_sim')[0])
424370

425-
# qp_iter = self.solver.get_stats('statistics')[-1][-1] # SQP_RTI specific
426-
# print(f"long_mpc timings: tot {self.solve_time:.2e}, qp {self.time_qp_solution:.2e}, lin {self.time_linearization:.2e}, \
427-
# integrator {self.time_integrator:.2e}, qp_iter {qp_iter}")
428-
# res = self.solver.get_residuals()
429-
# print(f"long_mpc residuals: {res[0]:.2e}, {res[1]:.2e}, {res[2]:.2e}, {res[3]:.2e}")
430-
# self.solver.print_statistics()
431-
432371
for i in range(N+1):
433372
self.x_sol[i] = self.solver.get(i, 'x')
434373
for i in range(N):
@@ -446,12 +385,8 @@ def run(self):
446385
self.last_cloudlog_t = t
447386
cloudlog.warning(f"Long mpc reset, solution_status: {self.solution_status}")
448387
self.reset()
449-
# reset = 1
450-
# print(f"long_mpc timings: total internal {self.solve_time:.2e}, external: {(time.monotonic() - t0):.2e} qp {self.time_qp_solution:.2e}, \
451-
# lin {self.time_linearization:.2e} qp_iter {qp_iter}, reset {reset}")
452388

453389

454390
if __name__ == "__main__":
455391
ocp = gen_long_ocp()
456392
AcadosOcpSolver.generate(ocp, json_file=JSON_FILE)
457-
# AcadosOcpSolver.build(ocp.code_export_directory, with_cython=True)

selfdrive/controls/lib/longitudinal_planner.py

Lines changed: 11 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,12 @@
99
from openpilot.common.realtime import DT_MDL
1010
from openpilot.selfdrive.modeld.constants import ModelConstants
1111
from openpilot.selfdrive.controls.lib.longcontrol import LongCtrlState
12-
from openpilot.selfdrive.controls.lib.longitudinal_mpc_lib.long_mpc import LongitudinalMpc
12+
from openpilot.selfdrive.controls.lib.longitudinal_mpc_lib.long_mpc import LongitudinalMpc, SOURCES
1313
from openpilot.selfdrive.controls.lib.longitudinal_mpc_lib.long_mpc import T_IDXS as T_IDXS_MPC
1414
from openpilot.selfdrive.controls.lib.drive_helpers import CONTROL_N, get_accel_from_plan
1515
from openpilot.selfdrive.car.cruise import V_CRUISE_MAX, V_CRUISE_UNSET
1616
from openpilot.common.swaglog import cloudlog
1717

18-
LON_MPC_STEP = 0.2 # first step is 0.2s
1918
A_CRUISE_MAX_VALS = [1.6, 1.2, 0.8, 0.6]
2019
A_CRUISE_MAX_BP = [0., 10.0, 25., 40.]
2120
CONTROL_N_T_IDX = ModelConstants.T_IDXS[:CONTROL_N]
@@ -26,14 +25,12 @@
2625
_A_TOTAL_MAX_V = [1.7, 3.2]
2726
_A_TOTAL_MAX_BP = [20., 40.]
2827

29-
3028
def get_max_accel(v_ego):
3129
return np.interp(v_ego, A_CRUISE_MAX_BP, A_CRUISE_MAX_VALS)
3230

3331
def get_coast_accel(pitch):
3432
return np.sin(pitch) * -5.65 - 0.3 # fitted from data using xx/projects/allow_throttle/compute_coast_accel.py
3533

36-
3734
def limit_accel_in_turns(v_ego, angle_steers, a_target, CP):
3835
"""
3936
This function returns a limited long acceleration allowed, depending on the existing lateral acceleration
@@ -52,8 +49,6 @@ class LongitudinalPlanner:
5249
def __init__(self, CP, init_v=0.0, init_a=0.0, dt=DT_MDL):
5350
self.CP = CP
5451
self.mpc = LongitudinalMpc(dt=dt)
55-
# TODO remove mpc modes when TR released
56-
self.mpc.mode = 'acc'
5752
self.fcw = False
5853
self.dt = dt
5954
self.allow_throttle = True
@@ -67,7 +62,6 @@ def __init__(self, CP, init_v=0.0, init_a=0.0, dt=DT_MDL):
6762
self.v_desired_trajectory = np.zeros(CONTROL_N)
6863
self.a_desired_trajectory = np.zeros(CONTROL_N)
6964
self.j_desired_trajectory = np.zeros(CONTROL_N)
70-
self.solverExecutionTime = 0.0
7165

7266
@staticmethod
7367
def parse_model(model_msg):
@@ -90,8 +84,6 @@ def parse_model(model_msg):
9084
return x, v, a, j, throttle_prob
9185

9286
def update(self, sm):
93-
mode = 'blended' if sm['selfdriveState'].experimentalMode else 'acc'
94-
9587
if len(sm['carControl'].orientationNED) == 3:
9688
accel_coast = get_coast_accel(sm['carControl'].orientationNED[1])
9789
else:
@@ -113,12 +105,9 @@ def update(self, sm):
113105
# No change cost when user is controlling the speed, or when standstill
114106
prev_accel_constraint = not (reset_state or sm['carState'].standstill)
115107

116-
if mode == 'acc':
117-
accel_clip = [ACCEL_MIN, get_max_accel(v_ego)]
118-
steer_angle_without_offset = sm['carState'].steeringAngleDeg - sm['liveParameters'].angleOffsetDeg
119-
accel_clip = limit_accel_in_turns(v_ego, steer_angle_without_offset, accel_clip, self.CP)
120-
else:
121-
accel_clip = [ACCEL_MIN, ACCEL_MAX]
108+
accel_clip = [ACCEL_MIN, get_max_accel(v_ego)]
109+
steer_angle_without_offset = sm['carState'].steeringAngleDeg - sm['liveParameters'].angleOffsetDeg
110+
accel_clip = limit_accel_in_turns(v_ego, steer_angle_without_offset, accel_clip, self.CP)
122111

123112
if reset_state:
124113
self.v_desired_filter.x = v_ego
@@ -127,7 +116,7 @@ def update(self, sm):
127116

128117
# Prevent divergence, smooth in current v_ego
129118
self.v_desired_filter.x = max(0.0, self.v_desired_filter.update(v_ego))
130-
x, v, a, j, throttle_prob = self.parse_model(sm['modelV2'])
119+
_, _, _, _, throttle_prob = self.parse_model(sm['modelV2'])
131120
# Don't clip at low speeds since throttle_prob doesn't account for creep
132121
self.allow_throttle = throttle_prob > ALLOW_THROTTLE_THRESHOLD or v_ego <= MIN_ALLOW_THROTTLE_SPEED
133122

@@ -141,7 +130,7 @@ def update(self, sm):
141130

142131
self.mpc.set_weights(prev_accel_constraint, personality=sm['selfdriveState'].personality)
143132
self.mpc.set_cur_state(self.v_desired_filter.x, self.a_desired)
144-
self.mpc.update(sm['radarState'], v_cruise, x, v, a, j, personality=sm['selfdriveState'].personality)
133+
self.mpc.update(sm['radarState'], v_cruise, personality=sm['selfdriveState'].personality)
145134

146135
self.v_desired_trajectory = np.interp(CONTROL_N_T_IDX, T_IDXS_MPC, self.mpc.v_solution)
147136
self.a_desired_trajectory = np.interp(CONTROL_N_T_IDX, T_IDXS_MPC, self.mpc.a_solution)
@@ -163,12 +152,13 @@ def update(self, sm):
163152
output_a_target_e2e = sm['modelV2'].action.desiredAcceleration
164153
output_should_stop_e2e = sm['modelV2'].action.shouldStop
165154

166-
if mode == 'acc':
155+
if (output_a_target_e2e < output_a_target_mpc) and sm['selfdriveState'].experimentalMode:
156+
output_a_target = output_a_target_e2e
157+
self.output_should_stop = output_should_stop_e2e
158+
self.mpc.source = SOURCES[3]
159+
else:
167160
output_a_target = output_a_target_mpc
168161
self.output_should_stop = output_should_stop_mpc
169-
else:
170-
output_a_target = min(output_a_target_mpc, output_a_target_e2e)
171-
self.output_should_stop = output_should_stop_e2e or output_should_stop_mpc
172162

173163
for idx in range(2):
174164
accel_clip[idx] = np.clip(accel_clip[idx], self.prev_accel_clip[idx] - 0.05, self.prev_accel_clip[idx] + 0.05)

selfdrive/controls/tests/test_following_distance.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,15 @@
44

55
from cereal import log
66

7-
from openpilot.selfdrive.controls.lib.longitudinal_mpc_lib.long_mpc import desired_follow_distance, get_T_FOLLOW
7+
from openpilot.selfdrive.controls.lib.longitudinal_mpc_lib.long_mpc import get_safe_obstacle_distance, get_stopped_equivalence_factor, get_T_FOLLOW
88
from openpilot.selfdrive.test.longitudinal_maneuvers.maneuver import Maneuver
99

1010

11+
def desired_follow_distance(v_ego, v_lead, t_follow=None):
12+
if t_follow is None:
13+
t_follow = get_T_FOLLOW()
14+
return get_safe_obstacle_distance(v_ego, t_follow) - get_stopped_equivalence_factor(v_lead)
15+
1116
def run_following_distance_simulation(v_lead, t_end=100.0, e2e=False, personality=0):
1217
man = Maneuver(
1318
'',

0 commit comments

Comments
 (0)