Files
StarPilot/selfdrive/controls/tests/test_longitudinal_planner.py
T
2026-04-24 18:19:18 -05:00

297 lines
11 KiB
Python

import sys
import types
from types import SimpleNamespace
import numpy as np
import pytest
from cereal import log
from opendbc.car.honda.interface import CarInterface
from opendbc.car.honda.values import CAR
import openpilot.selfdrive.controls.radard as radard_mod
from openpilot.selfdrive.controls.lib.longcontrol import LongCtrlState
from openpilot.selfdrive.controls.lib.longitudinal_mpc_lib.long_mpc import LongitudinalMpc
from openpilot.selfdrive.controls.lib.longitudinal_planner import LongitudinalPlanner, get_vehicle_min_accel
from openpilot.selfdrive.controls.radard import RadarD
from openpilot.selfdrive.modeld.constants import ModelConstants, Plan
def make_lead(*, status: bool, d_rel: float = 200.0, v_lead: float = 0.0, a_lead: float = 0.0,
radar: bool = True, model_prob: float = 0.0, a_lead_tau: float = 0.3):
lead = log.RadarState.LeadData.new_message()
lead.status = status
lead.dRel = d_rel
lead.vLead = v_lead
lead.vLeadK = v_lead
lead.aLeadK = a_lead
lead.aLeadTau = a_lead_tau
lead.vRel = 0.0
lead.aRel = 0.0
lead.modelProb = model_prob
lead.radar = radar
return lead
def fill_model_lead(lead_msg, lead, prob: float):
lead_msg.prob = float(prob)
lead_msg.probTime = 0.0
lead_msg.t = [float(t) for t in ModelConstants.LEAD_T_IDXS]
lead_msg.x = [float(lead.dRel + lead.vLead * t) for t in ModelConstants.LEAD_T_IDXS]
lead_msg.xStd = [0.5] * len(ModelConstants.LEAD_T_IDXS)
lead_msg.y = [0.0] * len(ModelConstants.LEAD_T_IDXS)
lead_msg.yStd = [0.5] * len(ModelConstants.LEAD_T_IDXS)
lead_msg.v = [float(lead.vLead)] * len(ModelConstants.LEAD_T_IDXS)
lead_msg.vStd = [0.5] * len(ModelConstants.LEAD_T_IDXS)
lead_msg.a = [float(lead.aLeadK)] * len(ModelConstants.LEAD_T_IDXS)
lead_msg.aStd = [0.5] * len(ModelConstants.LEAD_T_IDXS)
def make_model_lead_msg(*, d_rel: float, v_lead: float, prob: float):
model = log.ModelDataV2.new_message()
model.init('leadsV3', 1)
fill_model_lead(model.leadsV3[0], make_lead(status=True, d_rel=d_rel, v_lead=v_lead), prob)
return model.leadsV3[0]
def make_model(v_ego: float, desired_accel: float, gas_press_prob: float = 1.0, lead_one=None, lead_two=None):
model = log.ModelDataV2.new_message()
t_idxs = ModelConstants.T_IDXS
model.position.x = [float(v_ego * t) for t in t_idxs]
model.position.y = [0.0] * len(t_idxs)
model.position.z = [0.0] * len(t_idxs)
model.position.t = [float(t) for t in t_idxs]
model.velocity.x = [float(v_ego)] * len(t_idxs)
model.velocity.y = [0.0] * len(t_idxs)
model.velocity.z = [0.0] * len(t_idxs)
model.velocity.t = [float(t) for t in t_idxs]
model.acceleration.x = [0.0] * len(t_idxs)
model.acceleration.y = [0.0] * len(t_idxs)
model.acceleration.z = [0.0] * len(t_idxs)
model.acceleration.t = [float(t) for t in t_idxs]
model.init('leadsV3', 3)
fill_model_lead(model.leadsV3[0], lead_one if lead_one is not None else make_lead(status=False),
1.0 if lead_one is not None and lead_one.status else 0.0)
fill_model_lead(model.leadsV3[1], lead_two if lead_two is not None else make_lead(status=False),
1.0 if lead_two is not None and lead_two.status else 0.0)
fill_model_lead(model.leadsV3[2], make_lead(status=False), 0.0)
model.meta.disengagePredictions.gasPressProbs = [float(gas_press_prob)] * 6
model.action.desiredAcceleration = desired_accel
model.action.shouldStop = False
return model
def make_sm(v_ego: float, desired_accel: float, min_accel: float, *, experimental_mode: bool = True,
tracking_lead: bool = False, lead_one=None, lead_two=None,
gas_press_prob: float = 1.0, disable_throttle: bool = False):
return {
"carControl": SimpleNamespace(orientationNED=[0.0, 0.0, 0.0]),
"carState": SimpleNamespace(
vEgo=v_ego,
vEgoCluster=v_ego,
aEgo=0.0,
vCruise=100.0,
standstill=False,
steeringAngleDeg=0.0,
),
"controlsState": SimpleNamespace(
longControlState=LongCtrlState.pid,
forceDecel=False,
),
"liveParameters": SimpleNamespace(angleOffsetDeg=0.0),
"modelV2": make_model(v_ego, desired_accel, gas_press_prob=gas_press_prob, lead_one=lead_one, lead_two=lead_two),
"radarState": SimpleNamespace(
leadOne=lead_one if lead_one is not None else make_lead(status=False),
leadTwo=lead_two if lead_two is not None else make_lead(status=False),
),
"selfdriveState": SimpleNamespace(enabled=True, experimentalMode=experimental_mode, personality=0),
"starpilotPlan": SimpleNamespace(
vCruise=v_ego + 5.0,
minAcceleration=min_accel,
maxAcceleration=2.0,
disableThrottle=disable_throttle,
trackingLead=tracking_lead,
accelerationJerk=5.0,
dangerJerk=5.0,
speedJerk=5.0,
dangerFactor=1.0,
tFollow=1.45,
forcingStopLength=2,
),
}
def make_toggles(model_version: str = "v11"):
return SimpleNamespace(
taco_tune=False,
classic_model=False,
tinygrad_model=True,
model_version=model_version,
stop_distance=6.0,
vEgoStopping=0.5,
)
@pytest.mark.parametrize("model_version", ["v11", "v12"])
def test_experimental_mlsim_uses_vehicle_min_accel_floor(model_version):
v_ego = 18.0
desired_accel = -1.0
comfort_min_accel = -0.5
CP = CarInterface.get_non_essential_params(CAR.HONDA_CIVIC)
planner = LongitudinalPlanner(CP, init_v=v_ego)
sm = make_sm(v_ego, desired_accel, comfort_min_accel)
vehicle_min_accel = get_vehicle_min_accel(CP, v_ego)
assert vehicle_min_accel < comfort_min_accel
planner.update(sm, make_toggles(model_version))
assert planner.mode == "blended"
assert planner.mlsim
assert planner.output_a_target == pytest.approx(desired_accel, abs=1e-3)
assert planner.output_a_target < comfort_min_accel
@pytest.mark.parametrize("model_version", ["v11", "v12"])
def test_acc_mode_uses_close_raw_lead_when_tracking_lead_is_debounced(model_version):
v_ego = 5.0
CP = CarInterface.get_non_essential_params(CAR.HONDA_CIVIC)
planner = LongitudinalPlanner(CP, init_v=v_ego)
sm = make_sm(
v_ego,
desired_accel=-0.6,
min_accel=-1.0,
experimental_mode=False,
tracking_lead=False,
lead_one=make_lead(status=True, d_rel=24.0, v_lead=0.3),
)
sm["starpilotPlan"].vCruise = v_ego + 12.0
planner.update(sm, make_toggles(model_version))
assert planner.mode == "acc"
assert planner.raw_close_lead_needs_control(sm["radarState"].leadOne, v_ego)
assert planner.output_a_target == pytest.approx(
planner.get_close_lead_brake_cap(sm["radarState"].leadOne, v_ego, sm["starpilotPlan"].minAcceleration)
)
def test_modeld_action_passes_tomb_raider_longitudinal_params(monkeypatch):
monkeypatch.setenv("DEBUG", "0")
fake_commonmodel = types.ModuleType("openpilot.selfdrive.modeld.models.commonmodel_pyx")
fake_commonmodel.DrivingModelFrame = object
fake_commonmodel.CLContext = object
monkeypatch.setitem(sys.modules, fake_commonmodel.__name__, fake_commonmodel)
from openpilot.selfdrive.modeld import modeld
captured = {}
def fake_get_accel_from_plan(speeds, accels, t_idxs, *, action_t, vEgoStopping):
captured["speeds"] = speeds
captured["accels"] = accels
captured["t_idxs"] = t_idxs
captured["action_t"] = action_t
captured["vEgoStopping"] = vEgoStopping
return 0.4, True
monkeypatch.setattr(modeld, "get_accel_from_plan_tomb_raider", fake_get_accel_from_plan)
plan = np.zeros((1, ModelConstants.IDX_N, ModelConstants.PLAN_WIDTH), dtype=np.float32)
plan[0, :, Plan.VELOCITY] = 3.0
plan[0, :, Plan.ACCELERATION] = -0.1
prev_action = log.ModelDataV2.Action.new_message()
toggles = SimpleNamespace(vEgoStopping=0.42)
action = modeld.get_action_from_model(
{"plan": plan},
prev_action,
lat_action_t=0.2,
long_action_t=0.73,
v_ego=5.0,
mlsim=True,
is_v9=True,
starpilot_toggles=toggles,
)
assert captured["action_t"] == pytest.approx(0.73)
assert captured["vEgoStopping"] == pytest.approx(0.42)
assert list(captured["t_idxs"]) == ModelConstants.T_IDXS
np.testing.assert_allclose(captured["speeds"], 3.0)
np.testing.assert_allclose(captured["accels"], -0.1)
assert action.shouldStop
def test_allow_throttle_hysteresis_filters_gas_prob_chatter():
v_ego = 10.0
CP = CarInterface.get_non_essential_params(CAR.HONDA_CIVIC)
planner = LongitudinalPlanner(CP, init_v=v_ego)
sm = make_sm(v_ego, desired_accel=0.0, min_accel=-1.0, experimental_mode=False, gas_press_prob=0.5)
toggles = make_toggles()
planner.update(sm, toggles)
assert planner.model_allow_throttle
assert planner.allow_throttle
sm["modelV2"] = make_model(v_ego, desired_accel=0.0, gas_press_prob=0.37)
planner.update(sm, toggles)
assert planner.model_allow_throttle
assert planner.allow_throttle
sm["modelV2"] = make_model(v_ego, desired_accel=0.0, gas_press_prob=0.34)
planner.update(sm, toggles)
assert not planner.model_allow_throttle
assert not planner.allow_throttle
sm["modelV2"] = make_model(v_ego, desired_accel=0.0, gas_press_prob=0.43)
planner.update(sm, toggles)
assert not planner.model_allow_throttle
assert not planner.allow_throttle
sm["modelV2"] = make_model(v_ego, desired_accel=0.0, gas_press_prob=0.46)
planner.update(sm, toggles)
assert planner.model_allow_throttle
assert planner.allow_throttle
def test_radard_bias_resets_across_low_prob_lead_acquisition(monkeypatch):
monkeypatch.setattr(radard_mod, "get_starpilot_toggles", lambda sm=None: SimpleNamespace())
rd = RadarD()
rd.v_ego = 33.0
for _ in range(rd._bias_fd_k + 5):
bias = rd._update_lead_bias(0, make_model_lead_msg(d_rel=120.0, v_lead=33.0, prob=0.01))
assert bias == 0.0
bias = rd._update_lead_bias(0, make_model_lead_msg(d_rel=56.0, v_lead=33.0, prob=0.99))
assert bias == 0.0
assert len(rd.lead_drel_hists[0]) == 1
assert len(rd.vego_hists_bias[0]) == 1
def test_model_trajectory_requires_radar_anchor():
radar_lead = make_lead(status=True, d_rel=60.0, v_lead=20.0, radar=False, model_prob=0.99)
model = make_model(30.0, desired_accel=0.0)
fill_model_lead(model.leadsV3[0], make_lead(status=True, d_rel=60.0, v_lead=32.0), 0.99)
model.leadsV3[0].v = [32.0 + 2.0 * i for i in range(len(ModelConstants.LEAD_T_IDXS))]
vision_mpc = LongitudinalMpc()
vision_mpc.set_cur_state(30.0, 0.0)
vision_mpc.lead_v_filter.x = radar_lead.vLead
vision_traj = vision_mpc.process_lead(model.leadsV3[0], radar_lead, tracking_lead=True)
radar_mpc = LongitudinalMpc()
radar_mpc.set_cur_state(30.0, 0.0)
radar_lead.radar = True
radar_traj = radar_mpc.process_lead(model.leadsV3[0], radar_lead, tracking_lead=True)
assert np.allclose(vision_traj[:, 1], 20.0, atol=1e-3)
assert radar_traj[5, 1] > vision_traj[5, 1] + 1.0