Compare commits

...

20 Commits

Author SHA1 Message Date
Prabhaav Pillai 345cfe172e Refactor HybridExperimentalMode: Replace numpy with math for input validation and clamping 2026-08-27 04:12:29 -04:00
Prabhaav Pillai 075d664867 Enhance Hybrid Experimental Mode and HEM Telemetry Exporter with new features and optimizations 2026-08-27 03:51:26 -04:00
Prabhaav Pillai 801654fd8d Add HEM telemetry exporter and update forensic analysis tools
- Introduced `export_hem_telemetry.py` to extract and save telemetry data from target segments into a JSON file, including key selfdriveState fields.
- Modified `hem_forensic.py` to incorporate new diagnostic keys and update the logic for authority and stop detection.
- Enhanced `hem_stop_analyzer.py` to analyze stop detection failures with new metrics and improved comments for clarity.
- Updated `mode_sim.py` to reflect changes in hybrid mode updates and authority handling.
2026-08-27 02:03:33 -04:00
Prabhaav Pillai 50f8ada119 trying to figure out why rolling stop signs 2026-08-26 20:50:05 -04:00
Prabhaav Pillai 37a5826d15 trying to figure out why rolling stop signs 2026-08-26 20:31:36 -04:00
Prabhaav Pillai ef2850f619 Refine HybridExperimentalMode: improve departure signal identification and adjust low-speed acceleration lockout conditions 2026-08-26 16:41:51 -04:00
Prabhaav Pillai c37e9a2fc4 Refine HybridExperimentalMode: improve departure signal identification and adjust low-speed acceleration lockout conditions 2026-08-26 16:33:23 -04:00
Prabhaav Pillai ecc4158666 Refine HybridExperimentalMode: improve departure signal identification and adjust low-speed acceleration lockout conditions 2026-08-26 16:20:38 -04:00
Prabhaav Pillai 1d02eeac0b Refine HybridExperimentalMode: enhance vision filtering logic, improve standstill reset behavior, and expand low-speed acceleration lockout conditions 2026-08-26 16:11:12 -04:00
Prabhaav Pillai 513fc586cc Enhance HybridExperimentalMode: add vision filtering and standstill reset logic; fix low-speed acceleration lockout and near-stop prediction 2026-08-26 16:04:27 -04:00
Prabhaav Pillai 3393c99433 add logs 2026-08-26 15:43:09 -04:00
Prabhaav Pillai 8a174822e7 Add KINEMATIC_STOP_GAIN parameter to HybridExperimentalMode for enhanced stop-line braking control 2026-08-26 14:54:29 -04:00
Prabhaav Pillai 3265a98d91 Add forensic analysis tool for Hybrid Experimental Mode decisions
- Introduced `hem_forensic.py` to analyze and log the decision-making process of Hybrid Experimental Mode (HEM) during route playback.
- The tool captures internal states, detects stop roll-through incidents, and provides detailed per-frame logs.
- Added regression tests for HEM behavior in specific scenarios, ensuring correct braking behavior during partial slowdowns and gentle high-horizon slowdowns.
2026-08-26 01:58:23 -04:00
Prabhaav Pillai 5c49878dbd Refactor HEM parameters: rename HEMExpAuthority to HEMExpDominant and update related logic 2026-08-25 21:12:55 -04:00
Prabhaav Pillai 740a9cf4ef Add HEMExpAuthority parameter and enhance hybrid experimental mode logging 2026-08-25 19:53:59 -04:00
Prabhaav Pillai d07d6ae7b7 fix hem 2026-08-25 18:01:37 -04:00
Prabhaav Pillai 8b5e95a9d5 fix hem 2026-08-25 15:47:40 -04:00
Prabhaav Pillai 5fe9b3ac8d bug fix esim 2026-08-25 15:06:33 -04:00
Prabhaav Pillai 74b68242b2 bug fix missing attribute 2026-08-25 14:39:51 -04:00
Prabhaav Pillai 632db8bfd3 Add HybridExperimental mode 2026-08-25 04:42:59 -04:00
24 changed files with 3740 additions and 40 deletions
+4
View File
@@ -238,6 +238,10 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
{"CommunityFavorites", {PERSISTENT, STRING, "", "", 1}},
{"ConditionalChill", {PERSISTENT, BOOL, "0", "0", 1}},
{"ConditionalExperimental", {PERSISTENT, BOOL, "1", "0", 1, SETTINGS_SIMPLE}},
{"HybridExpBias", {PERSISTENT, FLOAT, "0", "0", 1}},
{"HybridExperimental", {PERSISTENT, BOOL, "0", "0", 1}},
{"HybridVisionBrakeSensitivity", {PERSISTENT, FLOAT, "1", "1", 1}},
{"HEMExpDominant", {CLEAR_ON_MANAGER_START, BOOL, "0", "0", 2}},
{"CurvatureData", {PERSISTENT | DONT_LOG, JSON, "{}", "{}"}},
{"CurveSpeedController", {PERSISTENT, BOOL, "1", "0", 1, SETTINGS_SIMPLE}},
{"CurveSpeedControllerNoLead", {PERSISTENT, BOOL, "0", "0", 1, SETTINGS_SIMPLE}},
@@ -518,7 +518,7 @@ class CarController(CarControllerBase):
main_accel_cmd = 0. if self.CP.flags & ToyotaFlags.SECOC.value else pcm_accel_cmd
can_sends.append(toyotacan.create_accel_command(self.packer, main_accel_cmd, pcm_cancel_cmd, self.permit_braking, self.standstill_req, lead,
CS.acc_type, fcw_alert, self.distance_button, starpilot_toggles.reverse_cruise_increase))
CS.acc_type, fcw_alert, self.distance_button, getattr(starpilot_toggles, "reverse_cruise_increase", False)))
if self.CP.flags & ToyotaFlags.SECOC.value:
acc_cmd_2 = toyotacan.create_accel_command_2(self.packer, pcm_accel_cmd)
acc_cmd_2 = add_mac(self.secoc_key,
@@ -538,7 +538,7 @@ class CarController(CarControllerBase):
can_sends.append(toyotacan.create_acc_cancel_command(self.packer))
else:
can_sends.append(toyotacan.create_accel_command(self.packer, 0, pcm_cancel_cmd, True, False, lead, CS.acc_type, False,
self.distance_button, starpilot_toggles.reverse_cruise_increase))
self.distance_button, getattr(starpilot_toggles, "reverse_cruise_increase", False)))
# *** hud ui ***
if self.CP.carFingerprint != CAR.TOYOTA_PRIUS_V:
+124 -13
View File
@@ -7,8 +7,10 @@ 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.common.params import Params
from openpilot.selfdrive.modeld.constants import ModelConstants
from openpilot.starpilot.common.model_versions import is_tinygrad_model_version
from openpilot.starpilot.controls.lib.hybrid_experimental_mode import HybridExperimentalMode
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_mpc_lib.long_mpc import desired_follow_distance
@@ -133,6 +135,26 @@ VISION_LEAD_APPROACH_BRAKING_DEFICIT_MIN = 0.75
VISION_LEAD_APPROACH_BRAKING_MIN_LEAD_BRAKE = 0.45
VISION_LEAD_APPROACH_BRAKING_FULL_LEAD_BRAKE = 1.20
PLANNER_SAFETY_WARNING_INTERVAL = 5.0
HEM_STATUS_LOG_INTERVAL = 10.0
HEM_AUTH_PUB_INTERVAL = 0.5
def _hem_log_timestamp() -> str:
"""Wall-clock timestamp with millisecond precision for the [HEM] live log."""
from datetime import datetime
now = datetime.now()
return now.strftime("%H:%M:%S") + f".{now.microsecond // 1000:03d}"
def _hem_format_diag_value(value):
"""Compact, deterministic formatting for the per-frame HEM diagnostic dump."""
if isinstance(value, (bool, np.bool_)):
return "1" if value else "0"
if isinstance(value, float):
return f"{value:.4f}"
if isinstance(value, (int, np.integer)):
return str(int(value))
return str(value)
VISION_LEAD_APPROACH_BRAKING_FLOOR_MIN_DECEL = 1.30
VISION_LEAD_APPROACH_BRAKING_FLOOR_MAX_DECEL = 1.75
VISION_LEAD_APPROACH_CONFIRM_TIME = 0.25
@@ -596,6 +618,11 @@ class LongitudinalPlanner:
self.duplicate_vision_comfort_lead_source = None
self.prev_experimental_mode = None
self.experimental_release_accel_until = 0.0
self.hybrid_controller = HybridExperimentalMode()
self._hem_status_log_t = 0.0
self._hem_logged_active = False
self._hem_auth_pub_t = 0.0
self._hem_params_memory = None
if self.is_preap:
try:
@@ -1884,6 +1911,36 @@ class LongitudinalPlanner:
floor = min(LC_MERGE_ACCEL_BIAS, cruise_cap)
return floor
def _log_hem_status(self, now_t, active, v_ego, a_chill, a_exp, a_fused):
hc = self.hybrid_controller
hc.record_diag = True
ts = _hem_log_timestamp()
if active != self._hem_logged_active:
self._hem_logged_active = active
self._hem_status_log_t = 0.0
print(f"[HEM] {ts} mode {'ON' if active else 'OFF'}")
if not active:
return
# Rich per-frame dump of every HEM decision variable so a missed stop can be
# diagnosed to the exact frame and signal (see hybrid_experimental_mode diag).
d = hc.diag
if d:
print(f"[HEM] {ts} " + " ".join(f"{k}={_hem_format_diag_value(v)}" for k, v in d.items()))
def _publish_hem_status(self, now_t):
if self._hem_params_memory is None:
try:
self._hem_params_memory = Params(memory=True)
except Exception:
return
if now_t - self._hem_auth_pub_t < HEM_AUTH_PUB_INTERVAL:
return
self._hem_auth_pub_t = now_t
try:
self._hem_params_memory.put_bool("HEMExpDominant", bool(self.hybrid_controller.last_exp_dominant))
except Exception:
pass
def update(self, sm, starpilot_toggles):
if self.is_preap:
self._preap_param_frame += 1
@@ -1936,6 +1993,7 @@ class LongitudinalPlanner:
self.a_desired = np.clip(sm['carState'].aEgo, accel_limits[0], accel_limits[1])
self.model_allow_throttle = True
self.model_allow_throttle_transition_t = 0.0
self.hybrid_controller.reset(float(self.a_desired))
# Prevent divergence, smooth in current v_ego
self.v_desired_filter.x = max(0.0, self.v_desired_filter.update(v_ego))
@@ -2284,26 +2342,31 @@ class LongitudinalPlanner:
model_launch_accel = self.get_model_launch_accel(model_launch_v, model_launch_a, action_t, scene_v_ego)
if classic_model:
output_a_target, output_should_stop = get_accel_from_plan_classic(
output_a_target_mpc, output_should_stop_mpc = get_accel_from_plan_classic(
self.CP, self.v_desired_trajectory, self.a_desired_trajectory, starpilot_toggles.vEgoStopping)
elif tinygrad_model:
output_a_target_mpc, output_should_stop_mpc = get_accel_from_plan(
self.v_desired_trajectory, self.a_desired_trajectory,
action_t=action_t, vEgoStopping=starpilot_toggles.vEgoStopping)
output_a_target_e2e = sm['modelV2'].action.desiredAcceleration
output_should_stop_e2e = sm['modelV2'].action.shouldStop
if self.mode == 'acc' or self.generation == 'v9':
output_a_target = output_a_target_mpc
output_should_stop = output_should_stop_mpc
else:
output_a_target = min(output_a_target_mpc, output_a_target_e2e)
output_should_stop = output_should_stop_e2e or output_should_stop_mpc
else:
output_a_target, output_should_stop = get_accel_from_plan(
output_a_target_mpc, output_should_stop_mpc = get_accel_from_plan(
self.v_desired_trajectory, self.a_desired_trajectory,
action_t=action_t, vEgoStopping=starpilot_toggles.vEgoStopping)
if bool(getattr(starpilot_toggles, "hybrid_experimental_mode", False)):
output_a_target = output_a_target_mpc
output_should_stop = output_should_stop_mpc
elif tinygrad_model and self.mode != 'acc' and self.generation != 'v9':
output_a_target_e2e = sm['modelV2'].action.desiredAcceleration
output_should_stop_e2e = sm['modelV2'].action.shouldStop
output_a_target = min(output_a_target_mpc, output_a_target_e2e)
output_should_stop = output_should_stop_e2e or output_should_stop_mpc
self._log_hem_status(now_t, False, scene_v_ego, output_a_target_mpc, output_a_target_e2e, output_a_target)
else:
output_a_target = output_a_target_mpc
output_should_stop = output_should_stop_mpc
self._log_hem_status(now_t, False, scene_v_ego, output_a_target_mpc, float('nan'), output_a_target)
comfort_output_accel_min = get_vehicle_min_accel(self.CP, v_ego) if experimental_mlsim else accel_limits_turns[0]
vision_cap_accel_min = min(comfort_output_accel_min, get_vehicle_min_accel(self.CP, v_ego))
output_accel_min = comfort_output_accel_min
@@ -2910,8 +2973,56 @@ class LongitudinalPlanner:
if force_slow_decel and scene_v_ego > 0.1:
output_a_target = min(output_a_target, FORCE_DECEL_MIN_ACCEL)
self.output_a_target = output_a_target
self.output_should_stop = bool(output_should_stop or vision_low_speed_stop_active)
a_chill_final = output_a_target
should_stop_chill = bool(output_should_stop or vision_low_speed_stop_active)
if bool(getattr(starpilot_toggles, "hybrid_experimental_mode", False)):
self.hybrid_controller.set_tuning(
getattr(starpilot_toggles, "hybrid_exp_bias", 0.0),
getattr(starpilot_toggles, "hybrid_vision_brake_sensitivity", 1.0),
)
a_exp_raw = float(sm['modelV2'].action.desiredAcceleration)
should_stop_exp = bool(sm['modelV2'].action.shouldStop)
# Pass the active lead that MPC is tracking (leadTwo when source == "lead1")
# so HEM is never blind to the radar lead actually being followed.
active_lead = self.lead_two if self.mpc.source == "lead1" else self.lead_one
a_fused, should_stop_fused = self.hybrid_controller.update(
v_ego=scene_v_ego,
v_cruise=v_cruise,
lead_one=active_lead,
model_v2=sm['modelV2'],
a_chill=a_chill_final,
a_exp=a_exp_raw,
should_stop_exp=should_stop_exp,
should_stop_chill=should_stop_chill,
gas_pressed=bool(getattr(sm['carState'], 'gasPressed', False)),
)
# HEM output can never exceed the physical
# vehicle acceleration envelope (same bounds as the non-hybrid path) or a
# per-frame jerk slew from the previously commanded target, regardless of
# tuning bias.
a_fused = float(np.clip(a_fused, output_accel_min, output_accel_max))
if not np.isfinite(a_fused):
a_fused = float(a_chill_final)
max_jerk_accel = float(getattr(sm['starpilotPlan'], 'accelerationJerk', 1.0)) * 3.0
max_jerk_brake = 4.0
max_delta_up = max_jerk_accel * self.dt
max_delta_down = max_jerk_brake * self.dt
prev_target = float(prev_output_a_target)
a_fused = float(np.clip(a_fused, prev_target - max_delta_down, prev_target + max_delta_up))
self.output_a_target = a_fused
self.output_should_stop = should_stop_fused
self._log_hem_status(now_t, True, scene_v_ego, a_chill_final, a_exp_raw, a_fused)
self._publish_hem_status(now_t)
else:
self.output_a_target = a_chill_final
self.output_should_stop = should_stop_chill
def publish(self, sm, pm):
plan_send = messaging.new_message('longitudinalPlan')
@@ -514,6 +514,48 @@ def test_experimental_mlsim_uses_vehicle_min_accel_floor(model_version):
assert planner.output_a_target < comfort_min_accel
def test_hybrid_mode_tempers_unconfirmed_vision_braking_with_chill():
v_ego = 20.0
desired_accel = -2.0
CP = CarInterface.get_non_essential_params(CAR.HONDA_CIVIC)
# HEM is the final arbitrator: on open road (no lead, model not predicting a
# stop) it blends the raw E2E brake with the conservative Chill target rather
# than passing the full -2.0 straight through, but it still brakes (no creep).
hybrid_toggles = SimpleNamespace(**vars(make_toggles("v11")), hybrid_experimental_mode=True)
planner_hybrid = LongitudinalPlanner(CP, init_v=v_ego)
sm = make_sm(v_ego, desired_accel, -2.0, experimental_mode=False)
planner_hybrid.update(sm, hybrid_toggles)
assert planner_hybrid.mode == "acc"
assert planner_hybrid.output_a_target < 0.0, "HEM must still brake on a strong Exp decel"
assert planner_hybrid.output_a_target > desired_accel, "HEM must temper the raw E2E brake with Chill"
# Without HEM, experimental mode lets the raw E2E target through.
planner_exp = LongitudinalPlanner(CP, init_v=v_ego)
sm_exp = make_sm(v_ego, desired_accel, -2.0, experimental_mode=True)
planner_exp.update(sm_exp, make_toggles("v11"))
assert planner_exp.output_a_target <= -1.5
def test_hybrid_mode_arbitrates_vision_braking_with_lead():
v_ego = 20.0
CP = CarInterface.get_non_essential_params(CAR.HONDA_CIVIC)
hybrid_toggles = SimpleNamespace(**vars(make_toggles("v11")), hybrid_experimental_mode=True)
# With a tracked lead, the hybrid grants full E2E braking authority.
planner = LongitudinalPlanner(CP, init_v=v_ego)
sm = make_sm(
v_ego,
desired_accel=-2.0,
min_accel=-2.0,
experimental_mode=False,
tracking_lead=True,
lead_one=make_lead(status=True, d_rel=30.0, v_lead=10.0),
)
planner.update(sm, hybrid_toggles)
assert planner.output_a_target < 0.0
def test_gm_pedal_vehicle_min_accel_uses_brand_when_car_name_is_missing():
CP = SimpleNamespace(
carName=None,
@@ -0,0 +1,284 @@
#!/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
@@ -175,6 +175,21 @@ def test_lateral_resume_delay_ignores_signal_cycles_that_never_slow_enough(monke
planner.shutdown()
def test_hybrid_mode_keeps_cem_detector_warm_with_modes_off(monkeypatch):
planner = make_planner(monkeypatch)
monkeypatch.setattr(planner.starpilot_cem, "update", lambda *args, **kwargs: None)
try:
toggles = make_toggles(hybrid_experimental_mode=True)
planner.update(0.0, False, make_sm(planner, frame=1, v_ego=20.0, left_blinker=False), toggles)
assert planner.starpilot_ccm.experimental_mode is False
assert planner.starpilot_cem.experimental_mode is False
finally:
planner.shutdown()
def test_radarless_follow_hold_applies_to_tracked_vision_lead(monkeypatch):
planner = StarPilotPlanner(Path("/tmp/nonexistent"), DummyThemeManager())
+32
View File
@@ -11,6 +11,7 @@ AOL_COLOR = rl.Color(10, 186, 181, 255)
ENGAGED_COLOR = rl.Color(22, 127, 64, 255)
OVERRIDE_COLOR = rl.Color(137, 146, 141, 255)
EXPERIMENTAL_COLOR = rl.Color(218, 111, 37, 255)
HYBRID_EXPERIMENTAL_COLOR = rl.Color(0, 153, 255, 255)
CEM_OVERRIDE_COLOR = rl.Color(255, 214, 0, 255)
SWITCHBACK_COLOR = rl.Color(139, 108, 197, 255)
TRAFFIC_COLOR = rl.Color(201, 34, 49, 255)
@@ -27,6 +28,31 @@ def is_longitudinal_only_active(state: UIState) -> bool:
return bool(state.sm["selfdriveState"].enabled and not car_control.latActive)
def _is_hybrid_experimental_mode(state: UIState) -> bool:
"""True when HEM (hybrid experimental) is the active longitudinal mode."""
return bool(state.starpilot_toggles.get("hybrid_experimental_mode", False))
def _hem_exp_dominant(state: UIState) -> bool:
"""True when the fused output tracks the E2E/vision input more than chill ACC.
Uses the planner's per-frame comparison (HEMExpDominant), which reflects the
active fusion regime rather than a raw vision weight.
"""
params_memory = getattr(state, "params_memory", None)
if params_memory is None:
return False
try:
return bool(int(params_memory.get("HEMExpDominant") or 0))
except (TypeError, ValueError):
return False
def _hem_border_color(state: UIState) -> rl.Color:
"""Blue when chill dominates the fusion, orange when experimental/vision dominates (like CEM)."""
return EXPERIMENTAL_COLOR if _hem_exp_dominant(state) else HYBRID_EXPERIMENTAL_COLOR
def _override_color_applies(state: UIState) -> bool:
"""Only gray the status when the active control mode is being overridden."""
if state.status != UIStatus.OVERRIDE:
@@ -57,6 +83,8 @@ def get_border_color(state: UIState):
if state.always_on_lateral_active:
return AOL_COLOR
# Only color the border for CEM/experimental while actually enabled.
if enabled and _is_hybrid_experimental_mode(state):
return _hem_border_color(state)
if enabled and state.conditional_status in CEM_DISABLED_OVERRIDE_STATUSES:
return CEM_OVERRIDE_COLOR
if enabled and state.sm["selfdriveState"].experimentalMode:
@@ -67,6 +95,8 @@ def get_border_color(state: UIState):
def get_path_edge_color(state: UIState):
if state.sm["selfdriveState"].enabled and _is_hybrid_experimental_mode(state):
return _hem_border_color(state)
if state.conditional_status in CEM_ACTIVE_STATUSES:
return EXPERIMENTAL_COLOR
return get_border_color(state)
@@ -85,6 +115,8 @@ def get_screen_edge_color(state: UIState):
return AOL_COLOR
# Keep the screen edge disengaged-blue when experimental mode is only the
# requested longitudinal mode, not the active driving state.
if enabled and _is_hybrid_experimental_mode(state):
return _hem_border_color(state)
if enabled and state.conditional_status in CEM_DISABLED_OVERRIDE_STATUSES:
return CEM_OVERRIDE_COLOR
if enabled and state.sm["selfdriveState"].experimentalMode:
+36 -1
View File
@@ -3,16 +3,20 @@ from types import SimpleNamespace
from openpilot.selfdrive.ui.lib.starpilot_status import (
DISENGAGED_COLOR,
ENGAGED_COLOR,
EXPERIMENTAL_COLOR,
HYBRID_EXPERIMENTAL_COLOR,
LONGITUDINAL_ONLY_COLOR,
AOL_COLOR,
OVERRIDE_COLOR,
get_border_color,
get_path_edge_color,
get_screen_edge_color,
)
from openpilot.selfdrive.ui.ui_state import UIStatus
def _state(*, enabled=False, lat_active=False, aol=False, status=None, events=()):
def _state(*, enabled=False, lat_active=False, aol=False, status=None, events=(), hybrid=False, hem_exp_dominant=None):
params_memory = {"HEMExpDominant": b"1"} if hem_exp_dominant else {}
return SimpleNamespace(
sm={
"selfdriveState": SimpleNamespace(enabled=enabled, experimentalMode=False),
@@ -24,6 +28,8 @@ def _state(*, enabled=False, lat_active=False, aol=False, status=None, events=()
switchback_mode_enabled=False,
traffic_mode_enabled=False,
conditional_status=0,
starpilot_toggles={"hybrid_experimental_mode": hybrid},
params_memory=SimpleNamespace(get=lambda key, default=None: params_memory.get(key, default)),
)
@@ -44,6 +50,35 @@ def test_lateral_active_colors_remain_unchanged():
assert _rgb(get_border_color(_state())) == _rgb(DISENGAGED_COLOR)
def test_hybrid_experimental_mode_uses_blue_border():
state = _state(enabled=True, lat_active=True, hybrid=True)
assert _rgb(get_border_color(state)) == _rgb(HYBRID_EXPERIMENTAL_COLOR)
assert _rgb(get_screen_edge_color(state)) == _rgb(HYBRID_EXPERIMENTAL_COLOR)
assert _rgb(get_path_edge_color(state)) == _rgb(HYBRID_EXPERIMENTAL_COLOR)
def test_hybrid_experimental_mode_uses_orange_when_exp_dominates():
state = _state(enabled=True, lat_active=True, hybrid=True, hem_exp_dominant=True)
assert _rgb(get_border_color(state)) == _rgb(EXPERIMENTAL_COLOR)
assert _rgb(get_screen_edge_color(state)) == _rgb(EXPERIMENTAL_COLOR)
assert _rgb(get_path_edge_color(state)) == _rgb(EXPERIMENTAL_COLOR)
def test_hybrid_experimental_mode_keeps_blue_when_chill_dominates():
state = _state(enabled=True, lat_active=True, hybrid=True, hem_exp_dominant=False)
assert _rgb(get_border_color(state)) == _rgb(HYBRID_EXPERIMENTAL_COLOR)
assert _rgb(get_screen_edge_color(state)) == _rgb(HYBRID_EXPERIMENTAL_COLOR)
assert _rgb(get_path_edge_color(state)) == _rgb(HYBRID_EXPERIMENTAL_COLOR)
def test_hybrid_experimental_mode_requires_enabled():
assert _rgb(get_border_color(_state(hybrid=True))) == _rgb(DISENGAGED_COLOR)
assert _rgb(get_border_color(_state(enabled=True, lat_active=True))) == _rgb(ENGAGED_COLOR)
def test_override_color_matches_active_control_mode():
lateral_override = SimpleNamespace(overrideLateral=True, overrideLongitudinal=False)
longitudinal_override = SimpleNamespace(overrideLateral=False, overrideLongitudinal=True)
@@ -2029,6 +2029,44 @@
"parent_key": "ConditionalChill",
"settings_tier": "advanced"
},
{
"key": "HybridExperimental",
"label": "Hybrid Experimental Mode",
"description": "Fuse the throttle response of chill ACC with the model's early, natural vision braking using a single continuous controller. Chill ACC always provides the hard safety distance, while the E2E trajectory is blended in for early stops. Unconfirmed vision braking is capped to a gentle coast, and vision is never allowed to under-brake.",
"picker_description": "Continuously blends Chill ACC with the model's vision braking.",
"data_type": "bool",
"ui_type": "toggle",
"is_parent_toggle": true,
"settings_tier": "advanced"
},
{
"key": "HybridExpBias",
"label": "E2E Authority Bias",
"description": "Biases the continuous hybrid between Chill and E2E. Negative favors Chill throttle and braking, positive favors E2E vision behavior. Range -1.0 to 1.0, default 0.0.",
"picker_description": "Biases authority between Chill and E2E vision.",
"data_type": "float",
"ui_type": "numeric",
"min": -1.0,
"max": 1.0,
"step": 0.05,
"precision": 2,
"parent_key": "HybridExperimental",
"settings_tier": "advanced"
},
{
"key": "HybridVisionBrakeSensitivity",
"label": "Vision Brake Sensitivity",
"description": "Scales how strongly the model's predicted stop horizon contributes to early braking. Below 1.0 reduces unprompted slowing; above 1.0 starts red-light and queue braking earlier. Range 0.0 to 2.0, default 1.0.",
"picker_description": "Scales early vision braking authority.",
"data_type": "float",
"ui_type": "numeric",
"min": 0.0,
"max": 2.0,
"step": 0.05,
"precision": 2,
"parent_key": "HybridExperimental",
"settings_tier": "advanced"
},
{
"key": "SLCAbbreviatedSources",
"label": "Show Abbreviated Icon Sources",
+1
View File
@@ -112,6 +112,7 @@ SAFE_MODE_MANAGED_KEYS = (
"ReduceLateralAccelerationSnow",
"ConditionalExperimental",
"ConditionalChill",
"HybridExperimental",
"CECurves",
"CECurvesLead",
"CELead",
+23 -2
View File
@@ -377,6 +377,19 @@ def speed_limit_controller_available(openpilot_longitudinal: bool, redneck_cruis
return openpilot_longitudinal or redneck_cruise
def get_longitudinal_modes(openpilot_longitudinal: bool, cem: bool, ccm: bool, hybrid: bool) -> tuple[bool, bool, bool]:
"""Resolve the mutually exclusive longitudinal control modes (CEM/CCM/Hybrid)."""
conditional_experimental_mode = bool(openpilot_longitudinal) and bool(cem)
conditional_chill_mode = bool(openpilot_longitudinal) and not conditional_experimental_mode and bool(ccm)
hybrid_experimental_mode = (
bool(openpilot_longitudinal) and
not conditional_experimental_mode and
not conditional_chill_mode and
bool(hybrid)
)
return conditional_experimental_mode, conditional_chill_mode, hybrid_experimental_mode
def migrate_cancel_button_controls(params: Params | None = None) -> bool:
params = params or Params(return_defaults=True)
if params.get_bool(CANCEL_BUTTON_MIGRATION_KEY) or not params.get_bool("RemapCancelToDistance"):
@@ -812,8 +825,16 @@ class StarPilotVariables:
self.migrate_prius_cluster_offset(str(toggle.car_model))
toggle.cluster_offset = self.get_value("ClusterOffset", cast=float, condition=toggle.car_make == "toyota")
toggle.conditional_experimental_mode = toggle.openpilot_longitudinal and self.get_value("ConditionalExperimental")
toggle.conditional_chill_mode = toggle.openpilot_longitudinal and not toggle.conditional_experimental_mode and self.get_value("ConditionalChill")
toggle.conditional_experimental_mode, toggle.conditional_chill_mode, toggle.hybrid_experimental_mode = get_longitudinal_modes(
toggle.openpilot_longitudinal,
self.get_value("ConditionalExperimental"),
self.get_value("ConditionalChill"),
self.get_value("HybridExperimental"),
)
toggle.hybrid_exp_bias = self.get_value(
"HybridExpBias", cast=float, condition=toggle.hybrid_experimental_mode, default=0.0, min=-1.0, max=1.0)
toggle.hybrid_vision_brake_sensitivity = self.get_value(
"HybridVisionBrakeSensitivity", cast=float, condition=toggle.hybrid_experimental_mode, default=1.0, min=0.0, max=2.0)
toggle.conditional_curves = self.get_value("CECurves", condition=toggle.conditional_experimental_mode)
toggle.conditional_curves_lead = self.get_value("CECurvesLead", condition=toggle.conditional_curves)
toggle.conditional_lead = self.get_value("CELead", condition=toggle.conditional_experimental_mode)
@@ -227,6 +227,18 @@ def test_missing_bounded_value_uses_explicit_default():
assert value == 1.0
def test_get_longitudinal_modes_is_mutually_exclusive():
# Hybrid wins when selected and CEM/CCM params are off.
assert spv.get_longitudinal_modes(True, False, False, True) == (False, False, True)
# CEM takes priority over the other modes when enabled.
assert spv.get_longitudinal_modes(True, True, True, True) == (True, False, False)
# CCM engages only when CEM is off.
assert spv.get_longitudinal_modes(True, False, True, False) == (False, True, False)
assert spv.get_longitudinal_modes(True, False, True, True) == (False, True, False)
# Nothing engages without openpilot longitudinal.
assert spv.get_longitudinal_modes(False, True, True, True) == (False, False, False)
def test_disabled_conditional_experimental_toggles_are_off(monkeypatch, tmp_path):
params_cls = spv.Params
@@ -0,0 +1,153 @@
#!/usr/bin/env python3
import math
from openpilot.common.realtime import DT_MDL
def lerp(a: float, b: float, t: float) -> float:
"""Linear interpolation / blend between a and b by weight t (0.0 to 1.0)."""
return (1.0 - t) * a + t * b
def clamp(val: float, low: float, high: float) -> float:
return max(low, min(high, val))
class HybridExperimentalMode:
"""
1. Open Road: Follows Chill MPC cruise & radar headway.
2. Slower Lead Approach: Blends smooth vision decel with MPC follow distance.
3. Stop Signs / Red Lights: Pure vision stopping authority (locks out positive cruise throttle).
4. Standstill / Stop Completion: Latches should_stop for LongControl mechanical brake hold.
5. Green Light / Lead Depart: Instant release back to Chill cruise acceleration.
"""
def __init__(self):
self.DT = DT_MDL
self.w_vision = 0.0
self.prev_a_target = 0.0
self.last_exp_dominant = False
self.diag = {}
self.record_diag = False
# Tunings
self.HYBRID_EXP_BIAS = 0.0
self.VISION_BRAKE_SENSITIVITY = 1.0
def reset(self, a_ego: float = 0.0):
"""Seed target with actual vehicle acceleration on engagement to prevent torque bumps."""
self.prev_a_target = float(a_ego) if math.isfinite(a_ego) else 0.0
self.w_vision = 0.0
self.last_exp_dominant = False
self.diag = {}
def set_tuning(self, exp_bias: float, vision_brake_sensitivity: float):
self.HYBRID_EXP_BIAS = clamp(exp_bias, -1.0, 1.0)
self.VISION_BRAKE_SENSITIVITY = clamp(vision_brake_sensitivity, 0.0, 2.0)
def update(self, v_ego, v_cruise, lead_one, model_v2, a_chill, a_exp,
should_stop_exp=False, should_stop_chill=False, gas_pressed=False):
# 0. Sanitize inputs
if not math.isfinite(a_chill):
a_chill = self.prev_a_target
if not math.isfinite(a_exp):
a_exp = a_chill
# 1. Trajectory Analysis
traj_v = getattr(getattr(model_v2, "velocity", None), "x", None)
has_full_trajectory = bool(traj_v and len(traj_v) >= 24 and all(math.isfinite(v) for v in traj_v))
if has_full_trajectory:
v_horizon = float(traj_v[-1])
v_short = float(traj_v[23]) # ~4.0s lookahead
v_min = float(min(traj_v))
else:
v_horizon = v_short = v_min = float(v_ego)
lead_status = bool(getattr(lead_one, "status", False))
lead_v = float(getattr(lead_one, "vLead", 0.0))
lead_d = float(getattr(lead_one, "dRel", 150.0))
# 2. Vision Departure / Driver Override Detection (Priority Check)
at_standstill = v_ego < 0.8
lead_departing = at_standstill and lead_status and (lead_v > 0.6) and ((lead_v - v_ego) > 0.4)
vision_departing = at_standstill and (v_horizon > 1.5) and (a_exp > 0.15)
driver_override = bool(gas_pressed)
is_departing = lead_departing or vision_departing or driver_override
# 3. Vision Stop & Decel Detection
horizon_stopping = not is_departing and (
(v_horizon < 0.8) or (has_full_trajectory and v_short < 1.5) or should_stop_exp
)
speed_drop_ratio = max(0.0, (v_ego - v_min) / max(v_ego, 2.0))
model_decel_strength = max(0.0, -a_exp * 0.5)
if is_departing:
raw_vision_metric = 0.0
w_target = 0.0
self.w_vision = 0.0
else:
if horizon_stopping:
raw_vision_metric = 1.0
elif lead_status and lead_d < 40.0:
raw_vision_metric = max(speed_drop_ratio, model_decel_strength)
else:
raw_vision_metric = max(speed_drop_ratio, model_decel_strength) * 0.5
w_target = clamp(raw_vision_metric * self.VISION_BRAKE_SENSITIVITY, 0.0, 1.0)
# 4. Dynamic Filter (Fast Attack, Smooth Decay)
if w_target > self.w_vision:
self.w_vision = min(1.0, self.w_vision + 0.15)
else:
self.w_vision = max(0.0, self.w_vision - 0.04)
# 5. Dual-Regime Fusion
a_brake_fused = min(a_chill, a_exp)
a_throttle_fused = a_chill + max(0.0, a_exp - a_chill) * self.HYBRID_EXP_BIAS
# Output Arbitration
is_stopping_event = (self.w_vision > 0.3) or horizon_stopping
self.last_exp_dominant = bool(is_stopping_event and not is_departing)
if self.last_exp_dominant:
a_out = a_brake_fused
else:
a_out = lerp(a_throttle_fused, a_brake_fused, self.w_vision)
# 6. Authoritative Standstill Handshake
standstill_intent = not is_departing and (v_ego < 0.5 and (v_horizon < 0.4 or should_stop_exp))
should_stop_fused = bool(should_stop_chill or (not is_departing and (should_stop_exp or standstill_intent)))
if self.record_diag:
self.diag = {
"v_ego": v_ego, "v_cruise": v_cruise,
"a_chill": a_chill, "a_exp": a_exp,
"should_stop_chill": should_stop_chill,
"should_stop_exp": should_stop_exp,
"lead_status": lead_status, "lead_d_rel": lead_d, "lead_v_lead": lead_v,
"has_full_trajectory": has_full_trajectory,
"v_horizon": v_horizon, "v_short": v_short, "v_min": v_min,
"speed_drop_ratio": speed_drop_ratio,
"model_decel_strength": model_decel_strength,
"raw_vision_metric": raw_vision_metric, "w_target": w_target,
"w_vision": self.w_vision,
"lead_departing": lead_departing,
"vision_departing": vision_departing,
"driver_override": driver_override,
"is_departing": is_departing,
"horizon_stopping": horizon_stopping,
"a_brake_fused": a_brake_fused,
"a_throttle_fused": a_throttle_fused,
"is_stopping_event": is_stopping_event,
"exp_dominant": self.last_exp_dominant,
"standstill_intent": standstill_intent,
"should_stop_fused": should_stop_fused,
"a_out": a_out,
"regime": "brake" if self.last_exp_dominant else "throttle",
"standstill": standstill_intent,
}
self.prev_a_target = a_out
return a_out, should_stop_fused
+3
View File
@@ -123,6 +123,9 @@ class StarPilotCard:
if getattr(starpilot_toggles, "safe_mode", False):
return
if getattr(starpilot_toggles, "hybrid_experimental_mode", False):
return
if starpilot_toggles.conditional_experimental_mode:
current_status = self.params_memory.get_int("CEStatus", default=CEStatus["OFF"])
override_value = next_manual_ce_status(current_status, sm["selfdriveState"].experimentalMode)
+5 -1
View File
@@ -220,7 +220,11 @@ class StarPilotPlanner:
self.starpilot_following.update(controls_enabled, v_ego, sm, starpilot_toggles)
conditional_tracking_active = controls_enabled or sm["starpilotCarState"].alwaysOnLateralEnabled
if conditional_tracking_active and bool(getattr(starpilot_toggles, "conditional_experimental_mode", False)):
if conditional_tracking_active and bool(getattr(starpilot_toggles, "hybrid_experimental_mode", False)):
self.starpilot_cem.update(v_ego, sm, starpilot_toggles, v_cruise)
self.starpilot_ccm.experimental_mode = False
self.starpilot_cem.experimental_mode = False
elif conditional_tracking_active and bool(getattr(starpilot_toggles, "conditional_experimental_mode", False)):
# Keep CEM's filters warm in AOL so engagement can inherit the current scene.
self.starpilot_cem.update(v_ego, sm, starpilot_toggles, v_cruise)
self.starpilot_ccm.experimental_mode = True
@@ -0,0 +1,336 @@
#!/usr/bin/env python3
import numpy as np
import pytest
from openpilot.starpilot.controls.lib.hybrid_experimental_mode import (
HybridExperimentalMode,
lerp,
)
class FakeLead:
def __init__(self, status=False, d_rel=150.0, v_lead=0.0):
self.status = status
self.dRel = float(d_rel)
self.vLead = float(v_lead)
class FakeModel:
def __init__(self, velocity=None, position=None):
self.velocity = type("Velocity", (), {"x": list(velocity)})() if velocity is not None else None
self.position = type("Position", (), {"x": list(position)})() if position is not None else None
def make_controller(prev=0.0):
controller = HybridExperimentalMode()
controller.reset(prev)
return controller
def run(controller, *, v_ego=20.0, v_cruise=30.0, lead=None, model=None, a_chill=0.5, a_exp=0.8, frames=80):
lead = lead if lead is not None else FakeLead()
if model is None:
model = FakeModel(velocity=[v_ego] * 33, position=list(np.linspace(0.0, 100.0, 33)))
result = 0.0
should_stop = False
for _ in range(frames):
result, should_stop = controller.update(v_ego, v_cruise, lead, model, a_chill, a_exp)
return result, should_stop
def test_update_returns_accel_and_should_stop_tuple():
controller = make_controller()
result = controller.update(20.0, 30.0, FakeLead(), FakeModel(velocity=[20.0] * 33), 0.5, 0.5)
assert isinstance(result, tuple) and len(result) == 2
assert isinstance(result[0], float)
assert isinstance(result[1], bool)
def test_open_road_throttle_follows_chill_without_bias():
# On open road with default bias, HEM yields to pure Chill MPC cruise.
controller = make_controller()
a, _ = run(controller, a_chill=0.5, a_exp=0.8)
assert a == pytest.approx(0.5, abs=1e-3)
def test_throttle_uses_exp_bias():
controller = make_controller()
controller.set_tuning(0.5, 1.0)
a, _ = run(controller, a_chill=0.5, a_exp=0.8)
expected = 0.5 + max(0.0, 0.8 - 0.5) * 0.5
assert a == pytest.approx(expected, abs=1e-3)
assert a > 0.5
def test_green_light_departure_from_standstill():
controller = make_controller(prev=-0.5)
controller.set_tuning(0.5, 1.0)
traj_v = np.linspace(0.0, 12.0, 33)
traj_x = np.linspace(0.0, 45.0, 33)
model = FakeModel(velocity=traj_v, position=traj_x)
a, should_stop = run(controller, v_ego=0.0, v_cruise=20.0, model=model, a_chill=1.0, a_exp=1.5, frames=40)
assert a > 1.0, f"Vehicle should depart promptly on green light, got {a}"
assert not should_stop
def test_standstill_hold_at_red_light_without_lead():
controller = make_controller(prev=0.0)
traj_v = np.zeros(33)
traj_x = np.zeros(33)
model = FakeModel(velocity=traj_v, position=traj_x)
a, should_stop = run(controller, v_ego=0.0, v_cruise=25.0, lead=FakeLead(status=False),
model=model, a_chill=0.2, a_exp=-1.0, frames=40)
assert a <= -0.4, f"Standstill brake must hold even when Chill cruise wants to go, got {a}"
assert should_stop, "Standstill at a predicted stop must assert should_stop"
def test_red_light_high_speed_approach_braking():
controller = make_controller(prev=0.0)
traj_v = np.linspace(25.0, 0.0, 33)
traj_x = np.linspace(0.0, 60.0, 33)
model = FakeModel(velocity=traj_v, position=traj_x)
a, _ = run(controller, v_ego=25.0, v_cruise=25.0, model=model, a_chill=0.5, a_exp=-2.0, frames=60)
assert a <= -2.0, f"Stopping deceleration should be fully honored without dilution, got {a}"
def test_red_light_low_speed_roll_prevent_dilution():
controller = make_controller(prev=-0.5)
traj_v = np.linspace(0.8, 0.0, 33)
traj_x = np.linspace(0.0, 3.0, 33)
model = FakeModel(velocity=traj_v, position=traj_x)
a, _ = run(controller, v_ego=0.8, v_cruise=20.0, model=model, a_chill=0.8, a_exp=-0.6, frames=30)
assert a <= -0.55, f"Vision stop clamp must enforce stopping bite at low speeds, got {a}"
def test_vision_stop_does_not_dilute_brake_with_cruise_throttle():
controller = make_controller(prev=0.0)
model = FakeModel(velocity=list(np.linspace(20.0, 0.0, 33)))
a, _ = run(controller, v_ego=20.0, v_cruise=20.0, lead=FakeLead(status=False),
model=model, a_chill=0.8, a_exp=-2.5, frames=20)
assert a <= -2.5, f"Cruise throttle must be locked out during a vision stop, got {a}"
def test_missing_or_corrupt_model_v2_fallbacks():
controller = make_controller()
model_none = FakeModel(velocity=None, position=None)
a, _ = run(controller, lead=FakeLead(status=False), model=model_none, a_chill=0.5, a_exp=0.2)
assert a > 0.35, "Controller should gracefully fallback when trajectory is None"
model_empty = FakeModel(velocity=[], position=[])
a, _ = run(controller, lead=FakeLead(status=False), model=model_empty, a_chill=0.6, a_exp=0.7)
assert a > 0.55, "Controller should gracefully handle empty trajectory lists"
def test_nan_a_exp_falls_back_to_chill_channel():
controller = make_controller(prev=0.0)
model = FakeModel(velocity=[20.0] * 33, position=list(np.linspace(0.0, 100.0, 33)))
a, _ = run(controller, model=model, a_chill=0.5, a_exp=float("nan"), frames=10)
assert np.isfinite(a), f"NaN a_exp must never propagate, got {a}"
assert 0.4 <= a <= 0.6, f"NaN a_exp should degrade to the Chill channel, got {a}"
def test_inf_a_exp_does_not_propagate():
controller = make_controller(prev=0.0)
model = FakeModel(velocity=[20.0] * 33, position=list(np.linspace(0.0, 100.0, 33)))
for a_exp in (float("inf"), float("-inf")):
controller.reset(0.0)
a, _ = run(controller, model=model, a_chill=0.5, a_exp=a_exp, frames=10)
assert np.isfinite(a), f"Non-finite a_exp={a_exp} must not propagate, got {a}"
def test_nan_a_chill_falls_back_to_last_target():
controller = make_controller(prev=0.0)
model = FakeModel(velocity=[20.0] * 33, position=list(np.linspace(0.0, 100.0, 33)))
a, _ = run(controller, model=model, a_chill=float("nan"), a_exp=0.8, frames=10)
assert np.isfinite(a), f"NaN a_chill must never propagate, got {a}"
def test_nan_in_trajectory_does_not_propagate():
controller = make_controller(prev=0.0)
v = [20.0] * 20 + [float("nan")] + [20.0] * 12
model = FakeModel(velocity=v, position=list(np.linspace(0.0, 100.0, 33)))
a, _ = run(controller, model=model, a_chill=0.5, a_exp=0.8, frames=10)
assert np.isfinite(a), f"NaN trajectory must not propagate, got {a}"
def test_reset_rejects_non_finite_seed():
controller = make_controller(prev=-1.5)
controller.reset(float("nan"))
assert controller.prev_a_target == 0.0
def test_reset_seeds_active_acceleration():
controller = make_controller(prev=0.0)
controller.reset(a_ego=-1.8)
assert controller.prev_a_target == -1.8
def test_tuning_clamping():
controller = make_controller()
controller.set_tuning(5.0, 9.0)
assert controller.HYBRID_EXP_BIAS == 1.0
assert controller.VISION_BRAKE_SENSITIVITY == 2.0
controller.set_tuning(-5.0, -1.0)
assert controller.HYBRID_EXP_BIAS == -1.0
assert controller.VISION_BRAKE_SENSITIVITY == 0.0
def test_slower_lead_closing_blends_vision_decel():
controller = make_controller(prev=0.0)
lead = FakeLead(status=True, d_rel=25.0, v_lead=5.0)
model = FakeModel(velocity=list(np.linspace(15.0, 8.0, 33)))
a, _ = run(controller, v_ego=15.0, v_cruise=20.0, lead=lead, model=model, a_chill=-0.4, a_exp=-0.6)
assert a < -0.3, f"Closing on a slower lead must brake, got {a}"
def test_cut_in_emergency_braking_preserves_chill_floor():
controller = make_controller(prev=0.0)
cut_in_lead = FakeLead(status=True, d_rel=6.0, v_lead=10.0)
model = FakeModel(velocity=[20.0] * 33)
for _ in range(8):
a, _ = controller.update(20.0, 25.0, cut_in_lead, model, a_chill=-3.2, a_exp=0.0)
assert a <= -2.5, f"Emergency cut-in should preserve the Chill brake floor, got {a}"
def test_no_hard_brake_on_gentle_high_horizon_slowdown():
# A gentle slowdown that KEEPS a high horizon speed (curve / slower traffic,
# ends at 15 m/s) must NOT trigger hard stop-to-zero braking.
controller = make_controller(prev=0.0)
traj_v = np.linspace(20.0, 15.0, 33)
traj_x = np.linspace(0.0, 100.0, 33)
model = FakeModel(velocity=traj_v, position=traj_x)
a, _ = run(controller, v_ego=20.0, v_cruise=20.0, lead=FakeLead(status=False),
model=model, a_chill=0.0, a_exp=-0.1, frames=5)
assert a > -0.5, f"Gentle high-horizon slowdown must not hard-brake, got {a}"
def test_lead_departure_releases_vision_latch():
controller = make_controller(prev=-1.0)
stop_model = FakeModel(velocity=np.zeros(33))
for _ in range(5):
controller.update(5.0, 25.0, FakeLead(status=False), stop_model, 0.0, -2.0)
assert controller.w_vision > 0.3
depart_model = FakeModel(velocity=[10.0] * 33)
lead = FakeLead(status=True, d_rel=30.0, v_lead=8.0)
a, should_stop = controller.update(0.3, 25.0, lead, depart_model, 0.5, 0.5)
assert controller.w_vision == 0.0, "Lead departure must release the vision latch instantly"
assert a > 0.0
assert not should_stop
def test_should_stop_chill_handshake():
controller = make_controller(prev=0.0)
model = FakeModel(velocity=[20.0] * 33)
a, should_stop = controller.update(20.0, 25.0, FakeLead(), model, 0.0, 0.0, should_stop_chill=True)
assert should_stop, "should_stop_chill must propagate into the fused stop flag"
def test_should_stop_exp_handshake():
controller = make_controller(prev=0.0)
model = FakeModel(velocity=[5.0] * 33)
a, should_stop = controller.update(5.0, 25.0, FakeLead(), model, 0.0, 0.0, should_stop_exp=True)
assert should_stop, "should_stop_exp must propagate into the fused stop flag"
def test_standstill_intent_asserts_should_stop():
controller = make_controller(prev=0.0)
model = FakeModel(velocity=np.zeros(33))
a, should_stop = controller.update(0.3, 20.0, FakeLead(), model, 0.0, 0.0)
assert should_stop, "Standstill intent at low speed must assert should_stop"
def test_no_should_stop_on_open_road():
controller = make_controller(prev=0.0)
model = FakeModel(velocity=[20.0] * 33)
a, should_stop = controller.update(20.0, 25.0, FakeLead(), model, 0.5, 0.5)
assert not should_stop, "Open road cruising must not assert should_stop"
def test_output_bounded_between_chill_and_exp():
# Defense-in-depth invariant: HEM output must never exceed the more aggressive of
# the two inputs, and can only dip below both via the intentional -0.6 standstill
# anchor (which keeps the car planted at a predicted stop). This guarantees HEM can
# never command something more aggressive than both Chill and Exp intended.
controller = make_controller(prev=0.0)
scenarios = [
(20.0, 0.5, 0.8), # both throttle
(20.0, 0.5, -2.0), # exp brakes, chill cruise
(20.0, -3.0, -0.5), # chill emergency, exp mild
(20.0, -1.0, -1.8), # both brake
(0.3, 0.8, -1.0), # standstill (anchor may engage)
(0.3, 0.5, 1.2), # departure
]
for v_ego, a_chill, a_exp in scenarios:
for traj in ([v_ego] * 33, list(np.linspace(max(v_ego, 1.0), 0.0, 33))):
controller.reset(0.0)
model = FakeModel(velocity=traj)
a, _ = controller.update(v_ego, 25.0, FakeLead(), model, a_chill, a_exp)
lo = min(a_chill, a_exp, -0.6)
hi = max(a_chill, a_exp)
assert lo - 1e-9 <= a <= hi + 1e-9, \
f"v_ego={v_ego} a_chill={a_chill} a_exp={a_exp} -> a_out={a} outside [{lo}, {hi}]"
# A positive Exp bias blends toward Exp but never overshoots the max of the two.
controller.set_tuning(0.5, 1.0)
controller.reset(0.0)
model = FakeModel(velocity=[20.0] * 33)
a, _ = controller.update(20.0, 25.0, FakeLead(), model, 0.5, 1.2)
assert 0.5 - 1e-9 <= a <= 1.2 + 1e-9
def test_dropped_trajectory_at_crawl_does_not_phantom_brake():
# Regression for the indexing fallback bug: if modelV2 drops frames or returns a
# too-short/corrupt trajectory at crawling speed (0.8 < v_ego < 1.5), the fallback
# must not fake a stop (v_short must only be trusted from a full trajectory).
for traj in ([], [1.2] * 5):
controller = make_controller(prev=0.0)
model = FakeModel(velocity=traj)
a, should_stop = controller.update(1.2, 20.0, FakeLead(), model, 0.3, 0.0)
assert a > 0.0, f"Short trajectory must not phantom-brake at crawl, got {a}"
assert not should_stop
def test_lead_departure_from_standstill_clears_anchor():
# Regression for the standstill deadlock: stopped behind a lead with v_horizon≈0,
# when the lead pulls away, departure must clear the anchor/latch immediately even
# before the vision horizon visually extends past the stop threshold.
controller = make_controller(prev=-1.0)
stop_model = FakeModel(velocity=np.zeros(33))
for _ in range(5):
controller.update(0.3, 25.0, FakeLead(), stop_model, 0.0, -1.0, should_stop_exp=True)
assert controller.w_vision > 0.3
lead = FakeLead(status=True, d_rel=6.0, v_lead=6.0) # lead accelerates away
depart_model = FakeModel(velocity=np.zeros(33)) # horizon still ~0 (not yet registered)
a, should_stop = controller.update(0.3, 25.0, lead, depart_model, 0.5, 0.3)
assert controller.w_vision == 0.0
assert not should_stop, "Lead departure must clear shouldStop despite v_horizon~=0"
assert a > 0.0, "Standstill anchor must not fight a departing lead"
def test_last_exp_dominant_true_when_vision_braking():
controller = make_controller(prev=0.0)
model = FakeModel(velocity=list(np.linspace(20.0, 0.2, 33)), position=list(np.linspace(0.0, 40.0, 33)))
run(controller, v_ego=20.0, v_cruise=25.0, lead=FakeLead(status=False),
model=model, a_chill=0.0, a_exp=-3.0, frames=60)
assert controller.last_exp_dominant
def test_last_exp_dominant_false_when_chill_brakes_for_lead():
controller = make_controller(prev=0.0)
lead = FakeLead(status=True, d_rel=5.0, v_lead=0.0)
model = FakeModel(velocity=[15.0] * 33, position=list(np.linspace(0.0, 100.0, 33)))
run(controller, v_ego=15.0, v_cruise=20.0, lead=lead, model=model, a_chill=-2.5, a_exp=1.0, frames=40)
assert not controller.last_exp_dominant
def test_last_exp_dominant_false_at_neutral_cruise():
controller = make_controller(prev=0.0)
run(controller, a_chill=0.0, a_exp=0.05, frames=10)
assert not controller.last_exp_dominant
@@ -193,6 +193,7 @@ export function TmuxLog() {
log: '',
selectorAction: null,
transport: "connecting",
manualStop: false,
});
const lifecycleVersion = ++tmuxLifecycleVersion;
let lifecycleTimer = null;
@@ -289,6 +290,12 @@ export function TmuxLog() {
return;
}
if (state.manualStop) {
stopLiveTransport();
state.transport = "stopped";
return;
}
if (!isTmuxRouteActive()) {
stopLiveTransport();
state.transport = "inactive";
@@ -328,11 +335,55 @@ export function TmuxLog() {
return "Tmux Live Log";
}
function togglePause() {
state.paused = !state.paused;
if (!state.paused) {
state.log = state.latest;
function clearLog() {
state.manualStop = false;
state.paused = false;
fetch("/api/tmux_log/clear", { method: "POST" })
.then(res => {
if (!res.ok) return res.text().then(msg => { throw new Error(msg); });
state.log = "";
state.latest = "";
showSnackbar("Log cleared — ready for a fresh run!", "success");
})
.catch(err => showSnackbar(`Clear failed: ${err.message}`, "error"));
}
function stopLog() {
// Freeze the displayed data and tear down the live transport so nothing more
// can overwrite the captured [HEM] log before the user copies it.
state.manualStop = true;
state.paused = true;
stopLiveTransport();
state.transport = "stopped";
showSnackbar("Log stopped — data frozen for copying.", "success");
}
function resumeLog() {
state.manualStop = false;
state.paused = false;
state.transport = "connecting";
updateTransportForLifecycle();
}
function copyLog() {
const text = state.log || "";
if (!text) {
showSnackbar("Nothing to copy yet.", "error");
return;
}
const done = ok => showSnackbar(ok ? `Copied ${text.length} chars to clipboard!` : "Copy failed.", ok ? "success" : "error");
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(text).then(() => done(true)).catch(() => done(false));
return;
}
const ta = document.createElement("textarea");
ta.value = text;
ta.style.position = "fixed";
ta.style.left = "-9999px";
document.body.appendChild(ta);
ta.select();
try { done(document.execCommand("copy")); } catch (e) { done(false); }
document.body.removeChild(ta);
}
function captureLog() {
@@ -384,11 +435,12 @@ export function TmuxLog() {
</div>
<div class="tmux-controls">
<button class="tmux-control-button" @click="${captureLog}">💾 Capture Log</button>
<button class="tmux-control-button" @click="${clearLog}">🧹 Clear Log</button>
<button class="tmux-control-button" @click="${() => state.manualStop ? resumeLog() : stopLog()}">${() => state.manualStop ? "▶️ Resume Log" : "⏹️ Stop Log"}</button>
<button class="tmux-control-button" @click="${copyLog}">📋 Copy Log</button>
<button class="tmux-control-button" @click="${captureLog}">💾 Save Log</button>
<button class="tmux-control-button" @click="${deleteSession}">🗑 Delete Log</button>
<button class="tmux-control-button" @click="${confirmDeleteAllSessions}">🧨 Delete All Logs</button>
<button class="tmux-control-button" @click="${downloadSessions}"> Download Log</button>
<button class="tmux-control-button" @click="${togglePause}">${() => state.paused ? "▶️ Resume Log" : "⏸️ Pause Log"}</button>
<button class="tmux-control-button" @click="${() => state.selectorAction = 'rename'}"> Rename Log</button>
</div>
+58 -12
View File
@@ -1208,6 +1208,37 @@ NAVIGATION_PERSISTED_LOCATION_MAX_AGE_SECONDS = 24 * 60 * 60
TMUX_LOGS_PATH = Path("/data/tmux_logs")
def _filter_hem_log_lines(output):
"""Reduce a tmux pane capture to only lines emitted by the [HEM] logger.
The Galaxy terminal is used to debug Hybrid Experimental Mode stopping, so only
[HEM] lines (which now carry a millisecond timestamp and full per-frame diag)
are surfaced; all other process output is dropped.
"""
if not output:
return ""
lines = [line for line in output.splitlines() if "[HEM]" in line]
return "\n".join(lines) + ("\n" if lines else "")
# Generous but bounded scrollback/capture depth for the [HEM] diagnostics. This
# is far beyond any stop-signal reproduction run (~20k lines ~= 16 min of HEM
# logging at 20 Hz) while keeping capture cost, tunnel bandwidth, and browser
# render work bounded so it cannot grow without limit or burden the device.
TMUX_HISTORY_LIMIT = 20000
TMUX_CAPTURE_LINES = 20000
def _ensure_tmux_session():
"""Create the comma session if needed, size it, and raise its scrollback limit
so the captured [HEM] window can hold the full run without overflow."""
if subprocess.run(["tmux", "has-session", "-t", "comma"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL).returncode != 0:
run_cmd(["tmux", "new-session", "-d", "-s", "comma", "-x", "240", "-y", "70", "bash"], "Started tmux session", "Failed to start tmux session")
else:
run_cmd(["tmux", "resize-window", "-t", "comma:0", "-x", "240", "-y", "70"], "Resized tmux window", "Failed to resize tmux window")
run_cmd(["tmux", "set-option", "-t", "comma:0", "history-limit", str(TMUX_HISTORY_LIMIT)], "Set tmux scrollback limit.", "Failed to set tmux scrollback limit.")
MODEL_DOWNLOAD_PARAM = "ModelToDownload"
MODEL_DOWNLOAD_ALL_PARAM = "DownloadAllModels"
MODEL_DOWNLOAD_PROGRESS_PARAM = "ModelDownloadProgress"
@@ -1523,6 +1554,7 @@ _TROUBLESHOOT_PERSONALITY_KEYS = [
_TROUBLESHOOT_CEM_KEYS = [
"ConditionalExperimental",
"HybridExperimental",
"CESpeed",
"CESpeedLead",
"CECurves",
@@ -5201,15 +5233,16 @@ def setup(app):
"updated": updated,
}), 200
if key in {"ConditionalExperimental", "ConditionalChill"}:
if key in {"ConditionalExperimental", "ConditionalChill", "HybridExperimental"}:
enabled = str_val.strip() in ("1", "true", "True")
params.put_bool(key, enabled)
updated = {key: enabled}
if enabled:
other_key = "ConditionalChill" if key == "ConditionalExperimental" else "ConditionalExperimental"
params.put_bool(other_key, False)
updated[other_key] = False
for other_key in ("ConditionalExperimental", "ConditionalChill", "HybridExperimental"):
if other_key != key:
params.put_bool(other_key, False)
updated[other_key] = False
update_starpilot_toggles()
return jsonify({
@@ -8321,16 +8354,15 @@ def setup(app):
@app.route("/api/tmux_log/live", methods=["GET"])
def stream_tmux_log():
if subprocess.run(["tmux", "has-session", "-t", "comma"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL).returncode != 0:
run_cmd(["tmux", "new-session", "-d", "-s", "comma", "-x", "240", "-y", "70", "bash"], "Started tmux session", "Failed to start tmux session")
else:
run_cmd(["tmux", "resize-window", "-t", "comma:0", "-x", "240", "-y", "70"], "Resized tmux window", "Failed to resize tmux window")
_ensure_tmux_session()
def generate():
last_output = ""
last_keepalive = 0.0
while True:
output = subprocess.check_output(["tmux", "capture-pane", "-t", "comma:0", "-p", "-S", "-1000"], text=True)
output = _filter_hem_log_lines(
subprocess.check_output(["tmux", "capture-pane", "-t", "comma:0", "-p", "-S", f"-{TMUX_CAPTURE_LINES}"], text=True)
)
if output != last_output:
yield "data: " + "\n".join(reversed(output.splitlines())).replace("\n", "\ndata: ") + "\n\n"
@@ -8350,19 +8382,33 @@ def setup(app):
@app.route("/api/tmux_log/snapshot", methods=["GET"])
def snapshot_tmux_log():
try:
output = subprocess.check_output(["tmux", "capture-pane", "-t", "comma:0", "-p", "-S", "-1000"], text=True)
output = subprocess.check_output(["tmux", "capture-pane", "-t", "comma:0", "-p", "-S", f"-{TMUX_CAPTURE_LINES}"], text=True)
except subprocess.CalledProcessError:
run_cmd(["tmux", "new-session", "-d", "-s", "comma", "-x", "240", "-y", "70", "bash"], "Started tmux session", "Failed to start tmux session")
output = subprocess.check_output(["tmux", "capture-pane", "-t", "comma:0", "-p", "-S", "-1000"], text=True)
_ensure_tmux_session()
output = subprocess.check_output(["tmux", "capture-pane", "-t", "comma:0", "-p", "-S", f"-{TMUX_CAPTURE_LINES}"], text=True)
except Exception as e:
return jsonify({"error": str(e)}), 500
output = _filter_hem_log_lines(output)
try:
live_text = "\n".join(reversed(output.splitlines()))
return jsonify({"data": live_text}), 200
except Exception as e:
return jsonify({"error": str(e)}), 500
@app.route("/api/tmux_log/clear", methods=["POST"])
def clear_tmux_log():
"""Start a clean slate for a fresh HEM test run: wipe tmux scrollback and the
visible pane so only newly-emitted [HEM] lines are captured."""
try:
_ensure_tmux_session()
subprocess.run(["tmux", "clear-history", "-t", "comma:0"], check=False)
subprocess.run(["tmux", "send-keys", "-t", "comma:0", "clear", "Enter"], check=False)
return jsonify({"message": "Log cleared!"}), 200
except Exception as e:
return jsonify({"error": str(e)}), 500
@app.route("/api/tmux_log/rename/<old>/<new>", methods=["PUT"])
def rename_tmux_log_path_params(old, new):
old_path = TMUX_LOGS_PATH / old
+6 -2
View File
@@ -10,7 +10,7 @@ from pathlib import Path
from cereal import log
from openpilot.common.util import sudo_read, sudo_write
from openpilot.common.gpio import gpio_set, gpio_init, get_irqs_for_action
from openpilot.system.hardware.base import HardwareBase, LPABase, ThermalConfig, ThermalZone
from openpilot.system.hardware.base import HardwareBase, LPABase, LPAError, ThermalConfig, ThermalZone
from openpilot.system.hardware.tici import iwlist
from openpilot.system.hardware.tici.esim import TiciLPA
from openpilot.system.hardware.tici.pins import GPIO
@@ -518,7 +518,11 @@ class Tici(HardwareBase):
# eSIM prime
dest = "/etc/NetworkManager/system-connections/esim.nmconnection"
if self.get_sim_lpa().is_comma_profile(sim_id) and not os.path.exists(dest):
try:
sim_has_comma_profile = self.get_sim_lpa().is_comma_profile(sim_id)
except LPAError:
sim_has_comma_profile = False
if sim_has_comma_profile and not os.path.exists(dest):
with open(Path(__file__).parent/'esim.nmconnection') as f, tempfile.NamedTemporaryFile(mode='w') as tf:
dat = f.read()
dat = dat.replace("sim-id=", f"sim-id={sim_id}")
+234
View File
@@ -0,0 +1,234 @@
#!/usr/bin/env python3
"""HEM Telemetry Exporter.
Extracts real telemetry from target segments and saves them to a portable JSON file,
capturing the raw model action intents, radar lead states, and planner controls.
"""
import os
import sys
import json
from pathlib import Path
# Add openpilot paths
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
from openpilot.tools.lib.logreader import LogReader, ReadMode
ROUTES_TO_EXPORT = [
("afb7ef2ed593d651/000000b3--9c3d58d585", 10, "Pure Exp Stop Baseline"),
("afb7ef2ed593d651/000000b3--9c3d58d585", 11, "Pure Exp Stop Baseline 2"),
("afb7ef2ed593d651/000000b3--9c3d58d585", 3, "Stop Sign"),
("afb7ef2ed593d651/000000b5--8ee86fef97", 0, "Stop Sign"),
("afb7ef2ed593d651/000000b7--9bfbaf247a", 1, "Stop Sign"),
("afb7ef2ed593d651/000000b9--976a3b50cb", 6, "Red Light"),
]
def serialize_lead(lead_msg):
if lead_msg is None:
return {"status": False, "dRel": 150.0, "vLead": 0.0, "aLeadK": 0.0, "yRel": 0.0, "radar": False}
return {
"status": bool(getattr(lead_msg, "status", False)),
"dRel": float(getattr(lead_msg, "dRel", 150.0)),
"vLead": float(getattr(lead_msg, "vLead", 0.0)),
"aLeadK": float(getattr(lead_msg, "aLeadK", 0.0)),
"yRel": float(getattr(lead_msg, "yRel", 0.0)),
"radar": bool(getattr(lead_msg, "radar", False)),
"modelProb": float(getattr(lead_msg, "modelProb", 0.0)),
}
def serialize_model(model_msg):
"""Extracts velocity, position, acceleration lists and raw action intents from modelV2."""
v_list = list(getattr(getattr(model_msg, "velocity", None), "x", []))
x_list = list(getattr(getattr(model_msg, "position", None), "x", []))
a_list = list(getattr(getattr(model_msg, "acceleration", None), "x", []))
action = getattr(model_msg, "action", None)
a_exp_raw = float(getattr(action, "desiredAcceleration", 0.0)) if action is not None else 0.0
should_stop_exp_raw = bool(getattr(action, "shouldStop", False)) if action is not None else False
return {
"velocity": v_list,
"position": x_list,
"acceleration": a_list,
"desiredAcceleration": a_exp_raw,
"shouldStop": should_stop_exp_raw,
}
def export_routes():
exported_data = {}
for route, seg, label in ROUTES_TO_EXPORT:
route_clean = route.replace("|", "/")
print(f"\nProcessing {route_clean} segment {seg} ({label})...")
local_paths = [
Path(f"/data/media/0/realdata/{route_clean}--{seg}"),
Path(os.path.expanduser(f"~/.comma/media/0/realdata/{route_clean}--{seg}")),
Path(f"./{route_clean}--{seg}"),
]
lr = None
for path in local_paths:
rlog_file = path / "rlog"
if rlog_file.exists():
lr = LogReader(str(rlog_file))
break
if lr is None:
dongle_id, log_id = route_clean.split("/", 1)
comma_id = f"{dongle_id}|{log_id}/{seg}"
try:
lr = LogReader(comma_id, default_mode=ReadMode.RLOG)
except Exception as e:
print(f"Failed to fetch {comma_id}: {e}")
continue
car_state_msgs = []
radar_state_msgs = []
model_msgs = []
splan_msgs = []
lplan_msgs = []
selfdrive_msgs = []
controls_state_msgs = []
car_control_msgs = []
try:
for msg in lr:
which = msg.which()
t = msg.logMonoTime * 1e-9
if which == "carState":
car_state_msgs.append((t, msg.carState))
elif which == "radarState":
radar_state_msgs.append((t, msg.radarState))
elif which == "modelV2":
model_msgs.append((t, msg.modelV2))
elif which == "starpilotPlan":
splan_msgs.append((t, msg.starpilotPlan))
elif which == "longitudinalPlan":
lplan_msgs.append((t, msg.longitudinalPlan))
elif which == "selfdriveState":
selfdrive_msgs.append((t, msg.selfdriveState))
elif which == "controlsState":
controls_state_msgs.append((t, msg.controlsState))
elif which == "carControl":
car_control_msgs.append((t, msg.carControl))
except Exception as e:
print(f"Error reading log: {e}")
continue
car_state_msgs.sort(key=lambda x: x[0])
radar_state_msgs.sort(key=lambda x: x[0])
model_msgs.sort(key=lambda x: x[0])
splan_msgs.sort(key=lambda x: x[0])
lplan_msgs.sort(key=lambda x: x[0])
selfdrive_msgs.sort(key=lambda x: x[0])
controls_state_msgs.sort(key=lambda x: x[0])
car_control_msgs.sort(key=lambda x: x[0])
frames = []
for t_model, model_msg in model_msgs:
cs = next((m for t, m in reversed(car_state_msgs) if t <= t_model), None)
rs = next((m for t, m in reversed(radar_state_msgs) if t <= t_model), None)
splan = next((m for t, m in reversed(splan_msgs) if t <= t_model), None)
lplan = next((m for t, m in reversed(lplan_msgs) if t <= t_model), None)
sd = next((m for t, m in reversed(selfdrive_msgs) if t <= t_model), None)
ctrl = next((m for t, m in reversed(controls_state_msgs) if t <= t_model), None)
cc = next((m for t, m in reversed(car_control_msgs) if t <= t_model), None)
if cs is None:
continue
enabled = False
state = 0
active = False
exp_mode = False
alert = ""
if sd is not None:
enabled = bool(sd.enabled)
state = int(sd.state.raw)
active = bool(sd.active)
exp_mode = bool(getattr(sd, "experimentalMode", False))
alert = str(getattr(sd, "alertText1", ""))
elif ctrl is not None:
enabled = bool(ctrl.enabled)
state = 2 if enabled else 0
active = enabled
if splan is not None and hasattr(splan, "experimentalMode"):
exp_mode = exp_mode or bool(splan.experimentalMode)
cc_enabled = bool(getattr(cc, "enabled", False))
cc_brake = 0.0
cc_accel = None
if cc is not None:
act = getattr(cc, "actuators", None)
if act is not None:
cc_brake = float(getattr(act, "brake", 0.0) or 0.0)
cc_accel = getattr(act, "accel", None)
op_braking = bool(cc_enabled and cc_brake > 0.05)
manual_brake = bool(cs.brakePressed) and not op_braking
# Lead tracks
lead_one_data = serialize_lead(getattr(rs, "leadOne", None))
lead_two_data = serialize_lead(getattr(rs, "leadTwo", None))
mpc_source = str(getattr(lplan, "longitudinalPlanSource", "lead0"))
# Raw model outputs
action = getattr(model_msg, "action", None)
a_exp_raw = float(getattr(action, "desiredAcceleration", 0.0)) if action is not None else 0.0
should_stop_exp_raw = bool(getattr(action, "shouldStop", False)) if action is not None else False
frames.append({
"t": t_model,
"v_ego": float(cs.vEgo),
"a_ego": float(getattr(cs, "aEgo", 0.0)),
"standstill": bool(getattr(cs, "standstill", False)),
"v_cruise": float(getattr(splan, "vCruise", getattr(cs, "vCruise", 0.0))),
# a_chill is the real planner-side output target from longitudinalPlan
"a_chill": float(getattr(lplan, "aTarget", 0.0)),
"should_stop_chill": bool(getattr(lplan, "shouldStop", False)),
# Raw E2E vision neural net output (not modified by MPC)
"a_exp_raw": a_exp_raw,
"should_stop_exp_raw": should_stop_exp_raw,
"accel_jerk": float(getattr(splan, "accelerationJerk", 1.0)),
"min_accel": float(getattr(splan, "minAcceleration", -3.5)),
"max_accel": float(getattr(splan, "maxAcceleration", 1.5)),
"mpc_source": mpc_source,
"lead_one": lead_one_data,
"lead_two": lead_two_data,
"model_trajectory": serialize_model(model_msg),
"enabled": enabled,
"state": state,
"active": active,
"exp_mode": exp_mode,
"alert": alert,
"brake_pressed": manual_brake,
"brake_raw": bool(cs.brakePressed),
"gas_pressed": bool(cs.gasPressed),
})
if frames:
t0 = frames[0]["t"]
for f in frames:
f["t"] = f["t"] - t0
key_name = f"{route_clean.split('/')[-1][:8]}_s{seg}"
exported_data[key_name] = {
"label": label,
"frames": frames,
}
print(f"Successfully processed {len(frames)} frames for {key_name}.")
output_file = "hem_routes_telemetry.json"
with open(output_file, "w") as f:
json.dump(exported_data, f, indent=2)
print(f"\nSaved export payload to {output_file}")
if __name__ == "__main__":
export_routes()
+783
View File
@@ -0,0 +1,783 @@
#!/usr/bin/env python3
"""HEM forensic analyzer: figure out exactly what HybridExperimentalMode decided
and why, on a logged route, down to the per-frame input params and every internal
gate. Designed to answer "why did HEM roll through that stop sign / red light?"
It replays a route segment(s) through the *real* LongitudinalPlanner (Chill / ACC)
and the *real* HybridExperimentalMode (using the logged modelV2 action for Exp),
and records HybridExperimentalMode's full internal decision state each frame
(vision weight, stop detection, throttle/brake fusion, standstill anchor, safety
barrier, slew filter). It then:
* auto-detects "stop roll-through" incidents (vehicle creeping/coasting through
a detected stop with no braking authority),
* prints a per-frame forensic log of the decision chain around the incident,
* prints a root-cause narrative naming the exact gate that failed, and
* writes a graph of the Exp input, Chill input, fused output, authority, vision
weight and regime.
Usage:
./dev python tools/replay/hem_forensic.py <dongleId>/<routeId> [--segments 0,1]
[--start 30] [--end 120] [--data_dir /path/to/routes] [--out hem_forensic.png]
[--show] [--csv out.csv] [--window 12]
[--set hybrid_exp_bias=0.2 --set hybrid_vision_brake_sensitivity=1.2]
Examples:
./dev python tools/replay/hem_forensic.py afb7ef2ed593d651/000000af--414a758637 --segments 0,1
./dev python tools/replay/hem_forensic.py afb7ef2ed593d651/000000af--414a758637 --segments 0,1 --window 20 --set hybrid_exp_bias=0.2
"""
from __future__ import annotations
import argparse
import bisect
import sys
import time
from pathlib import Path
from types import SimpleNamespace
from typing import Any
import numpy as np
ROOT_DIR = Path(__file__).resolve().parents[2]
if str(ROOT_DIR) not in sys.path:
sys.path.insert(0, str(ROOT_DIR))
from openpilot.common.constants import CV
from openpilot.common.realtime import DT_MDL
from openpilot.selfdrive.car.cruise import V_CRUISE_MAX, V_CRUISE_UNSET
from openpilot.selfdrive.controls.lib.longcontrol import LongCtrlState
from openpilot.selfdrive.controls.lib.longitudinal_planner import LongitudinalPlanner
from openpilot.starpilot.controls.lib.hybrid_experimental_mode import HybridExperimentalMode
from openpilot.tools.replay.mode_sim import (
FakeParams,
TOGGLE_DEFAULTS,
as_lead,
load_buffers,
parse_toggle_overrides,
resolve_segment_identifier,
)
# Services we need to reconstruct the Chill (ACC) planner + Exp input + HEM.
SERVICES = {
"carState", "radarState", "starpilotRadarState", "modelV2",
"longitudinalPlan", "selfdriveState", "starpilotPlan", "starpilotCarState",
"controlsState", "carParams", "liveParameters", "carControl",
}
# Fields in HybridExperimentalMode.diag that are part of the decision chain.
DIAG_KEYS = [
# inputs
"v_ego", "v_cruise", "a_chill", "a_exp",
"should_stop_chill", "should_stop_exp",
"lead_status", "lead_d_rel", "lead_v_lead",
# vision intent
"has_full_trajectory", "v_horizon", "v_short", "v_min",
"speed_drop_ratio", "model_decel_strength",
"raw_vision_metric", "w_target", "w_vision",
# departure / override
"lead_departing", "vision_departing", "driver_override", "is_departing",
"horizon_stopping",
# fusion
"a_brake_fused", "a_throttle_fused", "is_stopping_event", "exp_dominant",
# standstill handshake
"standstill_intent", "should_stop_fused", "a_out",
]
REGIME_KEYS = ["regime", "standstill"]
def merge_buffers(buffers_list):
"""Merge per-segment {service: [(t, msg), ...]} into one dict sorted by time."""
merged = {s: [] for s in SERVICES}
for bufs in buffers_list:
for s in SERVICES:
merged[s].extend(bufs.get(s, []))
for s in SERVICES:
merged[s].sort(key=lambda x: x[0])
return merged
def run_forensic(grid, bufs, toggles):
"""Replay through the real planner + HEM, recording every HEM internal gate."""
ts = {s: [t for t, _ in m] for s, m in bufs.items()}
ms = {s: [m for _, m in bufs[s]] for s in bufs}
def latest(service, t):
times = ts.get(service)
if not times:
return None
idx = bisect.bisect_right(times, t) - 1
return ms[service][idx] if idx >= 0 else None
cp_candidate = None
for _, cp in bufs.get("carParams", []):
if cp is not None:
cp_candidate = cp
break
if cp_candidate is None:
cp_candidate = SimpleNamespace(
brand="toyota", carFingerprint="TOYOTA_RAV4", openpilotLongitudinalControl=True,
pcmCruise=False, steerRatio=15.0, wheelbase=2.7, longitudinalActuatorDelay=0.2, flags=0,
)
planner_state = SimpleNamespace(
params=FakeParams(),
params_memory=FakeParams(),
starpilot_following=SimpleNamespace(following_lead=False, slower_lead=False),
starpilot_vcruise=SimpleNamespace(
slc=SimpleNamespace(experimental_mode=False),
stop_sign_confirmed=False,
forcing_stop=False,
),
)
hybrid = HybridExperimentalMode()
hybrid.record_diag = True # capture full per-frame decision state for the trace
hybrid.set_tuning(toggles.hybrid_exp_bias, toggles.hybrid_vision_brake_sensitivity)
chill_long_planner = LongitudinalPlanner(cp_candidate, dt=DT_MDL)
n = len(grid)
out = {k: np.zeros(n) for k in [
"v_ego", "v_cruise", "model_v0", "a_chill", "a_exp", "hem_a", "hem_authority",
"lead_d_rel", "lead_v_lead", "logged_aTarget", "a_ego",
]}
out["lead_status"] = np.zeros(n, dtype=bool)
out["brake_pressed"] = np.zeros(n, dtype=bool)
out["gas_pressed"] = np.zeros(n, dtype=bool)
for k in DIAG_KEYS:
if k in ("should_stop_chill", "should_stop_exp", "lead_status", "has_full_trajectory",
"lead_departing", "vision_departing", "driver_override", "is_departing",
"horizon_stopping", "is_stopping_event", "exp_dominant", "standstill_intent",
"should_stop_fused"):
out["hem_" + k] = np.zeros(n, dtype=bool)
else:
out["hem_" + k] = np.zeros(n)
out["hem_regime"] = np.zeros(n, dtype=bool)
out["hem_standstill"] = np.zeros(n, dtype=bool)
# Gates dropped in the new HEM design map onto the surviving output signal.
out["hem_a_anchored"] = np.full(n, np.nan)
out["hem_a_safe"] = np.zeros(n)
real_monotonic = time.monotonic
fake_clock = [0.0]
time.monotonic = lambda: fake_clock[0]
last_model_v2 = None
for _, m in bufs.get("modelV2", []):
if m is not None:
last_model_v2 = m
break
try:
for i, t in enumerate(grid):
fake_clock[0] = float(i) * DT_MDL
car_raw = latest("carState", t)
v_ego_raw = max(float(getattr(car_raw, "vEgo", 0.0)), 0.0)
car = SimpleNamespace(
vEgo=v_ego_raw,
vEgoCluster=max(float(getattr(car_raw, "vEgoCluster", v_ego_raw)), 0.0),
vCruise=float(getattr(car_raw, "vCruise", 0.0)),
standstill=bool(getattr(car_raw, "standstill", False)),
leftBlinker=bool(getattr(car_raw, "leftBlinker", False)),
rightBlinker=bool(getattr(car_raw, "rightBlinker", False)),
gasPressed=bool(getattr(car_raw, "gasPressed", False)),
brakePressed=bool(getattr(car_raw, "brakePressed", False)),
steeringAngleDeg=float(getattr(car_raw, "steeringAngleDeg", 0.0)),
aEgo=float(getattr(car_raw, "aEgo", 0.0)),
leftBlindspot=bool(getattr(car_raw, "leftBlindspot", False)),
rightBlindspot=bool(getattr(car_raw, "rightBlindspot", False)),
)
v_ego = car.vEgo
radar = latest("radarState", t)
lead = as_lead(getattr(radar, "leadOne", None) if radar is not None else None)
lead2 = as_lead(getattr(radar, "leadTwo", None) if radar is not None else None)
sradar = latest("starpilotRadarState", t)
lead_left = as_lead(getattr(sradar, "leadLeft", None) if sradar is not None else None)
lead_right = as_lead(getattr(sradar, "leadRight", None) if sradar is not None else None)
lplan = latest("longitudinalPlan", t)
sds = latest("selfdriveState", t)
splan = latest("starpilotPlan", t)
scs = latest("starpilotCarState", t)
model_v2 = latest("modelV2", t)
if model_v2 is not None:
last_model_v2 = model_v2
else:
model_v2 = last_model_v2 # carry last vision frame across a replay gap
live_params = latest("liveParameters", t)
car_control = latest("carControl", t)
controls_state = latest("controlsState", t)
car_params = latest("carParams", t) or cp_candidate
v_cruise = float(getattr(splan, "vCruise", 0.0) or 0.0)
if not (v_cruise > 0):
v_cruise_kph = float(getattr(car_raw, "vCruise", 0.0) or 0.0)
if 0 < v_cruise_kph < V_CRUISE_UNSET:
v_cruise = min(v_cruise_kph, V_CRUISE_MAX) * CV.KPH_TO_MS
else:
v_cruise = v_ego
tracking_lead = bool(getattr(splan, "trackingLead", False))
if not tracking_lead:
tracking_lead = bool(getattr(lplan, "hasLead", False))
t_follow = float(getattr(splan, "tFollow", 1.45))
following_lead = tracking_lead and lead.dRel < (t_follow * 2) * v_ego
planner_state.lead_one = lead
planner_state.tracking_lead = tracking_lead
sm_dict = {
"carState": car,
"radarState": SimpleNamespace(leadOne=lead, leadTwo=lead2),
"starpilotRadarState": SimpleNamespace(leadLeft=lead_left, leadRight=lead_right),
"starpilotCarState": SimpleNamespace(
trafficModeEnabled=bool(getattr(scs, "trafficModeEnabled", False)),
alwaysOnLateralEnabled=bool(getattr(scs, "alwaysOnLateralEnabled", False)),
dashboardStopSign=int(getattr(scs, "dashboardStopSign", 0)),
accelPressed=bool(getattr(scs, "accelPressed", False)),
),
"selfdriveState": SimpleNamespace(
enabled=bool(getattr(sds, "enabled", False)),
experimentalMode=False, # force ACC/Chill evaluation in MPC
personality=0,
),
"longitudinalPlan": SimpleNamespace(
hasLead=bool(getattr(lplan, "hasLead", False)),
allowThrottle=bool(getattr(lplan, "allowThrottle", True)),
shouldStop=bool(getattr(lplan, "shouldStop", False)),
aTarget=float(getattr(lplan, "aTarget", 0.0)),
),
"starpilotPlan": SimpleNamespace(
vCruise=v_cruise,
tFollow=t_follow,
trackingLead=tracking_lead,
redLight=bool(getattr(splan, "redLight", False)),
forcingStop=bool(getattr(splan, "forcingStop", False)),
forcingStopLength=float(getattr(splan, "forcingStopLength", 100.0)),
minAcceleration=float(getattr(splan, "minAcceleration", -3.5)),
maxAcceleration=float(getattr(splan, "maxAcceleration", 1.5)),
accelerationJerk=float(getattr(splan, "accelerationJerk", 1.0)),
dangerJerk=float(getattr(splan, "dangerJerk", 1.0)),
speedJerk=float(getattr(splan, "speedJerk", 1.0)),
dangerFactor=float(getattr(splan, "dangerFactor", 1.0)),
disableThrottle=bool(getattr(splan, "disableThrottle", False)),
),
"controlsState": SimpleNamespace(
longControlState=LongCtrlState.pid,
forceDecel=bool(getattr(controls_state, "forceDecel", False)),
curvature=0.0,
),
"liveParameters": SimpleNamespace(
angleOffsetDeg=float(getattr(live_params, "angleOffsetDeg", 0.0)) if live_params else 0.0,
),
"carControl": SimpleNamespace(
orientationNED=getattr(car_control, "orientationNED", [0.0, 0.0, 0.0]) if car_control else [0.0, 0.0, 0.0],
),
"modelV2": model_v2,
"carParams": car_params,
}
# Chill (ACC) acceleration target via the real planner.
chill_long_planner.update(sm_dict, toggles)
a_chill = float(chill_long_planner.output_a_target)
# Exp (vision) acceleration target from the logged model action.
a_exp = 0.0
if model_v2 is not None:
try:
action_obj = getattr(model_v2, "action", None)
if action_obj is not None and hasattr(action_obj, "desiredAcceleration"):
a_exp = float(action_obj.desiredAcceleration)
else:
accel_x = getattr(model_v2, "acceleration", None)
if accel_x is not None and len(accel_x.x):
a_exp = float(accel_x.x[0])
except Exception:
a_exp = 0.0
should_stop_exp = False
if model_v2 is not None:
try:
should_stop_exp = bool(getattr(getattr(model_v2, "action", None), "shouldStop", False))
except Exception:
should_stop_exp = False
a_hem, should_stop_fused = hybrid.update(
v_ego=v_ego, v_cruise=v_cruise, lead_one=lead, model_v2=model_v2,
a_chill=a_chill, a_exp=a_exp,
should_stop_exp=should_stop_exp,
should_stop_chill=bool(getattr(lplan, "shouldStop", False)),
)
model_v0 = v_ego
if model_v2 is not None:
try:
vel_x = getattr(model_v2, "velocity", None)
if vel_x is not None and len(vel_x.x):
model_v0 = float(vel_x.x[0])
except Exception:
model_v0 = v_ego
out["v_ego"][i] = v_ego
out["v_cruise"][i] = v_cruise
out["model_v0"][i] = model_v0
out["a_chill"][i] = a_chill
out["a_exp"][i] = a_exp
out["hem_a"][i] = a_hem
out["hem_authority"][i] = hybrid.w_vision
out["lead_status"][i] = lead.status
out["lead_d_rel"][i] = lead.dRel
out["lead_v_lead"][i] = lead.vLead
out["logged_aTarget"][i] = float(getattr(lplan, "aTarget", 0.0))
out["brake_pressed"][i] = car.brakePressed
out["gas_pressed"][i] = car.gasPressed
out["a_ego"][i] = car.aEgo
for k in DIAG_KEYS:
val = hybrid.diag.get(k)
if isinstance(val, bool):
out["hem_" + k][i] = bool(val)
else:
try:
out["hem_" + k][i] = float(val)
except Exception:
out["hem_" + k][i] = 0.0
out["hem_regime"][i] = hybrid.diag.get("regime") == "brake"
out["hem_standstill"][i] = bool(hybrid.diag.get("standstill", False))
out["hem_a_anchored"][i] = out["hem_a"][i] if out["hem_standstill"][i] else np.nan
out["hem_a_safe"][i] = out["hem_a"][i]
finally:
time.monotonic = real_monotonic
return out
def detect_incidents(out, t):
"""Find frames where a detected stop was rolled/crept through for lack of braking.
A roll-through frame: the car is actually moving (v_ego > 0.5 m/s), the vision
side has flagged a stop (w_vision > 0.3), yet the fused output is essentially
coasting because BOTH chill and exp inputs failed to request a stop:
a_chill (ACC) is not braking -> a_chill > -0.3
exp effective accel is non-negative -> a_exp_effective >= 0
Under those conditions a_brake_fused = min(a_chill, a_exp) lands at ~0 and HEM
coasts through the stop it claims to see.
"""
idxs = []
for i in range(len(t)):
v = out["v_ego"][i]
w = out["hem_w_vision"][i]
ac = out["hem_a_chill"][i]
ae_eff = out["hem_a_exp"][i]
aout = out["hem_a"][i]
if v > 0.5 and w > 0.3 and ac > -0.3 and ae_eff >= 0.0 and abs(aout) < 0.25:
idxs.append(i)
return idxs
def hard_brake_clusters(out, gap_sec=1.5):
"""Clusters of frames where the DRIVER slammed the brakes (a_ego < -2.0).
The driver's own hard brake is the ground-truth marker that HEM failed to stop
the car. Each cluster is a discrete human-intervention event."""
hb = hard_brake_idxs(out)
return cluster_indices(hb, gap_sec=gap_sec)
def classify_failure(out, sl, i_brake):
"""Classify WHY the driver had to brake, from the frames just before the brake."""
lead = int(3.0 / DT_MDL)
pre = slice(max(sl.start, i_brake - lead), i_brake)
if pre.stop <= pre.start:
return "UNKNOWN", {}
v = out["v_ego"][pre]
exp_eff = out["hem_a_exp"][pre]
wv = out["hem_w_vision"][pre]
hem = out["hem_a"][pre]
chill = out["hem_a_chill"][pre]
# out["hem_regime"] is True for the BRAKE regime.
brake_share = float(np.mean(out["hem_regime"][pre]))
throttle_share = 1.0 - brake_share
stats = {
"v_before": float(v[-1]), "v_max": float(v.max()),
"exp_eff_mean": float(exp_eff.mean()), "exp_eff_min": float(exp_eff.min()),
"w_vis_max": float(wv.max()), "w_vis_mean": float(wv.mean()),
"hem_min": float(hem.min()), "hem_mean": float(hem.mean()),
"chill_min": float(chill.min()),
"throttle_share": throttle_share,
}
if stats["hem_min"] > -0.35 and stats["w_vis_max"] < 0.5 and stats["exp_eff_mean"] >= 0.0:
mode = "NO_STOP_REQUEST" # HEM never braked; neither input requested a stop
elif stats["throttle_share"] > 0.6 and stats["exp_eff_mean"] < -0.2:
mode = "THROTTLE_OVERRIDE" # vision wanted to brake but HEM output positive throttle
elif stats["w_vis_max"] > 0.4 and stats["exp_eff_mean"] < -0.2 and stats["hem_min"] > -1.2:
mode = "WEAK_BRAKE" # vision saw the stop but braking authority was too weak
elif stats["w_vis_max"] > 0.3 and stats["exp_eff_mean"] < -0.1:
mode = "LATE_BRAKE" # HEM braked but too late / not enough distance
else:
mode = "UNKNOWN"
return mode, stats
def cluster_indices(idxs, gap_sec=2.0):
"""Split incident indices into clusters separated by > gap_sec of quiet time."""
if not idxs:
return []
gap = int(gap_sec / DT_MDL)
clusters = [[idxs[0]]]
for idx in idxs[1:]:
if idx - clusters[-1][-1] > gap:
clusters.append([idx])
else:
clusters[-1].append(idx)
return clusters
def hard_brake_idxs(out):
"""Frames where the DRIVER slammed the brakes (a_ego strongly negative) — a
human-intervention marker proving HEM was about to run the stop."""
return [i for i in range(len(out["a_ego"])) if out["a_ego"][i] < -2.0]
def select_main_cluster(clusters, hb):
"""Pick the incident cluster the driver actually intervened on (nearest hard
brake); fall back to the densest cluster when no driver braking occurred."""
if not clusters:
return []
if not hb:
return max(clusters, key=len)
best, best_gap = None, float("inf")
for cl in clusters:
gap = min(min(abs(i - j) for j in hb) for i in cl)
if gap < best_gap:
best_gap, best = gap, cl
return best
def window_around(idxs, t, out, span_sec=6.0, max_frames=400):
"""Select the incident cluster the driver braked on, return (slice, cluster)."""
if not idxs:
return None, []
clusters = cluster_indices(idxs)
main = select_main_cluster(clusters, hard_brake_idxs(out))
start = max(0, main[0] - int(span_sec / DT_MDL))
end = min(len(t) - 1, main[-1] + int(span_sec / DT_MDL))
if end - start > max_frames:
mid = (start + end) // 2
start = max(0, mid - max_frames // 2)
end = min(len(t) - 1, start + max_frames)
main = [i for i in main if start <= i <= end]
return slice(start, end + 1), main
def print_forensic_log(out, sl, t0):
print("\n" + "-" * 122)
print(" PER-FRAME HEM DECISION CHAIN")
print("-" * 122)
hdr = (f" {'t':>5} {'vEgo':>5} {'aEgo':>6} {'dvrBrk':>6} {'chill':>6} {'exp':>6} {'expEff':>6} "
f"{'wVis':>5} {'auth':>5} {'regime':>8} {'stand':>5} {'brakeF':>6} {'throtF':>6} {'hem_a':>6}")
print(hdr)
print(" " + "-" * 122)
for i in range(sl.start, sl.stop):
dt = t0[i]
row = [
f"{dt:5.1f}",
f"{out['v_ego'][i]:5.2f}",
f"{out['a_ego'][i]:6.2f}",
f"{'<<<<' if out['a_ego'][i] < -2.0 else ('Y' if out['brake_pressed'][i] else '-'):>6}",
f"{out['hem_a_chill'][i]:6.2f}",
f"{out['hem_a_exp'][i]:6.2f}",
f"{out['hem_a_exp'][i]:6.2f}",
f"{out['hem_w_vision'][i]:5.2f}",
f"{out['hem_authority'][i]:5.2f}",
f"{'brake' if out['hem_regime'][i] else 'throttle':>8}",
f"{'Y' if out['hem_standstill'][i] else '-':>5}",
f"{out['hem_a_brake_fused'][i]:6.2f}",
f"{out['hem_a_throttle_fused'][i]:6.2f}",
f"{out['hem_a'][i]:6.2f}",
]
print(" " + " ".join(row))
def print_root_cause(out, t0, brake_cluster, sl, mode, stats):
print("\n" + "=" * 78)
print(" ROOT-CAUSE DIAGNOSIS (driver-brake anchored)")
print("=" * 78)
if not brake_cluster:
print(" No hard driver brake (a_ego < -2.0) found; no human intervention to explain.")
print(" If a stop was still rolled through, pass --start/--end around it.")
return
i = brake_cluster[0]
dt = t0[i]
v = out["v_ego"][i]
print(f" Driver slammed the brakes at t = {dt:.1f}s (segment-relative), v_ego = {v:.1f} m/s "
f"({v * CV.MS_TO_MPH:.1f} mph), a_ego = {out['a_ego'][i]:.2f} m/s².")
print(f" HEM fused output at that instant: {out['hem_a'][i]:+.2f} m/s².")
labels = {
"NO_STOP_REQUEST": "HEM NEVER REQUESTED A STOP",
"THROTTLE_OVERRIDE": "HEM ACCELERATED DESPITE VISION STOP",
"WEAK_BRAKE": "HEM BRAKED BUT TOO WEAKLY",
"LATE_BRAKE": "HEM BRAKED BUT TOO LATE",
"UNKNOWN": "NO CLEAR FAILURE MODE (see trace)",
}
print(f"\n FAILURE MODE: {labels.get(mode, mode)}")
print("\n [HEM STATE IN THE ~3s BEFORE THE DRIVER BRAKE]")
print(f" Speed just before brake : {stats.get('v_before', 0):.1f} m/s (max {stats.get('v_max', 0):.1f})")
print(f" Vision weight w_vision : mean {stats.get('w_vis_mean', 0):.2f} | max {stats.get('w_vis_max', 0):.2f} (0.3+ = stop detected)")
print(f" Exp effective a : mean {stats.get('exp_eff_mean', 0):+.2f} | min {stats.get('exp_eff_min', 0):+.2f} m/s²")
print(f" Chill (ACC) a_chill : min {stats.get('chill_min', 0):+.2f} m/s²")
print(f" HEM output a_hem : mean {stats.get('hem_mean', 0):+.2f} | min {stats.get('hem_min', 0):+.2f} m/s²")
print(f" Throttle regime share : {100 * stats.get('throttle_share', 0):.0f}%")
print("\n [INTERPRETATION]")
if mode == "NO_STOP_REQUEST":
print(" • Vision never flagged a stop (w_vision stayed < 0.5) and Exp requested no brake")
print(" (a_exp_eff >= 0). Chill/ACC also requested no brake. HEM coasted, so the driver")
print(" had to brake. The stop/light was missed at the perception level (model predicted go).")
elif mode == "THROTTLE_OVERRIDE":
print(" • Vision DID see the stop (exp_eff < -0.2) but HEM spent >60% of the approach in the")
print(" THROTTLE regime and output positive accel. The throttle path (smooth_max of chill/exp)")
print(" or a departing flag won over the brake path, so HEM drove toward the light.")
elif mode == "WEAK_BRAKE":
print(" • Vision saw the stop (w_vision > 0.4) and Exp requested a brake, but HEM's output")
print(f" never went harder than {stats.get('hem_min', 0):.2f} m/s². That decel was not enough to")
print(" stop in time at the approach speed, so the driver had to brake hard.")
elif mode == "LATE_BRAKE":
print(" • HEM did brake but only started hard late in the approach (or the needed stop distance")
print(" exceeded what its braking could cover), forcing the driver to intervene.")
else:
print(" • Review the per-frame trace above; the inputs/state were mixed.")
print("\n [FIX DIRECTIONS]")
if mode in ("NO_STOP_REQUEST", "THROTTLE_OVERRIDE"):
print(" • When w_vision or exp_eff indicates a stop but neither input produces a brake, HEM")
print(" should synthesize its own kinematic brake (a_kinematic_stop) instead of blending 0's.")
print(" • Lock the braking regime (w_accel=0) harder when a stop is detected, so the throttle")
print(" path cannot override a vision brake.")
elif mode in ("WEAK_BRAKE", "LATE_BRAKE"):
print(" • HEM currently mirrors the model's Exp brake (a_exp_eff) and the ACC brake; when those")
print(" are too gentle for the required stop distance, HEM should apply a stronger floor based")
print(" on the kinematic stop calculation (-v²/2d) and vision brake sensitivity.")
print(" • Raise VISION_BRAKE_SENSITIVITY or lower the kinematic-stop distance threshold so the")
print(" vision stop requests decel earlier and harder.")
def plot_results(out, t0, args, incident_idxs):
import matplotlib
if not args.show:
matplotlib.use("Agg")
import matplotlib.pyplot as plt
n = len(t0)
fig, axs = plt.subplots(5, 1, figsize=(15, 17), sharex=True,
gridspec_kw={"height_ratios": [2, 2, 2, 2, 1.4]})
fig.suptitle(f"HEM forensic {args.route} segs {args.segments} "
f"(bias={args_set(args.set, 'hybrid_exp_bias', 0.0):+.2f}, "
f"vis_brake={args_set(args.set, 'hybrid_vision_brake_sensitivity', 1.0):.2f})",
fontsize=11)
# shade incident region
if incident_idxs:
for ax in axs:
x0 = t0[max(0, incident_idxs[0] - int(2 / DT_MDL))]
x1 = t0[min(n - 1, incident_idxs[-1] + int(2 / DT_MDL))]
ax.axvspan(x0, x1, color="red", alpha=0.08)
# speed
axs[0].plot(t0, out["v_ego"] * CV.MS_TO_MPH, color="black", lw=1.5, label="v_ego")
axs[0].plot(t0, out["v_cruise"] * CV.MS_TO_MPH, color="tab:blue", lw=1.0, ls="--", label="v_cruise")
axs[0].plot(t0, out["model_v0"] * CV.MS_TO_MPH, color="tab:green", lw=1.0, alpha=0.7, label="model v[0]")
axs[0].set_ylabel("mph")
axs[0].set_title("1) Speed")
axs[0].legend(loc="upper right", fontsize=8)
axs[0].grid(alpha=0.3)
# accel inputs vs output
axs[1].axhline(0, color="gray", lw=0.6)
hard_brake = out["a_ego"] < -2.0
if np.any(hard_brake):
axs[1].fill_between(t0, -3.5, 2.0, where=hard_brake, step="post",
color="crimson", alpha=0.15, label="driver hard brake")
axs[1].plot(t0, out["a_chill"], color="tab:blue", lw=1.1, ls="--", label="Chill (ACC) input")
axs[1].plot(t0, out["a_exp"], color="tab:orange", lw=1.1, ls=":", label="Exp (vision) input")
axs[1].plot(t0, out["hem_a"], color="tab:cyan", lw=1.6, label="HEM fused output")
axs[1].plot(t0, out["a_ego"], color="crimson", lw=1.0, alpha=0.8, label="a_ego (actual)")
axs[1].plot(t0, out["logged_aTarget"], color="gray", lw=0.9, alpha=0.6, label="logged aTarget")
axs[1].set_ylim(-3.5, 2.0)
axs[1].set_ylabel("accel (m/s²)")
axs[1].set_title("2) HEM inputs vs output vs actual (crimson = driver brake)")
axs[1].legend(loc="upper right", fontsize=7, ncol=2)
axs[1].legend(loc="upper right", fontsize=8, ncol=2)
axs[1].grid(alpha=0.3)
# vision weight, authority, exp_effective
axs[2].plot(t0, out["hem_w_vision"], color="tab:red", lw=1.3, label="w_vision")
axs[2].plot(t0, out["hem_authority"], color="tab:purple", lw=1.3, label="exp authority")
axs[2].plot(t0, out["hem_a_exp"], color="tab:olive", lw=1.0, ls="--", label="exp a")
axs[2].axhline(0.3, color="tab:red", lw=0.6, ls=":")
axs[2].set_ylim(-1, 1.5)
axs[2].set_ylabel("weight")
axs[2].set_title("3) Vision stop detection vs Exp effective accel")
axs[2].legend(loc="upper right", fontsize=8)
axs[2].grid(alpha=0.3)
# decision gates
axs[3].plot(t0, out["hem_a_brake_fused"], color="tab:blue", lw=1.2, label="brake-fused")
axs[3].plot(t0, out["hem_a_throttle_fused"], color="tab:orange", lw=1.2, label="throttle-fused")
axs[3].plot(t0, out["hem_a_anchored"], color="tab:green", lw=1.2, ls="--", label="after standstill")
axs[3].plot(t0, out["hem_a_safe"], color="tab:red", lw=1.2, ls="--", label="after safety")
axs[3].plot(t0, out["hem_a"], color="black", lw=1.6, label="final output")
axs[3].axhline(0, color="gray", lw=0.6)
axs[3].set_ylabel("accel (m/s²)")
axs[3].set_title("4) Fusion pipeline: which gate produced the output")
axs[3].legend(loc="upper right", fontsize=8, ncol=2)
axs[3].grid(alpha=0.3)
# regime + standstill
axs[4].fill_between(t0, 0.0, 1.0, where=out["hem_regime"], step="post",
color="tab:red", alpha=0.5, label="brake regime")
axs[4].fill_between(t0, 1.0, 2.0, where=out["hem_standstill"], step="post",
color="tab:green", alpha=0.5, label="standstill anchor")
axs[4].set_yticks([0.5, 1.5])
axs[4].set_yticklabels(["brake", "standstill"])
axs[4].set_ylim(-0.3, 2.3)
axs[4].set_xlabel("time (s, segment-relative)")
axs[4].set_title("5) Regime")
axs[4].legend(loc="upper right", fontsize=8)
axs[4].grid(axis="y", alpha=0.3)
fig.tight_layout()
if args.out:
p = Path(args.out)
p.parent.mkdir(parents=True, exist_ok=True)
fig.savefig(p, dpi=150)
print(f"Saved graph to {p}")
if args.show:
plt.show()
plt.close(fig)
def args_set(overrides, key, default):
for item in overrides:
k, sep, raw = item.partition("=")
if sep and k.strip() == key:
try:
return float(raw.strip())
except ValueError:
return default
return default
def main(argv=None):
parser = argparse.ArgumentParser(
description="Forensic analysis of what HybridExperimentalMode decided, and why, on a route.")
parser.add_argument("route", help="route: <dongleId>/<logId> (the part after connect.comma.ai/), "
"e.g. afb7ef2ed593d651/000000af--414a758637")
parser.add_argument("--segments", default="0", help="comma-separated segment indices, e.g. 0,1")
parser.add_argument("--data_dir", help="local directory containing route files")
parser.add_argument("--start", type=float, default=None, help="start seconds (segment-relative)")
parser.add_argument("--end", type=float, default=None, help="end seconds (segment-relative)")
parser.add_argument("--window", type=float, default=8.0,
help="seconds of context to print around each detected incident (default 8)")
parser.add_argument("--out", default="hem_forensic.png", help="output PNG path")
parser.add_argument("--show", action="store_true", help="show plot window")
parser.add_argument("--csv", default=None, help="optional CSV output of the time series")
parser.add_argument("--set", action="append", default=[], metavar="KEY=value",
help="override a toggle, e.g. --set hybrid_exp_bias=0.2 --set hybrid_vision_brake_sensitivity=1.2")
args = parser.parse_args(argv)
overrides = parse_toggle_overrides(args.set)
defaults = dict(TOGGLE_DEFAULTS)
defaults.update(overrides)
toggles = SimpleNamespace(**defaults)
for key, value in overrides.items():
print(f"toggle override: {key} = {value}")
segments = [int(s) for s in args.segments.split(",") if s.strip() != ""]
buffers_list = []
for seg in segments:
identifiers = resolve_segment_identifier(args.route, seg, args.data_dir)
if not identifiers:
print(f"No data found for {args.route} seg {seg}", file=sys.stderr)
return 1
print(f"Loading seg {seg}: {identifiers}")
bufs = load_buffers(identifiers, SERVICES)
if not any(bufs[s] for s in SERVICES):
print(f"No messages loaded for seg {seg}.", file=sys.stderr)
return 1
buffers_list.append(bufs)
bufs = merge_buffers(buffers_list)
all_min = min(t for s in SERVICES for t, _ in bufs[s])
all_max = max(t for s in SERVICES for t, _ in bufs[s])
start = all_min if args.start is None else all_min + args.start
end = all_max if args.end is None else min(all_max, all_min + args.end)
if end - start < DT_MDL:
print(f"Empty time window [{start:.1f}, {end:.1f}].", file=sys.stderr)
return 1
grid = np.arange(start, end, DT_MDL)
print(f"Window: {start - all_min:.1f}s -> {end - all_min:.1f}s ({len(grid)} frames at {1 / DT_MDL:.0f} Hz)")
t0 = grid - start
out = run_forensic(grid, bufs, toggles)
if args.csv:
import csv
csv_path = Path(args.csv)
csv_path.parent.mkdir(parents=True, exist_ok=True)
top_cols = ["v_ego", "v_cruise", "a_chill", "a_exp", "hem_a", "hem_authority",
"lead_status", "lead_d_rel", "lead_v_lead", "a_ego",
"brake_pressed", "gas_pressed", "logged_aTarget"]
diag_cols = ["hem_" + k for k in DIAG_KEYS] + ["hem_regime", "hem_standstill"]
seen = set()
unique_cols = []
for c in ["t"] + top_cols + diag_cols:
if c not in seen:
seen.add(c)
unique_cols.append(c)
with open(csv_path, "w", newline="") as f:
writer = csv.writer(f)
writer.writerow(unique_cols)
for i in range(len(t0)):
row = [f"{t0[i]:.3f}"]
for c in unique_cols[1:]:
row.append(f"{out[c][i]:.4f}")
writer.writerow(row)
print(f"Saved CSV to {csv_path}")
brake_clusters = hard_brake_clusters(out)
if brake_clusters:
main_brake = max(brake_clusters, key=len)
lead_sec = int(args.window / DT_MDL)
sl = slice(max(0, main_brake[0] - lead_sec),
min(len(t0), main_brake[-1] + int(3 / DT_MDL)))
if sl.stop - sl.start > 600:
mid = (sl.start + sl.stop) // 2
sl = slice(max(0, mid - 300), min(len(t0), mid + 300))
print(f"\nDetected {len(brake_clusters)} driver hard-brake interventions.")
print(f"Focusing on the main one at t={t0[main_brake[0]]:.1f}s..{t0[main_brake[-1]]:.1f}s; "
"use --start/--end to zoom elsewhere.")
mode, stats = classify_failure(out, sl, main_brake[0])
focus_incidents = main_brake
else:
print("\nNo hard driver brake (a_ego < -2.0) found in the window.")
print("Falling back to coast-through detection (vision saw a stop but HEM coasted).")
incident_idxs = detect_incidents(out, t0)
sl, focus_incidents = window_around(incident_idxs, t0, out, span_sec=args.window)
if sl is None:
sl = slice(0, len(t0))
mode, stats = None, {}
print_forensic_log(out, sl, t0)
print_root_cause(out, t0, focus_incidents, sl, mode, stats)
plot_results(out, t0, args, focus_incidents if focus_incidents else [])
return 0
if __name__ == "__main__":
raise SystemExit(main())
+325
View File
@@ -0,0 +1,325 @@
#!/usr/bin/env python3
import os
import sys
import numpy as np
import matplotlib.pyplot as plt
from pathlib import Path
# Add openpilot paths if running from tools/ or other subdirectories
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
from openpilot.common.realtime import DT_MDL
from openpilot.tools.lib.logreader import LogReader
from openpilot.starpilot.controls.lib.hybrid_experimental_mode import HybridExperimentalMode
# Define the specific routes and segments where stop-sign or red-light failures occurred
ROUTES_TO_ANALYZE = [
("afb7ef2ed593d651/000000b3--9c3d58d585", 3, "Stop Sign"),
("afb7ef2ed593d651/000000b3--9c3d58d585", 8, "Stop Sign"),
("afb7ef2ed593d651/000000b3--9c3d58d585", 10, "Stop Sign"),
("afb7ef2ed593d651/000000b3--9c3d58d585", 11, "Stop Sign"),
("afb7ef2ed593d651/000000b4--1a67659212", 1, "Stop Sign"),
("afb7ef2ed593d651/000000b5--8ee86fef97", 0, "Stop Sign"),
("afb7ef2ed593d651/000000b6--711631f3fd", 0, "Stop Sign"),
("afb7ef2ed593d651/000000b7--9bfbaf247a", 1, "Stop Sign"),
("afb7ef2ed593d651/000000b8--e3147dbfc3", 0, "Stop Sign"),
("afb7ef2ed593d651/000000b9--976a3b50cb", 0, "Stop Sign"),
("afb7ef2ed593d651/000000b9--976a3b50cb", 1, "Stop Sign"),
("afb7ef2ed593d651/000000b9--976a3b50cb", 2, "Stop Sign"),
("afb7ef2ed593d651/000000b9--976a3b50cb", 3, "Stop Sign"),
("afb7ef2ed593d651/000000b9--976a3b50cb", 6, "Red Light"),
]
class MockLead:
def __init__(self, status=False, d_rel=150.0, v_lead=0.0):
self.status = status
self.dRel = float(d_rel)
self.vLead = float(v_lead)
def fetch_telemetry(route_str, segment):
"""Loads specified segment logs. Attempts local lookups first, then falls back to public API."""
route_clean = route_str.replace("|", "/")
segment_str = f"{segment:02d}" if isinstance(segment, int) else str(segment)
# Search local paths first
local_paths = [
Path(f"/data/media/0/realdata/{route_clean}--{segment}"),
Path(os.path.expanduser(f"~/.comma/media/0/realdata/{route_clean}--{segment}")),
Path(f"./{route_clean}--{segment}"),
]
lr = None
for path in local_paths:
rlog_file = path / "rlog"
if rlog_file.exists():
print(f"Loading local data: {rlog_file}")
lr = LogReader(str(rlog_file))
break
if lr is None:
# Construct comma public database URL fallback
dongle_id, route_sig = route_clean.split("/", 1)
remote_url = f"https://commadata2.blob.core.windows.net/commadata2/{dongle_id}/{route_sig}/{segment}/rlog"
print(f"Loading remote data: {remote_url}")
try:
lr = LogReader(remote_url)
except Exception as e:
print(f"Failed to fetch {remote_url}: {e}")
return []
print("Parsing messages...")
car_state_msgs = []
model_msgs = []
splan_msgs = []
lplan_msgs = []
for msg in lr:
which = msg.which()
t = msg.logMonoTime * 1e-9
if which == "carState":
car_state_msgs.append((t, msg.carState))
elif which == "modelV2":
model_msgs.append((t, msg.modelV2))
elif which == "starpilotPlan":
splan_msgs.append((t, msg.starpilotPlan))
elif which == "longitudinalPlan":
lplan_msgs.append((t, msg.longitudinalPlan))
car_state_msgs.sort(key=lambda x: x[0])
model_msgs.sort(key=lambda x: x[0])
splan_msgs.sort(key=lambda x: x[0])
lplan_msgs.sort(key=lambda x: x[0])
# Synchronize messages on model 20Hz time-grid
data_frames = []
for t_model, model_msg in model_msgs:
# Find latest corresponding messages
cs = next((m for t, m in reversed(car_state_msgs) if t <= t_model), None)
splan = next((m for t, m in reversed(splan_msgs) if t <= t_model), None)
lplan = next((m for t, m in reversed(lplan_msgs) if t <= t_model), None)
if cs is None:
continue
v_ego = cs.vEgo
v_cruise = getattr(splan, "vCruise", getattr(cs, "vCruise", 0.0))
a_chill = getattr(lplan, "aTarget", 0.0)
a_exp = getattr(model_msg.action, "desiredAcceleration", 0.0)
data_frames.append({
"t": t_model,
"v_ego": v_ego,
"v_cruise": v_cruise,
"a_chill": a_chill,
"a_exp": a_exp,
"model": model_msg,
})
return data_frames
def simulate_hem(data_frames):
"""Simulates the controller over the synchronized messages."""
controller = HybridExperimentalMode()
controller.record_diag = True
sim_results = []
for frame in data_frames:
# Lead initialization
lead = MockLead(status=False)
# Execute state update
a_out, should_stop_fused = controller.update(
v_ego=frame["v_ego"],
v_cruise=frame["v_cruise"],
lead_one=lead,
model_v2=frame["model"],
a_chill=frame["a_chill"],
a_exp=frame["a_exp"],
)
diag = dict(controller.diag)
diag["a_out"] = float(a_out)
diag["should_stop_fused"] = bool(should_stop_fused)
diag["t_rel"] = frame["t"] - data_frames[0]["t"]
sim_results.append(diag)
return sim_results
def analyze_failures(route_str, segment, label, results):
"""Analyzes decisions inside the critical deceleration window."""
total_frames = len(results)
if total_frames == 0:
return None
# Find the primary deceleration zone (where v_ego drops, or should have dropped)
v_speeds = [r["v_ego"] for r in results]
max_v = max(v_speeds)
min_v = min(v_speeds)
# Identify frames with a stop visible to vision (horizon stop or exp stop flag)
active_frames = []
for idx, r in enumerate(results):
if r.get("horizon_stopping", False) or r.get("should_stop_exp", False) or r.get("w_vision", 0.0) > 0.1:
active_frames.append(idx)
if not active_frames:
return {
"route": route_str, "segment": segment, "label": label,
"outcome": "No stop event detected by model.",
"failure_mode": "Model did not predict stop point.",
}
start_idx = min(active_frames)
end_idx = max(active_frames)
# Analyze tracking variables inside the event window
latch_decays = 0
tracking_resets = 0
early_departures = 0
kinematic_collapses = 0
for idx in range(start_idx, end_idx + 1):
r = results[idx]
# 1. Check for premature latch decay (decaying while still moving fast)
if r.get("w_vision", 0.0) < 0.2 and r.get("v_ego", 0.0) > 2.0 and r.get("horizon_stopping", False):
latch_decays += 1
# 2. Check for stop detection being cleared while speed is still high
if idx > start_idx:
prev_r = results[idx - 1]
if prev_r.get("horizon_stopping", False) and not r.get("horizon_stopping", False):
if r.get("v_ego", 0.0) > 1.0 and not r.get("is_departing", False):
tracking_resets += 1
# 3. Check for departure lockout firing while still approaching a predicted stop
if r.get("is_departing", False) and r.get("v_ego", 0.0) > 1.5 and r.get("horizon_stopping", False):
early_departures += 1
# 4. Check if the brake floor collapsed near the stop line
if r.get("a_brake_fused", 0.0) > -0.1 and r.get("v_ego", 0.0) > 1.0 and r.get("horizon_stopping", False):
kinematic_collapses += 1
# Determine primary failure mode
failure_modes = []
if latch_decays > 5:
failure_modes.append("Premature Latch Decay (w_vision collapsed)")
if tracking_resets > 0:
failure_modes.append("Stop Detection Cleared Early")
if early_departures > 5:
failure_modes.append("Early Departure Lockout Bypass (is_departing while approaching)")
if kinematic_collapses > 5:
failure_modes.append("Kinematic Brake Floor Collapse near stop line")
failure_mode = " / ".join(failure_modes) if failure_modes else "Weak general deceleration tracking"
outcome = f"Blew past stop line. Min speed reached: {min_v:.2f} m/s." if min_v > 0.5 else "Stopped but late/harsh."
return {
"route": route_str,
"segment": segment,
"label": label,
"max_v": max_v,
"min_v": min_v,
"latch_decays": latch_decays,
"tracking_resets": tracking_resets,
"early_departures": early_departures,
"kinematic_collapses": kinematic_collapses,
"outcome": outcome,
"failure_mode": failure_mode,
}
def run_suite():
all_analyses = []
plot_data = {}
for route, seg, label in ROUTES_TO_ANALYZE:
print(f"\nAnalyzing {route} segment {seg} ({label})...")
frames = fetch_telemetry(route, seg)
if not frames:
print("No frames retrieved. Skipping.")
continue
results = simulate_hem(frames)
analysis = analyze_failures(route, seg, label, results)
if analysis:
all_analyses.append(analysis)
plot_data[f"{route.split('/')[-1][:8]}_s{seg}"] = (frames, results)
# 1. Export forensic summary text report
report_path = "hem_forensic_report.txt"
print(f"\nWriting forensic report to {report_path}...")
with open(report_path, "w") as f:
f.write("=== HYBRID EXPERIMENTAL MODE (HEM) FAILURE CASE STUDY SUMMARY ===\n")
f.write(f"Analyzed {len(all_analyses)} routes where vehicle rolled stop-lines\n")
f.write("==================================================================\n\n")
# General findings
f.write("COMMON STRUCTURAL ROOT CAUSES IDENTIFIED:\n")
f.write("------------------------------------------\n")
f.write("1. Stop Detection Cleared on Model Re-acceleration:\n")
f.write(" When the trajectory velocity endpoint flips positive (a_exp > 0.1, v_horizon > 1.2)\n")
f.write(" the 'vision_departing' / 'is_departing' signal fires TRUE and clears 'horizon_stopping'\n")
f.write(" while the car is still traveling at speed close to the line, dropping the brake floor.\n\n")
f.write("2. Insufficient Latch Sustainability (w_vision decays):\n")
f.write(" If the model stops outputting a low velocity endpoint, even briefly, the vision weight\n")
f.write(" decays toward zero and unlocks cruise throttle while still closing on the stop.\n\n")
f.write("DETAILED SEGMENT TELEMETRY BREAKDOWN:\n")
f.write("-------------------------------------\n")
for a in all_analyses:
f.write(f"Route: {a['route']} | Seg: {a['segment']} ({a['label']})\n")
f.write(f" • Velocity Profile : {a['max_v']:.1f} m/s -> {a['min_v']:.1f} m/s\n")
f.write(f" • Tracker Resets : {a['tracking_resets']} frames\n")
f.write(f" • Early Departures : {a['early_departures']} frames\n")
f.write(f" • Outcome : {a['outcome']}\n")
f.write(f" • Primary Fault : {a['failure_mode']}\n")
f.write("------------------------------------------------------------------\n")
# 2. Generate tiled plot of critical variables for a subset of failures
print("Generating diagnostic plots...")
plot_keys = list(plot_data.keys())[:4] # Plot up to 4 significant failures for space
if not plot_keys:
return
fig, axes = plt.subplots(len(plot_keys), 2, figsize=(15, 3 * len(plot_keys)), sharex="col")
if len(plot_keys) == 1:
axes = np.expand_dims(axes, axis=0)
for idx, key in enumerate(plot_keys):
frames, results = plot_data[key]
t = [r["t_rel"] for r in results]
v_ego = [r["v_ego"] for r in results]
w_vis = [r["w_vision"] for r in results]
a_out = [r["a_out"] for r in results]
a_kin = [r["a_brake_fused"] for r in results]
# Left column: Speeds and Latch activations
ax_l = axes[idx, 0]
ax_l.plot(t, v_ego, color="black", lw=1.5, label="v_ego (m/s)")
ax_l_twin = ax_l.twinx()
ax_l_twin.plot(t, w_vis, color="orange", alpha=0.7, ls="--", label="w_vision (latch)")
ax_l.set_title(f"Run: {key} - Speeds", fontsize=10)
ax_l.set_ylabel("Speed (m/s)")
ax_l_twin.set_ylabel("Latch Active", color="orange")
ax_l.grid(alpha=0.3)
if idx == 0:
ax_l.legend(loc="upper left")
ax_l_twin.legend(loc="upper right")
# Right column: Acceleration commands
ax_r = axes[idx, 1]
ax_r.plot(t, a_out, color="blue", lw=1.5, label="a_out (fused)")
ax_r.plot(t, a_kin, color="red", alpha=0.6, ls=":", label="a_kinematic_stop")
ax_r.set_title(f"Run: {key} - Acceleration Commands", fontsize=10)
ax_r.set_ylabel("Accel (m/s²)")
ax_r.grid(alpha=0.3)
if idx == 0:
ax_r.legend(loc="upper right")
plt.tight_layout()
plot_out_path = "hem_failures_analysis.png"
plt.savefig(plot_out_path, dpi=150)
print(f"Diagnostic graph saved to {plot_out_path}")
if __name__ == "__main__":
run_suite()
+325
View File
@@ -0,0 +1,325 @@
#!/usr/bin/env python3
import os
import sys
import numpy as np
import matplotlib.pyplot as plt
from pathlib import Path
# Add openpilot paths if running from tools/ or other subdirectories
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
from openpilot.common.realtime import DT_MDL
from openpilot.tools.lib.logreader import LogReader
from openpilot.starpilot.controls.lib.hybrid_experimental_mode import HybridExperimentalMode
# Define the specific routes and segments where stop-sign or red-light failures occurred
ROUTES_TO_ANALYZE = [
("afb7ef2ed593d651/000000b3--9c3d58d585", 3, "Stop Sign"),
("afb7ef2ed593d651/000000b3--9c3d58d585", 8, "Stop Sign"),
("afb7ef2ed593d651/000000b3--9c3d58d585", 10, "Stop Sign"),
("afb7ef2ed593d651/000000b3--9c3d58d585", 11, "Stop Sign"),
("afb7ef2ed593d651/000000b4--1a67659212", 1, "Stop Sign"),
("afb7ef2ed593d651/000000b5--8ee86fef97", 0, "Stop Sign"),
("afb7ef2ed593d651/000000b6--711631f3fd", 0, "Stop Sign"),
("afb7ef2ed593d651/000000b7--9bfbaf247a", 1, "Stop Sign"),
("afb7ef2ed593d651/000000b8--e3147dbfc3", 0, "Stop Sign"),
("afb7ef2ed593d651/000000b9--976a3b50cb", 0, "Stop Sign"),
("afb7ef2ed593d651/000000b9--976a3b50cb", 1, "Stop Sign"),
("afb7ef2ed593d651/000000b9--976a3b50cb", 2, "Stop Sign"),
("afb7ef2ed593d651/000000b9--976a3b50cb", 3, "Stop Sign"),
("afb7ef2ed593d651/000000b9--976a3b50cb", 6, "Red Light"),
]
class MockLead:
def __init__(self, status=False, d_rel=150.0, v_lead=0.0):
self.status = status
self.dRel = float(d_rel)
self.vLead = float(v_lead)
def fetch_telemetry(route_str, segment):
"""Loads specified segment logs. Attempts local lookups first, then falls back to public API."""
route_clean = route_str.replace("|", "/")
segment_str = f"{segment:02d}" if isinstance(segment, int) else str(segment)
# Search local paths first
local_paths = [
Path(f"/data/media/0/realdata/{route_clean}--{segment}"),
Path(os.path.expanduser(f"~/.comma/media/0/realdata/{route_clean}--{segment}")),
Path(f"./{route_clean}--{segment}"),
]
lr = None
for path in local_paths:
rlog_file = path / "rlog"
if rlog_file.exists():
print(f"Loading local data: {rlog_file}")
lr = LogReader(str(rlog_file))
break
if lr is None:
# Construct comma public database URL fallback
dongle_id, route_sig = route_clean.split("/", 1)
remote_url = f"https://commadata2.blob.core.windows.net/commadata2/{dongle_id}/{route_sig}/{segment}/rlog"
print(f"Loading remote data: {remote_url}")
try:
lr = LogReader(remote_url)
except Exception as e:
print(f"Failed to fetch {remote_url}: {e}")
return []
print("Parsing messages...")
car_state_msgs = []
model_msgs = []
splan_msgs = []
lplan_msgs = []
for msg in lr:
which = msg.which()
t = msg.logMonoTime * 1e-9
if which == "carState":
car_state_msgs.append((t, msg.carState))
elif which == "modelV2":
model_msgs.append((t, msg.modelV2))
elif which == "starpilotPlan":
splan_msgs.append((t, msg.starpilotPlan))
elif which == "longitudinalPlan":
lplan_msgs.append((t, msg.longitudinalPlan))
car_state_msgs.sort(key=lambda x: x[0])
model_msgs.sort(key=lambda x: x[0])
splan_msgs.sort(key=lambda x: x[0])
lplan_msgs.sort(key=lambda x: x[0])
# Synchronize messages on model 20Hz time-grid
data_frames = []
for t_model, model_msg in model_msgs:
# Find latest corresponding messages
cs = next((m for t, m in reversed(car_state_msgs) if t <= t_model), None)
splan = next((m for t, m in reversed(splan_msgs) if t <= t_model), None)
lplan = next((m for t, m in reversed(lplan_msgs) if t <= t_model), None)
if cs is None:
continue
v_ego = cs.vEgo
v_cruise = getattr(splan, "vCruise", getattr(cs, "vCruise", 0.0))
a_chill = getattr(lplan, "aTarget", 0.0)
a_exp = getattr(model_msg.action, "desiredAcceleration", 0.0)
data_frames.append({
"t": t_model,
"v_ego": v_ego,
"v_cruise": v_cruise,
"a_chill": a_chill,
"a_exp": a_exp,
"model": model_msg,
})
return data_frames
def simulate_hem(data_frames):
"""Simulates the controller over the synchronized messages."""
controller = HybridExperimentalMode()
controller.record_diag = True
sim_results = []
for frame in data_frames:
# Lead initialization
lead = MockLead(status=False)
# Execute state update
a_out, should_stop_fused = controller.update(
v_ego=frame["v_ego"],
v_cruise=frame["v_cruise"],
lead_one=lead,
model_v2=frame["model"],
a_chill=frame["a_chill"],
a_exp=frame["a_exp"],
)
diag = dict(controller.diag)
diag["a_out"] = float(a_out)
diag["should_stop_fused"] = bool(should_stop_fused)
diag["t_rel"] = frame["t"] - data_frames[0]["t"]
sim_results.append(diag)
return sim_results
def analyze_failures(route_str, segment, label, results):
"""Analyzes decisions inside the critical deceleration window."""
total_frames = len(results)
if total_frames == 0:
return None
# Find the primary deceleration zone (where v_ego drops, or should have dropped)
v_speeds = [r["v_ego"] for r in results]
max_v = max(v_speeds)
min_v = min(v_speeds)
# Identify frames with a stop visible to vision (horizon stop or exp stop flag)
active_frames = []
for idx, r in enumerate(results):
if r.get("horizon_stopping", False) or r.get("should_stop_exp", False) or r.get("w_vision", 0.0) > 0.1:
active_frames.append(idx)
if not active_frames:
return {
"route": route_str, "segment": segment, "label": label,
"outcome": "No stop event detected by model.",
"failure_mode": "Model did not predict stop point.",
}
start_idx = min(active_frames)
end_idx = max(active_frames)
# Analyze tracking variables inside the event window
latch_decays = 0
tracking_resets = 0
early_departures = 0
kinematic_collapses = 0
for idx in range(start_idx, end_idx + 1):
r = results[idx]
# 1. Check for premature latch decay (decaying while still moving fast)
if r.get("w_vision", 0.0) < 0.2 and r.get("v_ego", 0.0) > 2.0 and r.get("horizon_stopping", False):
latch_decays += 1
# 2. Check for stop detection being cleared while speed is still high
if idx > start_idx:
prev_r = results[idx - 1]
if prev_r.get("horizon_stopping", False) and not r.get("horizon_stopping", False):
if r.get("v_ego", 0.0) > 1.0 and not r.get("is_departing", False):
tracking_resets += 1
# 3. Check for departure lockout firing while still approaching a predicted stop
if r.get("is_departing", False) and r.get("v_ego", 0.0) > 1.5 and r.get("horizon_stopping", False):
early_departures += 1
# 4. Check if the brake floor collapsed near the stop line
if r.get("a_brake_fused", 0.0) > -0.1 and r.get("v_ego", 0.0) > 1.0 and r.get("horizon_stopping", False):
kinematic_collapses += 1
# Determine primary failure mode
failure_modes = []
if latch_decays > 5:
failure_modes.append("Premature Latch Decay (w_vision collapsed)")
if tracking_resets > 0:
failure_modes.append("Stop Detection Cleared Early")
if early_departures > 5:
failure_modes.append("Early Departure Lockout Bypass (is_departing while approaching)")
if kinematic_collapses > 5:
failure_modes.append("Kinematic Brake Floor Collapse near stop line")
failure_mode = " / ".join(failure_modes) if failure_modes else "Weak general deceleration tracking"
outcome = f"Blew past stop line. Min speed reached: {min_v:.2f} m/s." if min_v > 0.5 else "Stopped but late/harsh."
return {
"route": route_str,
"segment": segment,
"label": label,
"max_v": max_v,
"min_v": min_v,
"latch_decays": latch_decays,
"tracking_resets": tracking_resets,
"early_departures": early_departures,
"kinematic_collapses": kinematic_collapses,
"outcome": outcome,
"failure_mode": failure_mode,
}
def run_suite():
all_analyses = []
plot_data = {}
for route, seg, label in ROUTES_TO_ANALYZE:
print(f"\nAnalyzing {route} segment {seg} ({label})...")
frames = fetch_telemetry(route, seg)
if not frames:
print("No frames retrieved. Skipping.")
continue
results = simulate_hem(frames)
analysis = analyze_failures(route, seg, label, results)
if analysis:
all_analyses.append(analysis)
plot_data[f"{route.split('/')[-1][:8]}_s{seg}"] = (frames, results)
# 1. Export forensic summary text report
report_path = "hem_forensic_report.txt"
print(f"\nWriting forensic report to {report_path}...")
with open(report_path, "w") as f:
f.write("=== HYBRID EXPERIMENTAL MODE (HEM) FAILURE CASE STUDY SUMMARY ===\n")
f.write(f"Analyzed {len(all_analyses)} routes where vehicle rolled stop-lines\n")
f.write("==================================================================\n\n")
# General findings
f.write("COMMON STRUCTURAL ROOT CAUSES IDENTIFIED:\n")
f.write("------------------------------------------\n")
f.write("1. Stop Detection Cleared on Model Re-acceleration:\n")
f.write(" When the trajectory velocity endpoint flips positive (a_exp > 0.1, v_horizon > 1.2)\n")
f.write(" the 'vision_departing' / 'is_departing' signal fires TRUE and clears 'horizon_stopping'\n")
f.write(" while the car is still traveling at speed close to the line, dropping the brake floor.\n\n")
f.write("2. Insufficient Latch Sustainability (w_vision decays):\n")
f.write(" If the model stops outputting a low velocity endpoint, even briefly, the vision weight\n")
f.write(" decays toward zero and unlocks cruise throttle while still closing on the stop.\n\n")
f.write("DETAILED SEGMENT TELEMETRY BREAKDOWN:\n")
f.write("-------------------------------------\n")
for a in all_analyses:
f.write(f"Route: {a['route']} | Seg: {a['segment']} ({a['label']})\n")
f.write(f" • Velocity Profile : {a['max_v']:.1f} m/s -> {a['min_v']:.1f} m/s\n")
f.write(f" • Tracker Resets : {a['tracking_resets']} frames\n")
f.write(f" • Early Departures : {a['early_departures']} frames\n")
f.write(f" • Outcome : {a['outcome']}\n")
f.write(f" • Primary Fault : {a['failure_mode']}\n")
f.write("------------------------------------------------------------------\n")
# 2. Generate tiled plot of critical variables for a subset of failures
print("Generating diagnostic plots...")
plot_keys = list(plot_data.keys())[:4] # Plot up to 4 significant failures for space
if not plot_keys:
return
fig, axes = plt.subplots(len(plot_keys), 2, figsize=(15, 3 * len(plot_keys)), sharex="col")
if len(plot_keys) == 1:
axes = np.expand_dims(axes, axis=0)
for idx, key in enumerate(plot_keys):
frames, results = plot_data[key]
t = [r["t_rel"] for r in results]
v_ego = [r["v_ego"] for r in results]
w_vis = [r["w_vision"] for r in results]
a_out = [r["a_out"] for r in results]
a_kin = [r["a_brake_fused"] for r in results]
# Left column: Speeds and Latch activations
ax_l = axes[idx, 0]
ax_l.plot(t, v_ego, color="black", lw=1.5, label="v_ego (m/s)")
ax_l_twin = ax_l.twinx()
ax_l_twin.plot(t, w_vis, color="orange", alpha=0.7, ls="--", label="w_vision (latch)")
ax_l.set_title(f"Run: {key} - Speeds", fontsize=10)
ax_l.set_ylabel("Speed (m/s)")
ax_l_twin.set_ylabel("Latch Active", color="orange")
ax_l.grid(alpha=0.3)
if idx == 0:
ax_l.legend(loc="upper left")
ax_l_twin.legend(loc="upper right")
# Right column: Acceleration commands
ax_r = axes[idx, 1]
ax_r.plot(t, a_out, color="blue", lw=1.5, label="a_out (fused)")
ax_r.plot(t, a_kin, color="red", alpha=0.6, ls=":", label="a_kinematic_stop")
ax_r.set_title(f"Run: {key} - Acceleration Commands", fontsize=10)
ax_r.set_ylabel("Accel (m/s²)")
ax_r.grid(alpha=0.3)
if idx == 0:
ax_r.legend(loc="upper right")
plt.tight_layout()
plot_out_path = "hem_failures_analysis.png"
plt.savefig(plot_out_path, dpi=150)
print(f"Diagnostic graph saved to {plot_out_path}")
if __name__ == "__main__":
run_suite()
+840
View File
@@ -0,0 +1,840 @@
#!/usr/bin/env python3
"""Offline mode-scenario estimator for CEM / Chill (CCM) / HEM / Exp.
Replays a logged route segment through the real StarPilot mode state machines
(ConditionalExperimentalMode, ConditionalChillMode, HybridExperimentalMode)
and openpilot's LongitudinalPlanner, then plots and prints what each mode's
experimental intent and acceleration target would have been at every 20 Hz frame.
Usage:
./dev python tools/replay/mode_sim.py <route> [--segment 0] [--start 30] [--end 120]
[--data_dir /path/to/routes] [--out graph.png] [--show]
[--set CESpeed=20 --set CEModelStopTime=3.0 --set CCMLead=true] [--csv out.csv]
Examples:
./dev python tools/replay/mode_sim.py afb7ef2ed593d651/00000095--9dcb90357c --segment 9
"""
from __future__ import annotations
import argparse
import bisect
import sys
import time
from pathlib import Path
from types import SimpleNamespace
from typing import Any
import numpy as np
ROOT_DIR = Path(__file__).resolve().parents[2]
if str(ROOT_DIR) not in sys.path:
sys.path.insert(0, str(ROOT_DIR))
from openpilot.common.constants import CV
from openpilot.common.realtime import DT_MDL
from openpilot.selfdrive.car.cruise import V_CRUISE_MAX, V_CRUISE_UNSET
from openpilot.selfdrive.controls.lib.longcontrol import LongCtrlState
from openpilot.selfdrive.controls.lib.longitudinal_planner import LongitudinalPlanner, get_accel_from_plan
from openpilot.selfdrive.controls.lib.longitudinal_mpc_lib.long_mpc import COMFORT_BRAKE
from openpilot.starpilot.common.experimental_state import CCStatus, CEStatus
from openpilot.starpilot.common.starpilot_utilities import calculate_lane_width, calculate_road_curvature
from openpilot.starpilot.common.starpilot_variables import CRUISING_SPEED, MINIMUM_LATERAL_ACCELERATION, PLANNER_TIME
from openpilot.starpilot.controls.lib.conditional_chill_mode import ConditionalChillMode
from openpilot.starpilot.controls.lib.conditional_experimental_mode import ConditionalExperimentalMode
from openpilot.starpilot.controls.lib.hybrid_experimental_mode import HybridExperimentalMode
from openpilot.tools.lib.logreader import LogReader, ReadMode, parse_direct, parse_indirect
from openpilot.tools.lib.route import SegmentRange
SERVICES = {
"carState", "radarState", "starpilotRadarState", "modelV2",
"longitudinalPlan", "selfdriveState", "starpilotPlan", "starpilotCarState",
"controlsState", "carParams", "liveParameters", "carControl",
}
CE_LABELS = {
CEStatus["CURVATURE"]: "CURVE",
CEStatus["LEAD"]: "LEAD",
CEStatus["SIGNAL"]: "SIGNAL",
CEStatus["SPEED"]: "SPEED",
CEStatus["SPEED_LIMIT"]: "SPEED_LIMIT",
CEStatus["STOP_LIGHT"]: "STOP_LIGHT",
}
CC_LABELS = {
CCStatus["LEAD"]: "LEAD",
CCStatus["SPEED"]: "SPEED",
}
CC_AUTO_STATUSES = {CCStatus["SPEED"], CCStatus["LEAD"]}
# CEM/CCM toggle defaults (units in m/s where appropriate).
TOGGLE_DEFAULTS: dict[str, Any] = {
# CEM
"conditional_curves": True,
"conditional_curves_lead": True,
"conditional_lead": True,
"conditional_slower_lead": True,
"conditional_stopped_lead": True,
"conditional_open_road": False,
"conditional_limit": 0.0, # CESpeed, m/s (0 = disabled)
"conditional_limit_lead": 0.0, # CESpeedLead, m/s (0 = disabled)
"conditional_signal": 0.0, # CESignalSpeed, m/s (0 = disabled)
"conditional_model_stop_time": 0.0, # CEModelStopTime, s (0 = disabled)
"conditional_signal_lane_detection": False,
"lane_detection_width": 0.0,
# Chill (CCM)
"conditional_chill_speed": 30 * CV.MPH_TO_MS, # CCMSpeed
"conditional_chill_speed_lead": 25 * CV.MPH_TO_MS, # CCMSpeedLead
"conditional_chill_speed_margin": 3 * CV.MPH_TO_MS, # CCMSetSpeedMargin
"conditional_chill_lead": True,
"conditional_chill_launch_assist": False,
# HEM
"hybrid_exp_bias": 0.0,
"hybrid_vision_brake_sensitivity": 1.0,
# StarPilot / Planner toggles
"taco_tune": False,
"classic_model": False,
"tinygrad_model": False,
"vEgoStopping": 0.05,
"hybrid_experimental_mode": False,
"radar_takeoffs": False,
"lane_change_close_gap": False,
"minimum_lane_change_speed": 0.0,
"model_version": None,
}
BOOL_KEYS = {
"conditional_curves", "conditional_curves_lead", "conditional_lead",
"conditional_slower_lead", "conditional_stopped_lead", "conditional_open_road",
"conditional_signal_lane_detection", "conditional_chill_lead",
"conditional_chill_launch_assist", "taco_tune", "classic_model", "tinygrad_model",
"hybrid_experimental_mode", "radar_takeoffs", "lane_change_close_gap",
}
class FakeParams:
def __init__(self):
self.bools = {}
self.ints = {}
def get_bool(self, key):
return bool(self.bools.get(key, False))
def put_bool(self, key, value):
self.bools[key] = bool(value)
def get_int(self, key, default=0):
return int(self.ints.get(key, default))
def put_int(self, key, value):
self.ints[key] = int(value)
def default_lead(status=False):
return SimpleNamespace(
status=status,
dRel=float("inf"),
yRel=0.0,
vRel=0.0,
aRel=0.0,
vLead=0.0,
vLeadK=0.0,
dPath=0.0,
vLat=0.0,
aLeadK=0.0,
aLeadTau=1.5,
fcw=False,
modelProb=0.0,
radar=False,
)
def as_lead(raw):
if raw is None:
return default_lead()
try:
status = bool(getattr(raw, "status", False))
except Exception:
status = False
return SimpleNamespace(
status=status,
dRel=float(getattr(raw, "dRel", float("inf"))),
yRel=float(getattr(raw, "yRel", 0.0)),
vRel=float(getattr(raw, "vRel", 0.0)),
aRel=float(getattr(raw, "aRel", 0.0)),
vLead=float(getattr(raw, "vLead", 0.0)),
vLeadK=float(getattr(raw, "vLeadK", getattr(raw, "vLead", 0.0))),
dPath=float(getattr(raw, "dPath", 0.0)),
vLat=float(getattr(raw, "vLat", 0.0)),
aLeadK=float(getattr(raw, "aLeadK", 0.0)),
aLeadTau=float(getattr(raw, "aLeadTau", 1.5)),
fcw=bool(getattr(raw, "fcw", False)),
modelProb=float(getattr(raw, "modelProb", 0.0)),
radar=bool(getattr(raw, "radar", False)),
)
def local_segment_files(data_dir, route_name, segment):
data_root = Path(data_dir)
segment_names = (f"{route_name}--{segment}", f"{route_name.replace('|', '/')}/{segment}")
filenames = ("rlog.zst", "rlog.bz2", "qlog.zst", "qlog.bz2")
identifiers = []
for segment_name in segment_names:
for filename in filenames:
candidate = data_root / segment_name / filename
if candidate.exists():
identifiers.append(str(candidate))
for filename in filenames:
explorer = data_root / f"{route_name}--{segment}--{filename}"
if explorer.exists():
identifiers.append(str(explorer))
return identifiers
def resolve_segment_identifier(route, segment, data_dir):
direct = parse_direct(route)
if direct is not None:
return [str(direct)]
parsed = parse_indirect(route)
sr = SegmentRange(parsed)
route_name = sr.route_name.replace("/", "|")
if data_dir:
identifiers = local_segment_files(data_dir, route_name, segment)
if identifiers:
return identifiers
return [f"{route_name}--{segment}"]
def load_buffers(identifiers, services):
bufs = {s: [] for s in services}
for identifier in identifiers:
try:
logreader = LogReader(identifier, default_mode=ReadMode.RLOG)
except Exception as exc:
print(f"Unable to open {identifier}: {exc}", file=sys.stderr)
continue
for evt in logreader:
which = evt.which()
if which not in services:
continue
try:
t = evt.logMonoTime * 1e-9
except Exception:
continue
bufs[which].append((t, getattr(evt, which)))
for service in services:
bufs[service].sort(key=lambda x: x[0])
return bufs
def parse_toggle_overrides(args_set):
overrides = {}
for item in args_set or []:
key, sep, raw = item.partition("=")
key = key.strip()
if not sep or key not in TOGGLE_DEFAULTS:
print(f"warning: ignoring unknown toggle override '{item}'", file=sys.stderr)
continue
raw = raw.strip()
if key in BOOL_KEYS:
overrides[key] = raw.lower() in {"1", "true", "yes", "on"}
else:
try:
overrides[key] = float(raw)
except ValueError:
print(f"warning: ignoring non-numeric toggle override '{item}'", file=sys.stderr)
return overrides
def run_simulation(grid, bufs, toggles):
ts = {s: [t for t, _ in m] for s, m in bufs.items()}
ms = {s: [m for _, m in bufs[s]] for s in bufs}
def latest(service, t):
times = ts.get(service)
if not times:
return None
idx = bisect.bisect_right(times, t) - 1
return ms[service][idx] if idx >= 0 else None
cp_candidate = None
for _, cp in bufs.get("carParams", []):
if cp is not None:
cp_candidate = cp
break
if cp_candidate is None:
cp_candidate = SimpleNamespace(
brand="toyota",
carFingerprint="TOYOTA_RAV4",
openpilotLongitudinalControl=True,
pcmCruise=False,
steerRatio=15.0,
wheelbase=2.7,
longitudinalActuatorDelay=0.2,
flags=0,
)
planner_state = SimpleNamespace(
params=FakeParams(),
params_memory=FakeParams(),
starpilot_following=SimpleNamespace(following_lead=False, slower_lead=False),
starpilot_vcruise=SimpleNamespace(
slc=SimpleNamespace(experimental_mode=False),
stop_sign_confirmed=False,
forcing_stop=False,
),
)
cem = ConditionalExperimentalMode(planner_state)
cem_ccm_detector = ConditionalExperimentalMode(planner_state)
ccm = ConditionalChillMode(planner_state, cem_ccm_detector)
hybrid = HybridExperimentalMode()
hybrid.set_tuning(toggles.hybrid_exp_bias, toggles.hybrid_vision_brake_sensitivity)
# Full openpilot Longitudinal Planner for true Chill (ACC) MPC execution
chill_long_planner = LongitudinalPlanner(cp_candidate, dt=DT_MDL)
n = len(grid)
out = {
"t": grid,
"v_ego": np.zeros(n),
"v_cruise": np.zeros(n),
"model_v0": np.zeros(n),
"lead_status": np.zeros(n, dtype=bool),
"lead_dRel": np.zeros(n),
"lead_vLead": np.zeros(n),
"cem_exp": np.zeros(n, dtype=bool),
"cem_status": np.zeros(n, dtype=int),
"ccm_exp": np.zeros(n, dtype=bool),
"ccm_status": np.zeros(n, dtype=int),
"chill_active": np.zeros(n, dtype=bool),
"logged_exp": np.zeros(n, dtype=bool),
"logged_aTarget": np.zeros(n),
"a_chill": np.zeros(n),
"a_exp": np.zeros(n),
"hem_a": np.zeros(n),
"hem_authority": np.zeros(n),
}
real_monotonic = time.monotonic
fake_clock = [0.0]
time.monotonic = lambda: fake_clock[0]
try:
for i, t in enumerate(grid):
fake_clock[0] = float(i) * DT_MDL
car_raw = latest("carState", t)
v_ego_raw = max(float(getattr(car_raw, "vEgo", 0.0)), 0.0)
car = SimpleNamespace(
vEgo=v_ego_raw,
vEgoCluster=max(float(getattr(car_raw, "vEgoCluster", v_ego_raw)), 0.0),
vCruise=float(getattr(car_raw, "vCruise", 0.0)),
standstill=bool(getattr(car_raw, "standstill", False)),
leftBlinker=bool(getattr(car_raw, "leftBlinker", False)),
rightBlinker=bool(getattr(car_raw, "rightBlinker", False)),
gasPressed=bool(getattr(car_raw, "gasPressed", False)),
brakePressed=bool(getattr(car_raw, "brakePressed", False)),
steeringAngleDeg=float(getattr(car_raw, "steeringAngleDeg", 0.0)),
aEgo=float(getattr(car_raw, "aEgo", 0.0)),
leftBlindspot=bool(getattr(car_raw, "leftBlindspot", False)),
rightBlindspot=bool(getattr(car_raw, "rightBlindspot", False)),
)
v_ego = car.vEgo
radar = latest("radarState", t)
lead = as_lead(getattr(radar, "leadOne", None) if radar is not None else None)
lead2 = as_lead(getattr(radar, "leadTwo", None) if radar is not None else None)
sradar = latest("starpilotRadarState", t)
lead_left = as_lead(getattr(sradar, "leadLeft", None) if sradar is not None else None)
lead_right = as_lead(getattr(sradar, "leadRight", None) if sradar is not None else None)
lplan = latest("longitudinalPlan", t)
sds = latest("selfdriveState", t)
splan = latest("starpilotPlan", t)
scs = latest("starpilotCarState", t)
model_v2 = latest("modelV2", t)
live_params = latest("liveParameters", t)
car_control = latest("carControl", t)
controls_state = latest("controlsState", t)
car_params = latest("carParams", t) or cp_candidate
controls_enabled = bool(getattr(sds, "enabled", False))
conditional_tracking_active = controls_enabled or bool(getattr(scs, "alwaysOnLateralEnabled", False))
v_cruise = float(getattr(splan, "vCruise", 0.0) or 0.0)
if not (v_cruise > 0):
v_cruise_kph = float(getattr(car_raw, "vCruise", 0.0) or 0.0)
if 0 < v_cruise_kph < V_CRUISE_UNSET:
v_cruise = min(v_cruise_kph, V_CRUISE_MAX) * CV.KPH_TO_MS
else:
v_cruise = v_ego
tracking_lead = bool(getattr(splan, "trackingLead", False))
if not tracking_lead:
tracking_lead = bool(getattr(lplan, "hasLead", False))
model_length = 0.0
if model_v2 is not None:
try:
pos_x = getattr(model_v2, "position", None)
if pos_x is not None and len(pos_x.x):
model_length = float(pos_x.x[-1])
except Exception:
model_length = 0.0
forcing_stop = bool(getattr(splan, "forcingStop", False))
raw_model_stopped = model_length < CRUISING_SPEED * PLANNER_TIME
model_stopped = raw_model_stopped or forcing_stop
road_curvature_detected = False
if model_v2 is not None:
try:
road_curvature, _time_to_curve = calculate_road_curvature(model_v2, v_ego)
road_curvature_detected = (
(1 / abs(road_curvature)) ** 0.5 < v_ego > CRUISING_SPEED
and not (car.leftBlinker or car.rightBlinker)
)
except Exception:
road_curvature_detected = False
try:
curvature = float(getattr(controls_state, "curvature", 0.0))
except Exception:
curvature = 0.0
lateral_accel = v_ego ** 2 * curvature
driving_in_curve = abs(lateral_accel) >= MINIMUM_LATERAL_ACCELERATION
lane_width_left = 0.0
lane_width_right = 0.0
if model_v2 is not None:
try:
lane_lines = getattr(model_v2, "laneLines", None)
road_edges = getattr(model_v2, "roadEdges", None)
if lane_lines is not None and len(lane_lines) >= 4:
edge_left = road_edges[0] if road_edges is not None and len(road_edges) else None
edge_right = road_edges[1] if road_edges is not None and len(road_edges) > 1 else None
lane_width_left = calculate_lane_width(lane_lines[0], lane_lines[1], edge_left)
lane_width_right = calculate_lane_width(lane_lines[3], lane_lines[2], edge_right)
except Exception:
pass
t_follow = float(getattr(splan, "tFollow", 1.45))
following_lead = tracking_lead and lead.dRel < (t_follow * 2) * v_ego
slower_lead = False
if (conditional_tracking_active and tracking_lead and toggles.conditional_slower_lead
and lead.status and lead.vLead < v_ego):
distance_factor = max(lead.dRel - (lead.vLead * t_follow), 1)
braking_offset = float(np.clip(min(v_ego - lead.vLead, lead.vLead) - COMFORT_BRAKE, 1, distance_factor))
slower_lead = braking_offset > 1
planner_state.lead_one = lead
planner_state.tracking_lead = tracking_lead
planner_state.model_length = model_length
planner_state.raw_model_stopped = raw_model_stopped
planner_state.model_stopped = model_stopped
planner_state.road_curvature_detected = road_curvature_detected
planner_state.driving_in_curve = driving_in_curve
planner_state.lane_width_left = lane_width_left
planner_state.lane_width_right = lane_width_right
planner_state.starpilot_following.following_lead = following_lead
planner_state.starpilot_following.slower_lead = slower_lead
planner_state.starpilot_vcruise.forcing_stop = forcing_stop
raw_personality = getattr(sds, "personality", None)
if hasattr(raw_personality, "raw"):
personality_val = int(raw_personality.raw)
elif isinstance(raw_personality, (int, float)):
personality_val = int(raw_personality)
else:
personality_val = 0
long_ctrl_state = getattr(controls_state, "longControlState", LongCtrlState.pid)
sm_dict = {
"carState": car,
"radarState": SimpleNamespace(leadOne=lead, leadTwo=lead2),
"starpilotRadarState": SimpleNamespace(leadLeft=lead_left, leadRight=lead_right),
"starpilotCarState": SimpleNamespace(
trafficModeEnabled=bool(getattr(scs, "trafficModeEnabled", False)),
alwaysOnLateralEnabled=bool(getattr(scs, "alwaysOnLateralEnabled", False)),
dashboardStopSign=int(getattr(scs, "dashboardStopSign", 0)),
accelPressed=bool(getattr(scs, "accelPressed", False)),
),
"selfdriveState": SimpleNamespace(
enabled=controls_enabled,
experimentalMode=False, # Enforce ACC/Chill mode evaluation in MPC
personality=personality_val,
),
"longitudinalPlan": SimpleNamespace(
hasLead=bool(getattr(lplan, "hasLead", False)),
allowThrottle=bool(getattr(lplan, "allowThrottle", True)),
shouldStop=bool(getattr(lplan, "shouldStop", False)),
aTarget=float(getattr(lplan, "aTarget", 0.0)),
),
"starpilotPlan": SimpleNamespace(
vCruise=v_cruise,
tFollow=t_follow,
trackingLead=tracking_lead,
redLight=bool(getattr(splan, "redLight", False)),
forcingStop=forcing_stop,
forcingStopLength=float(getattr(splan, "forcingStopLength", 100.0)),
minAcceleration=float(getattr(splan, "minAcceleration", -3.5)),
maxAcceleration=float(getattr(splan, "maxAcceleration", 1.5)),
accelerationJerk=float(getattr(splan, "accelerationJerk", 1.0)),
dangerJerk=float(getattr(splan, "dangerJerk", 1.0)),
speedJerk=float(getattr(splan, "speedJerk", 1.0)),
dangerFactor=float(getattr(splan, "dangerFactor", 1.0)),
disableThrottle=bool(getattr(splan, "disableThrottle", False)),
),
"controlsState": SimpleNamespace(
longControlState=long_ctrl_state,
forceDecel=bool(getattr(controls_state, "forceDecel", False)),
curvature=curvature,
),
"liveParameters": SimpleNamespace(
angleOffsetDeg=float(getattr(live_params, "angleOffsetDeg", 0.0)) if live_params else 0.0,
),
"carControl": SimpleNamespace(
orientationNED=getattr(car_control, "orientationNED", [0.0, 0.0, 0.0]) if car_control else [0.0, 0.0, 0.0],
),
"modelV2": model_v2,
"carParams": car_params,
}
# Run CEM / CCM State Machines
if conditional_tracking_active:
cem.update(v_ego, sm_dict, toggles, v_cruise)
ccm.update(v_ego, v_cruise, sm_dict, toggles)
else:
cem.experimental_mode = False
cem.status_value = CEStatus["OFF"]
ccm.experimental_mode = True
ccm.status_value = CCStatus["OFF"]
chill_active = (not ccm.experimental_mode) and ccm.status_value in CC_AUTO_STATUSES
# Compute accurate MPC Chill (ACC) Acceleration
chill_long_planner.update(sm_dict, toggles)
a_chill = float(chill_long_planner.output_a_target)
# Determine realistic Exp acceleration target
a_exp = 0.0
if model_v2 is not None:
try:
action_obj = getattr(model_v2, "action", None)
if action_obj is not None and hasattr(action_obj, "desiredAcceleration"):
a_exp = float(action_obj.desiredAcceleration)
else:
accel_x = getattr(model_v2, "acceleration", None)
if accel_x is not None and len(accel_x.x):
a_exp = float(accel_x.x[0])
except Exception:
a_exp = 0.0
# Hybrid Experimental Mode continuous fusion
a_hem, _should_stop_fused = hybrid.update(
v_ego=v_ego, v_cruise=v_cruise, lead_one=lead, model_v2=model_v2,
a_chill=a_chill, a_exp=a_exp,
)
authority = hybrid.w_vision
model_v0 = v_ego
if model_v2 is not None:
try:
vel_x = getattr(model_v2, "velocity", None)
if vel_x is not None and len(vel_x.x):
model_v0 = float(vel_x.x[0])
except Exception:
model_v0 = v_ego
out["v_ego"][i] = v_ego
out["v_cruise"][i] = v_cruise
out["model_v0"][i] = model_v0
out["lead_status"][i] = lead.status
out["lead_dRel"][i] = lead.dRel
out["lead_vLead"][i] = lead.vLead
out["cem_exp"][i] = bool(cem.experimental_mode)
out["cem_status"][i] = int(cem.status_value)
out["ccm_exp"][i] = bool(ccm.experimental_mode)
out["ccm_status"][i] = int(ccm.status_value)
out["chill_active"][i] = chill_active
out["logged_exp"][i] = bool(getattr(sds, "experimentalMode", False))
out["logged_aTarget"][i] = float(getattr(lplan, "aTarget", 0.0))
out["a_chill"][i] = a_chill
out["a_exp"][i] = a_exp
out["hem_a"][i] = a_hem
out["hem_authority"][i] = authority
finally:
time.monotonic = real_monotonic
return out
def intervals_for(t, keys):
"""Group consecutive identical non-zero status keys into [start, end, key] spans."""
spans = []
cur = 0
for i, k in enumerate(keys):
if k and k == cur:
spans[-1][1] = t[i]
elif k:
spans.append([t[i], t[i], k])
cur = k
else:
cur = 0
return spans
def print_summary(out):
t = out["t"]
n = len(t)
if n == 0:
print("No simulation frames to summarize.")
return
v_ego_mph = out["v_ego"] * CV.MS_TO_MPH
v_cruise_mph = out["v_cruise"] * CV.MS_TO_MPH
standstill_mask = out["v_ego"] < 0.2
standstill_pct = 100.0 * np.mean(standstill_mask)
stops = 0
for i in range(1, n):
if standstill_mask[i] and not standstill_mask[i - 1]:
stops += 1
cem_switches = int(np.sum(np.diff(out["cem_exp"].astype(int)) != 0))
ccm_switches = int(np.sum(np.diff(out["chill_active"].astype(int)) != 0))
print("\n" + "=" * 78)
print(f" MODE SIMULATION SUMMARY & TELEMETRY LOG ({t[0]:.1f}s -> {t[-1]:.1f}s | {n} frames)")
print("=" * 78)
print("\n[1. DRIVE PROFILE]")
print(f" • Speed (v_ego) : Min {v_ego_mph.min():.1f} mph | Avg {v_ego_mph.mean():.1f} mph | Max {v_ego_mph.max():.1f} mph")
print(f" • Cruise Set Speed : Min {v_cruise_mph.min():.1f} mph | Avg {v_cruise_mph.mean():.1f} mph | Max {v_cruise_mph.max():.1f} mph")
print(f" • Standstill Time : {standstill_pct:.1f}% ({stops} full stop events)")
print(f" • Lead Presence : {100.0 * np.mean(out['lead_status']):.1f}% of route")
print("\n[2. MODE INTENT & TRIGGER BREAKDOWN]")
print(f" • Logged (Actual) : Exp {100.0 * out['logged_exp'].mean():.1f}%")
print(f" • CEM (Conditional) : Exp {100.0 * out['cem_exp'].mean():.1f}% | {cem_switches} mode toggles")
cem_reasons = []
for status_code, label in CE_LABELS.items():
pct = 100.0 * np.mean((out["cem_status"] == status_code) & out["cem_exp"])
if pct > 0.1:
cem_reasons.append(f"{label}: {pct:.1f}%")
if cem_reasons:
print(f" └─ CEM Active Triggers -> {', '.join(cem_reasons)}")
print(f" • Chill Mode (CCM) : Chill {100.0 * out['chill_active'].mean():.1f}% | Exp {100.0 * out['ccm_exp'].mean():.1f}% | {ccm_switches} toggles")
ccm_reasons = []
for status_code, label in CC_LABELS.items():
pct = 100.0 * np.mean((out["ccm_status"] == status_code) & out["chill_active"])
if pct > 0.1:
ccm_reasons.append(f"{label}: {pct:.1f}%")
if ccm_reasons:
print(f" └─ Chill Active Reasons -> {', '.join(ccm_reasons)}")
auth = out["hem_authority"]
chill_dom = 100.0 * np.mean(auth < 0.35)
blended = 100.0 * np.mean((auth >= 0.35) & (auth <= 0.65))
exp_dom = 100.0 * np.mean(auth > 0.65)
print(f" • HEM (Hybrid) : Mean Exp Authority {auth.mean():.2f}")
print(f" └─ Distribution -> Chill-dominant (<0.35): {chill_dom:.1f}% | Blended (0.35-0.65): {blended:.1f}% | Exp-dominant (>0.65): {exp_dom:.1f}%")
print("\n[3. ACCELERATION ENVELOPE (m/s²)]")
print(f" {'Signal':<16} | {'Min (Max Brake)':<15} | {'Mean':<10} | {'Max (Max Throttle)':<18}")
print(" " + "-" * 66)
print(f" {'Logged aTarget':<16} | {out['logged_aTarget'].min():<15.2f} | {out['logged_aTarget'].mean():<10.2f} | {out['logged_aTarget'].max():<18.2f}")
print(f" {'Chill a':<16} | {out['a_chill'].min():<15.2f} | {out['a_chill'].mean():<10.2f} | {out['a_chill'].max():<18.2f}")
print(f" {'Exp a':<16} | {out['a_exp'].min():<15.2f} | {out['a_exp'].mean():<10.2f} | {out['a_exp'].max():<18.2f}")
print(f" {'HEM Fused a':<16} | {out['hem_a'].min():<15.2f} | {out['hem_a'].mean():<10.2f} | {out['hem_a'].max():<18.2f}")
print("\n[4. KEY EVENT CHRONOLOGY (Sampled Significant Transitions)]")
print(f" {'Time (s)':<9} | {'vEgo':<8} | {'CEM Status':<12} | {'CCM':<7} | {'a_chill':<8} | {'a_exp':<8} | {'HEM a':<8} | {'Auth':<5} | Event Context")
print(" " + "-" * 95)
step = max(1, n // 18)
sample_indices = set(range(0, n, step))
hard_brakes = np.where(out["hem_a"] < -1.2)[0]
if len(hard_brakes) > 0:
sample_indices.update(hard_brakes[::max(1, len(hard_brakes)//4)])
standstills = np.where(standstill_mask)[0]
if len(standstills) > 0:
sample_indices.update(standstills[::max(1, len(standstills)//3)])
sorted_indices = sorted(list(sample_indices))[:22]
for idx in sorted_indices:
cur_t = t[idx] - t[0]
ego_mph = v_ego_mph[idx]
cem_st = CE_LABELS.get(out["cem_status"][idx], "OFF") if out["cem_exp"][idx] else "CHILL"
ccm_st = "CHILL" if out["chill_active"][idx] else "EXP"
ac = out["a_chill"][idx]
ae = out["a_exp"][idx]
ah = out["hem_a"][idx]
au = out["hem_authority"][idx]
ctx = []
if out["lead_status"][idx]:
ctx.append(f"Lead {out['lead_dRel'][idx]:.0f}m")
if ego_mph < 1.0:
ctx.append("Standstill")
elif ah < -1.0:
ctx.append("Braking")
elif ah > 0.8:
ctx.append("Accelerating")
else:
ctx.append("Cruising")
if abs(ac - ae) > 1.0:
ctx.append("Disagreement")
context_str = ", ".join(ctx)
print(f" {cur_t:<9.1f} | {ego_mph:<5.1f}mph | {cem_st:<12} | {ccm_st:<7} | {ac:<8.2f} | {ae:<8.2f} | {ah:<8.2f} | {au:<5.2f} | {context_str}")
print("=" * 78 + "\n")
def plot_results(out, toggles, args):
import matplotlib
if not args.show:
matplotlib.use("Agg")
import matplotlib.pyplot as plt
t = out["t"]
n = len(t)
fig, (ax_speed, ax_mode, ax_accel) = plt.subplots(
3, 1, figsize=(14, 10), sharex=True,
gridspec_kw={"height_ratios": [2, 3, 2]},
)
fig.suptitle(f"Mode scenario estimate {args.route} seg {args.segment}", fontsize=11)
ax_speed.plot(t, out["v_ego"] * CV.MS_TO_MPH, color="black", lw=1.4, label="v_ego")
ax_speed.plot(t, out["v_cruise"] * CV.MS_TO_MPH, color="tab:blue", lw=1.0, ls="--", label="v_cruise")
ax_speed.plot(t, out["model_v0"] * CV.MS_TO_MPH, color="tab:green", lw=1.0, alpha=0.7, label="model v[0]")
ax_speed.set_ylabel("mph")
ax_speed.legend(loc="upper right", fontsize=8, ncol=3)
ax_speed.grid(alpha=0.3)
rows = [
("Exp (always)", out["t"], np.ones(n, dtype=bool), "tab:blue", 3.0),
("CEM", out["t"], out["cem_exp"], "tab:orange", 2.0),
("Chill", out["t"], out["chill_active"], "tab:green", 1.0),
("Logged", out["t"], out["logged_exp"], "tab:gray", 0.0),
]
for label, tt, active, color, ycenter in rows:
ax_mode.fill_between(tt, ycenter - 0.28, ycenter + 0.28, where=active,
step="post", color=color, alpha=0.55, edgecolor="none")
cem_spans = intervals_for(t, [out["cem_status"][i] if out["cem_exp"][i] else 0 for i in range(n)])
for a, b, k in cem_spans:
if b - a > 0.8 and k in CE_LABELS:
ax_mode.text((a + b) / 2, 2.0 + 0.34, CE_LABELS[k], ha="center", fontsize=7, color="tab:orange")
cc_spans = intervals_for(t, [out["ccm_status"][i] if out["chill_active"][i] else 0 for i in range(n)])
for a, b, k in cc_spans:
if b - a > 0.8 and k in CC_LABELS:
ax_mode.text((a + b) / 2, 1.0 + 0.34, CC_LABELS[k], ha="center", fontsize=7, color="tab:green")
ax_hem = ax_mode.twinx()
ax_hem.plot(t, out["hem_authority"], color="tab:cyan", lw=1.2, label="HEM exp-authority")
ax_hem.set_ylim(0, 1)
ax_hem.set_ylabel("HEM exp authority", color="tab:cyan", fontsize=8)
ax_hem.tick_params(axis="y", labelcolor="tab:cyan", labelsize=7)
ax_mode.set_yticks([3.0, 2.0, 1.0, 0.0])
ax_mode.set_yticklabels(["Exp", "CEM", "Chill", "Logged"])
ax_mode.set_ylim(-0.6, 3.6)
ax_mode.grid(axis="y", alpha=0.3)
ax_accel.plot(t, out["logged_aTarget"], color="tab:gray", lw=1.2, label="logged aTarget")
ax_accel.plot(t, out["a_chill"], color="tab:blue", lw=1.0, ls="--", label="chill (ACC) a")
ax_accel.plot(t, out["a_exp"], color="tab:orange", lw=1.0, ls=":", label="exp (model) a")
ax_accel.plot(t, out["hem_a"], color="tab:cyan", lw=1.4, label="HEM fused a")
ax_accel.set_ylabel("accel (m/s²)")
ax_accel.set_xlabel("time (s, segment-relative)")
ax_accel.legend(loc="upper right", fontsize=8, ncol=2)
ax_accel.grid(alpha=0.3)
fig.tight_layout()
if args.out:
out_path = Path(args.out)
out_path.parent.mkdir(parents=True, exist_ok=True)
fig.savefig(out_path, dpi=150)
print(f"Saved graph to {out_path}")
if args.show:
plt.show()
plt.close(fig)
def main(argv=None):
parser = argparse.ArgumentParser(
description="Estimate CEM/Chill/HEM/Exp mode intent from a logged route and plot a comparison graph.")
parser.add_argument("route", help="route (e.g. dongle|2023-07-27--13-01-19 or dongle/2023-07-27--13-01-19/0)")
parser.add_argument("--segment", type=int, default=0, help="segment index (default 0)")
parser.add_argument("--data_dir", help="local directory containing route files")
parser.add_argument("--start", type=float, default=None, help="start seconds (segment-relative)")
parser.add_argument("--end", type=float, default=None, help="end seconds (segment-relative)")
parser.add_argument("--out", default="mode_sim.png", help="output PNG path")
parser.add_argument("--show", action="store_true", help="show the plot window instead of saving only")
parser.add_argument("--csv", default=None, help="optional CSV output of the time series")
parser.add_argument("--set", action="append", default=[], metavar="KEY=value",
help="override a mode toggle, e.g. --set CESpeed=20 --set CEModelStopTime=3.0 --set CCMLead=false")
args = parser.parse_args(argv)
overrides = parse_toggle_overrides(args.set)
defaults = dict(TOGGLE_DEFAULTS)
defaults.update(overrides)
toggles = SimpleNamespace(**defaults)
for key, value in overrides.items():
print(f"toggle override: {key} = {value}")
identifiers = resolve_segment_identifier(args.route, args.segment, args.data_dir)
if not identifiers:
print(f"No segment data found for {args.route} seg {args.segment}", file=sys.stderr)
return 1
print(f"Loading: {identifiers}")
bufs = load_buffers(identifiers, SERVICES)
if not any(bufs[s] for s in SERVICES):
print("No messages loaded.", file=sys.stderr)
return 1
all_min = min(t for s in SERVICES for t, _ in bufs[s])
all_max = max(t for s in SERVICES for t, _ in bufs[s])
start = all_min if args.start is None else all_min + args.start
end = all_max if args.end is None else min(all_max, all_min + args.end)
if end - start < DT_MDL:
print(f"Empty time window [{start:.1f}, {end:.1f}]. Check --start/--end.", file=sys.stderr)
return 1
grid = np.arange(start, end, DT_MDL)
print(f"Window: {start - all_min:.1f}s -> {end - all_min:.1f}s ({len(grid)} frames at {1 / DT_MDL:.0f} Hz)")
out = run_simulation(grid, bufs, toggles)
if args.csv:
import csv
csv_path = Path(args.csv)
csv_path.parent.mkdir(parents=True, exist_ok=True)
with open(csv_path, "w", newline="") as f:
writer = csv.writer(f)
writer.writerow(["t", "v_ego", "v_cruise", "model_v0", "cem_exp", "cem_status",
"ccm_exp", "ccm_status", "chill_active", "logged_exp", "logged_aTarget",
"a_chill", "a_exp", "hem_a", "hem_authority"])
for i in range(len(out["t"])):
writer.writerow([out["t"][i], out["v_ego"][i], out["v_cruise"][i], out["model_v0"][i],
int(out["cem_exp"][i]), out["cem_status"][i],
int(out["ccm_exp"][i]), out["ccm_status"][i], int(out["chill_active"][i]),
int(out["logged_exp"][i]), out["logged_aTarget"][i],
out["a_chill"][i], out["a_exp"][i], out["hem_a"][i], out["hem_authority"][i]])
print(f"Saved CSV to {csv_path}")
print_summary(out)
plot_results(out, toggles, args)
return 0
if __name__ == "__main__":
raise SystemExit(main())