mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-09-05 23:53:40 +08:00
160 lines
5.9 KiB
Python
160 lines
5.9 KiB
Python
from types import SimpleNamespace
|
|
|
|
import numpy as np
|
|
import pytest
|
|
|
|
from openpilot.starpilot.common.model_lab import (
|
|
LATERAL_PLAN_COLUMNS,
|
|
compose_model_outputs,
|
|
hybrid_action_values,
|
|
is_small_model_metadata,
|
|
model_lab_manifest_eligible,
|
|
normalize_model_lab_config,
|
|
validate_model_lab_selection,
|
|
)
|
|
|
|
|
|
def _catalog_model(version="v15", *, small=True, eligible=True, artifact_available=True, artifact_installed=True):
|
|
return {
|
|
"version": version,
|
|
"small": small,
|
|
"modelLabEligible": eligible,
|
|
"modelLabArtifactAvailable": artifact_available,
|
|
"modelLabArtifactInstalled": artifact_installed,
|
|
}
|
|
|
|
|
|
def test_manifest_eligibility_uses_explicit_size_and_legacy_gpu_inference():
|
|
assert not is_small_model_metadata({"model_size": "small", "uses_external_gpu": True})
|
|
assert not is_small_model_metadata({"model_size": "chestnut", "uses_external_gpu": False})
|
|
assert is_small_model_metadata({"uses_external_gpu": False})
|
|
assert not is_small_model_metadata({"uses_external_gpu": True})
|
|
assert model_lab_manifest_eligible({"uses_external_gpu": False}, "v15")
|
|
assert not model_lab_manifest_eligible({"uses_external_gpu": True}, "v16")
|
|
assert not model_lab_manifest_eligible({"model_lab_eligible": False}, "v15")
|
|
assert not model_lab_manifest_eligible({"uses_external_gpu": False}, "v7")
|
|
|
|
|
|
def test_config_normalization_is_closed_by_default():
|
|
assert normalize_model_lab_config("not-json") == {
|
|
"enabled": False,
|
|
"lateralModel": "",
|
|
"longitudinalModel": "",
|
|
}
|
|
assert normalize_model_lab_config('{"enabled": true, "lateralModel": " lat ", "longitudinalModel": "long"}') == {
|
|
"enabled": True,
|
|
"lateralModel": "lat",
|
|
"longitudinalModel": "long",
|
|
}
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("chestnut_ready", "catalog", "lateral", "longitudinal", "expected"),
|
|
[
|
|
(False, {"lat": _catalog_model(), "long": _catalog_model()}, "lat", "long", "Chestnut"),
|
|
(True, {"lat": _catalog_model()}, "lat", "missing", "current manifest"),
|
|
(True, {"lat": _catalog_model(small=False), "long": _catalog_model()}, "lat", "long", "Chestnut-class"),
|
|
(True, {"lat": _catalog_model(artifact_available=False), "long": _catalog_model()}, "lat", "long", "no precompiled AMD"),
|
|
(True, {"lat": _catalog_model(artifact_installed=False), "long": _catalog_model()}, "lat", "long", "not downloaded"),
|
|
],
|
|
)
|
|
def test_selection_validation_rejects_unsafe_pairs(chestnut_ready, catalog, lateral, longitudinal, expected):
|
|
error = validate_model_lab_selection(
|
|
{"enabled": True, "lateralModel": lateral, "longitudinalModel": longitudinal},
|
|
catalog,
|
|
chestnut_ready=chestnut_ready,
|
|
)
|
|
assert expected in error
|
|
|
|
|
|
def test_selection_validation_accepts_distinct_ready_small_mixed_version_models():
|
|
catalog = {"lat": _catalog_model("v15"), "long": _catalog_model("v9")}
|
|
assert validate_model_lab_selection(
|
|
{"enabled": True, "lateralModel": "lat", "longitudinalModel": "long"},
|
|
catalog,
|
|
chestnut_ready=True,
|
|
) is None
|
|
assert validate_model_lab_selection({"enabled": False}, {}, chestnut_ready=False) is None
|
|
|
|
|
|
def test_composition_assigns_lateral_and_longitudinal_outputs_without_mutation():
|
|
lateral_plan = np.full((1, 33, 15), 11.0, dtype=np.float32)
|
|
longitudinal_plan = np.full((1, 33, 15), 22.0, dtype=np.float32)
|
|
lateral_action = np.array([[1.5, 2.5]], dtype=np.float32)
|
|
longitudinal_action = np.array([[3.5, 4.5]], dtype=np.float32)
|
|
lateral = {
|
|
"plan": lateral_plan,
|
|
"plan_stds": lateral_plan + 1,
|
|
"action": lateral_action,
|
|
"action_stds": lateral_action + 1,
|
|
"lane_lines": np.array([111.0]),
|
|
"pose": np.array([113.0]),
|
|
"lead": np.array([112.0]),
|
|
}
|
|
longitudinal = {
|
|
"plan": longitudinal_plan,
|
|
"plan_stds": longitudinal_plan + 2,
|
|
"action": longitudinal_action,
|
|
"action_stds": longitudinal_action + 2,
|
|
"lane_lines": np.array([221.0]),
|
|
"pose": np.array([223.0]),
|
|
"lead": np.array([222.0]),
|
|
}
|
|
|
|
composed = compose_model_outputs(lateral, longitudinal)
|
|
|
|
lateral_columns = set(LATERAL_PLAN_COLUMNS)
|
|
for column in range(15):
|
|
expected = 11.0 if column in lateral_columns else 22.0
|
|
np.testing.assert_array_equal(composed["plan"][..., column], expected)
|
|
assert composed["action"][0, 0] == lateral_action[0, 0]
|
|
assert composed["action"][0, 1] == longitudinal_action[0, 1]
|
|
assert composed["lane_lines"] is lateral["lane_lines"]
|
|
assert composed["pose"] is lateral["pose"]
|
|
assert composed["lead"] is longitudinal["lead"]
|
|
np.testing.assert_array_equal(lateral_plan, 11.0)
|
|
np.testing.assert_array_equal(longitudinal_plan, 22.0)
|
|
|
|
composed_on_longitudinal_frame = compose_model_outputs(lateral, longitudinal, longitudinal)
|
|
assert composed_on_longitudinal_frame["pose"] is longitudinal["pose"]
|
|
|
|
|
|
def test_composition_fails_closed_for_incompatible_plan_contracts():
|
|
with pytest.raises(ValueError, match="incompatible"):
|
|
compose_model_outputs(
|
|
{"plan": np.zeros((1, 33, 15))},
|
|
{"plan": np.zeros((1, 32, 15))},
|
|
)
|
|
|
|
|
|
def test_composition_does_not_leak_longitudinal_values_into_lateral_only_fields():
|
|
composed = compose_model_outputs(
|
|
{"plan": np.zeros((1, 33, 15))},
|
|
{
|
|
"plan": np.zeros((1, 33, 15)),
|
|
"desired_curvature": np.ones((1, 1)),
|
|
"lane_lines": np.ones((1, 4, 33, 2)),
|
|
},
|
|
)
|
|
assert "action" not in composed
|
|
assert "desired_curvature" not in composed
|
|
assert "lane_lines" not in composed
|
|
|
|
|
|
def test_composition_rejects_asymmetric_action_contracts():
|
|
with pytest.raises(ValueError, match="matching action outputs"):
|
|
compose_model_outputs(
|
|
{"plan": np.zeros((1, 33, 15))},
|
|
{"plan": np.zeros((1, 33, 15)), "action": np.ones((1, 2))},
|
|
)
|
|
|
|
|
|
def test_hybrid_action_uses_only_the_assigned_responsibility():
|
|
lateral = SimpleNamespace(desiredCurvature=0.125, desiredAcceleration=99, shouldStop=True)
|
|
longitudinal = SimpleNamespace(desiredCurvature=88, desiredAcceleration=-0.75, shouldStop=False)
|
|
assert hybrid_action_values(lateral, longitudinal) == {
|
|
"desiredCurvature": 0.125,
|
|
"desiredAcceleration": -0.75,
|
|
"shouldStop": False,
|
|
}
|