Keep longitudinal braking progressive through handoffs

This commit is contained in:
rav4kumar
2026-07-27 11:50:09 -07:00
parent a1e8174095
commit 718d5abbed
9 changed files with 415 additions and 9 deletions
@@ -1,4 +1,3 @@
#!/usr/bin/env python3
from collections import deque
from dataclasses import dataclass, field
from enum import IntEnum
@@ -424,7 +423,7 @@ class AccelController:
separation = path.robust_departure_separation(lead_index)
if math.isfinite(separation) and path.departure_references[lead_index] is None:
path.departure_references[lead_index] = separation
raw_departure = ((has_lead and envelope.departure_lead_speed > STOP_HOLD_CREEP_SPEED
raw_departure = ((has_lead and min(envelope.selected_lead_speed, envelope.departure_lead_speed) > STOP_HOLD_CREEP_SPEED
and envelope.departure_cap > STOP_HOLD_CREEP_SPEED)
or (not envelope.lead_status and path.lead_loss_frames >= self.lead_loss_hold_frames))
departed = self._creep_departure(path, envelope) or raw_departure
@@ -44,6 +44,7 @@ MATCHED_PACE_DECEL_RATE = 0.50
BRAKING_ACCEL_LIMIT_THRESHOLD = -0.11
MPC_DECEL_JERK_COST_MULTIPLIER = 1.05
MPC_DECEL_JERK_MAX_REQUIRED_DECEL = 0.80
MPC_DECEL_JERK_MAX_REQUIRED_DECEL_RATE = 0.35
MPC_DECEL_JERK_MAX_TARGET_REDUCTION = 9.0
STOP_HOLD_EGO_SPEED = 0.30
@@ -526,6 +526,26 @@ class TestPaceAndLifecycle:
assert results[launch_index].departure_launching
assert results[launch_index].effective_accel_max == pytest.approx(results[launch_index].positive_accel_max)
def test_stopped_governing_lead_rejects_route_51d_radar_speed_pulse_without_delaying_departure(self):
controller = make_controller()
enter_stop_hold(controller, v_ego=0.0)
speed_pulse = (0.1361, 0.1731, 0.2146, 0.2253, 0.2137, 0.1877)
distances = (6.0, 6.0, 6.0, 5.96, 6.04, 6.04)
for distance, speed in zip(distances, speed_pulse, strict=True):
radar = make_radar(make_lead(status=True, d_rel=distance, v_lead_k=speed, radar_track_id=4887),
make_lead(status=True, d_rel=6.08, v_lead_k=0.0, radar_track_id=4905))
held = update(controller, radar, base_speed=8.0, v_ego=0.0)
assert held.state == AccelControllerState.stopHold
assert held.target_speed == 0.0 and not held.launching
departing = make_radar(make_lead(status=True, d_rel=6.4, v_lead_k=2.0, radar_track_id=4887),
make_lead(status=True, d_rel=6.48, v_lead_k=2.0, radar_track_id=4905))
results = [update(controller, departing, base_speed=8.0, v_ego=0.0) for _ in range(STOP_HOLD_EXIT_FRAMES)]
assert all(result.state == AccelControllerState.stopHold for result in results[:-1])
assert results[-1].launching and results[-1].departure_launching
def test_reused_radar_does_not_pulse_stop_hold_or_departure_target(self):
controller = make_controller()
enter_stop_hold(controller)
@@ -1,3 +1,4 @@
from collections import deque
import inspect
import math
from types import SimpleNamespace
@@ -7,6 +8,7 @@ import pytest
from cereal import custom, log, messaging
from opendbc.car.interfaces import ACCEL_MAX, ACCEL_MIN
from openpilot.common.realtime import DT_MDL
from openpilot.selfdrive.controls.lib.longitudinal_mpc_lib.long_mpc import N, LongitudinalMpc
from openpilot.selfdrive.controls.lib.longitudinal_mpc_lib.long_mpc import LongitudinalPlanSource as MpcLongitudinalPlanSource
from openpilot.sunnypilot.selfdrive.controls.lib.accel_personality import AccelControllerState, AccelProfile
@@ -40,6 +42,9 @@ def planner_for_mpc_test(*, target_speed=15.0, active=True, is_e2e=False, mpc_ac
is_e2e_calls = []
planner.is_e2e = lambda _sm: is_e2e_calls.append(True) or is_e2e
planner._accel_jerk_smoothing_blocked = False
planner._accel_required_decel_samples = deque(maxlen=4)
planner._accel_required_decel_lead = -1
planner._dt = DT_MDL
planner.mpc = SimpleNamespace(source=mpc_source, last_solution_status=0)
planner.update_accel_controller = lambda *_args, **_kwargs: setattr(
planner, "accel_controller_result",
@@ -272,6 +277,50 @@ def test_ineligible_required_decel_blocks_smoothing_only_until_the_restriction_e
assert rearmed_calls[0][1] == {"jerk_cost_multiplier": MPC_DECEL_JERK_COST_MULTIPLIER}
def test_consistently_tightening_lead_releases_smoothing_until_the_restriction_ends():
planner, _ = planner_for_mpc_test(
state=AccelControllerState.restrict, selected_lead=0, required_decel=0.18,
)
_, calls = run_controller_mpc(planner)
routine_result = planner.accel_controller_result
multipliers = [calls[0][1]["jerk_cost_multiplier"]]
for required_decel in (0.20, 0.23, 0.25):
result = SimpleNamespace(**(vars(routine_result) | {"required_decel": required_decel}))
planner.update_accel_controller = lambda *_args, result=result, **_kwargs: setattr(planner, "accel_controller_result", result)
_, calls = run_controller_mpc(planner)
multipliers.append(calls[0][1]["jerk_cost_multiplier"])
assert multipliers == [MPC_DECEL_JERK_COST_MULTIPLIER] * 3 + [1.0]
easing_result = SimpleNamespace(**(vars(routine_result) | {"required_decel": 0.20}))
planner.update_accel_controller = lambda *_args, **_kwargs: setattr(planner, "accel_controller_result", easing_result)
_, calls = run_controller_mpc(planner)
assert calls[0][1] == {"jerk_cost_multiplier": 1.0}
free_result = SimpleNamespace(**(vars(routine_result) | {"state": AccelControllerState.free, "target_speed": 20.0}))
planner.update_accel_controller = lambda *_args, **_kwargs: setattr(planner, "accel_controller_result", free_result)
run_controller_mpc(planner)
planner.update_accel_controller = lambda *_args, **_kwargs: setattr(planner, "accel_controller_result", routine_result)
_, calls = run_controller_mpc(planner)
assert calls[0][1] == {"jerk_cost_multiplier": MPC_DECEL_JERK_COST_MULTIPLIER}
def test_one_frame_required_decel_noise_does_not_disable_routine_smoothing():
planner, _ = planner_for_mpc_test(
state=AccelControllerState.restrict, selected_lead=0, required_decel=0.18,
)
_, calls = run_controller_mpc(planner)
routine_result = planner.accel_controller_result
multipliers = [calls[0][1]["jerk_cost_multiplier"]]
for required_decel in (0.24, 0.19, 0.22):
result = SimpleNamespace(**(vars(routine_result) | {"required_decel": required_decel}))
planner.update_accel_controller = lambda *_args, result=result, **_kwargs: setattr(planner, "accel_controller_result", result)
_, calls = run_controller_mpc(planner)
multipliers.append(calls[0][1]["jerk_cost_multiplier"])
assert multipliers == [MPC_DECEL_JERK_COST_MULTIPLIER] * 4
@pytest.mark.parametrize(
("state", "selected_lead", "launching", "required_decel", "target_speed", "mpc_source"),
[
@@ -335,10 +384,11 @@ def test_radar_freshness_is_computed_once_and_shared_with_dec_and_controller():
planner._read_accel_controller_params = lambda: None
planner.events_sp = SimpleNamespace(clear=lambda: None)
dec_freshness = []
planner.dec = SimpleNamespace(update=lambda _sm, *, radar_fresh: dec_freshness.append(radar_fresh))
planner.dec = SimpleNamespace(update=lambda _sm, *, radar_fresh, planner_accel: dec_freshness.append(radar_fresh))
planner.e2e_alerts_helper = SimpleNamespace(update=lambda *_args: None)
planner.accel_personality = int(AccelProfile.normal)
planner.accel_personality_enabled = True
planner.output_a_target = 0.0
planner.a_desired = 0.0
planner.v_desired_filter = SimpleNamespace(x=10.0)
planner.mpc = SimpleNamespace(source=log.LongitudinalPlan.LongitudinalPlanSource.cruise)
@@ -29,6 +29,12 @@ class WMACConstants:
MODEL_DECEL_START = -0.5
MODEL_DECEL_RANGE = 2.0
MODEL_DECEL_TREND_FRAMES = 4
MODEL_DECEL_TREND_ACCEL = -0.075
MODEL_DECEL_TREND_RATE = 0.35
MODEL_DECEL_TREND_MAX_MPC_ACCEL = 0.075
MODEL_DECEL_TREND_MAX_COMMAND_STEP = 0.15
MODEL_DECEL_TREND_RELEASE_ACCEL = -0.02
ENDPOINT_URGENCY_GAIN = 1.3
CRITICAL_ENDPOINT_FACTOR = 0.3
CRITICAL_URGENCY_GAIN = 1.5
+60 -2
View File
@@ -6,12 +6,15 @@ See the LICENSE.md file in the root directory for more details.
"""
# Version = 2025-6-30
from collections import deque
import math
from typing import Literal
from cereal import messaging
from numpy import interp
from opendbc.car import structs
from openpilot.common.params import Params
from openpilot.common.realtime import DT_MDL
from openpilot.sunnypilot.selfdrive.controls.lib.dec.constants import WMACConstants
ModeType = Literal['acc', 'blended']
@@ -168,6 +171,10 @@ class DynamicExperimentalController:
self._expected_distance = 0.0
self._trajectory_valid = False
self._raw_urgency = 0.0
self._model_accel_samples = deque(maxlen=WMACConstants.MODEL_DECEL_TREND_FRAMES)
self._model_decel_trending = False
self._model_decel_latched = False
self._planner_accel = math.nan
def _read_params(self) -> None:
if self._frame % WMACConstants.PARAM_READ_FRAMES == 0:
@@ -234,6 +241,7 @@ class DynamicExperimentalController:
self._expected_distance = 0.0
self._trajectory_valid = False
self._update_model_decel_trend(md)
urgency = self._model_action_urgency(md)
position_valid = len(md.position.x) == WMACConstants.TRAJECTORY_SIZE
@@ -247,6 +255,31 @@ class DynamicExperimentalController:
self._has_slow_down = self._slow_down_tracker.update(self._raw_urgency)
self._urgency = self._slow_down_tracker.value
def _update_model_decel_trend(self, md) -> None:
try:
desired_accel = float(md.action.desiredAcceleration)
except (AttributeError, OverflowError, TypeError, ValueError):
desired_accel = math.nan
if not math.isfinite(desired_accel):
self._reset_model_decel_trend()
else:
self._model_accel_samples.append(desired_accel)
history = tuple(self._model_accel_samples)
self._model_decel_trending = (len(history) == self._model_accel_samples.maxlen
and history[-1] <= WMACConstants.MODEL_DECEL_TREND_ACCEL
and (history[0] - history[-1]) / (DT_MDL * (len(history) - 1)) > WMACConstants.MODEL_DECEL_TREND_RATE
and all(after <= before for before, after in zip(history[:-1], history[1:], strict=True))
and sum(after < before for before, after in zip(history[:-1], history[1:], strict=True)) >= 2)
if len(history) == self._model_accel_samples.maxlen and all(
accel >= WMACConstants.MODEL_DECEL_TREND_RELEASE_ACCEL for accel in history
):
self._model_decel_latched = False
def _reset_model_decel_trend(self) -> None:
self._model_accel_samples.clear()
self._model_decel_trending = False
self._model_decel_latched = False
def _radar_acc_lead_score(self, lead_one) -> float:
radar_track_id = int(getattr(lead_one, 'radarTrackId', -1))
return float(lead_one.status and (bool(getattr(lead_one, 'radar', False)) or radar_track_id >= 0))
@@ -290,11 +323,21 @@ class DynamicExperimentalController:
return urgency
def _model_decel_handoff_ready(self) -> bool:
try:
mpc_accel = float(self._mpc.a_solution[1])
return (math.isfinite(mpc_accel) and mpc_accel <= WMACConstants.MODEL_DECEL_TREND_MAX_MPC_ACCEL
and math.isfinite(self._planner_accel) and self._planner_accel <= WMACConstants.MODEL_DECEL_TREND_MAX_MPC_ACCEL
and self._planner_accel - self._model_accel_samples[-1] <= WMACConstants.MODEL_DECEL_TREND_MAX_COMMAND_STEP)
except (AttributeError, IndexError, OverflowError, TypeError, ValueError):
return False
def _desired_mode(self) -> tuple[ModeType, bool]:
standstill = self._standstill_count > WMACConstants.STANDSTILL_FRAMES
urgent_slow_down = self._has_slow_down and self._raw_urgency > WMACConstants.URGENT_SLOW_DOWN_PROB
if not self._CP.radarUnavailable and self._has_current_radar_acc_lead:
self._reset_model_decel_trend()
return 'acc', True
radar_stale = not self._radar_fresh if self._has_mpc_fcw else self._radar_stale_frames > 1
@@ -304,8 +347,14 @@ class DynamicExperimentalController:
return 'blended', True
if not self._CP.radarUnavailable and self._has_radar_acc_lead:
self._reset_model_decel_trend()
return 'acc', True
entering_model_slowdown = self._model_decel_trending and self._model_decel_handoff_ready() and not self._model_decel_latched
self._model_decel_latched |= entering_model_slowdown
if self._model_decel_latched:
return 'blended', entering_model_slowdown
if self._has_mpc_fcw:
return 'blended', True
@@ -319,15 +368,24 @@ class DynamicExperimentalController:
return 'acc', False
def update(self, sm: messaging.SubMaster, *, radar_fresh: bool = True) -> None:
def update(self, sm: messaging.SubMaster, *, radar_fresh: bool = True, planner_accel: float | None = None) -> None:
self._read_params()
self.set_mpc_fcw_crash_cnt()
try:
self._planner_accel = float(planner_accel)
except (OverflowError, TypeError, ValueError):
self._planner_accel = math.nan
self._update_calculations(sm, radar_fresh)
self._active = sm['selfdriveState'].experimentalMode and self._enabled
if not self._active:
model_decel_latched = self._model_decel_latched
self._reset_model_decel_trend()
if model_decel_latched:
self._mode_manager.request_mode('acc', immediate=True)
mode, immediate = self._desired_mode()
self._mode_manager.request_mode(mode, immediate=immediate, hold_frames=WMACConstants.EMERGENCY_HOLD_FRAMES,
cancel_hold=not self._CP.radarUnavailable and self._has_radar_acc_lead)
self._mode_manager.update()
self._active = sm['selfdriveState'].experimentalMode and self._enabled
self._frame += 1
@@ -77,6 +77,7 @@ def mock_cp():
def mock_mpc():
class MPC:
crash_cnt = 0
a_solution = [0.0, 0.0]
return MPC()
@@ -159,6 +160,159 @@ def test_model_should_stop_triggers_blended_without_valid_trajectory(mock_cp, mo
assert controller.mode() == "blended"
def test_confirmed_model_decel_trend_enters_blended_before_a_large_command(mock_cp, mock_mpc, default_sm):
controller = DynamicExperimentalController(mock_cp, mock_mpc, params=MockParams())
default_sm['radarState'] = MockRadarState(status=0.0)
for desired_acceleration in (-0.02, -0.05, -0.08):
default_sm['modelV2'] = MockModelData(valid=False, desired_acceleration=desired_acceleration)
controller.update(default_sm, planner_accel=0.0)
assert controller.mode() == "acc"
default_sm['modelV2'] = MockModelData(valid=False, desired_acceleration=-0.12)
controller.update(default_sm, planner_accel=0.0)
assert controller._model_decel_trending
assert not controller._has_slow_down
assert controller.mode() == "blended"
def test_confirmed_model_decel_handoff_stays_latched_through_a_plateau(mock_cp, mock_mpc, default_sm):
controller = DynamicExperimentalController(mock_cp, mock_mpc, params=MockParams())
default_sm['radarState'] = MockRadarState(status=0.0)
for desired_acceleration in (-0.02, -0.05, -0.08, -0.12):
default_sm['modelV2'] = MockModelData(valid=False, desired_acceleration=desired_acceleration)
controller.update(default_sm, planner_accel=0.0)
for _ in range(WMACConstants.EMERGENCY_HOLD_FRAMES + WMACConstants.EXIT_BLENDED_FRAMES + 1):
default_sm['modelV2'] = MockModelData(valid=False, desired_acceleration=-0.12)
controller.update(default_sm, planner_accel=0.0)
assert not controller._model_decel_trending
assert controller._model_decel_latched
assert controller.mode() == "blended"
for _ in range(WMACConstants.MODEL_DECEL_TREND_FRAMES + WMACConstants.EXIT_BLENDED_FRAMES):
default_sm['modelV2'] = MockModelData(valid=False, desired_acceleration=0.0)
controller.update(default_sm, planner_accel=0.0)
assert not controller._model_decel_latched
assert controller.mode() == "acc"
def test_model_decel_trend_never_overrides_a_radar_lead(mock_cp, mock_mpc, default_sm):
controller = DynamicExperimentalController(mock_cp, mock_mpc, params=MockParams())
for desired_acceleration in (-0.02, -0.05, -0.08, -0.12):
default_sm['modelV2'] = MockModelData(valid=False, desired_acceleration=desired_acceleration)
controller.update(default_sm)
assert not controller._model_accel_samples
assert not controller._model_decel_latched
assert controller._has_radar_acc_lead
assert controller.mode() == "acc"
def test_radar_acquisition_clears_a_latched_model_decel_handoff(mock_cp, mock_mpc, default_sm):
controller = DynamicExperimentalController(mock_cp, mock_mpc, params=MockParams())
default_sm['radarState'] = MockRadarState(status=0.0)
for desired_acceleration in (-0.02, -0.05, -0.08, -0.12):
default_sm['modelV2'] = MockModelData(valid=False, desired_acceleration=desired_acceleration)
controller.update(default_sm, planner_accel=0.0)
assert controller._model_decel_latched
default_sm['radarState'] = MockRadarState(status=1.0, radar=True, radarTrackId=7)
controller.update(default_sm, planner_accel=0.0)
assert not controller._model_accel_samples
assert not controller._model_decel_latched
assert controller.mode() == "acc"
def test_model_decel_trend_does_not_accumulate_while_dec_is_inactive(mock_cp, mock_mpc, default_sm):
controller = DynamicExperimentalController(mock_cp, mock_mpc, params=MockParams())
default_sm['radarState'] = MockRadarState(status=0.0)
default_sm['selfdriveState'].experimentalMode = False
for desired_acceleration in (-0.02, -0.05, -0.08, -0.12):
default_sm['modelV2'] = MockModelData(valid=False, desired_acceleration=desired_acceleration)
controller.update(default_sm, planner_accel=0.0)
assert not controller._model_accel_samples
assert not controller._model_decel_latched
default_sm['selfdriveState'].experimentalMode = True
controller.update(default_sm, planner_accel=0.0)
assert not controller._model_decel_trending
assert controller.mode() == "acc"
def test_disabling_dec_clears_a_latched_model_decel_mode(mock_cp, mock_mpc, default_sm):
controller = DynamicExperimentalController(mock_cp, mock_mpc, params=MockParams())
default_sm['radarState'] = MockRadarState(status=0.0)
for desired_acceleration in (-0.02, -0.05, -0.08, -0.12):
default_sm['modelV2'] = MockModelData(valid=False, desired_acceleration=desired_acceleration)
controller.update(default_sm, planner_accel=0.0)
assert controller._model_decel_latched
assert controller.mode() == "blended"
default_sm['selfdriveState'].experimentalMode = False
default_sm['modelV2'] = MockModelData(valid=False, desired_acceleration=0.0)
controller.update(default_sm, planner_accel=0.0)
assert not controller._model_decel_latched
assert controller.mode() == "acc"
def test_model_decel_trend_waits_while_mpc_is_accelerating(mock_cp, mock_mpc, default_sm):
controller = DynamicExperimentalController(mock_cp, mock_mpc, params=MockParams())
default_sm['radarState'] = MockRadarState(status=0.0)
mock_mpc.a_solution[1] = 0.5
for desired_acceleration in (-0.02, -0.05, -0.08, -0.12):
default_sm['modelV2'] = MockModelData(valid=False, desired_acceleration=desired_acceleration)
controller.update(default_sm, planner_accel=0.0)
assert controller._model_decel_trending
assert controller.mode() == "acc"
def test_steep_model_decel_trend_defers_to_the_existing_urgent_path(mock_cp, mock_mpc, default_sm):
controller = DynamicExperimentalController(mock_cp, mock_mpc, params=MockParams())
default_sm['radarState'] = MockRadarState(status=0.0)
for desired_acceleration in (0.0, -0.2, -0.4, -0.6):
default_sm['modelV2'] = MockModelData(valid=False, desired_acceleration=desired_acceleration)
controller.update(default_sm, planner_accel=0.05)
assert controller._model_decel_trending
assert controller.mode() == "acc"
def test_model_decel_trend_waits_while_the_planner_is_accelerating(mock_cp, mock_mpc, default_sm):
controller = DynamicExperimentalController(mock_cp, mock_mpc, params=MockParams())
default_sm['radarState'] = MockRadarState(status=0.0)
for desired_acceleration in (-0.02, -0.05, -0.08, -0.12):
default_sm['modelV2'] = MockModelData(valid=False, desired_acceleration=desired_acceleration)
controller.update(default_sm, planner_accel=0.2)
assert controller._model_decel_trending
assert controller.mode() == "acc"
def test_alternating_model_accel_noise_does_not_trigger_an_early_handoff(mock_cp, mock_mpc, default_sm):
controller = DynamicExperimentalController(mock_cp, mock_mpc, params=MockParams())
default_sm['radarState'] = MockRadarState(status=0.0)
for desired_acceleration in (0.0, -0.2, 0.0, -0.2):
default_sm['modelV2'] = MockModelData(valid=False, desired_acceleration=desired_acceleration)
controller.update(default_sm)
assert not controller._model_decel_trending
assert controller.mode() == "acc"
def test_radar_lead_keeps_acc_over_model_slowdown(mock_cp, mock_mpc, default_sm):
controller = DynamicExperimentalController(mock_cp, mock_mpc, params=MockParams())
default_sm['radarState'] = MockRadarState(status=1.0, radar=True, radarTrackId=7)
@@ -5,6 +5,9 @@ This file is part of sunnypilot and is licensed under the MIT License.
See the LICENSE.md file in the root directory for more details.
"""
from collections import deque
import math
from cereal import messaging, custom
from opendbc.car import structs
from openpilot.common.constants import CV
@@ -14,7 +17,8 @@ from openpilot.selfdrive.car.cruise import V_CRUISE_MAX
from openpilot.sunnypilot import get_sanitize_int_param
from openpilot.sunnypilot.selfdrive.controls.lib.accel_personality import AccelController, AccelControllerState, AccelProfile
from openpilot.sunnypilot.selfdrive.controls.lib.accel_personality.constants import (
MPC_DECEL_JERK_COST_MULTIPLIER, MPC_DECEL_JERK_MAX_REQUIRED_DECEL, MPC_DECEL_JERK_MAX_TARGET_REDUCTION,
MPC_DECEL_JERK_COST_MULTIPLIER, MPC_DECEL_JERK_MAX_REQUIRED_DECEL, MPC_DECEL_JERK_MAX_REQUIRED_DECEL_RATE,
MPC_DECEL_JERK_MAX_TARGET_REDUCTION,
)
from openpilot.sunnypilot.selfdrive.controls.lib.dec.dec import DynamicExperimentalController
from openpilot.sunnypilot.selfdrive.controls.lib.e2e_alerts_helper import E2EAlertsHelper
@@ -43,6 +47,9 @@ class LongitudinalPlannerSP:
self.accel_controller = AccelController(CP, dt=dt)
self.accel_controller_result = None
self._accel_jerk_smoothing_blocked = False
self._accel_required_decel_samples = deque(maxlen=4)
self._accel_required_decel_lead = -1
self._dt = dt
self._radar_log_mono_time = None
self._radar_fresh_this_cycle = True
@@ -158,8 +165,18 @@ class LongitudinalPlannerSP:
actuating and prev_accel_constraint and result.state == AccelControllerState.restrict and result.selected_lead >= 0
and not result.launching and target_reduction > 1e-6
)
if not lead_restriction or result.selected_lead != self._accel_required_decel_lead or not math.isfinite(result.required_decel):
self._accel_required_decel_samples.clear()
if lead_restriction and math.isfinite(result.required_decel):
self._accel_required_decel_samples.append(result.required_decel)
self._accel_required_decel_lead = result.selected_lead if lead_restriction else -1
required_decel_history = tuple(self._accel_required_decel_samples)
tightening_lead = (len(required_decel_history) == self._accel_required_decel_samples.maxlen
and (required_decel_history[-1] - required_decel_history[0]) /
(self._dt * (len(required_decel_history) - 1)) > MPC_DECEL_JERK_MAX_REQUIRED_DECEL_RATE
and sum(after > before for before, after in zip(required_decel_history[:-1], required_decel_history[1:], strict=True)) >= 2)
smoothing_eligible = (lead_restriction and target_reduction < MPC_DECEL_JERK_MAX_TARGET_REDUCTION
and 0.0 < result.required_decel < MPC_DECEL_JERK_MAX_REQUIRED_DECEL)
and 0.0 < result.required_decel < MPC_DECEL_JERK_MAX_REQUIRED_DECEL and not tightening_lead)
smoothing_blocked = getattr(self, '_accel_jerk_smoothing_blocked', False)
if previous_mpc_failed:
smoothing_blocked = True
@@ -185,7 +202,7 @@ class LongitudinalPlannerSP:
self._radar_fresh_this_cycle = self._update_radar_freshness(sm)
self._read_accel_controller_params()
self.events_sp.clear()
self.dec.update(sm, radar_fresh=self._radar_fresh_this_cycle)
self.dec.update(sm, radar_fresh=self._radar_fresh_this_cycle, planner_accel=self.output_a_target)
self.e2e_alerts_helper.update(sm, self.events_sp)
def publish_longitudinal_plan_sp(self, sm: messaging.SubMaster, pm: messaging.PubMaster) -> None:
@@ -13,7 +13,8 @@ from openpilot.selfdrive.test.longitudinal_maneuvers.plant import PRIUS_TSS2_ROU
from openpilot.sunnypilot.selfdrive.controls.lib import longitudinal_planner as longitudinal_planner_sp
from openpilot.sunnypilot.selfdrive.controls.lib.accel_personality import AccelControllerState, AccelProfile
from openpilot.sunnypilot.selfdrive.controls.lib.accel_personality.constants import (
MATCHED_PACE_DECEL_RATE, MPC_DECEL_JERK_COST_MULTIPLIER, PACE_TARGET_RESERVE,
MATCHED_PACE_DECEL_RATE, MPC_DECEL_JERK_COST_MULTIPLIER, MPC_DECEL_JERK_MAX_REQUIRED_DECEL_RATE, PACE_TARGET_RESERVE,
STOP_HOLD_EXIT_FRAMES,
)
ACTUATOR_DYNAMICS = (
@@ -387,6 +388,35 @@ def test_dec_retains_acc_through_route_like_radar_marker_dropout():
assert trace.solver_failures == 0
def test_dec_uses_confirmed_model_slowdown_while_the_handoff_is_still_gentle():
def model_action(current_time: float, _v_ego: float, _a_ego: float) -> tuple[float, bool]:
if current_time < 1.0:
return 0.0, False
if current_time < 1.75:
return -0.5 * (current_time - 1.0), False
if current_time < 3.25:
return -0.375, False
return -0.375 - 0.5 * (current_time - 3.25), False
trace = _run(
duration=4.5, controller_enabled=True, dec_enabled=True, e2e=True, lead_relevancy=False, speed=22.0,
v_cruise=22.0, model_action_fn=model_action, actuator_delay=0.15, actuator_lag=0.20,
)
mode_changes = np.flatnonzero(np.asarray(trace.dec_mode)[1:] != np.asarray(trace.dec_mode)[:-1]) + 1
response = trace.time >= 0.5
assert len(mode_changes) == 1
assert trace.dec_mode[mode_changes[0]] == "blended"
assert trace.time[mode_changes[0]] <= 1.20 + 1e-9
assert -0.10 < trace.a_target[mode_changes[0]] < 0.0
assert np.all(np.asarray(trace.dec_mode)[(trace.time >= 1.75) & (trace.time < 3.25)] == "blended")
assert np.max(np.abs(_command_jerk(trace)[response[1:]])) < 3.0
assert not trace.fcw.any()
assert trace.raw_radar_passthrough.all()
assert np.all(trace.mpc_calls == 1)
assert trace.solver_failures == 0
def test_clear_road_launch_is_prompt_and_profiles_separate_above_launch_speed():
traces = [
_run(
@@ -467,6 +497,43 @@ def test_lead_bound_routine_decel_uses_smoothing_without_delaying_initial_brakin
assert smoothed.solver_failures == 0
def test_tightening_lead_releases_smoothing_before_late_catchup(monkeypatch):
event_time = 3.0
lead_jerk = 1.02
max_lead_decel = 2.22
ramp_time = max_lead_decel / lead_jerk
def lead_speed(current_time: float) -> float:
braking_time = max(current_time - event_time, 0.0)
ramp = min(braking_time, ramp_time)
return 16.9 - 0.5 * lead_jerk * ramp**2 - max_lead_decel * max(braking_time - ramp_time, 0.0)
common = dict(
duration=7.0, profile=AccelProfile.eco, lead_relevancy=True, speed=15.9, distance_lead=35.7,
v_lead=lead_speed, v_cruise=17.4, actuator_delay=0.15, actuator_lag=0.20,
)
stock = _run(controller_enabled=False, **common)
monkeypatch.setattr(longitudinal_planner_sp, "MPC_DECEL_JERK_MAX_REQUIRED_DECEL_RATE", np.inf)
always_smoothed = _run(controller_enabled=True, **common)
monkeypatch.setattr(longitudinal_planner_sp, "MPC_DECEL_JERK_MAX_REQUIRED_DECEL_RATE", MPC_DECEL_JERK_MAX_REQUIRED_DECEL_RATE)
trace = _run(controller_enabled=True, **common)
response = trace.time >= event_time
response_jerk = trace.time[1:] >= event_time
required_decel_rate = (trace.required_decel[3:] - trace.required_decel[:-3]) / (3 * DT_MDL)
gap = trace.distance_lead - trace.distance
always_smoothed_gap = always_smoothed.distance_lead - always_smoothed.distance
assert np.max(required_decel_rate[trace.required_decel[3:] >= 0.15]) > MPC_DECEL_JERK_MAX_REQUIRED_DECEL_RATE
assert _first_time_below(trace, -0.5) <= _first_time_below(stock, -0.5) + 1e-9
assert _first_time_below(trace, -0.5) <= _first_time_below(always_smoothed, -0.5) - DT_MDL + 1e-9
assert np.max(np.abs(np.diff(trace.a_target)[response_jerk] / DT_MDL)) < 3.0
assert not _has_brake_coast_brake(trace.a_target[response])
assert not _has_propulsion_brake_cycle(trace.a_target[response])
assert np.min(gap) >= np.min(always_smoothed_gap) - 1e-6
assert not stock.fcw.any() and not always_smoothed.fcw.any() and not trace.fcw.any()
assert stock.solver_failures == always_smoothed.solver_failures == trace.solver_failures == 0
def test_prius_route_model_launches_without_a_dead_pedal():
trace = _run(
duration=3.0, controller_enabled=True, profile=1, lead_relevancy=False, speed=0.0,
@@ -496,6 +563,40 @@ def test_stop_hold_survives_short_full_field_dropout():
_assert_no_new_solver_failures(trace, baseline)
def test_route_51d_duplicate_lead_speed_pulse_cannot_release_stop_hold():
pulse_start = 1.0
departure_time = 2.0
pulse_speeds = (0.1361, 0.1731, 0.2146, 0.2253, 0.2137, 0.1877)
pulse_distances = (6.0, 6.0, 6.0, 5.96, 6.04, 6.04)
def lead_speed(current_time: float) -> float:
return 0.0 if current_time < departure_time else 2.0
def observe(current_time: float, lead_name: str, truth: LeadObservation) -> LeadObservation:
result = truth | {"radar": True, "radarTrackId": 4887 if lead_name == "leadOne" else 4905}
pulse_frame = round((current_time - pulse_start) / DT_MDL)
if 0 <= pulse_frame < len(pulse_speeds):
speed = pulse_speeds[pulse_frame] if lead_name == "leadOne" else 0.0
distance = pulse_distances[pulse_frame] if lead_name == "leadOne" else 6.08
result |= {"dRel": distance, "vLead": speed, "vLeadK": speed, "vRel": speed, "aLeadK": 0.0}
return result
trace = _run(
duration=3.0, controller_enabled=True, lead_relevancy=True, speed=0.0, distance_lead=6.0,
v_lead=lead_speed, v_cruise=8.0, lead_observation_fn=observe, actuator_delay=0.15, actuator_lag=0.20,
)
pulse = (trace.time >= pulse_start) & (trace.time < pulse_start + len(pulse_speeds) * DT_MDL)
launched = np.flatnonzero((trace.time >= departure_time) & trace.launching)
assert np.all(trace.state[pulse] == int(AccelControllerState.stopHold))
assert np.all(trace.target_speed[pulse] == 0.0)
assert np.max(trace.speed[pulse]) < 0.01
assert len(launched) and trace.time[launched[0]] <= departure_time + STOP_HOLD_EXIT_FRAMES * DT_MDL + 1e-9
assert not _has_propulsion_brake_cycle(trace.a_target[trace.time >= departure_time])
assert not trace.fcw.any()
assert trace.solver_failures == 0
@pytest.mark.parametrize(("actuator_delay", "actuator_lag"), ACTUATOR_DYNAMICS, ids=ACTUATOR_IDS)
def test_stopped_lead_requires_four_departure_frames_and_launches_within_one_second(actuator_delay, actuator_lag):
departure_time = 1.0