Files
StarPilot/starpilot/controls/tests/test_personality_longitudinal_profiles.py
2026-09-12 00:16:28 -04:00

270 lines
10 KiB
Python

import math
import sys
from enum import IntEnum
from types import ModuleType, SimpleNamespace
import pytest
from openpilot.starpilot.common.accel_profile import ACCELERATION_PROFILES, DECELERATION_PROFILES, get_accel_profile_curve_values
from openpilot.starpilot.common.longitudinal_personality_profiles import default_personality_profiles, profile_document
def _module(name, **attributes):
module = ModuleType(name)
for key, value in attributes.items():
setattr(module, key, value)
return module
class _Params:
def __init__(self, *args, **kwargs):
self.writes = []
def put_nonblocking(self, key, value):
self.writes.append((key, value))
def put_bool(self, key, value):
self.writes.append((key, value))
sys.modules["openpilot.common.constants"] = _module(
"openpilot.common.constants", CV=SimpleNamespace(KPH_TO_MS=1 / 3.6, MPH_TO_MS=0.44704),
)
sys.modules["openpilot.common.params"] = _module("openpilot.common.params", Params=_Params)
sys.modules["openpilot.selfdrive.car.cruise"] = _module(
"openpilot.selfdrive.car.cruise", V_CRUISE_MAX=145, V_CRUISE_UNSET=255,
)
sys.modules["openpilot.selfdrive.controls.lib.longitudinal_planner"] = _module(
"openpilot.selfdrive.controls.lib.longitudinal_planner", A_CRUISE_MIN=-1.0, get_max_accel=lambda _v_ego: 2.0,
)
sys.modules["openpilot.starpilot.controls.lib.starpilot_vcruise"] = _module(
"openpilot.starpilot.controls.lib.starpilot_vcruise",
get_active_slc_control_target=lambda enabled, set_speed_limit, target, offset, overridden_speed, *_args, **_kwargs: (
float(overridden_speed or target) + float(offset) if enabled and set_speed_limit else 0.0
),
)
from openpilot.starpilot.controls.lib.starpilot_acceleration import StarPilotAcceleration
class Personality(IntEnum):
aggressive = 0
standard = 1
relaxed = 2
def _sm(*, traffic=False, personality=Personality.standard, lead=False, force_decel=False):
lead_state = SimpleNamespace(status=lead, vLead=0.0, aLeadK=-1.0 if lead else 0.0, dRel=10.0 if lead else 1000.0)
return {
"carControl": SimpleNamespace(orientationNED=[0.0, 0.0, 0.0]),
"carState": SimpleNamespace(vCruise=80.0, vEgoCluster=0.0, standstill=False),
"controlsState": SimpleNamespace(forceDecel=force_decel),
"radarState": SimpleNamespace(leadOne=lead_state, leadTwo=SimpleNamespace(status=False, vLead=0.0, aLeadK=0.0, dRel=1000.0)),
"selfdriveState": SimpleNamespace(personality=personality),
"starpilotCarState": SimpleNamespace(
ecoGear=False, forceCoast=False, pulseAndGlide=False, sportGear=False, trafficModeEnabled=traffic,
),
}
def _planner(v_cruise=20.0):
return SimpleNamespace(
starpilot_cem=SimpleNamespace(stop_light_detected=False),
starpilot_following=SimpleNamespace(disable_throttle=False),
starpilot_vcruise=SimpleNamespace(slc_target=0.0, slc_offset=0.0, slc=SimpleNamespace(overridden_speed=0.0), forcing_stop=False),
starpilot_weather=SimpleNamespace(weather_id=0, reduce_acceleration=0.0),
v_cruise=v_cruise,
)
def _toggles(document):
return SimpleNamespace(
acceleration_profile=ACCELERATION_PROFILES["STANDARD"],
custom_accel_profile=False,
custom_accel_profile_breakpoints=[0.0, 5.0, 10.0, 15.0, 20.0, 25.0, 40.0],
custom_accel_profile_values=[],
custom_personalities=True,
deceleration_profile=DECELERATION_PROFILES["STANDARD"],
ev_tuning=False,
longitudinal_personality_profiles=document,
map_acceleration=False,
map_deceleration=False,
personality_ev_tuning=False,
personality_truck_tuning=False,
pulse_glide_speed_delta=0.0,
redneck_cruise=False,
set_speed_limit=False,
set_speed_offset=0.0,
speed_limit_controller=False,
truck_tuning=False,
)
def _document(*, enabled=True):
return profile_document(default_personality_profiles(False), enabled=enabled)
@pytest.mark.parametrize(
("profile", "personality", "traffic_mode", "acceleration", "braking"),
[
("aggressive", Personality.aggressive, False, 1.10, 0.60),
("standard", Personality.standard, False, 1.25, 0.75),
("relaxed", Personality.relaxed, False, 1.40, 0.90),
("traffic", Personality.aggressive, True, 1.55, 1.05),
],
)
def test_real_enum_shaped_personality_selects_each_explicit_profile_override(
profile, personality, traffic_mode, acceleration, braking,
):
document = _document()
document["profiles"][profile]["acceleration"] = {"preset": "custom", "curve": [acceleration] * 10}
document["profiles"][profile]["braking"] = {"preset": "custom", "curve": [braking] * 10}
controller = StarPilotAcceleration(_planner(v_cruise=5.0))
controller.update(10.0, _sm(traffic=traffic_mode, personality=personality), _toggles(document))
assert controller.max_accel == pytest.approx(acceleration)
assert controller.min_accel == pytest.approx(-braking)
def test_master_toggle_disables_profile_document_overrides():
document = _document()
document["profiles"]["standard"]["acceleration"] = {"preset": "custom", "curve": [1.25] * 10}
toggles = _toggles(document)
toggles.custom_personalities = False
controller = StarPilotAcceleration(_planner())
controller.update(0.0, _sm(personality=Personality.standard), toggles)
assert controller.max_accel == pytest.approx(2.0)
@pytest.mark.parametrize(
("profile", "personality", "traffic_mode", "legacy_max_accel", "legacy_min_accel"),
[
("traffic", Personality.aggressive, True, 1.1, -0.35),
("aggressive", Personality.aggressive, False, 2.0, -1.0),
("standard", Personality.standard, False, 2.0, -1.0),
("relaxed", Personality.relaxed, False, 2.0, -1.0),
],
)
def test_disabled_active_profile_keeps_legacy_acceleration_path(
profile, personality, traffic_mode, legacy_max_accel, legacy_min_accel,
):
document = _document()
document["profiles"][profile]["acceleration"] = {"preset": "custom", "curve": [1.25] * 10}
document["profiles"][profile]["braking"] = {"preset": "custom", "curve": [0.75] * 10}
toggles = _toggles(document)
setattr(toggles, f"{profile}_personality_profile", False)
controller = StarPilotAcceleration(_planner())
controller.update(0.0, _sm(traffic=traffic_mode, personality=personality), toggles)
assert controller.max_accel == pytest.approx(legacy_max_accel)
assert controller.min_accel == pytest.approx(legacy_min_accel)
def test_detected_truck_curve_is_used_without_enabling_legacy_truck_tuning():
document = _document()
document["profiles"]["standard"]["acceleration"] = {"preset": "sport_plus", "curve": []}
toggles = _toggles(document)
toggles.personality_truck_tuning = True
controller = StarPilotAcceleration(_planner())
controller.update(0.0, _sm(personality=Personality.standard), toggles)
expected = get_accel_profile_curve_values(ACCELERATION_PROFILES["SPORT_PLUS"], False, True)[0]
assert controller.max_accel == pytest.approx(expected)
def test_fresh_profile_defaults_keep_legacy_acceleration_and_braking():
document = _document()
toggles = _toggles(document)
toggles.custom_accel_profile = True
toggles.custom_accel_profile_values = [3.0] * 7
controller = StarPilotAcceleration(_planner())
controller.update(0.0, _sm(personality=Personality.aggressive), toggles)
assert controller.max_accel == pytest.approx(3.0)
assert controller.min_accel == pytest.approx(-1.0)
controller.update(0.0, _sm(traffic=True), toggles)
assert controller.max_accel == pytest.approx(1.1)
assert controller.min_accel == pytest.approx(-0.35)
def test_absent_disabled_malformed_partial_wrong_version_and_nonfinite_use_legacy_path():
candidates = [None, {}, _document(enabled=False), _document(), _document(), _document()]
candidates[3]["schemaVersion"] = 99
del candidates[4]["profiles"]["standard"]["braking"]
candidates[5]["profiles"]["standard"]["acceleration"] = {"preset": "custom", "curve": [math.nan] * 10}
for candidate in candidates:
toggles = _toggles(candidate)
toggles.custom_accel_profile = True
toggles.custom_accel_profile_values = [3.0] * 7
controller = StarPilotAcceleration(_planner())
controller.update(0.0, _sm(), toggles)
assert controller.max_accel == pytest.approx(3.0)
assert controller.min_accel == pytest.approx(-1.0)
def test_map_gear_force_coast_and_weather_precedence_remains_explicit():
document = _document()
document["profiles"]["standard"]["acceleration"] = {"preset": "custom", "curve": [3.5] * 10}
document["profiles"]["standard"]["braking"] = {"preset": "custom", "curve": [2.0] * 10}
toggles = _toggles(document)
toggles.map_acceleration = True
toggles.map_deceleration = True
sm = _sm()
sm["starpilotCarState"].ecoGear = True
planner = _planner()
planner.starpilot_weather.weather_id = 1
planner.starpilot_weather.reduce_acceleration = 0.25
controller = StarPilotAcceleration(planner)
controller.update(0.0, sm, toggles)
assert controller.max_accel == pytest.approx(1.5 * 0.75)
assert controller.min_accel == pytest.approx(-0.5)
assert all(key != "LongitudinalPersonalityProfiles" for key, _value in controller.params.writes)
sm["starpilotCarState"].forceCoast = True
controller.update(0.0, sm, toggles)
assert controller.min_accel == pytest.approx(-0.5)
def test_custom_acceleration_and_braking_use_the_selected_twenty_mph_point():
document = _document()
document["profiles"]["standard"]["acceleration"] = {
"preset": "custom", "curve": [1.0 + 0.1 * index for index in range(10)],
}
document["profiles"]["standard"]["braking"] = {
"preset": "custom", "curve": [0.75 + 0.1 * index for index in range(10)],
}
controller = StarPilotAcceleration(_planner(v_cruise=5.0))
controller.update(20.0 * 0.44704, _sm(), _toggles(document))
assert controller.max_accel == pytest.approx(1.2)
assert controller.min_accel == pytest.approx(-0.95)
def test_custom_braking_only_shapes_explicit_cruise_deceleration_and_never_reduces_hazard_authority():
document = _document()
document["profiles"]["standard"]["braking"] = {"preset": "custom", "curve": [0.5] * 10}
toggles = _toggles(document)
controller = StarPilotAcceleration(_planner(v_cruise=30.0))
controller.update(10.0, _sm(lead=False), toggles)
assert controller.min_accel <= -1.0
controller = StarPilotAcceleration(_planner(v_cruise=5.0))
controller.update(10.0, _sm(lead=False), toggles)
assert controller.min_accel == pytest.approx(-0.5)
controller.update(10.0, _sm(lead=True), toggles)
assert controller.min_accel <= -1.0
controller.update(10.0, _sm(force_decel=True), toggles)
assert controller.min_accel <= -1.0