Files
StarPilot/openpilot/starpilot/longitudinal/tests/test_cem_green_release.py
T
firestar5683 80fbb4f6db Feature registration
Register vehicle controls, planner modes, UI actions and Galaxy services after their providers exist. Wire the shared runtime entrypoints and cover their combined behavior.

Co-authored-by: Adeeb Shihadeh <adeebshihadeh@gmail.com>
Co-authored-by: Daniel Koepping <elkoled@gmail.com>
Co-authored-by: Danny <dannyirwin31@yahoo.com>
Co-authored-by: DimitriChrysafis <Dimitri.chrysafis@gmail.com>
Co-authored-by: Dom <168790843+firestar5683@users.noreply.github.com>
Co-authored-by: Harald Schäfer <harald.the.engineer@gmail.com>
Co-authored-by: Kacper Rączy <gfw.kra@gmail.com>
Co-authored-by: Prabhaav Pillai <prabhaav@msu.edu>
Co-authored-by: dirwin31 <83434411+dirwin31@users.noreply.github.com>
Co-authored-by: firestarsdog <229254897+firestarsdog@users.noreply.github.com>
Co-authored-by: stef <19478336+stefpi@users.noreply.github.com>
Co-authored-by: whoisdomi <dcantu83@gmail.com>

Co-authored-by: Intelli <contact@intelli.software>
Co-authored-by: haraschax <6804392+haraschax@users.noreply.github.com>
Co-authored-by: prabhaavp <prabhaav@msu.edu>
2026-10-14 20:02:39 -05:00

281 lines
12 KiB
Python

"""CEM stationary model-clear release through the planner and mode consumer."""
from types import SimpleNamespace
import json
import tempfile
import unittest
from opendbc.car.hyundai.interface import CarInterface
from opendbc.car.hyundai.values import CAR
from openpilot.cereal import log, messaging
from openpilot.common.params import Params
from openpilot.selfdrive.controls.lib.longcontrol import LongCtrlState
from openpilot.selfdrive.controls.lib.longitudinal_planner import LongitudinalPlanner
from openpilot.selfdrive.controls.plannerd import apply_conditional_stop_hold
from openpilot.starpilot.conditional_mode.consumer import ModeConsumer
from openpilot.starpilot.conditional_mode.planner_host import ConditionalPlannerHost
from openpilot.starpilot.conditional_mode.policy import ModeChoice
from openpilot.starpilot.conditional_mode.preferences import SavedPreferences, encode_preferences
from openpilot.starpilot.conditional_mode.runtime_settings import DOCUMENT_KEY
from openpilot.starpilot.conditional_mode.status import settings_fingerprint
from openpilot.starpilot.conditional_mode.tests.test_projection import BOOT, MONO, FakeSubMaster, serialized_scene
from openpilot.starpilot.conditional_mode.tests.test_stop_commit_transport import RED_97_X, RED_97_V
# Exact recorded segment64 model at4044.285490106; no color inference.
CLEAR_64_X = (0.0,
0.0,
0.00042629241943359375,
-7.086992263793945e-05,
0.0029163360595703125,
0.007503509521484375,
0.0150146484375,
0.038543701171875,
0.07806396484375,
0.1441650390625,
0.25537109375,
0.45263671875,
0.74267578125,
1.1796875,
1.7470703125,
2.541015625,
3.560546875,
4.78515625,
6.24609375,
7.93359375,
9.828125,
11.9140625,
13.96875,
16.25,
18.40625,
20.546875,
22.59375,
24.3125,
25.9375,
27.5,
28.984375,
30.328125,
31.59375)
CLEAR_64_V = (-0.001239776611328125,
0.004302978515625,
0.01226806640625,
0.012725830078125,
0.033294677734375,
0.07177734375,
0.125732421875,
0.205322265625,
0.34033203125,
0.5322265625,
0.78125,
1.103515625,
1.5,
1.9453125,
2.453125,
2.96484375,
3.525390625,
4.05859375,
4.55859375,
5.04296875,
5.51171875,
5.94140625,
6.35546875,
6.765625,
7.1953125,
7.65625,
8.2421875,
8.78125,
9.3671875,
10.03125,
10.6953125,
11.3671875,
12.015625)
CLEAR_64_ACCEL = 0.5698226690292358
class CallerFrame(FakeSubMaster):
def __init__(self, payloads, stamp):
super().__init__(payloads, stamp)
for name in payloads:
self.logMonoTime[name] = stamp
self.recv_time[name] = stamp / 1e9
self.seen[name] = self.valid[name] = self.alive[name] = True
def all_checks(self, services):
return all(self.valid[name] and self.alive[name] for name in services)
class CemGreenCallerTests(unittest.TestCase):
def setUp(self):
directory = self.enterContext(tempfile.TemporaryDirectory())
self.params = Params(directory)
self.params.put(DOCUMENT_KEY, json.loads(encode_preferences(SavedPreferences(mode=ModeChoice.CEM))), block=True)
self.cp = CarInterface.get_non_essential_params(CAR.HYUNDAI_IONIQ_6)
self.cp.openpilotLongitudinalControl = True
self.owner = ConditionalPlannerHost(self.params, slc_runtime_enabled=False)
self.addCleanup(self.owner.close)
self.consumer = ModeConsumer()
self.planner = LongitudinalPlanner(self.cp, init_v=4.27)
self.tick = 0
self.experimental = False
self.drive = MONO - 1_000_000_000
def cycle(self, *, stopped=False, position=None, velocity=None, should_stop=True,
personality=log.LongitudinalPersonality.aggressive, unavailable=None, stale=None,
repeat_model=False, lead=False, desired_accel=None, horizon=20.):
now = MONO + self.tick * 50_000_000
self.tick += 1
stamp = now - 2_000_000
boot = BOOT + now - MONO
payloads = serialized_scene(stamp=boot - 2_000_000, speed=0. if stopped else 4.27,
horizon=horizon, standstill=stopped, lead_present=lead,
experimental_mode=self.experimental)
for name in ('modelV2', 'controlsState', 'selfdriveState', 'carControl', 'carState'):
event = messaging.new_message(name, valid=True)
setattr(event, name, payloads[name])
value = getattr(event, name)
if name == 'modelV2':
value.action.shouldStop = should_stop
value.action.desiredAcceleration = (desired_accel if desired_accel is not None else
-.2 if should_stop else CLEAR_64_ACCEL)
value.orientationRate.z = [0.] * 33
value.meta.disengagePredictions.gasPressProbs = [1.] * 6
if position is not None:
value.position.x = position
if velocity is not None:
value.velocity.x = velocity
elif name == 'controlsState':
value.curvature = 0.
value.longControlState = LongCtrlState.stopping if stopped else LongCtrlState.pid
elif name == 'selfdriveState':
value.personality = personality
elif name == 'carControl':
value.orientationNED = [0.] * 3
elif name == 'carState':
value.vEgoRaw = 0. if stopped else 4.27
payloads[name] = getattr(messaging.log_from_bytes(event.to_bytes()), name)
vehicle = messaging.new_message('vehicleParameters', valid=True)
payloads['vehicleParameters'] = messaging.log_from_bytes(vehicle.to_bytes()).vehicleParameters
sm = CallerFrame(payloads, stamp)
if unavailable is not None:
sm.alive[unavailable] = False
if stale is not None:
sm.logMonoTime[stale] = now - 120_000_000
sm.recv_time[stale] = (now - 120_000_000) / 1e9
if repeat_model:
sm.logMonoTime['modelV2'] = stamp - 50_000_000
sm.recv_time['modelV2'] = (stamp - 50_000_000) / 1e9
self.planner.update(sm, now_ns=now, drive_id=self.drive)
self.assertEqual(self.planner.mpc.solution_status, 0)
proposal, snapshot = self.owner.sample(sm, self.cp, self.planner, now_mono_ns=now,
now_boot_ns=boot, sample_skew_ns=1000, drive_id=self.drive, native_plan_valid=sm.all_checks(payloads))
held = apply_conditional_stop_hold(self.owner, self.planner, sm, self.cp, now, self.drive, now_boot_ns=boot)
event = self.owner.status.attach(None, proposal, snapshot, now_ns=now, drive_id=self.drive,
model_ns=stamp, car_state_ns=stamp)
status = messaging.log_from_bytes(event.to_bytes()).slcState
result = self.consumer.sample(status, now_ns=now, now_boot_ns=boot, sample_skew_ns=1000,
message_ns=now, receipt_ns=now, drive_id=self.drive, model_ns=stamp, car_state_ns=stamp,
authority=unavailable is None, stock_experimental=False, choice=proposal.choice,
settings_fingerprint=settings_fingerprint(snapshot))
self.experimental = result.experimental
published = []
self.planner.publish(sm, SimpleNamespace(send=lambda service, value: published.append(value)
if service == 'longitudinalPlan' else None))
return proposal, result, held, published[-1].longitudinalPlan
def prime(self):
for _ in range(45):
self.cycle()
self.assertTrue(self.owner.mode.projector.stop_detector.committed)
for _ in range(12):
_, result, held, plan = self.cycle(stopped=True, position=RED_97_X, velocity=RED_97_V)
self.assertTrue(result.experimental and held and plan.shouldStop)
self.assertLessEqual(plan.aTarget, 0.)
self.assertTrue(self.owner.mode.projector.stop_detector.standstill_committed)
def test_red_hold_and_clear_release_reach_actual_planner_publication(self):
self.prime()
for tick in range(18):
_, result, held, plan = self.cycle(stopped=True, position=CLEAR_64_X, velocity=CLEAR_64_V, should_stop=False)
if tick < 10:
self.assertTrue(result.experimental and held and plan.shouldStop)
elif tick >= 12:
self.assertFalse(result.experimental or held or plan.shouldStop)
self.assertFalse(self.owner.mode.projector.stop_detector.committed)
self.assertFalse(held or result.experimental or plan.shouldStop)
self.assertGreater(plan.aTarget, 0.)
def test_missing_source_and_transient_clear_cannot_release_stop(self):
self.prime()
for _ in range(6):
self.cycle(stopped=True, position=CLEAR_64_X, velocity=CLEAR_64_V, should_stop=False)
self.cycle(stopped=True, position=CLEAR_64_X, velocity=CLEAR_64_V, should_stop=False, unavailable='modelV2')
for _ in range(6):
_, result, held, plan = self.cycle(stopped=True, position=CLEAR_64_X, velocity=CLEAR_64_V, should_stop=False)
self.assertTrue(result.experimental and held and plan.shouldStop)
self.cycle(stopped=True, position=RED_97_X, velocity=RED_97_V)
self.assertTrue(self.owner.mode.projector.stop_detector.standstill_committed)
def test_alive_transport_gap_and_repeated_model_restart_clear_dwell(self):
self.prime()
for changes in ({'stale': 'radarState'}, {'repeat_model': True}):
for _ in range(6):
self.cycle(stopped=True, position=CLEAR_64_X, velocity=CLEAR_64_V, should_stop=False)
self.cycle(stopped=True, position=CLEAR_64_X, velocity=CLEAR_64_V, should_stop=False, **changes)
for _ in range(6):
_, result, held, plan = self.cycle(stopped=True, position=CLEAR_64_X, velocity=CLEAR_64_V, should_stop=False)
self.assertTrue(result.experimental and held and plan.shouldStop)
self.cycle(stopped=True, position=RED_97_X, velocity=RED_97_V)
def test_should_stop_veto_and_short_geometry_cannot_be_false_clear(self):
self.prime()
for _ in range(18):
_, result, held, plan = self.cycle(stopped=True, position=CLEAR_64_X, velocity=CLEAR_64_V)
self.assertTrue(result.experimental and held and plan.shouldStop)
for _ in range(18):
_, result, held, plan = self.cycle(stopped=True, horizon=52., should_stop=False)
self.assertTrue(result.experimental and held and plan.shouldStop)
def test_personality_changes_preserve_red_hold_and_allow_clear_release(self):
self.prime()
personalities = (log.LongitudinalPersonality.aggressive, log.LongitudinalPersonality.standard,
log.LongitudinalPersonality.relaxed)
for personality in personalities:
_, result, held, plan = self.cycle(stopped=True, position=RED_97_X, velocity=RED_97_V,
personality=personality)
self.assertTrue(result.experimental and held and plan.shouldStop)
for tick in range(18):
_, result, held, plan = self.cycle(stopped=True, position=CLEAR_64_X, velocity=CLEAR_64_V,
should_stop=False, personality=personalities[tick % 3])
self.assertFalse(held or result.experimental or plan.shouldStop)
def test_stationary_launch_requires_complete_positive_tail_and_no_lead(self):
self.prime()
damaged_position = list(CLEAR_64_X)
damaged_position[12] = damaged_position[11] - .1
cases = ({'velocity': [0.] * 33}, {'velocity': [0.] * 32},
{'velocity': list(CLEAR_64_V[:-1]) + [0.]}, {'velocity': list(CLEAR_64_V[:-1]) + [float('nan')]},
{'desired_accel': 0.}, {'lead': True}, {'position': damaged_position})
for changes in cases:
for _ in range(14):
arguments = {'stopped': True, 'position': CLEAR_64_X, 'velocity': CLEAR_64_V, 'should_stop': False}
arguments.update(changes)
_, result, held, plan = self.cycle(**arguments)
self.assertTrue(result.experimental and held and plan.shouldStop)
for _ in range(18):
_, result, held, plan = self.cycle(stopped=True, position=CLEAR_64_X, velocity=CLEAR_64_V, should_stop=False)
self.assertFalse(held or result.experimental or plan.shouldStop)
def test_launch_proof_does_not_clear_moving_stop_or_remaining_distance(self):
self.prime()
for _ in range(18):
_, result, _, _ = self.cycle(position=CLEAR_64_X, velocity=CLEAR_64_V, should_stop=False)
self.assertTrue(result.experimental)
self.assertTrue(self.owner.mode.projector.stop_detector.committed)
for _ in range(18):
_, result, held, plan = self.cycle(stopped=True, horizon=4., velocity=CLEAR_64_V, should_stop=False)
self.assertTrue(result.experimental and held and plan.shouldStop)
def test_cold_clear_trajectory_cannot_acquire_committed_stop(self):
for _ in range(18):
self.cycle(stopped=True, position=CLEAR_64_X, velocity=CLEAR_64_V, should_stop=False)
self.assertFalse(self.owner.mode.projector.stop_detector.committed)
self.assertFalse(self.owner.mode.projector.stop_detector.standstill_committed)