mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-09-01 21:53:42 +08:00
285 lines
12 KiB
Python
285 lines
12 KiB
Python
#!/usr/bin/env python3
|
|
import numpy as np
|
|
import pytest
|
|
from types import SimpleNamespace
|
|
|
|
from cereal import log
|
|
from opendbc.car.honda.interface import CarInterface
|
|
from opendbc.car.honda.values import CAR
|
|
from openpilot.selfdrive.controls.lib.longitudinal_planner import LongitudinalPlanner, get_vehicle_min_accel
|
|
from openpilot.selfdrive.modeld.constants import ModelConstants
|
|
|
|
TRAJ_LEN = len(ModelConstants.T_IDXS)
|
|
|
|
|
|
def make_lead(*, status, d_rel=200.0, v_lead=0.0, a_lead=0.0, radar=False, model_prob=0.0):
|
|
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.vRel = 0.0
|
|
lead.aRel = 0.0
|
|
lead.yRel = 0.0
|
|
lead.modelProb = model_prob
|
|
lead.radar = radar
|
|
return lead
|
|
|
|
|
|
def make_model(v_ego, desired_accel, *, velocity_traj=None, should_stop=False):
|
|
model = log.ModelDataV2.new_message()
|
|
model.init('leadsV3', 3)
|
|
t_idxs = ModelConstants.T_IDXS
|
|
n = len(t_idxs)
|
|
|
|
model.position.x = [float(v_ego * t) for t in t_idxs]
|
|
model.position.y = [0.0] * n
|
|
model.position.z = [0.0] * n
|
|
model.position.t = [float(t) for t in t_idxs]
|
|
|
|
if velocity_traj is None:
|
|
model.velocity.x = [float(v_ego)] * n
|
|
else:
|
|
model.velocity.x = [float(x) for x in velocity_traj]
|
|
model.velocity.y = [0.0] * n
|
|
model.velocity.z = [0.0] * n
|
|
model.velocity.t = [float(t) for t in t_idxs]
|
|
|
|
model.acceleration.x = [0.0] * n
|
|
model.acceleration.y = [0.0] * n
|
|
model.acceleration.z = [0.0] * n
|
|
model.acceleration.t = [float(t) for t in t_idxs]
|
|
|
|
model.action.desiredAcceleration = desired_accel
|
|
model.action.shouldStop = should_stop
|
|
return model
|
|
|
|
|
|
def make_sm(v_ego, desired_accel, min_accel, *, experimental_mode=True, tracking_lead=False,
|
|
lead_one=None, velocity_traj=None, should_stop=False):
|
|
if lead_one is None:
|
|
lead_one = make_lead(status=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=0, forceDecel=False),
|
|
"liveParameters": SimpleNamespace(angleOffsetDeg=0.0),
|
|
"modelV2": make_model(v_ego, desired_accel, velocity_traj=velocity_traj, should_stop=should_stop),
|
|
"radarState": SimpleNamespace(leadOne=lead_one, leadTwo=make_lead(status=False)),
|
|
"selfdriveState": SimpleNamespace(enabled=True, experimentalMode=experimental_mode, personality=0),
|
|
"starpilotCarState": SimpleNamespace(accelPressed=False),
|
|
"starpilotPlan": SimpleNamespace(
|
|
vCruise=v_ego + 5.0,
|
|
minAcceleration=min_accel,
|
|
maxAcceleration=2.0,
|
|
disableThrottle=False,
|
|
trackingLead=tracking_lead,
|
|
accelerationJerk=5.0,
|
|
dangerJerk=5.0,
|
|
speedJerk=5.0,
|
|
dangerFactor=1.0,
|
|
tFollow=1.45,
|
|
forcingStop=False,
|
|
redLight=False,
|
|
forcingStopLength=2,
|
|
),
|
|
}
|
|
|
|
|
|
def make_toggles(*, hybrid=False, exp_bias=0.0, sens=1.0):
|
|
return SimpleNamespace(
|
|
taco_tune=False,
|
|
classic_model=False,
|
|
tinygrad_model=True,
|
|
model_version="v11",
|
|
vEgoStopping=0.5,
|
|
radar_takeoffs=False,
|
|
hybrid_experimental_mode=hybrid,
|
|
hybrid_exp_bias=exp_bias,
|
|
hybrid_vision_brake_sensitivity=sens,
|
|
)
|
|
|
|
|
|
def assert_outputs_equal(p1, p2, label=""):
|
|
assert p1.output_a_target == pytest.approx(p2.output_a_target, abs=1e-9), f"{label} aTarget mismatch"
|
|
assert p1.output_should_stop == p2.output_should_stop, f"{label} shouldStop mismatch"
|
|
np.testing.assert_allclose(p1.v_desired_trajectory, p2.v_desired_trajectory, atol=1e-9)
|
|
np.testing.assert_allclose(p1.a_desired_trajectory, p2.a_desired_trajectory, atol=1e-9)
|
|
|
|
|
|
# 1. Toggle equivalence (regression safety)
|
|
def _run_equivalence_scenarios(experimental_mode):
|
|
CP = CarInterface.get_non_essential_params(CAR.HONDA_CIVIC)
|
|
|
|
scenarios = [
|
|
dict(v_ego=20.0, desired_accel=0.4, min_accel=-2.0, tracking_lead=False,
|
|
lead_one=None, velocity_traj=None, should_stop=False, label="chill-cruise"),
|
|
dict(v_ego=20.0, desired_accel=-0.6, min_accel=-2.0, tracking_lead=True,
|
|
lead_one=make_lead(status=True, d_rel=30.0, v_lead=18.0, radar=True, model_prob=1.0),
|
|
velocity_traj=None, should_stop=False, label="slow-lead"),
|
|
dict(v_ego=25.0, desired_accel=-1.2, min_accel=-3.0, tracking_lead=True,
|
|
lead_one=make_lead(status=True, d_rel=12.0, v_lead=3.0, radar=True, model_prob=1.0),
|
|
velocity_traj=list(np.linspace(25.0, 2.0, TRAJ_LEN)), should_stop=True, label="stop-approach"),
|
|
]
|
|
|
|
for scen in scenarios:
|
|
label = scen.pop("label")
|
|
hybrid_planner = LongitudinalPlanner(CP, init_v=scen["v_ego"])
|
|
stock_planner = LongitudinalPlanner(CP, init_v=scen["v_ego"])
|
|
sm = make_sm(experimental_mode=experimental_mode, **scen)
|
|
hybrid_planner.update(sm, make_toggles(hybrid=True) if False else make_toggles(hybrid=False))
|
|
stock_planner.update(sm, make_toggles())
|
|
assert_outputs_equal(hybrid_planner, stock_planner, label=f"chill/{label}" if not experimental_mode else f"exp/{label}")
|
|
|
|
|
|
def test_hem_off_matches_stock_chill_mode():
|
|
_run_equivalence_scenarios(experimental_mode=False)
|
|
|
|
|
|
def test_hem_off_matches_stock_experimental_mode():
|
|
_run_equivalence_scenarios(experimental_mode=True)
|
|
|
|
|
|
# 2. Stop sign / red light approach
|
|
def test_stop_sign_approach_commands_pure_model_braking():
|
|
CP = CarInterface.get_non_essential_params(CAR.HONDA_CIVIC)
|
|
planner = LongitudinalPlanner(CP, init_v=20.0)
|
|
toggles = make_toggles(hybrid=True)
|
|
sm = make_sm(20.0, -2.5, -4.0, experimental_mode=False,
|
|
velocity_traj=list(np.linspace(20.0, 0.0, TRAJ_LEN)), should_stop=True)
|
|
# Enough frames for the jerk slew limiter to ramp to full vision braking.
|
|
for _ in range(20):
|
|
planner.update(sm, toggles)
|
|
assert planner.output_a_target <= -2.0, "Exp stop braking must not be diluted by cruise throttle"
|
|
assert planner.output_a_target < 0.0, "Cruise throttle must be locked out during a vision stop"
|
|
|
|
|
|
def test_stop_line_handshake_asserts_should_stop():
|
|
CP = CarInterface.get_non_essential_params(CAR.HONDA_CIVIC)
|
|
planner = LongitudinalPlanner(CP, init_v=0.3)
|
|
toggles = make_toggles(hybrid=True)
|
|
sm = make_sm(0.3, -1.0, -4.0, experimental_mode=False,
|
|
velocity_traj=[0.0] * TRAJ_LEN, should_stop=True)
|
|
for _ in range(6):
|
|
planner.update(sm, toggles)
|
|
assert planner.output_should_stop, "Standstill at a predicted stop must assert shouldStop"
|
|
assert planner.output_a_target <= -0.4, "Standstill brake must be held at the stop line"
|
|
|
|
|
|
# 3. Slower-lead approach (radar + vision blend within safety)
|
|
def test_slower_lead_approach_blends_vision_prebrake_within_safety():
|
|
CP = CarInterface.get_non_essential_params(CAR.HONDA_CIVIC)
|
|
planner = LongitudinalPlanner(CP, init_v=20.0)
|
|
toggles = make_toggles(hybrid=True)
|
|
lead = make_lead(status=True, d_rel=25.0, v_lead=0.4, radar=True, model_prob=1.0)
|
|
sm = make_sm(20.0, -1.5, -4.0, experimental_mode=False, tracking_lead=True, lead_one=lead,
|
|
velocity_traj=list(np.linspace(20.0, 5.0, TRAJ_LEN)), should_stop=False)
|
|
for _ in range(15):
|
|
planner.update(sm, toggles)
|
|
# Vision pre-braking must be engaged while closing on a slower lead.
|
|
assert planner.output_a_target <= -1.0, "Vision pre-brake should be blended in on a slow lead"
|
|
# ...but it must never exceed the physical / commanded deceleration floors.
|
|
assert planner.output_a_target >= get_vehicle_min_accel(CP, 20.0)
|
|
assert planner.output_a_target >= -4.0
|
|
|
|
|
|
# 4. Green light / lead departure (instant unlatch)
|
|
def test_green_light_departure_instantly_clears_should_stop():
|
|
CP = CarInterface.get_non_essential_params(CAR.HONDA_CIVIC)
|
|
planner = LongitudinalPlanner(CP, init_v=0.3)
|
|
toggles = make_toggles(hybrid=True)
|
|
|
|
sm_stop = make_sm(0.3, -1.0, -4.0, experimental_mode=False,
|
|
velocity_traj=[0.0] * TRAJ_LEN, should_stop=True)
|
|
for _ in range(6):
|
|
planner.update(sm_stop, toggles)
|
|
assert planner.output_should_stop
|
|
assert planner.output_a_target <= -0.4
|
|
|
|
# Green light / lead pulls away: horizon ramps up and Exp accelerates. Run a
|
|
# few frames so the jerk slew ramps the commanded accel up from the brake hold.
|
|
sm_go = make_sm(0.3, 1.5, -4.0, experimental_mode=False,
|
|
velocity_traj=list(np.linspace(0.0, 8.0, TRAJ_LEN)), should_stop=False)
|
|
sm_go["starpilotPlan"].vCruise = 15.0
|
|
for _ in range(6):
|
|
planner.update(sm_go, toggles)
|
|
|
|
assert not planner.output_should_stop, "Departure must instantly clear the stop latch"
|
|
assert planner.output_a_target > 0.0, "Departure must deliver positive cruise acceleration"
|
|
|
|
|
|
def test_lead_departure_clears_stop_without_sticky_latch():
|
|
CP = CarInterface.get_non_essential_params(CAR.HONDA_CIVIC)
|
|
planner = LongitudinalPlanner(CP, init_v=0.3)
|
|
toggles = make_toggles(hybrid=True)
|
|
|
|
sm_stop = make_sm(0.3, -1.0, -4.0, experimental_mode=False, tracking_lead=True,
|
|
lead_one=make_lead(status=True, d_rel=3.0, v_lead=0.1, radar=True, model_prob=1.0),
|
|
velocity_traj=[0.0] * TRAJ_LEN, should_stop=True)
|
|
for _ in range(6):
|
|
planner.update(sm_stop, toggles)
|
|
assert planner.output_should_stop
|
|
|
|
# Lead accelerates away from standstill and opens up a safe gap.
|
|
sm_go = make_sm(0.3, 1.0, -4.0, experimental_mode=False, tracking_lead=True,
|
|
lead_one=make_lead(status=True, d_rel=25.0, v_lead=10.0, radar=True, model_prob=1.0),
|
|
velocity_traj=list(np.linspace(0.0, 7.0, TRAJ_LEN)), should_stop=False)
|
|
sm_go["starpilotPlan"].vCruise = 15.0
|
|
for _ in range(6):
|
|
planner.update(sm_go, toggles)
|
|
|
|
# HEM's own vision latch releases instantly (no sticky vision authority).
|
|
assert planner.hybrid_controller.w_vision == 0.0
|
|
assert not planner.output_should_stop, "Lead departure must clear shouldStop without lag"
|
|
assert planner.output_a_target > 0.0
|
|
|
|
|
|
# 5. Safety clamping preservation
|
|
def test_radar_cut_in_preserves_chill_floor_within_physical_limits():
|
|
CP = CarInterface.get_non_essential_params(CAR.HONDA_CIVIC)
|
|
planner = LongitudinalPlanner(CP, init_v=20.0)
|
|
toggles = make_toggles(hybrid=True)
|
|
lead = make_lead(status=True, d_rel=6.0, v_lead=10.0, radar=True, model_prob=1.0)
|
|
sm = make_sm(20.0, 0.0, -3.5, experimental_mode=False, tracking_lead=True, lead_one=lead,
|
|
velocity_traj=[20.0] * TRAJ_LEN, should_stop=False)
|
|
for _ in range(15):
|
|
planner.update(sm, toggles)
|
|
assert planner.output_a_target <= -1.0, "Radar cut-in must brake hard at the planner output"
|
|
assert planner.output_a_target >= -3.5, "Output must never exceed the commanded accel_min floor"
|
|
assert planner.output_a_target >= get_vehicle_min_accel(CP, 20.0), "Physical decel limit respected"
|
|
|
|
|
|
def test_hem_output_clamped_to_physical_limits_under_corrupt_exp():
|
|
CP = CarInterface.get_non_essential_params(CAR.HONDA_CIVIC)
|
|
lo = get_vehicle_min_accel(CP, 20.0)
|
|
for a_exp in (float("nan"), -100.0, 100.0):
|
|
planner = LongitudinalPlanner(CP, init_v=20.0)
|
|
sm = make_sm(20.0, a_exp, -3.5, experimental_mode=False,
|
|
velocity_traj=[20.0] * TRAJ_LEN, should_stop=False)
|
|
for _ in range(3):
|
|
planner.update(sm, make_toggles(hybrid=True))
|
|
assert np.isfinite(planner.output_a_target), f"corrupt a_exp={a_exp} must not propagate"
|
|
assert lo - 1e-6 <= planner.output_a_target <= 2.0 + 1e-6
|
|
|
|
|
|
def test_output_stays_within_physical_limits_across_scenarios():
|
|
CP = CarInterface.get_non_essential_params(CAR.HONDA_CIVIC)
|
|
scenarios = [
|
|
dict(v_ego=20.0, desired_accel=-2.5, min_accel=-4.0, tracking_lead=False,
|
|
lead_one=None, velocity_traj=list(np.linspace(20.0, 0.0, TRAJ_LEN)), should_stop=True),
|
|
dict(v_ego=20.0, desired_accel=0.5, min_accel=-3.0, tracking_lead=True,
|
|
lead_one=make_lead(status=True, d_rel=40.0, v_lead=22.0, radar=True, model_prob=1.0),
|
|
velocity_traj=None, should_stop=False),
|
|
]
|
|
for scen in scenarios:
|
|
planner = LongitudinalPlanner(CP, init_v=scen["v_ego"])
|
|
sm = make_sm(experimental_mode=False, **scen)
|
|
for _ in range(6):
|
|
planner.update(sm, make_toggles(hybrid=True))
|
|
assert planner.output_a_target >= get_vehicle_min_accel(CP, scen["v_ego"])
|
|
assert planner.output_a_target <= 2.0
|