Compare commits

...

1 Commits

Author SHA1 Message Date
Prabhaav Pillai 632db8bfd3 Add HybridExperimental mode 2026-08-25 04:42:59 -04:00
15 changed files with 1373 additions and 19 deletions
+3
View File
@@ -238,6 +238,9 @@ 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}},
{"CurvatureData", {PERSISTENT | DONT_LOG, JSON, "{}", "{}"}},
{"CurveSpeedController", {PERSISTENT, BOOL, "1", "0", 1, SETTINGS_SIMPLE}},
{"CurveSpeedControllerNoLead", {PERSISTENT, BOOL, "0", "0", 1, SETTINGS_SIMPLE}},
+30 -11
View File
@@ -9,6 +9,7 @@ from openpilot.common.filter_simple import FirstOrderFilter
from openpilot.common.realtime import DT_MDL
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
@@ -596,6 +597,7 @@ 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()
if self.is_preap:
try:
@@ -1936,6 +1938,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 +2287,42 @@ 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)):
self.hybrid_controller.set_tuning(
getattr(starpilot_toggles, "hybrid_exp_bias", 0.0),
getattr(starpilot_toggles, "hybrid_vision_brake_sensitivity", 1.0),
)
output_a_target_e2e = sm['modelV2'].action.desiredAcceleration
output_should_stop_e2e = sm['modelV2'].action.shouldStop
output_a_target = self.hybrid_controller.update(
v_ego=scene_v_ego,
v_cruise=v_cruise,
lead_one=self.lead_one,
model_v2=sm['modelV2'],
a_chill=output_a_target_mpc,
a_exp=output_a_target_e2e,
)
output_should_stop = output_should_stop_mpc or output_should_stop_e2e
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
else:
output_a_target = output_a_target_mpc
output_should_stop = output_should_stop_mpc
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
@@ -514,6 +514,46 @@ def test_experimental_mlsim_uses_vehicle_min_accel_floor(model_version):
assert planner.output_a_target < comfort_min_accel
def test_hybrid_mode_shields_unconfirmed_vision_braking():
v_ego = 20.0
desired_accel = -2.0
CP = CarInterface.get_non_essential_params(CAR.HONDA_CIVIC)
# Hybrid mode clamps unconfirmed vision braking to a gentle coast (no lead,
# model not predicting a stop), instead of passing the raw E2E target.
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.6
# Without the hybrid shield, 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,
@@ -175,6 +175,23 @@ def test_lateral_resume_delay_ignores_signal_cycles_that_never_slow_enough(monke
planner.shutdown()
def test_hybrid_mode_runs_continuous_controller(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.hybrid_controller is not None
assert planner.hybrid_acceleration != 0.0
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())
+12
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,11 @@ 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 _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 +63,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 HYBRID_EXPERIMENTAL_COLOR
if enabled and state.conditional_status in CEM_DISABLED_OVERRIDE_STATUSES:
return CEM_OVERRIDE_COLOR
if enabled and state.sm["selfdriveState"].experimentalMode:
@@ -67,6 +75,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 HYBRID_EXPERIMENTAL_COLOR
if state.conditional_status in CEM_ACTIVE_STATUSES:
return EXPERIMENTAL_COLOR
return get_border_color(state)
@@ -85,6 +95,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 HYBRID_EXPERIMENTAL_COLOR
if enabled and state.conditional_status in CEM_DISABLED_OVERRIDE_STATUSES:
return CEM_OVERRIDE_COLOR
if enabled and state.sm["selfdriveState"].experimentalMode:
+17 -1
View File
@@ -3,16 +3,18 @@ from types import SimpleNamespace
from openpilot.selfdrive.ui.lib.starpilot_status import (
DISENGAGED_COLOR,
ENGAGED_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):
return SimpleNamespace(
sm={
"selfdriveState": SimpleNamespace(enabled=enabled, experimentalMode=False),
@@ -24,6 +26,7 @@ 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},
)
@@ -44,6 +47,19 @@ def test_lateral_active_colors_remain_unchanged():
assert _rgb(get_border_color(_state())) == _rgb(DISENGAGED_COLOR)
def test_hybrid_experimental_mode_uses_purple_border():
state = _state(enabled=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_requires_enabled():
assert _rgb(get_border_color(_state(hybrid=True))) == _rgb(DISENGAGED_COLOR)
assert _rgb(get_border_color(_state(enabled=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": "Continuous Hybrid Control",
"description": "Fuse the crisp throttle response of classical ACC with the model's early, natural vision braking using a single continuous controller. Classical ACC always provides the hard safety distance, while the model's vision 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,152 @@
#!/usr/bin/env python3
import numpy as np
from openpilot.common.realtime import DT_MDL
from openpilot.selfdrive.controls.lib.longitudinal_mpc_lib.long_mpc import STOP_DISTANCE
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 float((1.0 - t) * a + t * b)
def sigmoid(x: float, k: float = 4.0, x0: float = 0.0) -> float:
"""Smooth 0-to-1 activation curve."""
z = np.clip(-k * (x - x0), -30.0, 30.0)
return float(1.0 / (1.0 + np.exp(z)))
def smooth_min(a: float, b: float, k: float = 6.0) -> float:
return lerp(b, a, sigmoid(b - a, k=k))
def smooth_max(a: float, b: float, k: float = 6.0) -> float:
return lerp(b, a, sigmoid(a - b, k=k))
class HybridExperimentalMode:
"""
Fuses Chill Mode (radar/lead tracking) and Experimental Mode (vision/stop signs/lights):
1. Detects vision braking intent from the E2E model trajectory.
2. Blends Chill and Exp acceleration smoothly based on intent.
3. Holds 0 m/s at standstills to prevent creep.
4. Falls back safely to Chill if lead vehicle distance is compromised.
5. Slew-rate limits acceleration to respect vehicle jerk limits.
"""
# Base physical actuator jerk limits (m/s^3)
BASE_MAX_JERK_BRAKE = 3.5
BASE_MAX_JERK_ACCEL = 5.5
# Base safety floor parameters
BASE_T_FOLLOW = 1.45
BASE_STOP_DISTANCE = STOP_DISTANCE # 6.0 m
def __init__(self):
self.DT = DT_MDL
self.prev_a_target = 0.0
self.exp_authority = 0.5
# User tuning
self.HYBRID_EXP_BIAS = 0.0 # [-1.0, 1.0]
self.VISION_BRAKE_SENSITIVITY = 1.0 # [0.0, 2.0]
# Active profile parameters
self.t_follow = self.BASE_T_FOLLOW
self.jerk_factor = 1.0
self._update_profile_limits(self.t_follow, self.jerk_factor)
def reset(self, a: float = 0.0):
self.prev_a_target = float(a)
self.exp_authority = 0.5
def set_tuning(self, exp_bias: float, vision_brake_sensitivity: float, t_follow=None, jerk_factor=None):
self.HYBRID_EXP_BIAS = float(np.clip(exp_bias, -1.0, 1.0))
self.VISION_BRAKE_SENSITIVITY = float(np.clip(vision_brake_sensitivity, 0.0, 2.0))
if t_follow is not None or jerk_factor is not None:
self._update_profile_limits(t_follow, jerk_factor)
def _update_profile_limits(self, t_follow, jerk_factor=1.0):
"""Updates safety headway floor and jerk limits based on active driving profile."""
self.t_follow = float(t_follow) if t_follow is not None else self.BASE_T_FOLLOW
self.jerk_factor = float(np.clip(jerk_factor if jerk_factor is not None else 1.0, 0.25, 2.0))
# Safety buffer floors
self.T_FOLLOW_SAFE = float(np.clip(self.t_follow * 0.80, 1.00, 1.60))
self.D_STATIC_SAFE = float(np.clip(self.BASE_STOP_DISTANCE * 0.75, 3.5, 6.0))
# Jerk rate limits
self.MAX_JERK_ACCEL = float(np.clip(self.BASE_MAX_JERK_ACCEL * self.jerk_factor, 2.5, 8.0))
self.MAX_JERK_BRAKE = float(np.clip(self.BASE_MAX_JERK_BRAKE * self.jerk_factor, 1.8, 6.0))
@staticmethod
def _get_model_trajectory_v(model_v2, v_ego: float) -> np.ndarray:
velocity = getattr(model_v2, "velocity", None)
traj_v = getattr(velocity, "x", None) if velocity is not None else None
if traj_v is None or len(traj_v) == 0:
return np.array([v_ego], dtype=float)
return np.asarray(traj_v, dtype=float)
def update(self, v_ego, v_cruise, lead_one, model_v2, a_chill, a_exp,
t_follow=None, jerk_factor=None):
# 0. Sync profile parameters if passed per-frame
if (t_follow is not None and abs(t_follow - self.t_follow) > 1e-4) or \
(jerk_factor is not None and abs(jerk_factor - self.jerk_factor) > 1e-4):
self._update_profile_limits(t_follow, jerk_factor)
lead_status = bool(getattr(lead_one, "status", False))
lead_d_rel = float(getattr(lead_one, "dRel", 150.0))
# 1. VISION INTENT DETECTION (How urgently does the vision model want to slow?)
traj_v = self._get_model_trajectory_v(model_v2, v_ego)
v_terminal = float(traj_v[-1])
v_min = float(np.min(traj_v))
v_ref = max(v_ego, 2.0)
speed_drop_ratio = max(0.0, (v_ego - v_min) / v_ref)
stop_ahead_intent = max(0.0, (v_ego - v_terminal) / v_ref) * sigmoid(v_ego, k=3.0, x0=1.0)
model_decel_strength = max(0.0, -a_exp / 3.0)
raw_vision_metric = max(speed_drop_ratio, stop_ahead_intent, model_decel_strength)
w_vision = float(np.clip(raw_vision_metric * self.VISION_BRAKE_SENSITIVITY, 0.0, 1.0))
# Experimental authority weight (bias + vision confidence)
base_auth = 0.5 + (0.35 * self.HYBRID_EXP_BIAS)
alpha_exp = float(np.clip(base_auth + (0.5 * w_vision), 0.0, 1.0))
self.exp_authority = alpha_exp
# 2. ACCELERATION FUSION (Throttle vs Braking Regimes)
# Throttle: Be responsive (smooth_max), but prioritize stopping if vision sees a stop
a_throttle_optimal = smooth_max(a_chill, a_exp, k=4.0)
a_throttle_conservative = smooth_min(a_chill, a_exp, k=4.0)
a_throttle_fused = lerp(a_throttle_optimal, a_throttle_conservative, w_vision)
# Braking: Smoothly hand control to Exp based on vision confidence
a_brake_fused = lerp(a_chill, a_exp, alpha_exp)
# Pick between Accel and Brake regimes
phase_metric = smooth_min(a_chill, a_exp, k=4.0)
w_accel = sigmoid(phase_metric, k=3.0, x0=-0.1)
a_fused = lerp(a_brake_fused, a_throttle_fused, w_accel)
# 3. STANDSTILL ANCHOR (Prevent creeping at 0 mph)
is_stopped = sigmoid(0.3 - v_ego, k=8.0, x0=0.0)
is_terminal_stopped = sigmoid(0.8 - v_terminal, k=4.0, x0=0.0)
standstill_weight = is_stopped * is_terminal_stopped
a_anchored = lerp(a_fused, smooth_min(a_fused, 0.0, k=8.0), standstill_weight)
# 4. SAFETY BARRIER (Lead Vehicle Proximity Check)
d_static_effective = self.D_STATIC_SAFE + max(0.0, 1.5 * (1.0 - (v_ego / 4.0)))
d_safe = (v_ego * self.T_FOLLOW_SAFE) + d_static_effective
# Compute safety risk if lead is within minimum buffer
distance_ratio = (lead_d_rel - d_static_effective) / max(d_safe - d_static_effective, 1.0)
lead_safety_risk = sigmoid(1.0 - distance_ratio, k=5.0, x0=0.0) * float(lead_status)
# Fall back to Chill braking if Chill is more conservative
a_emergency_brake = smooth_min(a_anchored, a_chill, k=6.0)
a_safe = lerp(a_anchored, a_emergency_brake, lead_safety_risk)
# 5. ASYMMETRIC SLEW FILTER (Limit Jerk)
jerk_limit = self.MAX_JERK_ACCEL if a_safe >= self.prev_a_target else self.MAX_JERK_BRAKE
max_delta = jerk_limit * self.DT
self.prev_a_target = float(np.clip(a_safe, self.prev_a_target - max_delta, self.prev_a_target + max_delta))
return self.prev_a_target
+60 -1
View File
@@ -24,6 +24,7 @@ from openpilot.starpilot.common.starpilot_utilities import calculate_lane_width,
from openpilot.starpilot.common.starpilot_variables import CRUISING_SPEED, MINIMUM_LATERAL_ACCELERATION, PLANNER_TIME, THRESHOLD
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.starpilot.controls.lib.starpilot_acceleration import StarPilotAcceleration
from openpilot.starpilot.controls.lib.starpilot_events import StarPilotEvents
from openpilot.starpilot.controls.lib.starpilot_following import StarPilotFollowing
@@ -68,6 +69,7 @@ class StarPilotPlanner:
self.starpilot_acceleration = StarPilotAcceleration(self)
self.starpilot_cem = ConditionalExperimentalMode(self)
self.starpilot_ccm = ConditionalChillMode(self, self.starpilot_cem)
self.hybrid_controller = HybridExperimentalMode()
self.starpilot_events = StarPilotEvents(self, error_log, ThemeManager)
self.starpilot_following = StarPilotFollowing(self)
self.starpilot_vcruise = StarPilotVCruise(self)
@@ -100,6 +102,7 @@ class StarPilotPlanner:
self.road_curvature = 0
self.time_to_curve = 0
self.v_cruise = 0
self.hybrid_acceleration = 0.0
self.gps_position = None
@@ -220,7 +223,29 @@ 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)):
# Continuous Hybrid Experimental Control: instead of a binary CEM/CCM mode
# switch, fuse the classical Chill ACC target with the model's E2E
# trajectory through the HybridExperimentalMode controller. CEM's detector
# stays warm so red-light/stop-sign scene state (redLight, forcing_stop)
# remains accurate. Experimental Mode stays off so the MPC keeps producing
# the crisp classical a_chill that the hybrid is built on.
self.starpilot_cem.update(v_ego, sm, starpilot_toggles, v_cruise)
self.starpilot_ccm.experimental_mode = False
self.starpilot_cem.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),
)
self.hybrid_acceleration = self.hybrid_controller.update(
v_ego=v_ego,
v_cruise=v_cruise,
lead_one=self.lead_one,
model_v2=sm["modelV2"],
a_chill=self._get_chill_accel(v_ego, v_cruise),
a_exp=self._get_vision_exp_accel(sm),
)
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
@@ -296,6 +321,40 @@ class StarPilotPlanner:
self.tracking_lead_filter.update(following_lead)
return self.tracking_lead_filter.x >= THRESHOLD
def _get_vision_exp_accel(self, sm):
"""Model's raw predicted acceleration for the E2E (Experimental) channel."""
try:
accel_x = sm["modelV2"].acceleration.x
if len(accel_x) > 0:
return float(accel_x[0])
except (AttributeError, IndexError, TypeError):
pass
try:
return float(sm["modelV2"].action.desiredAcceleration)
except (AttributeError, TypeError, ValueError):
return 0.0
def _get_chill_accel(self, v_ego, v_cruise):
"""Cruise/lead tracking acceleration for the Chill (ACC) channel."""
max_accel = float(getattr(self.starpilot_acceleration, "max_accel", 0.0) or 0.0)
min_accel = float(getattr(self.starpilot_acceleration, "min_accel", 0.0) or 0.0)
if max_accel <= 0.0 and min_accel == 0.0:
max_accel, min_accel = 1.5, -2.0
a_chill = float(np.clip((v_cruise - v_ego) / 2.0, min_accel, max_accel))
lead = getattr(self, "lead_one", None)
if lead is not None and bool(getattr(lead, "status", False)):
d_rel = float(getattr(lead, "dRel", float("inf")))
v_lead = float(getattr(lead, "vLead", v_ego))
t_follow = float(getattr(self.starpilot_following, "t_follow", 1.45) or 1.45)
desired_gap = max(v_ego * t_follow, 4.0)
if d_rel < desired_gap:
gap_deficit = max(desired_gap - d_rel, 0.0)
closing = max(0.0, v_ego - v_lead)
lead_decel = -min(closing / 2.0 + gap_deficit / 4.0, 3.0)
a_chill = min(a_chill, lead_decel)
return float(np.clip(a_chill, min_accel, max_accel))
def publish(self, theme_updated, sm, pm, starpilot_toggles, serialized_toggles=""):
starpilot_plan_send = messaging.new_message("starpilotPlan")
starpilot_plan_send.valid = sm.all_checks(service_list=["carState", "controlsState", "selfdriveState", "radarState"])
@@ -0,0 +1,125 @@
import numpy as np
import pytest
from openpilot.starpilot.controls.lib.hybrid_experimental_mode import (
HybridExperimentalMode,
sigmoid,
soft_max,
soft_min,
)
class FakeLead:
def __init__(self, status=False, d_rel=150.0, v_lead=0.0):
self.status = status
self.dRel = d_rel
self.vLead = v_lead
class FakeModel:
def __init__(self, velocity=None):
self.velocity = type("Velocity", (), {"x": velocity})()
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()
model = model if model is not None else FakeModel(velocity=[v_ego] * 20)
result = 0.0
for _ in range(frames):
result = controller.update(v_ego, v_cruise, lead, model, a_chill, a_exp)
return result
def test_soft_operators_are_continuous_and_bounded():
for value in (-5.0, -0.1, 0.0, 0.1, 5.0):
assert 0.0 < sigmoid(value) < 1.0
assert soft_max(1.0, 2.0) == pytest.approx(2.0, abs=1e-2)
assert soft_min(1.0, 2.0) == pytest.approx(1.0, abs=1e-2)
def test_throttle_fusion_leans_toward_snappier_target():
controller = make_controller()
# No lead, no vision stop: the continuous fusion routes toward the more
# confident throttle (exp 0.8) without a mode switch.
a = run(controller, a_chill=0.5, a_exp=0.8)
assert a > 0.75
assert a < 0.85
def test_vision_stop_horizon_grants_full_braking():
controller = make_controller()
model = FakeModel(velocity=np.linspace(20.0, 0.1, 20))
# The model trajectory decays to a terminal stop, so w_vision -> 1 and the
# early vision braking curve takes over.
a = run(controller, lead=FakeLead(status=False), model=model, a_chill=-0.5, a_exp=-2.0)
assert a <= -1.5
def test_phantom_brake_shield_caps_unconfirmed_vision_braking():
controller = make_controller()
# No lead and a flat model horizon: unconfirmed vision braking is clamped to
# the gentle coast limit instead of passing the raw E2E target through.
a = run(controller, lead=FakeLead(status=False), a_chill=0.0, a_exp=-2.0)
assert -0.6 <= a <= -0.5
def test_cbf_safety_floor_prevents_under_braking_near_close_lead():
controller = make_controller()
# Vision wants throttle but a close lead means the Control Barrier Function
# smoothly forces the target toward the Chill safety floor.
lead = FakeLead(status=True, d_rel=5.0, v_lead=0.0)
a = run(controller, lead=lead, a_chill=-1.0, a_exp=0.5)
assert a == pytest.approx(-1.0, abs=1e-2)
def test_cbf_is_inactive_when_lead_is_far():
controller = make_controller()
lead = FakeLead(status=True, d_rel=80.0, v_lead=25.0)
a = run(controller, lead=lead, a_chill=0.5, a_exp=0.8)
assert a > 0.7
def test_jerk_slew_limits_single_frame_step():
controller = make_controller(prev=0.0)
lead = FakeLead(status=False)
model = FakeModel(velocity=[20.0] * 20)
a = controller.update(20.0, 30.0, lead, model, 2.0, 2.0)
assert a == pytest.approx(controller.MAX_JERK * controller.DT, abs=1e-3)
def test_reset_clears_integrator_state():
controller = make_controller(prev=0.0)
lead = FakeLead(status=True, d_rel=20.0, v_lead=15.0)
model = FakeModel(velocity=[20.0] * 20)
controller.update(20.0, 30.0, lead, model, -1.0, -2.0)
assert controller.prev_a_target != 0.0
controller.reset(0.0)
assert controller.prev_a_target == 0.0
def test_set_tuning_clamps_to_documented_ranges():
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
controller.set_tuning(0.5, 1.5)
assert controller.HYBRID_EXP_BIAS == 0.5
assert controller.VISION_BRAKE_SENSITIVITY == 1.5
def test_missing_model_velocity_falls_back_to_ego_speed():
controller = make_controller()
model = FakeModel(velocity=None)
lead = FakeLead(status=False)
# Falls back to a constant-velocity horizon, so no vision braking is injected.
a = run(controller, lead=lead, model=model, a_chill=0.5, a_exp=0.2)
assert a > 0.4
+6 -4
View File
@@ -1523,6 +1523,7 @@ _TROUBLESHOOT_PERSONALITY_KEYS = [
_TROUBLESHOOT_CEM_KEYS = [
"ConditionalExperimental",
"HybridExperimental",
"CESpeed",
"CESpeedLead",
"CECurves",
@@ -5201,15 +5202,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({
+837
View File
@@ -0,0 +1,837 @@
#!/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 = hybrid.update(v_ego, v_cruise, lead, model_v2, a_chill, a_exp)
authority = hybrid.exp_authority
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())