sunnypilot v2026.002.000
version: sunnypilot v2026.002.000 (staging) date: 2026-07-21T07:44:02 master commit: 2c334ede443d7391d27575af8c854a095ba702a8
This commit is contained in:
commit
de77c89cf6
3995 files changed
+1260688
No files matched your search
Whitespace-only changes.
@@ -0,0 +1,46 @@
|
||||
import pytest
|
||||
import itertools
|
||||
from openpilot.common.parameterized import parameterized_class
|
||||
|
||||
from openpilot.cereal import log
|
||||
|
||||
from openpilot.selfdrive.controls.lib.longitudinal_mpc_lib.long_mpc import get_safe_obstacle_distance, get_stopped_equivalence_factor, get_T_FOLLOW
|
||||
from openpilot.selfdrive.test.longitudinal_maneuvers.maneuver import Maneuver
|
||||
|
||||
|
||||
def desired_follow_distance(v_ego, v_lead, t_follow=None):
|
||||
if t_follow is None:
|
||||
t_follow = get_T_FOLLOW()
|
||||
return get_safe_obstacle_distance(v_ego, t_follow) - get_stopped_equivalence_factor(v_lead)
|
||||
|
||||
def run_following_distance_simulation(v_lead, t_end=100.0, e2e=False, personality=0):
|
||||
man = Maneuver(
|
||||
'',
|
||||
duration=t_end,
|
||||
initial_speed=float(v_lead),
|
||||
lead_relevancy=True,
|
||||
initial_distance_lead=100,
|
||||
speed_lead_values=[v_lead],
|
||||
breakpoints=[0.],
|
||||
e2e=e2e,
|
||||
personality=personality,
|
||||
)
|
||||
valid, output = man.evaluate()
|
||||
assert valid
|
||||
return output[-1,2] - output[-1,1]
|
||||
|
||||
|
||||
@parameterized_class(("e2e", "personality", "speed"), itertools.product(
|
||||
[True, False], # e2e
|
||||
[log.LongitudinalPersonality.relaxed, # personality
|
||||
log.LongitudinalPersonality.standard,
|
||||
log.LongitudinalPersonality.aggressive],
|
||||
[0,10,35])) # speed
|
||||
class TestFollowingDistance:
|
||||
def test_following_distance(self):
|
||||
v_lead = float(self.speed)
|
||||
simulation_steady_state = run_following_distance_simulation(v_lead, e2e=self.e2e, personality=self.personality)
|
||||
correct_steady_state = desired_follow_distance(v_lead, v_lead, get_T_FOLLOW(self.personality))
|
||||
err_ratio = 0.2 if self.e2e else 0.1
|
||||
abs_err_margin = 0.5 if v_lead > 0.0 else 1.15
|
||||
assert simulation_steady_state == pytest.approx(correct_steady_state, abs=err_ratio * correct_steady_state + abs_err_margin)
|
||||
@@ -0,0 +1,56 @@
|
||||
from openpilot.common.parameterized import parameterized
|
||||
|
||||
from openpilot.cereal import log
|
||||
from opendbc.car.structs import car
|
||||
from opendbc.car.car_helpers import interfaces
|
||||
from opendbc.car.honda.values import CAR as HONDA
|
||||
from opendbc.car.toyota.values import CAR as TOYOTA
|
||||
from opendbc.car.nissan.values import CAR as NISSAN
|
||||
from opendbc.car.gm.values import CAR as GM
|
||||
from opendbc.car.vehicle_model import VehicleModel
|
||||
from openpilot.common.realtime import DT_CTRL
|
||||
from openpilot.selfdrive.car.helpers import convert_to_capnp
|
||||
from openpilot.selfdrive.controls.lib.latcontrol_pid import LatControlPID
|
||||
from openpilot.selfdrive.controls.lib.latcontrol_torque import LatControlTorque
|
||||
from openpilot.selfdrive.controls.lib.latcontrol_angle import LatControlAngle
|
||||
from openpilot.selfdrive.locationd.helpers import Pose
|
||||
from openpilot.common.mock.generators import generate_livePose
|
||||
from openpilot.sunnypilot.selfdrive.car import interfaces as sunnypilot_interfaces
|
||||
|
||||
|
||||
class TestLatControl:
|
||||
|
||||
@parameterized.expand([(HONDA.HONDA_CIVIC, LatControlPID), (TOYOTA.TOYOTA_RAV4, LatControlTorque),
|
||||
(NISSAN.NISSAN_LEAF, LatControlAngle), (GM.CHEVROLET_BOLT_EUV, LatControlTorque)])
|
||||
def test_saturation(self, car_name, controller):
|
||||
CarInterface = interfaces[car_name]
|
||||
CP = CarInterface.get_non_essential_params(car_name)
|
||||
CP_SP = CarInterface.get_non_essential_params_sp(CP, car_name)
|
||||
CI = CarInterface(CP, CP_SP)
|
||||
sunnypilot_interfaces.setup_interfaces(CI)
|
||||
CP_SP = convert_to_capnp(CP_SP)
|
||||
VM = VehicleModel(CP)
|
||||
|
||||
controller = controller(CP.as_reader(), CP_SP.as_reader(), CI, DT_CTRL)
|
||||
|
||||
CS = car.CarState.new_message()
|
||||
CS.vEgo = 30
|
||||
CS.steeringPressed = False
|
||||
|
||||
params = log.LiveParametersData.new_message()
|
||||
|
||||
lp = generate_livePose()
|
||||
pose = Pose.from_live_pose(lp.livePose)
|
||||
|
||||
# Saturate for curvature limited and controller limited
|
||||
for _ in range(1000):
|
||||
_, _, lac_log = controller.update(True, CS, VM, params, False, 0, pose, True, 0.2)
|
||||
assert lac_log.saturated
|
||||
|
||||
for _ in range(1000):
|
||||
_, _, lac_log = controller.update(True, CS, VM, params, False, 0, pose, False, 0.2)
|
||||
assert not lac_log.saturated
|
||||
|
||||
for _ in range(1000):
|
||||
_, _, lac_log = controller.update(True, CS, VM, params, False, 1, pose, False, 0.2)
|
||||
assert lac_log.saturated
|
||||
@@ -0,0 +1,48 @@
|
||||
from openpilot.common.parameterized import parameterized
|
||||
|
||||
from openpilot.cereal import log
|
||||
from opendbc.car.structs import car
|
||||
from opendbc.car.car_helpers import interfaces
|
||||
from opendbc.car.toyota.values import CAR as TOYOTA
|
||||
from opendbc.car.vehicle_model import VehicleModel
|
||||
from openpilot.common.realtime import DT_CTRL
|
||||
from openpilot.selfdrive.controls.lib.latcontrol_torque import LatControlTorque, LAT_ACCEL_REQUEST_BUFFER_SECONDS
|
||||
|
||||
from openpilot.selfdrive.car.helpers import convert_to_capnp
|
||||
from openpilot.selfdrive.locationd.helpers import Pose
|
||||
from openpilot.common.mock.generators import generate_livePose
|
||||
from openpilot.sunnypilot.selfdrive.car import interfaces as sunnypilot_interfaces
|
||||
|
||||
def get_controller(car_name):
|
||||
CarInterface = interfaces[car_name]
|
||||
CP = CarInterface.get_non_essential_params(car_name)
|
||||
CP_SP = CarInterface.get_non_essential_params_sp(CP, car_name)
|
||||
CI = CarInterface(CP, CP_SP)
|
||||
sunnypilot_interfaces.setup_interfaces(CI)
|
||||
CP_SP = convert_to_capnp(CP_SP)
|
||||
VM = VehicleModel(CP)
|
||||
controller = LatControlTorque(CP.as_reader(), CP_SP.as_reader(), CI, DT_CTRL)
|
||||
return controller, VM
|
||||
|
||||
class TestLatControlTorqueBuffer:
|
||||
|
||||
@parameterized.expand([(TOYOTA.TOYOTA_COROLLA_TSS2,)])
|
||||
def test_request_buffer_consistency(self, car_name):
|
||||
buffer_steps = int(LAT_ACCEL_REQUEST_BUFFER_SECONDS / DT_CTRL)
|
||||
controller, VM = get_controller(car_name)
|
||||
|
||||
CS = car.CarState.new_message()
|
||||
CS.vEgo = 30
|
||||
CS.steeringPressed = False
|
||||
params = log.LiveParametersData.new_message()
|
||||
|
||||
lp = generate_livePose()
|
||||
pose = Pose.from_live_pose(lp.livePose)
|
||||
|
||||
for _ in range(buffer_steps):
|
||||
controller.update(True, CS, VM, params, False, 0.001, pose, False, 0.2)
|
||||
assert all(val != 0 for val in controller.lat_accel_request_buffer)
|
||||
|
||||
for _ in range(buffer_steps):
|
||||
controller.update(False, CS, VM, params, False, 0.0, pose, False, 0.2)
|
||||
assert all(val == 0 for val in controller.lat_accel_request_buffer)
|
||||
@@ -0,0 +1,31 @@
|
||||
import openpilot.cereal.messaging as messaging
|
||||
|
||||
from opendbc.car.toyota.values import CAR as TOYOTA
|
||||
from openpilot.selfdrive.test.process_replay import replay_process_with_name
|
||||
|
||||
|
||||
class TestLeads:
|
||||
def test_radar_fault(self):
|
||||
# if there's no radar-related can traffic, radard should either not respond or respond with an error
|
||||
# this is tightly coupled with underlying car radar_interface implementation, but it's a good sanity check
|
||||
def single_iter_pkg():
|
||||
# single iter package, with meaningless cans and empty carState/modelV2
|
||||
msgs = []
|
||||
for _ in range(500):
|
||||
can = messaging.new_message("can", 1)
|
||||
cs = messaging.new_message("carState")
|
||||
cp = messaging.new_message("carParams")
|
||||
msgs.append(can.as_reader())
|
||||
msgs.append(cs.as_reader())
|
||||
msgs.append(cp.as_reader())
|
||||
model = messaging.new_message("modelV2")
|
||||
msgs.append(model.as_reader())
|
||||
|
||||
return msgs
|
||||
|
||||
msgs = [m for _ in range(3) for m in single_iter_pkg()]
|
||||
out = replay_process_with_name("card", msgs, fingerprint=TOYOTA.TOYOTA_COROLLA_TSS2)
|
||||
states = [m for m in out if m.which() == "liveTracks"]
|
||||
failures = [not state.valid for state in states]
|
||||
|
||||
assert len(states) == 0 or all(failures)
|
||||
@@ -0,0 +1,60 @@
|
||||
from openpilot.cereal import custom
|
||||
from opendbc.car.structs import car
|
||||
from openpilot.selfdrive.controls.lib.longcontrol import LongCtrlState, long_control_state_trans
|
||||
|
||||
|
||||
|
||||
|
||||
class TestLongControlStateTransition:
|
||||
|
||||
def test_stay_stopped(self):
|
||||
CP = car.CarParams.new_message()
|
||||
CP_SP = custom.CarParamsSP.new_message()
|
||||
active = True
|
||||
current_state = LongCtrlState.stopping
|
||||
next_state = long_control_state_trans(CP, CP_SP, active, current_state, v_ego=0.1,
|
||||
should_stop=True, brake_pressed=False, cruise_standstill=False)
|
||||
assert next_state == LongCtrlState.stopping
|
||||
next_state = long_control_state_trans(CP, CP_SP, active, current_state, v_ego=0.1,
|
||||
should_stop=False, brake_pressed=True, cruise_standstill=False)
|
||||
assert next_state == LongCtrlState.stopping
|
||||
next_state = long_control_state_trans(CP, CP_SP, active, current_state, v_ego=0.1,
|
||||
should_stop=False, brake_pressed=False, cruise_standstill=True)
|
||||
assert next_state == LongCtrlState.stopping
|
||||
next_state = long_control_state_trans(CP, CP_SP, active, current_state, v_ego=1.0,
|
||||
should_stop=False, brake_pressed=False, cruise_standstill=False)
|
||||
assert next_state == LongCtrlState.pid
|
||||
active = False
|
||||
next_state = long_control_state_trans(CP, CP_SP, active, current_state, v_ego=1.0,
|
||||
should_stop=False, brake_pressed=False, cruise_standstill=False)
|
||||
assert next_state == LongCtrlState.off
|
||||
|
||||
def test_engage():
|
||||
CP = car.CarParams.new_message()
|
||||
CP_SP = custom.CarParamsSP.new_message()
|
||||
active = True
|
||||
current_state = LongCtrlState.off
|
||||
next_state = long_control_state_trans(CP, CP_SP, active, current_state, v_ego=0.1,
|
||||
should_stop=True, brake_pressed=False, cruise_standstill=False)
|
||||
assert next_state == LongCtrlState.stopping
|
||||
next_state = long_control_state_trans(CP, CP_SP, active, current_state, v_ego=0.1,
|
||||
should_stop=False, brake_pressed=True, cruise_standstill=False)
|
||||
assert next_state == LongCtrlState.stopping
|
||||
next_state = long_control_state_trans(CP, CP_SP, active, current_state, v_ego=0.1,
|
||||
should_stop=False, brake_pressed=False, cruise_standstill=True)
|
||||
assert next_state == LongCtrlState.stopping
|
||||
next_state = long_control_state_trans(CP, CP_SP, active, current_state, v_ego=0.1,
|
||||
should_stop=False, brake_pressed=False, cruise_standstill=False)
|
||||
assert next_state == LongCtrlState.pid
|
||||
|
||||
def test_starting():
|
||||
CP = car.CarParams.new_message(startingState=True, vEgoStarting=0.5)
|
||||
CP_SP = custom.CarParamsSP.new_message()
|
||||
active = True
|
||||
current_state = LongCtrlState.starting
|
||||
next_state = long_control_state_trans(CP, CP_SP, active, current_state, v_ego=0.1,
|
||||
should_stop=False, brake_pressed=False, cruise_standstill=False)
|
||||
assert next_state == LongCtrlState.starting
|
||||
next_state = long_control_state_trans(CP, CP_SP, active, current_state, v_ego=1.0,
|
||||
should_stop=False, brake_pressed=False, cruise_standstill=False)
|
||||
assert next_state == LongCtrlState.pid
|
||||
@@ -0,0 +1,73 @@
|
||||
import numpy as np
|
||||
from openpilot.cereal import messaging
|
||||
from opendbc.car.structs import car
|
||||
from opendbc.car import ACCELERATION_DUE_TO_GRAVITY
|
||||
from opendbc.car import structs
|
||||
from opendbc.car.lateral import get_friction, FRICTION_THRESHOLD
|
||||
from openpilot.common.realtime import DT_MDL
|
||||
from openpilot.selfdrive.locationd.torqued import TorqueEstimator, MIN_BUCKET_POINTS, POINTS_PER_BUCKET, STEER_BUCKET_BOUNDS
|
||||
|
||||
np.random.seed(0)
|
||||
|
||||
LA_ERR_STD = 1.0
|
||||
INPUT_NOISE_STD = 0.08
|
||||
V_EGO = 30.0
|
||||
|
||||
WARMUP_BUCKET_POINTS = (1.5*MIN_BUCKET_POINTS).astype(int)
|
||||
STRAIGHT_ROAD_LA_BOUNDS = (0.02, 0.03)
|
||||
|
||||
ROLL_BIAS_DEG = 2.0
|
||||
ROLL_COMPENSATION_BIAS = ACCELERATION_DUE_TO_GRAVITY*float(np.sin(np.deg2rad(ROLL_BIAS_DEG)))
|
||||
TORQUE_TUNE = structs.CarParams.LateralTorqueTuning(latAccelFactor=2.0, latAccelOffset=0.0, friction=0.2)
|
||||
TORQUE_TUNE_BIASED = structs.CarParams.LateralTorqueTuning(latAccelFactor=2.0, latAccelOffset=-ROLL_COMPENSATION_BIAS, friction=0.2)
|
||||
|
||||
def generate_inputs(torque_tune, la_err_std, input_noise_std=None):
|
||||
rng = np.random.default_rng(0)
|
||||
steer_torques = np.concat([rng.uniform(bnd[0], bnd[1], pts) for bnd, pts in zip(STEER_BUCKET_BOUNDS, WARMUP_BUCKET_POINTS, strict=True)])
|
||||
la_errs = rng.normal(scale=la_err_std, size=steer_torques.size)
|
||||
frictions = np.array([get_friction(la_err, 0.0, FRICTION_THRESHOLD, torque_tune) for la_err in la_errs])
|
||||
lat_accels = torque_tune.latAccelFactor*steer_torques + torque_tune.latAccelOffset + frictions
|
||||
if input_noise_std is not None:
|
||||
steer_torques += rng.normal(scale=input_noise_std, size=steer_torques.size)
|
||||
lat_accels += rng.normal(scale=input_noise_std, size=steer_torques.size)
|
||||
return steer_torques, lat_accels
|
||||
|
||||
def get_warmed_up_estimator(steer_torques, lat_accels):
|
||||
est = TorqueEstimator(car.CarParams())
|
||||
for steer_torque, lat_accel in zip(steer_torques, lat_accels, strict=True):
|
||||
est.filtered_points.add_point(steer_torque, lat_accel)
|
||||
return est
|
||||
|
||||
def simulate_straight_road_msgs(est):
|
||||
carControl = messaging.new_message('carControl').carControl
|
||||
carOutput = messaging.new_message('carOutput').carOutput
|
||||
carState = messaging.new_message('carState').carState
|
||||
livePose = messaging.new_message('livePose').livePose
|
||||
carControl.latActive = True
|
||||
carState.vEgo = V_EGO
|
||||
carState.steeringPressed = False
|
||||
ts = DT_MDL*np.arange(2*POINTS_PER_BUCKET)
|
||||
steer_torques = np.concat((np.linspace(-0.03, -0.02, POINTS_PER_BUCKET), np.linspace(0.02, 0.03, POINTS_PER_BUCKET)))
|
||||
lat_accels = TORQUE_TUNE.latAccelFactor * steer_torques
|
||||
for t, steer_torque, lat_accel in zip(ts, steer_torques, lat_accels, strict=True):
|
||||
carOutput.actuatorsOutput.torque = float(-steer_torque)
|
||||
livePose.orientationNED = {'x': float(np.deg2rad(ROLL_BIAS_DEG)), 'valid': True}
|
||||
livePose.angularVelocityDevice = {'z': float(lat_accel / V_EGO), 'valid': True}
|
||||
livePose.inputsOK, livePose.sensorsOK, livePose.posenetOK = True, True, True
|
||||
livePose.timestamp = int(t * 1e9)
|
||||
for which, msg in (('carControl', carControl), ('carOutput', carOutput), ('carState', carState), ('livePose', livePose)):
|
||||
est.handle_log(t, which, msg)
|
||||
|
||||
def test_estimated_offset():
|
||||
steer_torques, lat_accels = generate_inputs(TORQUE_TUNE_BIASED, la_err_std=LA_ERR_STD, input_noise_std=INPUT_NOISE_STD)
|
||||
est = get_warmed_up_estimator(steer_torques, lat_accels)
|
||||
msg = est.get_msg()
|
||||
# TODO add lataccelfactor and friction check when we have more accurate estimates
|
||||
assert abs(msg.liveTorqueParameters.latAccelOffsetRaw - TORQUE_TUNE_BIASED.latAccelOffset) < 0.1
|
||||
|
||||
def test_straight_road_roll_bias():
|
||||
steer_torques, lat_accels = generate_inputs(TORQUE_TUNE, la_err_std=LA_ERR_STD, input_noise_std=INPUT_NOISE_STD)
|
||||
est = get_warmed_up_estimator(steer_torques, lat_accels)
|
||||
simulate_straight_road_msgs(est)
|
||||
msg = est.get_msg()
|
||||
assert (msg.liveTorqueParameters.latAccelOffsetRaw < -0.05) and np.isfinite(msg.liveTorqueParameters.latAccelOffsetRaw)
|
||||
Reference in new issue
Block a user