Files
sunnypilot/openpilot/selfdrive/locationd/test/test_lagd.py
T
2026-08-10 09:30:22 -07:00

160 lines
7.1 KiB
Python

import random
import numpy as np
import time
import unittest
from functools import cache
from openpilot.common.test import OpenpilotTestCase
from openpilot.cereal import messaging, log
from opendbc.car.structs import car
from openpilot.selfdrive.locationd.lagd import LateralLagEstimator, retrieve_initial_lag, masked_normalized_cross_correlation, \
BLOCK_NUM_NEEDED, BLOCK_SIZE, MIN_OKAY_WINDOW_SEC, VERSION, MIN_LAG, MAX_LAG
from openpilot.selfdrive.test.process_replay.migration import migrate, migrate_carParams
from openpilot.selfdrive.locationd.test.test_locationd_scenarios import TEST_ROUTE
from openpilot.common.params import Params
from openpilot.tools.lib.logreader import LogReader
from openpilot.common.hardware import PC
MAX_ERR_FRAMES = 1
DT = 0.05
LAGD_MIN_LAG_FRAMES, LAGD_MAX_LAG_FRAMES = int(round(MIN_LAG / DT)), int(round(MAX_LAG / DT))
@cache
def get_test_car_params():
lr = migrate(LogReader(TEST_ROUTE), [migrate_carParams])
return next(m for m in lr if m.which() == "carParams").carParams
def process_messages(estimator, lag_frames, n_frames, vego=25.0, rejection_threshold=0.0):
for i in range(n_frames):
t = i * estimator.dt
desired_la = np.cos(10 * t) * 0.3
actual_la = np.cos(10 * (t - lag_frames * estimator.dt)) * 0.3
# if sample is masked out, set it to desired value (no lag)
rejected = random.uniform(0, 1) < rejection_threshold
if rejected:
actual_la = desired_la
desired_cuvature = float(desired_la / (vego ** 2))
actual_yr = float(actual_la / vego)
msgs = [
(t, "carControl", car.CarControl(latActive=not rejected)),
(t, "carState", car.CarState(vEgo=vego, steeringPressed=False)),
(t, "controlsState", log.ControlsState(desiredCurvature=desired_cuvature)),
(t, "deviceMotion", log.DeviceMotion(angularVelocityDevice=log.DeviceMotion.XYZMeasurement(z=actual_yr, valid=True),
posenetOK=True, inputsOK=True)),
(t, "extrinsicsCalibration", log.ExtrinsicsCalibration(rpyCalib=[0, 0, 0], calStatus=log.ExtrinsicsCalibration.Status.calibrated)),
]
for t, w, m in msgs:
estimator.handle_log(t, w, m)
estimator.update_points()
estimator.update_estimate()
class TestLagd(OpenpilotTestCase):
def test_read_saved_params(self):
params = Params()
CP = get_test_car_params()
msg = messaging.new_message('lateralDelay')
msg.lateralDelay.lateralDelayEstimate = random.random()
msg.lateralDelay.validBlocks = random.randint(1, 10)
msg.lateralDelay.version = VERSION
params.put("LiveDelay", msg.to_bytes(), block=True)
params.put("CarParamsPrevRoute", CP.as_builder().to_bytes(), block=True)
saved_lag_params = retrieve_initial_lag(params, CP)
assert saved_lag_params is not None
lag, valid_blocks = saved_lag_params
assert lag == msg.lateralDelay.lateralDelayEstimate
assert valid_blocks == msg.lateralDelay.validBlocks
def test_read_invalid_saved_params(self, subtests):
params = Params()
CP = get_test_car_params()
for msg_dict in [{'version': 0}, {'status': 'invalid'}, {'validBlocks': 100}]:
with subtests.test(msg=f"lateralDelay={msg_dict}"):
msg = messaging.new_message('lateralDelay')
msg.lateralDelay = msg_dict
params.put("LiveDelay", msg.to_bytes(), block=True)
params.put("CarParamsPrevRoute", CP.as_builder().to_bytes(), block=True)
assert retrieve_initial_lag(params, CP) is None
def test_ncc(self):
rng = np.random.default_rng()
lag_frames = random.randint(1, 19)
desired_sig = np.sin(np.arange(0.0, 10.0, 0.1))
actual_sig = np.sin(np.arange(0.0, 10.0, 0.1) - lag_frames * 0.1)
mask = np.ones(len(desired_sig), dtype=bool)
corr = masked_normalized_cross_correlation(desired_sig, actual_sig, mask, 200)[len(desired_sig) - 1:len(desired_sig) + 20]
assert np.argmax(corr) == lag_frames
# add some noise
desired_sig += rng.normal(0, 0.05, len(desired_sig))
actual_sig += rng.normal(0, 0.05, len(actual_sig))
corr = masked_normalized_cross_correlation(desired_sig, actual_sig, mask, 200)[len(desired_sig) - 1:len(desired_sig) + 20]
assert np.argmax(corr) in range(lag_frames - MAX_ERR_FRAMES, lag_frames + MAX_ERR_FRAMES + 1)
# mask out 40% of the values, and make them noise
mask = rng.choice([True, False], size=len(desired_sig), p=[0.6, 0.4])
desired_sig[~mask] = rng.normal(0, 1, size=np.sum(~mask))
actual_sig[~mask] = rng.normal(0, 1, size=np.sum(~mask))
corr = masked_normalized_cross_correlation(desired_sig, actual_sig, mask, 200)[len(desired_sig) - 1:len(desired_sig) + 20]
assert np.argmax(corr) in range(lag_frames - MAX_ERR_FRAMES, lag_frames + MAX_ERR_FRAMES + 1)
def test_empty_estimator(self):
mocked_CP = car.CarParams(steerActuatorDelay=0.5)
estimator = LateralLagEstimator(mocked_CP, DT)
msg = estimator.get_msg(True)
assert msg.lateralDelay.status == 'unestimated'
assert np.allclose(msg.lateralDelay.lateralDelay, estimator.initial_lag)
assert np.allclose(msg.lateralDelay.lateralDelayEstimate, estimator.initial_lag)
assert msg.lateralDelay.validBlocks == 0
assert msg.lateralDelay.calPerc == 0
def test_estimator_basics(self, subtests):
for lag_frames in range(LAGD_MIN_LAG_FRAMES, LAGD_MAX_LAG_FRAMES - 1):
with subtests.test(msg=f"lag_frames={lag_frames}"):
mocked_CP = car.CarParams(steerActuatorDelay=0.5)
estimator = LateralLagEstimator(mocked_CP, DT, min_recovery_buffer_sec=0.0, min_yr=0.0)
process_messages(estimator, lag_frames, int(MIN_OKAY_WINDOW_SEC / DT) + BLOCK_NUM_NEEDED * BLOCK_SIZE)
msg = estimator.get_msg(True)
assert msg.lateralDelay.status == 'estimated'
assert np.allclose(msg.lateralDelay.lateralDelay, lag_frames * DT, atol=0.01)
assert np.allclose(msg.lateralDelay.lateralDelayEstimate, lag_frames * DT, atol=0.01)
assert np.allclose(msg.lateralDelay.lateralDelayEstimateStd, 0.0, atol=0.01)
assert msg.lateralDelay.validBlocks == BLOCK_NUM_NEEDED
assert msg.lateralDelay.calPerc == 100
def test_estimator_masking(self):
mocked_CP, lag_frames = car.CarParams(steerActuatorDelay=0.5), random.randint(LAGD_MIN_LAG_FRAMES, LAGD_MAX_LAG_FRAMES - 1)
estimator = LateralLagEstimator(mocked_CP, DT, min_recovery_buffer_sec=0.0, min_yr=0.0, min_valid_block_count=1)
process_messages(estimator, lag_frames, (int(MIN_OKAY_WINDOW_SEC / DT) + BLOCK_SIZE) * 2, rejection_threshold=0.4)
msg = estimator.get_msg(True)
assert np.allclose(msg.lateralDelay.lateralDelayEstimate, lag_frames * DT, atol=0.01)
assert np.allclose(msg.lateralDelay.lateralDelayEstimateStd, 0.0, atol=0.01)
assert msg.lateralDelay.calPerc == 100
@unittest.skipIf(PC, "only on device")
def test_estimator_performance(self):
mocked_CP = car.CarParams(steerActuatorDelay=0.5)
estimator = LateralLagEstimator(mocked_CP, DT)
ds = []
for _ in range(1000):
st = time.perf_counter()
estimator.update_points()
estimator.update_estimate()
d = time.perf_counter() - st
ds.append(d)
assert np.mean(ds) < DT