diff --git a/selfdrive/controls/controlsd.py b/selfdrive/controls/controlsd.py index 375df1f5e..2e0c42068 100644 --- a/selfdrive/controls/controlsd.py +++ b/selfdrive/controls/controlsd.py @@ -10,12 +10,19 @@ from openpilot.common.realtime import config_realtime_process, DT_CTRL, Priority from openpilot.common.swaglog import cloudlog from opendbc.car.car_helpers import interfaces +from opendbc.car.gm.values import CAR as GM_CAR from opendbc.car.vehicle_model import VehicleModel from openpilot.selfdrive.controls.lib.drive_helpers import clip_curvature from openpilot.selfdrive.controls.lib.latcontrol import LatControl from openpilot.selfdrive.controls.lib.latcontrol_pid import LatControlPID from openpilot.selfdrive.controls.lib.latcontrol_angle import LatControlAngle, STEER_ANGLE_SATURATION_THRESHOLD -from openpilot.selfdrive.controls.lib.latcontrol_torque import LatControlTorque +from openpilot.selfdrive.controls.lib.latcontrol_torque import ( + BOLT_2018_2021_STEER_RATIO_TEST_SCALE, + BOLT_2017_STEER_RATIO_TEST_SCALE, + LatControlTorque, + bolt_2018_2021_lateral_testing_ground_active, + bolt_2017_lateral_testing_ground_active, +) from openpilot.selfdrive.controls.lib.longcontrol import LongControl from openpilot.selfdrive.modeld.modeld import LAT_SMOOTH_SECONDS from openpilot.selfdrive.locationd.helpers import PoseCalibrator, Pose @@ -92,6 +99,10 @@ class Controls: lp = self.sm['liveParameters'] x = max(lp.stiffnessFactor, 0.1) sr = max(lp.steerRatio, 0.1) + if self.CP.carFingerprint == GM_CAR.CHEVROLET_BOLT_CC_2017 and bolt_2017_lateral_testing_ground_active(): + sr *= BOLT_2017_STEER_RATIO_TEST_SCALE + elif self.CP.carFingerprint == GM_CAR.CHEVROLET_BOLT_CC_2018_2021 and bolt_2018_2021_lateral_testing_ground_active(): + sr *= BOLT_2018_2021_STEER_RATIO_TEST_SCALE self.VM.update_params(x, sr) steer_angle_without_offset = math.radians(CS.steeringAngleDeg - lp.angleOffsetDeg) diff --git a/selfdrive/controls/lib/latcontrol_torque.py b/selfdrive/controls/lib/latcontrol_torque.py index 0598df6df..11ff7081b 100644 --- a/selfdrive/controls/lib/latcontrol_torque.py +++ b/selfdrive/controls/lib/latcontrol_torque.py @@ -10,6 +10,7 @@ from openpilot.common.filter_simple import FirstOrderFilter from openpilot.common.pid import PIDController from openpilot.selfdrive.controls.lib.drive_helpers import MIN_SPEED from openpilot.selfdrive.controls.lib.latcontrol import LatControl +from openpilot.starpilot.common.testing_grounds import testing_ground # At higher speeds (25+mph) we can assume: # Lateral acceleration achieved by a specific car correlates to @@ -57,12 +58,68 @@ BOLT_2017_CARS = ( ) BOLT_CARS = BOLT_2022_2023_CARS + BOLT_2018_2021_CARS + BOLT_2017_CARS +BOLT_2017_LATERAL_TESTING_GROUND_ID = testing_ground.id_3 +BOLT_2017_STEER_RATIO_TEST_SCALE = 1.045 +BOLT_2017_TORQUE_SCALE_BP = [0.0, 0.2, 0.5, 1.0, 1.5, 2.5] +BOLT_2017_TORQUE_SCALE_LEFT = [1.0, 1.0, 1.05, 1.04, 1.03, 1.02] +BOLT_2017_TORQUE_SCALE_RIGHT = [1.0, 1.0, 1.04, 1.03, 1.01, 1.0] + +BOLT_2018_2021_LATERAL_TESTING_GROUND_ID = testing_ground.id_4 +BOLT_2018_2021_STEER_RATIO_TEST_SCALE = 1.01 +BOLT_2018_2021_TORQUE_GAIN_LEFT = 0.10 +BOLT_2018_2021_TORQUE_GAIN_RIGHT = 0.065 +BOLT_2018_2021_TORQUE_RISE = 0.24 +BOLT_2018_2021_TORQUE_FALL = 1.8 +BOLT_2018_2021_JERK_TAPER_CUTOFF = 0.55 +BOLT_2018_2021_FRICTION_MULT = 1.11 +BOLT_2018_2021_FRICTION_THRESHOLD_BUMP = 0.018 +BOLT_2018_2021_FRICTION_THRESHOLD_SPEED = 8.0 + def get_friction_threshold(v_ego: float) -> float: # Keep the speed-scaled friction threshold behavior. return float(np.interp(v_ego, [1 * CV.MPH_TO_MS, 20 * CV.MPH_TO_MS, 75 * CV.MPH_TO_MS], [0.16, 0.19, 0.27])) +def bolt_2017_lateral_testing_ground_active() -> bool: + return testing_ground.use(BOLT_2017_LATERAL_TESTING_GROUND_ID) + + +def get_bolt_2017_torque_scale(desired_lateral_accel: float) -> float: + if desired_lateral_accel == 0.0: + return 1.0 + + scale_values = BOLT_2017_TORQUE_SCALE_LEFT if desired_lateral_accel > 0.0 else BOLT_2017_TORQUE_SCALE_RIGHT + return float(np.interp(abs(desired_lateral_accel), BOLT_2017_TORQUE_SCALE_BP, scale_values)) + + +def bolt_2018_2021_lateral_testing_ground_active() -> bool: + return testing_ground.use(BOLT_2018_2021_LATERAL_TESTING_GROUND_ID) + + +def get_bolt_2018_2021_torque_scale(desired_lateral_accel: float) -> float: + if desired_lateral_accel == 0.0: + return 1.0 + + gain = BOLT_2018_2021_TORQUE_GAIN_LEFT if desired_lateral_accel > 0.0 else BOLT_2018_2021_TORQUE_GAIN_RIGHT + abs_lateral_accel = abs(desired_lateral_accel) + mid_corner_bump = (1.0 - math.exp(-abs_lateral_accel / BOLT_2018_2021_TORQUE_RISE)) * math.exp(-abs_lateral_accel / BOLT_2018_2021_TORQUE_FALL) + return 1.0 + gain * mid_corner_bump + + +def get_bolt_2018_2021_dynamic_torque_scale(desired_lateral_accel: float, desired_lateral_jerk: float) -> float: + base_scale = get_bolt_2018_2021_torque_scale(desired_lateral_accel) + extra_scale = max(base_scale - 1.0, 0.0) + jerk_taper = 1.0 / (1.0 + (abs(desired_lateral_jerk) / BOLT_2018_2021_JERK_TAPER_CUTOFF) ** 2) + return 1.0 + (extra_scale * jerk_taper) + + +def get_bolt_2018_2021_friction_threshold(v_ego: float) -> float: + base_threshold = get_friction_threshold(v_ego) + low_speed_bump = BOLT_2018_2021_FRICTION_THRESHOLD_BUMP / (1.0 + (max(v_ego, 0.0) / BOLT_2018_2021_FRICTION_THRESHOLD_SPEED) ** 2) + return base_threshold + low_speed_bump + + class LatControlTorque(LatControl): def __init__(self, CP, CI, dt): super().__init__(CP, CI, dt) @@ -94,6 +151,7 @@ class LatControlTorque(LatControl): self.torque_ff_scale_neg = 1.0 self.torque_deadzone_boost = float(getattr(self.torque_params, "kfDEPRECATED", 0.0)) self.torque_ki_mult = 1.0 + self.bolt_2018_2021_test_active = self.is_bolt_2018_2021 and bolt_2018_2021_lateral_testing_ground_active() if self.is_bolt: kp_scale = getattr(self.torque_params, "kp", getattr(self.torque_params, "kpDEPRECATED", 1.0)) @@ -169,7 +227,14 @@ class LatControlTorque(LatControl): ff_scale = np.interp(ff, [-FF_SCALE_BLEND_LAT_ACCEL, 0.0, FF_SCALE_BLEND_LAT_ACCEL], [self.torque_ff_scale_neg, 1.0, self.torque_ff_scale_pos]) ff *= ff_scale - ff += get_friction(error_with_lsf + JERK_GAIN * desired_lateral_jerk, lateral_accel_deadzone, get_friction_threshold(CS.vEgo), self.torque_params) + friction_threshold = get_friction_threshold(CS.vEgo) + if self.bolt_2018_2021_test_active: + friction_threshold = get_bolt_2018_2021_friction_threshold(CS.vEgo) + effective_friction = self.torque_params.as_builder() + effective_friction.friction *= BOLT_2018_2021_FRICTION_MULT + else: + effective_friction = self.torque_params + ff += get_friction(error_with_lsf + JERK_GAIN * desired_lateral_jerk, lateral_accel_deadzone, friction_threshold, effective_friction) deadzone_boost_active = False if self.torque_deadzone_boost > 0.0 and abs(gravity_adjusted_future_lateral_accel) < DEADZONE_BOOST_LAT_ACCEL: boost_scale = np.interp(abs(gravity_adjusted_future_lateral_accel), [0.0, DEADZONE_BOOST_LAT_ACCEL], [1.0, 0.0]) @@ -182,6 +247,10 @@ class LatControlTorque(LatControl): CS.vEgo < self.low_speed_reset_threshold or unwind_detected) output_lataccel = self.pid.update(pid_log.error, error_rate=-measurement_rate, speed=CS.vEgo, feedforward=ff, freeze_integrator=freeze_integrator) output_torque = self.torque_from_lateral_accel(output_lataccel, self.torque_params) + if self.is_bolt_2017 and bolt_2017_lateral_testing_ground_active(): + output_torque *= get_bolt_2017_torque_scale(setpoint) + elif self.bolt_2018_2021_test_active: + output_torque *= get_bolt_2018_2021_dynamic_torque_scale(setpoint, desired_lateral_jerk) pid_log.active = True pid_log.p = float(self.pid.p) diff --git a/selfdrive/controls/tests/test_latcontrol.py b/selfdrive/controls/tests/test_latcontrol.py index eb82c54fd..be5ff16e7 100644 --- a/selfdrive/controls/tests/test_latcontrol.py +++ b/selfdrive/controls/tests/test_latcontrol.py @@ -1,7 +1,7 @@ from parameterized import parameterized from types import SimpleNamespace -from cereal import car, log +from cereal import car, custom, log 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 @@ -9,19 +9,49 @@ 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.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.controls.lib.latcontrol_pid import LatControlPID +from openpilot.selfdrive.controls.lib.latcontrol_torque import ( + LatControlTorque, + get_friction_threshold, + get_bolt_2017_torque_scale, + get_bolt_2018_2021_dynamic_torque_scale, + get_bolt_2018_2021_friction_threshold, + get_bolt_2018_2021_torque_scale, +) class TestLatControl: + def test_bolt_2017_testing_ground_scale_curve(self): + assert get_bolt_2017_torque_scale(0.1) == 1.0 + assert get_bolt_2017_torque_scale(-0.1) == 1.0 + assert get_bolt_2017_torque_scale(0.5) > get_bolt_2017_torque_scale(-0.5) + assert 1.0 < get_bolt_2017_torque_scale(1.2) < get_bolt_2017_torque_scale(0.5) + assert get_bolt_2017_torque_scale(-2.5) == 1.0 + + def test_bolt_2018_2021_testing_ground_scale_curve(self): + assert get_bolt_2018_2021_torque_scale(0.0) == 1.0 + assert get_bolt_2018_2021_torque_scale(0.2) > get_bolt_2018_2021_torque_scale(0.08) + assert get_bolt_2018_2021_torque_scale(0.4) > get_bolt_2018_2021_torque_scale(-0.4) + assert get_bolt_2018_2021_torque_scale(2.0) < get_bolt_2018_2021_torque_scale(0.8) + assert get_bolt_2018_2021_dynamic_torque_scale(0.4, 0.8) < get_bolt_2018_2021_dynamic_torque_scale(0.4, 0.1) + + def test_bolt_2018_2021_friction_threshold_curve(self): + low = get_bolt_2018_2021_friction_threshold(2.0) + mid = get_bolt_2018_2021_friction_threshold(10.0) + high = get_bolt_2018_2021_friction_threshold(30.0) + assert low > get_friction_threshold(2.0) + assert mid > get_friction_threshold(10.0) + assert high > get_friction_threshold(30.0) + assert (low - get_friction_threshold(2.0)) > (mid - get_friction_threshold(10.0)) > (high - get_friction_threshold(30.0)) + @parameterized.expand([(HONDA.HONDA_CIVIC, LatControlPID), (TOYOTA.TOYOTA_RAV4, LatControlTorque), (NISSAN.NISSAN_LEAF, LatControlAngle), (GM.CHEVROLET_BOLT_ACC_2022_2023, LatControlTorque)]) def test_saturation(self, car_name, controller): CarInterface = interfaces[car_name] CP = CarInterface.get_non_essential_params(car_name) - CI = CarInterface(CP) + CI = CarInterface(CP, custom.StarPilotCarParams.new_message()) VM = VehicleModel(CP) controller = controller(CP.as_reader(), CI, DT_CTRL) diff --git a/starpilot/common/testing_grounds.py b/starpilot/common/testing_grounds.py index 8bc6cea13..bb3539985 100644 --- a/starpilot/common/testing_grounds.py +++ b/starpilot/common/testing_grounds.py @@ -46,17 +46,17 @@ TESTING_GROUNDS_SLOT_DEFINITIONS = ( }, { "id": TESTING_GROUND_3, - "name": "Unused", - "description": "", - "aLabel": "A", - "bLabel": "B", + "name": "Bolt 2017 Lat Tune", + "description": "2017 Bolt manual lateral A/B sandbox for steer-ratio and torque-curve testing.", + "aLabel": "A - Installed tune", + "bLabel": "B - 2017 lateral test", }, { "id": TESTING_GROUND_4, - "name": "Unused", - "description": "", - "aLabel": "A", - "bLabel": "B", + "name": "Bolt 18-21 Lat Tune", + "description": "Bolt 2018-2021 lateral torque A/B sandbox.", + "aLabel": "A - Installed tune", + "bLabel": "B - Bolt lateral test", }, { "id": TESTING_GROUND_5, diff --git a/starpilot/system/the_pond/assets/components/tools/testing_ground.js b/starpilot/system/the_pond/assets/components/tools/testing_ground.js index a6fd23586..46e9ba329 100644 --- a/starpilot/system/the_pond/assets/components/tools/testing_ground.js +++ b/starpilot/system/the_pond/assets/components/tools/testing_ground.js @@ -96,11 +96,11 @@ function isModeActive(mode) { function getSelectedMode() { const selectedSlot = getSelectedSlot() - if (!selectedSlot) return "A" + if (!selectedSlot) return "Not active" if (String(state.data?.activeSlot || "").trim() === String(state.selectedSlot || "").trim()) { return String(state.data?.activeVariant || "").trim().toUpperCase() || getDefaultMode(selectedSlot) } - return getDefaultMode(selectedSlot) + return "Not active" } function modeButtonClass(mode) { @@ -188,19 +188,8 @@ async function selectMode(mode) { async function selectSlot(slotValue) { const normalizedSlot = String(slotValue || "").trim() - state.selectedSlot = normalizedSlot if (!normalizedSlot || state.busy) return - - const activeSlot = String(state.data?.activeSlot || "").trim() - if (normalizedSlot === activeSlot) return - - const success = await applySelection(normalizedSlot, "A", false) - if (!success) { - state.selectedSlot = activeSlot - if (state.error) { - showSnackbar(state.error, "error") - } - } + state.selectedSlot = normalizedSlot } function initialize() { @@ -253,6 +242,10 @@ export function TestingGround() { +
+ Only one Testing Ground can be active at a time. Switching slots only changes what you're viewing, and the active test stays enabled until you explicitly choose another mode. +
+ ${() => getSelectedSlot() ? html`