mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-09-30 19:33:45 +08:00
211 lines
7.6 KiB
Python
211 lines
7.6 KiB
Python
import sys
|
|
import types
|
|
from types import SimpleNamespace
|
|
|
|
# Mock C-extensions and modules not available in x86 dev environment
|
|
if "cereal.messaging" not in sys.modules:
|
|
try:
|
|
import cereal.messaging # noqa: F401
|
|
except ImportError:
|
|
class DummyMaster:
|
|
pass
|
|
|
|
msg_mod = types.ModuleType("cereal.messaging")
|
|
msg_mod.SubMaster = DummyMaster
|
|
msg_mod.PubMaster = DummyMaster
|
|
sys.modules["cereal.messaging"] = msg_mod
|
|
|
|
if "openpilot.selfdrive.locationd.calibrationd" not in sys.modules:
|
|
try:
|
|
import openpilot.selfdrive.locationd.calibrationd
|
|
except ImportError:
|
|
calib_mod = types.ModuleType("openpilot.selfdrive.locationd.calibrationd")
|
|
calib_mod.HEIGHT_INIT = [1.22]
|
|
sys.modules["openpilot.selfdrive.locationd.calibrationd"] = calib_mod
|
|
|
|
if "openpilot.selfdrive.ui.ui_state" not in sys.modules:
|
|
try:
|
|
import openpilot.selfdrive.ui.ui_state # noqa: F401
|
|
except ImportError:
|
|
ui_state_mod = types.ModuleType("openpilot.selfdrive.ui.ui_state")
|
|
ui_state_mod.ui_state = SimpleNamespace(
|
|
sm=SimpleNamespace(valid={}),
|
|
status=0,
|
|
always_on_lateral_active=False,
|
|
is_metric=False,
|
|
starpilot_toggles={},
|
|
started_frame=0,
|
|
)
|
|
ui_state_mod.UIStatus = SimpleNamespace(DISENGAGED=0, ENGAGED=1, OVERRIDE=2)
|
|
sys.modules["openpilot.selfdrive.ui.ui_state"] = ui_state_mod
|
|
|
|
import numpy as np
|
|
import pytest
|
|
|
|
from openpilot.selfdrive.ui.onroad.model_renderer import ModelRenderer
|
|
|
|
|
|
@pytest.fixture
|
|
def renderer():
|
|
r = object.__new__(ModelRenderer)
|
|
r._car_space_transform = np.array([
|
|
[580.0, -480.0, 0.0],
|
|
[400.0, 0.0, -480.0],
|
|
[1.0, 0.0, 0.0],
|
|
], dtype=np.float32)
|
|
r._transform_dirty = False
|
|
r._clip_region = SimpleNamespace(x=-500, y=-500, width=3160, height=2080)
|
|
r._get_active_leads = list
|
|
return r
|
|
|
|
|
|
def test_batched_projection_matches_single_line_projection(renderer):
|
|
rng = np.random.default_rng(42)
|
|
lines = [
|
|
np.column_stack((np.linspace(0, 100, 33), rng.normal(0, 3, 33), rng.normal(0, 0.5, 33))).astype(np.float32)
|
|
for _ in range(6)
|
|
]
|
|
widths = [0.1, 0.15, 0.15, 0.1, 0.05, 0.05]
|
|
max_idx = 30
|
|
max_distance = 90.0
|
|
|
|
# Batched call
|
|
batched_polys = renderer._map_lines_to_polygons(lines, widths, 0.0, max_idx, max_distance)
|
|
|
|
# Individual calls via _map_line_to_polygon
|
|
individual_polys = [
|
|
renderer._map_line_to_polygon(line, w, 0.0, max_idx, max_distance)
|
|
for line, w in zip(lines, widths, strict=True)
|
|
]
|
|
|
|
assert len(batched_polys) == len(lines)
|
|
for b_poly, i_poly in zip(batched_polys, individual_polys, strict=True):
|
|
assert b_poly.dtype == np.float32
|
|
assert b_poly.shape == i_poly.shape
|
|
np.testing.assert_allclose(b_poly, i_poly, rtol=1e-5, atol=1e-3)
|
|
|
|
|
|
def test_empty_lines_and_zero_lengths(renderer):
|
|
lines = [
|
|
np.empty((0, 3), dtype=np.float32),
|
|
np.array([[10, 1, 0], [20, 2, 0]], dtype=np.float32),
|
|
np.empty((0, 3), dtype=np.float32),
|
|
]
|
|
widths = [0.1, 0.2, 0.1]
|
|
polys = renderer._map_lines_to_polygons(lines, widths, 0.0, 10, 100.0)
|
|
|
|
assert len(polys) == 3
|
|
assert polys[0].shape == (0, 2)
|
|
assert polys[1].shape[0] > 0
|
|
assert polys[2].shape == (0, 2)
|
|
|
|
|
|
def test_lead_vehicle_clipping_parity(renderer):
|
|
lead_mock = SimpleNamespace(dRel=30.0, yRel=0.0, status=True)
|
|
renderer._get_active_leads = lambda: [("ego", lead_mock)]
|
|
|
|
rng = np.random.default_rng(123)
|
|
lines = [
|
|
np.column_stack((np.linspace(0, 80, 50), rng.normal(0, 1, 50), np.zeros(50))).astype(np.float32)
|
|
for _ in range(4)
|
|
]
|
|
widths = [0.1] * 4
|
|
|
|
batched_polys = renderer._map_lines_to_polygons(lines, widths, 0.0, 45, 80.0, clip_by_lead=True)
|
|
unclipped_polys = renderer._map_lines_to_polygons(lines, widths, 0.0, 45, 80.0, clip_by_lead=False)
|
|
individual_polys = [
|
|
renderer._map_line_to_polygon(l, w, 0.0, 45, 80.0, clip_by_lead=True)
|
|
for l, w in zip(lines, widths, strict=True)
|
|
]
|
|
|
|
# Parity: batched output must match individual output
|
|
for b_poly, i_poly in zip(batched_polys, individual_polys, strict=True):
|
|
assert b_poly.shape == i_poly.shape
|
|
np.testing.assert_allclose(b_poly, i_poly, rtol=1e-5, atol=1e-3)
|
|
|
|
# Functional guarantee: lines MUST be truncated before the lead vehicle at 30.0m - 1.5m
|
|
for b_poly, u_poly in zip(batched_polys, unclipped_polys, strict=True):
|
|
assert b_poly.shape[0] < u_poly.shape[0]
|
|
|
|
|
|
def test_allow_invert_hill_geometry(renderer):
|
|
x = np.linspace(5, 100, 40, dtype=np.float32)
|
|
y = np.zeros(40, dtype=np.float32)
|
|
z = np.sin(np.linspace(0, np.pi, 40)).astype(np.float32) * 5.0
|
|
line = np.column_stack((x, y, z))
|
|
|
|
poly_no_invert = renderer._map_lines_to_polygons([line], [0.2], 0.0, 39, 100.0, allow_invert=False)[0]
|
|
poly_invert = renderer._map_lines_to_polygons([line], [0.2], 0.0, 39, 100.0, allow_invert=True)[0]
|
|
individual_no_invert = renderer._map_line_to_polygon(line, 0.2, 0.0, 39, 100.0, allow_invert=False)
|
|
|
|
# Parity check
|
|
assert poly_no_invert.shape == individual_no_invert.shape
|
|
np.testing.assert_allclose(poly_no_invert, individual_no_invert, rtol=1e-5, atol=1e-3)
|
|
|
|
# Functional guarantee: allow_invert=False MUST discard downward reverse-slope points
|
|
assert poly_no_invert.shape[0] < poly_invert.shape[0]
|
|
|
|
|
|
def test_clipping_region_bounds_parity(renderer):
|
|
renderer._clip_region = SimpleNamespace(x=100, y=100, width=500, height=500)
|
|
rng = np.random.default_rng(999)
|
|
lines = [
|
|
np.column_stack((np.linspace(1, 150, 60), rng.uniform(-10, 10, 60), np.zeros(60))).astype(np.float32)
|
|
for _ in range(3)
|
|
]
|
|
widths = [0.1, 0.2, 0.3]
|
|
|
|
batched = renderer._map_lines_to_polygons(lines, widths, 0.0, 55, 150.0)
|
|
individual = [renderer._map_line_to_polygon(l, w, 0.0, 55, 150.0) for l, w in zip(lines, widths, strict=True)]
|
|
|
|
for b, i in zip(batched, individual, strict=True):
|
|
assert b.shape == i.shape
|
|
np.testing.assert_allclose(b, i, rtol=1e-5, atol=1e-3)
|
|
|
|
|
|
def test_radar_update_decoupled_from_model_regeneration(monkeypatch):
|
|
from unittest.mock import MagicMock
|
|
import pyray as rl
|
|
from openpilot.selfdrive.ui.onroad.model_renderer import ModelPoints
|
|
|
|
r = object.__new__(ModelRenderer)
|
|
r._path = ModelPoints()
|
|
r._path.raw_points = np.zeros((10, 3), dtype=np.float32)
|
|
r._transform_dirty = False
|
|
r._started_frame = 0
|
|
r._should_render_lead_indicator = lambda rs: True
|
|
r._update_model = MagicMock()
|
|
r._update_leads = MagicMock()
|
|
r._update_adjacent_leads = MagicMock()
|
|
r._draw_lane_lines = MagicMock()
|
|
r._draw_path = MagicMock()
|
|
r._draw_lead_indicator = MagicMock()
|
|
r._draw_radar_tracks = MagicMock()
|
|
r._update_raw_points = MagicMock()
|
|
r._params = SimpleNamespace(get_bool=lambda *args, **kwargs: False)
|
|
|
|
from openpilot.selfdrive.ui.ui_state import ui_state
|
|
class MockSM:
|
|
recv_frame = {"liveCalibration": 1, "modelV2": 1}
|
|
updated = {"carParams": False, "modelV2": False, "radarState": True}
|
|
valid = {"radarState": True, "starpilotRadarState": False}
|
|
def __getitem__(self, k):
|
|
return SimpleNamespace(openpilotLongitudinalControl=False, experimentalMode=False, leadOne=None, position=None, height=[1.22])
|
|
|
|
mock_sm = MockSM()
|
|
monkeypatch.setattr(ui_state, "sm", mock_sm)
|
|
monkeypatch.setattr(ui_state, "started_frame", 0)
|
|
|
|
# Frame 1: Only radarState updated. _update_model must NOT run, but _update_leads MUST run.
|
|
r._render(rl.Rectangle(0, 0, 100, 100))
|
|
assert not r._update_model.called, "Model geometry should not reproject on radarState alone"
|
|
assert r._update_leads.called, "Lead indicators should update on radarState"
|
|
|
|
# Frame 2: modelV2 updated. _update_model MUST run.
|
|
mock_sm.updated["modelV2"] = True
|
|
mock_sm.updated["radarState"] = False
|
|
r._update_model.reset_mock()
|
|
r._render(rl.Rectangle(0, 0, 100, 100))
|
|
assert r._update_model.called, "Model geometry should reproject when modelV2 updates"
|
|
|