Files
StarPilot/starpilot/common/tests/test_longitudinal_personality_profiles.py
firestar5683 c3e4ec630f fix
2026-09-14 15:41:25 -05:00

624 lines
28 KiB
Python

import json
import math
from pathlib import Path
import numpy as np
import pytest
import openpilot.starpilot.common.longitudinal_personality_profiles as lpp
from openpilot.starpilot.common.accel_profile import (
ACCELERATION_PROFILES,
get_accel_profile_curve_values,
interpolate_accel_profile,
)
from openpilot.starpilot.common.longitudinal_personality_profiles import (
ACCELERATION_SPEEDS_MPH,
BRAKING_SPEEDS_MPH,
CURVE_BOUNDS,
FOLLOWING_PRESET_CURVES,
FOLLOWING_SPEEDS_MPH,
PERSONALITY_IDS,
PROFILE_SCHEMA_VERSION,
active_personality_id,
category_curve,
default_personality_profiles,
initial_custom_curve,
is_truck_fingerprint,
interpolate_category_curve,
load_personality_profiles,
profile_document,
resolve_personality_profile,
serialize_personality_profiles,
strict_personality_profiles,
update_personality_profile,
)
def test_document_is_versioned_disabled_and_declares_exact_axes_and_units():
document = profile_document(default_personality_profiles(False), enabled=False)
assert document["schemaVersion"] == PROFILE_SCHEMA_VERSION == 3
assert document["enabled"] is False
assert document["axes"] == {
"acceleration": {
"speed": {"unit": "mph", "values": list(ACCELERATION_SPEEDS_MPH)},
"value": {"unit": "m/s^2", "meaning": "maximum_requested_acceleration"},
},
"braking": {
"speed": {"unit": "mph", "values": list(BRAKING_SPEEDS_MPH)},
"value": {"unit": "m/s^2", "meaning": "cruise_slc_deceleration_magnitude"},
},
"following": {
"speed": {"unit": "mph", "values": list(FOLLOWING_SPEEDS_MPH)},
"value": {"unit": "s", "meaning": "base_time_headway"},
},
}
assert set(document["profiles"]) == set(PERSONALITY_IDS)
for profile in document["profiles"].values():
assert set(profile) == {"acceleration", "braking", "following"}
@pytest.mark.parametrize("fingerprint", [
"RAM 1500 5TH GEN",
"RAM HD 5TH GEN",
"FORD F-150 14TH GEN",
"FORD MAVERICK 1ST GEN",
"FORD RANGER 2ND GEN",
"CHEVROLET SILVERADO 1500 2020",
"HONDA RIDGELINE 2017",
"HYUNDAI SANTA CRUZ 2025",
])
def test_supported_truck_fingerprints_select_the_truck_curve(fingerprint):
assert is_truck_fingerprint(fingerprint) is True
@pytest.mark.parametrize("fingerprint", [None, "", "HONDA CIVIC 2022", "FORD EXPLORER 6TH GEN"])
def test_non_truck_fingerprints_do_not_select_the_truck_curve(fingerprint):
assert is_truck_fingerprint(fingerprint) is False
def test_enabling_without_a_stored_document_preserves_dom_defaults():
document = lpp.synchronise_profile_document_enabled(None, True, ev_tuning=False, truck_tuning=False)
assert document == profile_document(default_personality_profiles(False), enabled=True)
def test_fresh_profiles_do_not_override_legacy_personality_until_selected():
document = lpp.synchronise_profile_document_enabled(None, True, False, False)
for personality in PERSONALITY_IDS:
assert all(document["profiles"][personality][category] == {"preset": "dom_default", "curve": []}
for category in ("acceleration", "braking", "following"))
assert lpp.resolve_personality_category(document, False, 0, "acceleration") is None
assert lpp.resolve_personality_category(document, False, 1, "braking") is None
assert lpp.resolve_personality_category(document, False, 2, "following") is None
def test_enabling_does_not_overwrite_a_malformed_stored_document():
assert lpp.synchronise_profile_document_enabled({"schemaVersion": 99}, True, False, False) is None
def test_disabling_without_a_stored_document_does_not_create_one():
assert lpp.synchronise_profile_document_enabled(None, False, ev_tuning=False, truck_tuning=False) is None
def test_every_state_affecting_personality_param_is_included_in_bulk_restore_parked_guard():
assert lpp.PERSONALITY_PARKED_PARAM_KEYS == (
lpp.PERSONALITY_ADVANCED_PARAM_KEYS
| lpp.PERSONALITY_FOLLOW_PARAM_KEYS
| lpp.PERSONALITY_PROFILE_ENABLE_PARAM_KEYS
| {"CustomPersonalities"}
)
assert len(lpp.PERSONALITY_PARKED_PARAM_KEYS) == 32
def test_legacy_follow_values_reject_coercion_and_out_of_range_inputs():
for invalid in (True, "1.25", math.nan, math.inf, 0.49, 3.01):
with pytest.raises(ValueError):
lpp.validate_personality_follow_value(invalid)
assert lpp.validate_personality_follow_value(0.5) == 0.5
assert lpp.validate_personality_follow_value(1.25) == 1.25
assert lpp.validate_personality_follow_value(3) == 3.0
def test_advanced_personality_values_reject_coercion_and_out_of_range_inputs():
for invalid in (True, "50", math.nan, math.inf, 24.9, 200.1):
with pytest.raises(ValueError):
lpp.validate_personality_advanced_value(invalid)
assert lpp.validate_personality_advanced_value(50) == 50.0
assert lpp.validate_personality_advanced_value(100.0) == 100.0
assert lpp.validate_personality_advanced_value(72.34567) == 72.3457
def test_runtime_loader_uses_detected_truck_curve_without_changing_legacy_truck_flag():
source = (Path(__file__).parents[1] / "starpilot_variables.py").read_text(encoding="utf-8")
assert "toggle.longitudinal_personality_profiles = migrate_profile_document(profile_settings_raw) or {}" in source
assert "is_truck_fingerprint(CP.carFingerprint) or truck_tuning_param" in source
assert ") and not toggle.personality_ev_tuning" in source
assert "toggle.truck_tuning = truck_tuning_param" in source
def test_runtime_loader_maps_each_personality_enable_param_to_the_exact_runtime_boolean():
loader = getattr(lpp, "load_personality_profile_enable_values", None)
assert callable(loader)
persisted = {
"TrafficPersonalityProfile": True,
"AggressivePersonalityProfile": False,
"StandardPersonalityProfile": True,
"RelaxedPersonalityProfile": False,
}
requested = []
def get_value(key):
requested.append(key)
return persisted[key]
assert loader(get_value) == {
"traffic_personality_profile": True,
"aggressive_personality_profile": False,
"standard_personality_profile": True,
"relaxed_personality_profile": False,
}
assert requested == list(persisted)
def test_acceleration_presets_select_truck_automatically_and_ev_wins_if_both_are_true():
config = {"preset": "sport", "curve": []}
assert category_curve("acceleration", config, False, True) == get_accel_profile_curve_values(2, False, True)
assert category_curve("acceleration", config, True, True) == get_accel_profile_curve_values(2, True, False)
def test_declared_custom_axes_use_exact_ten_mph_breakpoints():
assert ACCELERATION_SPEEDS_MPH == tuple(range(0, 91, 10))
assert BRAKING_SPEEDS_MPH == ACCELERATION_SPEEDS_MPH
def test_boolean_axis_values_are_not_accepted_as_numeric_breakpoints():
invalid = profile_document(default_personality_profiles(False), enabled=True)
invalid["axes"]["acceleration"]["speed"]["values"][0] = False
assert lpp.strict_profile_document(invalid) is None
def test_strict_document_rejects_unversioned_partial_extra_or_axis_changes():
valid = profile_document(default_personality_profiles(False), enabled=True)
assert strict_personality_profiles(valid) == valid["profiles"]
assert strict_personality_profiles(json.dumps(valid)) == valid["profiles"]
invalid_documents = [
valid["profiles"],
{**valid, "schemaVersion": 99},
{**valid, "enabled": 1},
{**valid, "extra": True},
{key: value for key, value in valid.items() if key != "axes"},
]
wrong_axis = json.loads(json.dumps(valid))
wrong_axis["axes"]["acceleration"]["speed"]["values"][0] = 1
invalid_documents.append(wrong_axis)
partial = json.loads(json.dumps(valid))
del partial["profiles"]["standard"]["braking"]
invalid_documents.append(partial)
for invalid in invalid_documents:
assert strict_personality_profiles(invalid) is None
def test_strict_document_rejects_boolean_non_finite_fractional_and_out_of_range_values():
for value in (True, False, math.nan, math.inf, -math.inf, "1.0", 6.1):
invalid = profile_document(default_personality_profiles(False), enabled=True)
invalid["profiles"]["standard"]["acceleration"] = {"preset": "custom", "curve": [1.0] * 10}
invalid["profiles"]["standard"]["acceleration"]["curve"][0] = value
assert strict_personality_profiles(invalid) is None
def test_custom_curve_bounds_preserve_low_acceleration_and_enforce_requested_ceilings():
valid = profile_document(default_personality_profiles(False), enabled=True)
valid["profiles"]["standard"]["acceleration"] = {"preset": "custom", "curve": [0.35] + [3.5] * 9}
valid["profiles"]["standard"]["braking"] = {"preset": "custom", "curve": [2.0] * 10}
assert strict_personality_profiles(valid) == valid["profiles"]
# A saved v2 curve can exceed the new-authoring ceiling; new points cannot.
with pytest.raises(ValueError):
update_personality_profile(valid["profiles"], "standard", "acceleration", "custom", [3.51] * 10, False)
invalid_braking = json.loads(json.dumps(valid))
invalid_braking["profiles"]["standard"]["braking"]["curve"][0] = 2.01
assert strict_personality_profiles(invalid_braking) is None
@pytest.mark.parametrize("value", [3.51, 4.0, 5.0, 6.0])
@pytest.mark.parametrize("enabled", [False, True])
def test_saved_v2_high_acceleration_keeps_schema_and_runtime_behaviour(value, enabled):
document = profile_document(default_personality_profiles(False), enabled=enabled)
curve = [value, 3.5, 2.0, 1.5, 1.0, 0.8, 0.6, 0.4, 0.2, 0.0]
document["profiles"]["aggressive"]["acceleration"] = {"preset": "custom", "curve": curve}
raw = json.dumps(document)
assert lpp.strict_profile_document(raw) == document
assert lpp.migrate_profile_document(raw) == document
assert load_personality_profiles(raw, False) == document["profiles"]
assert json.loads(serialize_personality_profiles(document["profiles"], False, enabled=enabled)) == document
assert lpp.synchronise_profile_document_enabled(raw, not enabled, False) == {**document, "enabled": not enabled}
resolved = resolve_personality_profile(raw, False, 0)
assert resolved == (document["profiles"]["aggressive"] if enabled else None)
if enabled:
assert resolved is not None
for speed_mph in (-1.0, 0.0, 2.5, 5.0, 10.0, 25.0, 90.0, 100.0):
assert interpolate_category_curve("acceleration", speed_mph * 0.44704, resolved["acceleration"], False) == pytest.approx(
interpolate_accel_profile(speed_mph * 0.44704, curve, [speed * 0.44704 for speed in ACCELERATION_SPEEDS_MPH])
)
assert json.dumps(document) == raw
@pytest.mark.parametrize("value", [3.51, 4.0, 5.0, 6.0])
def test_edit_saved_v2_high_point_preserves_other_points_and_profiles(value):
profiles = default_personality_profiles(False)
profiles["aggressive"]["acceleration"] = {"preset": "custom", "curve": [value] * 10}
raw = json.dumps(profiles)
curve = [3.0] + [value] * 9
updated = update_personality_profile(profiles, "aggressive", "acceleration", "custom", curve, False)
assert updated["aggressive"]["acceleration"] == {"preset": "custom", "curve": curve}
assert json.dumps(profiles) == raw
for profile_id in ("traffic", "standard", "relaxed"):
assert updated[profile_id] == profiles[profile_id]
with pytest.raises(ValueError):
update_personality_profile(updated, "aggressive", "acceleration", "custom", [value] * 10, False)
def test_saved_high_points_cannot_be_created_moved_increased_or_rounded_into_permission():
profiles = default_personality_profiles(False)
profiles["aggressive"]["acceleration"] = {"preset": "custom", "curve": [4.0] + [1.0] * 9}
for curve in ([4.1] + [1.0] * 9, [4.00001] + [1.0] * 9, [1.0, 4.0] + [1.0] * 8):
with pytest.raises(ValueError):
update_personality_profile(profiles, "aggressive", "acceleration", "custom", curve, False)
with pytest.raises(ValueError):
update_personality_profile(profiles, "standard", "acceleration", "custom", [4.0] + [1.0] * 9, False)
malformed = json.loads(json.dumps(profiles))
malformed["relaxed"]["following"]["curve"] = [True]
with pytest.raises(ValueError):
update_personality_profile(malformed, "aggressive", "acceleration", "custom", [4.0] + [1.0] * 9, False)
def test_disabled_document_never_resolves_an_override():
disabled = profile_document(default_personality_profiles(False), enabled=False)
for traffic, personality in ((True, 0), (False, 0), (False, 1), (False, 2)):
assert resolve_personality_profile(disabled, traffic, personality) is None
def test_context_mapping_is_traffic_first_then_cereal_zero_one_two():
assert active_personality_id(True, 99) == "traffic"
assert active_personality_id(False, 0) == "aggressive"
assert active_personality_id(False, 1) == "standard"
assert active_personality_id(False, 2) == "relaxed"
for invalid in (-1, 3, 0.5, True, False, math.nan, math.inf, "1", None):
assert active_personality_id(False, invalid) is None
for malformed_traffic in (1, 0, "1", "0", "true", "false", None):
assert active_personality_id(malformed_traffic, 0) is None
def test_enabled_document_resolves_each_profile_and_revalidates_runtime_boundary():
document = profile_document(default_personality_profiles(False), enabled=True)
assert resolve_personality_profile(document, True, 2) == document["profiles"]["traffic"]
assert resolve_personality_profile(document, False, 0) == document["profiles"]["aggressive"]
assert resolve_personality_profile(document, False, 1) == document["profiles"]["standard"]
assert resolve_personality_profile(document, False, 2) == document["profiles"]["relaxed"]
malformed = json.loads(json.dumps(document))
malformed["profiles"]["standard"]["acceleration"]["curve"] = [math.nan] * 7
assert resolve_personality_profile(malformed, False, 1) is None
assert resolve_personality_profile(document, False, 1.0) is None
def test_acceleration_presets_match_dom_curves_for_gas_ev_and_truck():
profile_ids = {
"standard": ACCELERATION_PROFILES["STANDARD"],
"eco": ACCELERATION_PROFILES["ECO"],
"sport": ACCELERATION_PROFILES["SPORT"],
"sport_plus": ACCELERATION_PROFILES["SPORT_PLUS"],
}
for ev_tuning, truck_tuning in ((False, False), (True, False), (False, True)):
for preset, profile_id in profile_ids.items():
config = {"preset": preset, "curve": []}
assert category_curve("acceleration", config, ev_tuning, truck_tuning) == get_accel_profile_curve_values(
profile_id, ev_tuning, truck_tuning
)
def test_custom_initialisation_seeds_from_selected_acceleration_preset():
current = {"preset": "sport", "curve": []}
assert initial_custom_curve("acceleration", current, ev_tuning=False, truck_tuning=False) == \
lpp._sample_config_on_custom_axis("acceleration", current, False, False)
truck_curve = lpp._sample_config_on_custom_axis("acceleration", current, False, True)
assert max(truck_curve) > CURVE_BOUNDS["acceleration"][1]
assert initial_custom_curve("acceleration", current, ev_tuning=False, truck_tuning=True) == [
min(max(value, CURVE_BOUNDS["acceleration"][0]), CURVE_BOUNDS["acceleration"][1])
for value in truck_curve
]
def test_custom_initialisation_uses_ev_over_truck_when_both_flags_are_set():
current = {"preset": "standard", "curve": []}
assert initial_custom_curve("acceleration", current, ev_tuning=True, truck_tuning=True) == \
lpp._sample_config_on_custom_axis("acceleration", current, True, False)
def test_truck_detection_accepts_live_canonical_fingerprint_identifiers():
for fingerprint in (
"RAM_1500_5TH_GEN",
"RAM_HD_5TH_GEN",
"FORD_F_150_MK14",
"FORD_MAVERICK_MK1",
"FORD_RANGER_MK2",
"CHEVROLET_SILVERADO",
"HONDA_RIDGELINE",
"HYUNDAI_SANTA_CRUZ_2025",
):
assert is_truck_fingerprint(fingerprint), fingerprint
assert not is_truck_fingerprint("HYUNDAI_SANTA_FE_2022")
assert not is_truck_fingerprint(None)
def test_custom_initialisation_seeds_braking_from_selected_preset():
assert initial_custom_curve(
"braking", {"preset": "eco", "curve": []}, ev_tuning=True, truck_tuning=True
) == [0.5] * 10
assert initial_custom_curve(
"braking", {"preset": "sport", "curve": []}, ev_tuning=False, truck_tuning=False
) == [2.0] * 10
def test_dom_default_custom_initialisation_uses_effective_legacy_curve():
legacy_curve = [1.1, 1.0, 0.9, 0.8, 0.7, 0.6, 0.5]
current = {"preset": "dom_default", "curve": []}
assert initial_custom_curve("acceleration", current, True, True, legacy_curve=legacy_curve) == [
round(lpp._linear_interp(speed, lpp._V1_ACCELERATION_SPEEDS_MPH, legacy_curve), 4)
for speed in ACCELERATION_SPEEDS_MPH
]
def test_existing_custom_curve_is_never_reseeded():
curve = [round(1.0 + index * 0.1, 4) for index in range(10)]
current = {"preset": "custom", "curve": curve}
assert initial_custom_curve("acceleration", current, True, True) == curve
def test_profile_update_is_atomic_and_accepts_bounded_following_category():
profiles = default_personality_profiles(True)
curve = [round(1.0 + index * 0.1, 4) for index in range(10)]
updated = update_personality_profile(profiles, "standard", "acceleration", "custom", curve, True, False)
assert profiles["standard"]["acceleration"]["preset"] == "dom_default"
assert updated["standard"]["acceleration"] == {"preset": "custom", "curve": curve}
following = [0.75 + index * 0.1 for index in range(10)]
updated = update_personality_profile(updated, "standard", "following", "custom", following, True, False)
assert profiles["standard"]["following"]["preset"] == "dom_default"
assert updated["standard"]["following"] == {"preset": "custom", "curve": [round(value, 4) for value in following]}
for invalid in ([0.74] * 10, [3.01] * 10, [math.nan] * 10, [True] * 10, [1.0] * 9):
with pytest.raises(ValueError):
update_personality_profile(updated, "standard", "following", "custom", invalid, True, False)
def test_serialization_requires_explicit_enabled_state_and_preserves_it():
profiles = default_personality_profiles(False)
with pytest.raises(TypeError):
serialize_personality_profiles(profiles, False)
encoded = serialize_personality_profiles(profiles, False, enabled=False)
document = json.loads(encoded)
assert document == profile_document(profiles, enabled=False)
assert strict_personality_profiles(encoded) is None
assert " " not in encoded
def test_loader_is_ui_only_fallback_and_does_not_partially_repair_persisted_document():
defaults = default_personality_profiles(False)
assert load_personality_profiles(None, False) == defaults
malformed = profile_document(defaults, enabled=True)
malformed["profiles"]["standard"]["acceleration"] = {"preset": "custom", "curve": [1.0] * 7}
malformed["profiles"]["standard"]["acceleration"]["curve"][0] = math.nan
assert load_personality_profiles(malformed, False) == defaults
assert strict_personality_profiles(malformed) is None
def test_custom_interpolation_uses_ten_mph_dom_segments_and_clamps_endpoints():
config = {"preset": "custom", "curve": [1.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0]}
breakpoints = [speed * 0.44704 for speed in ACCELERATION_SPEEDS_MPH]
assert interpolate_category_curve("acceleration", -1.0, config, False, False) == 1.0
assert interpolate_category_curve("acceleration", 2.5 * 0.44704, config, False, False) == pytest.approx(
interpolate_accel_profile(2.5 * 0.44704, config["curve"], breakpoints)
)
assert interpolate_category_curve("acceleration", 5.0 * 0.44704, config, False, False) == pytest.approx(1.5)
assert interpolate_category_curve("acceleration", 100.0, config, False, False) == pytest.approx(2.0)
def test_named_presets_are_canonical_without_unused_curve_points():
profiles = default_personality_profiles(False)
updated = update_personality_profile(profiles, "traffic", "acceleration", "eco", [], False, False)
assert updated["traffic"]["acceleration"] == {"preset": "eco", "curve": []}
document = profile_document(updated, enabled=True)
assert strict_personality_profiles(document) == updated
def test_following_presets_and_custom_curve_use_exact_ten_mph_linear_axis():
for preset, curve in FOLLOWING_PRESET_CURVES.items():
assert category_curve("following", {"preset": preset, "curve": []}, False, False) == list(curve)
assert FOLLOWING_SPEEDS_MPH == tuple(range(0, 91, 10))
config = {"preset": "custom", "curve": [0.75 + 0.1 * index for index in range(10)]}
assert interpolate_category_curve("following", 0.0, config, False, False) == pytest.approx(0.75)
assert interpolate_category_curve("following", 5.0 * 0.44704, config, False, False) == pytest.approx(0.80)
assert interpolate_category_curve("following", 90.0 * 0.44704, config, False, False) == pytest.approx(1.65)
def test_following_custom_initialisation_uses_effective_legacy_curve():
legacy_curve = [1.0 + index * 0.05 for index in range(10)]
current = {"preset": "dom_default", "curve": []}
assert initial_custom_curve("following", current, False, False, legacy_curve=legacy_curve) == legacy_curve
def test_v2_uses_one_shared_ten_mph_custom_axis():
assert PROFILE_SCHEMA_VERSION == 3
expected = tuple(range(0, 91, 10))
assert ACCELERATION_SPEEDS_MPH == expected
assert BRAKING_SPEEDS_MPH == expected
assert FOLLOWING_SPEEDS_MPH == expected
def test_fresh_profiles_start_with_dom_defaults():
profiles = default_personality_profiles(False)
for profile in profiles.values():
assert profile == {
"acceleration": {"preset": "dom_default", "curve": []},
"braking": {"preset": "dom_default", "curve": []},
"following": {"preset": "dom_default", "curve": []},
}
def test_following_presets_match_stock_dom_personalities_exactly():
assert FOLLOWING_PRESET_CURVES == {
"close": (1.25, 1.0),
"medium": (1.45, 1.2),
"far": (1.6, 1.4),
"traffic": (0.75, 1.6),
"legacy_close": (1.25, 1.25),
"legacy_medium": (1.45, 1.45),
"legacy_far": (1.75, 1.75),
}
@pytest.mark.parametrize("category", ["acceleration", "braking"])
def test_custom_longitudinal_curves_interpolate_on_exact_ten_mph_points(category):
curve = [0.75 + index * 0.1 for index in range(10)]
config = {"preset": "custom", "curve": curve}
assert interpolate_category_curve(category, 20 * 0.44704, config, False, False) == pytest.approx(curve[2])
assert interpolate_category_curve(category, 25 * 0.44704, config, False, False) == pytest.approx((curve[2] + curve[3]) / 2)
def test_named_acceleration_presets_keep_native_dom_interpolation():
config = {"preset": "sport", "curve": []}
native_curve = get_accel_profile_curve_values(ACCELERATION_PROFILES["SPORT"], False, False)
for speed_mps in (0.0, 2.5, 7.5, 17.5, 32.0, 45.0):
assert interpolate_category_curve("acceleration", speed_mps, config, False, False) == pytest.approx(
interpolate_accel_profile(speed_mps, native_curve)
)
def test_reference_curves_are_profile_specific_and_use_the_custom_axis():
references = lpp.personality_reference_curves(False, False)
assert references["traffic"]["acceleration"] != references["aggressive"]["acceleration"]
assert references["aggressive"]["following"] == [1.25] * 5 + [1.2, 1.1, 1.0, 1.0, 1.0]
assert references["standard"]["following"] == [1.45] * 5 + [1.4, 1.3, 1.2, 1.2, 1.2]
assert references["relaxed"]["following"] == [1.6] * 5 + [1.56, 1.48, 1.4, 1.4, 1.4]
for profile in references.values():
for curve in profile.values():
assert len(curve) == 10
assert all(math.isfinite(value) for value in curve)
def test_exact_v1_document_migrates_whole_or_not_at_all():
legacy_axes = {
"acceleration": {
"speed": {"unit": "mph", "values": [0.0, 11.184681, 22.369363, 33.554044, 44.738726, 55.923407, 89.477452]},
"value": {"unit": "m/s^2", "meaning": "maximum_requested_acceleration"},
},
"braking": {
"speed": {"unit": "mph", "values": [0.0, 11.184681, 22.369363, 33.554044, 44.738726, 55.923407, 89.477452]},
"value": {"unit": "m/s^2", "meaning": "cruise_slc_deceleration_magnitude"},
},
"following": {
"speed": {"unit": "mph", "values": list(range(0, 91, 10))},
"value": {"unit": "s", "meaning": "base_time_headway"},
},
}
legacy_profiles = {
personality: {
"acceleration": {"preset": "standard", "curve": []},
"braking": {"preset": "standard", "curve": []},
"following": {"preset": "medium", "curve": []},
}
for personality in PERSONALITY_IDS
}
legacy_profiles["aggressive"]["acceleration"] = {"preset": "custom", "curve": [1.0, 1.5, 2.0, 2.5, 3.0, 3.5, 4.0]}
legacy = {"schemaVersion": 1, "enabled": True, "axes": legacy_axes, "profiles": legacy_profiles}
migrated = lpp.migrate_profile_document(legacy)
assert migrated is not None
assert migrated["schemaVersion"] == PROFILE_SCHEMA_VERSION
assert migrated["enabled"] is True
migrated_acceleration = migrated["profiles"]["aggressive"]["acceleration"]
assert migrated_acceleration["preset"] == "custom"
assert len(migrated_acceleration["curve"]) == 10
assert max(migrated_acceleration["curve"]) == CURVE_BOUNDS["acceleration"][1]
assert migrated_acceleration["legacyCurve"] == legacy_profiles["aggressive"]["acceleration"]["curve"]
assert interpolate_category_curve("acceleration", 40.0, migrated_acceleration, False, False) == 4.0
assert migrated["profiles"]["standard"] == {
**legacy_profiles["standard"],
"following": {"preset": "legacy_medium", "curve": []},
}
malformed = json.loads(json.dumps(legacy))
malformed["profiles"]["aggressive"]["acceleration"]["curve"][0] = True
assert lpp.migrate_profile_document(malformed) is None
def test_migrated_custom_acceleration_and_braking_preserve_v1_runtime_behaviour():
source_axis_ms = [0.0, 5.0, 10.0, 15.0, 20.0, 25.0, 40.0]
for category, legacy_curve in (
("acceleration", [1.0, 1.4, 1.8, 2.2, 2.6, 3.0, 3.4]),
("braking", [0.5, 0.65, 0.8, 0.95, 1.1, 1.25, 1.4]),
):
display_curve = [round(float(value), 4) for value in np.interp(np.array(ACCELERATION_SPEEDS_MPH) * 0.44704, source_axis_ms, legacy_curve)]
config = {"preset": "custom", "curve": display_curve, "legacyCurve": legacy_curve}
for speed_mps in np.linspace(0.0, 40.0, 161):
expected = float(np.interp(speed_mps, source_axis_ms, legacy_curve))
assert interpolate_category_curve(category, float(speed_mps), config, False, False) == pytest.approx(expected)
def test_v2_legacy_curve_is_strictly_scoped_to_valid_custom_acceleration_and_braking():
profiles = default_personality_profiles(False)
profiles["aggressive"]["acceleration"] = {
"preset": "custom", "curve": [1.0] * 10, "legacyCurve": [1.0] * 7,
}
assert lpp.strict_profile_document(profile_document(profiles, enabled=True)) is not None
invalid_named = json.loads(json.dumps(profiles))
invalid_named["aggressive"]["acceleration"] = {"preset": "sport", "curve": [], "legacyCurve": [1.0] * 7}
assert lpp.strict_profile_document(profile_document(invalid_named, enabled=True)) is None
invalid_following = json.loads(json.dumps(profiles))
invalid_following["aggressive"]["following"] = {
"preset": "custom", "curve": [1.25] * 10, "legacyCurve": [1.25] * 7,
}
assert lpp.strict_profile_document(profile_document(invalid_following, enabled=True)) is None
invalid_boolean = json.loads(json.dumps(profiles))
invalid_boolean["aggressive"]["acceleration"]["legacyCurve"][0] = True
assert lpp.strict_profile_document(profile_document(invalid_boolean, enabled=True)) is None
def test_noop_custom_update_keeps_saved_legacy_runtime_curve():
profiles = default_personality_profiles(False)
profiles["aggressive"]["acceleration"] = {
"preset": "custom", "curve": [3.5] * 10, "legacyCurve": [4.0] * 7,
}
updated = update_personality_profile(profiles, "aggressive", "acceleration", "custom", [3.5] * 10, False)
assert updated == profiles
assert interpolate_category_curve("acceleration", 10.0, updated["aggressive"]["acceleration"], False) == 4.0
def test_editing_a_migrated_custom_curve_retires_the_legacy_runtime_contract():
profiles = default_personality_profiles(False)
profiles["aggressive"]["acceleration"] = {
"preset": "custom", "curve": [1.0] * 10, "legacyCurve": [1.0] * 7,
}
updated = update_personality_profile(
profiles, "aggressive", "acceleration", "custom", [1.2] * 10, False, False,
)
assert updated["aggressive"]["acceleration"] == {"preset": "custom", "curve": [1.2] * 10}
def test_initial_custom_curve_resamples_named_preset_to_custom_axis():
curve = initial_custom_curve("acceleration", {"preset": "sport", "curve": []}, False, False)
assert len(curve) == 10
config = {"preset": "sport", "curve": []}
for speed_mph, value in zip(ACCELERATION_SPEEDS_MPH, curve, strict=True):
assert value == pytest.approx(interpolate_category_curve("acceleration", speed_mph * 0.44704, config, False, False), abs=5e-5)