Files
StarPilot/openpilot/starpilot/curve_speed/tests/test_runtime.py
T
firestar5683 3e9f070c70 Curve Speed
Resolve curve speed and smoothing through a dedicated learning and preference owner.

Co-authored-by: whoisdomi <dcantu83@gmail.com>
2026-10-14 20:02:39 -05:00

244 lines
10 KiB
Python

import unittest
from dataclasses import replace
from types import SimpleNamespace as NS
from openpilot.starpilot.curve_speed.runtime import DriverEvent, Frame, Runtime
from openpilot.starpilot.curve_speed.status import StatusPublisher, observation
from openpilot.starpilot.curve_speed.target import CurveProfile
BASE = 10_000_000_000
def frame(n: int, **kwargs) -> Frame:
now = BASE + n * 50_000_000
profile = CurveProfile((0.01,), (0.0,), now)
return replace(Frame(now, now, True, profile, True, True, True, False, 30.0, 25.0,
0.01, False, False, False, False, 1, False), **kwargs)
def arm_selected(rt: Runtime) -> None:
for n in range(1, 27):
item = frame(n)
rt.step(item)
rt.confirm_applied(item.now_ns, applied=True)
assert rt.was_controlling
class TestCurveRuntime(unittest.TestCase):
def test_disabled_diagnostic_has_no_authority(self):
result = Runtime(enabled=False).step(frame(1))
assert result.candidate_mps is not None
self.assertLess(result.candidate_mps, 30.0)
self.assertIsNone(result.ceiling_mps)
def test_qualified_cap_and_axis_exclusions(self):
rt = Runtime(enabled=True)
self.assertIsNotNone(rt.step(frame(1)).ceiling_mps)
self.assertIsNone(rt.step(frame(2, system_longitudinal=False)).ceiling_mps)
self.assertIsNone(rt.step(frame(3, force_decel=True)).ceiling_mps)
self.assertIsNotNone(rt.step(frame(4, blinker=True)).ceiling_mps)
self.assertIsNone(rt.step(frame(5, long_active=False)).ceiling_mps)
def test_manual_gas_releases_cap_even_when_long_active_stays_true(self):
rt = Runtime(enabled=True)
arm_selected(rt)
self.assertIsNone(rt.step(frame(27, gas_pressed=True)).ceiling_mps)
self.assertIsNone(rt.step(frame(28, gas_pressed=False)).ceiling_mps)
self.assertTrue(rt.override)
def test_candidate_loses_to_other_cap_and_cannot_claim_feedback(self):
rt = Runtime(enabled=True)
for n in range(1, 27):
item = frame(n)
self.assertIsNotNone(rt.step(item).ceiling_mps)
self.assertTrue(rt.confirm_applied(item.now_ns, applied=False))
self.assertFalse(rt.was_controlling)
rt.step(frame(27, brake_pressed=True, long_active=False))
self.assertEqual(rt.curve.revision, 0)
self.assertFalse(rt.glow)
def test_accel_press_latches_override_and_confirmation_does_not(self):
rt = Runtime(enabled=True)
arm_selected(rt)
press = DriverEvent('card-a', 1, BASE + 26 * 50_000_000, 'curveAccelPress', 'accel', BASE + 26 * 50_000_000)
self.assertIsNone(rt.step(frame(27), event=press).ceiling_mps)
self.assertIsNone(rt.step(frame(28)).ceiling_mps)
self.assertTrue(rt.override)
# An old producer session cannot reset the high-water mark.
replay = DriverEvent('card-old', 3, BASE + 26 * 50_000_000, 'curveAccelPress', 'accel', BASE + 26 * 50_000_000)
rt.step(frame(29), event=replay)
self.assertEqual(rt.event_session, 'card-a')
rt.step(frame(30, measured_curvature=0.0, profile=CurveProfile((0.0,), (0.0,), BASE + 30 * 50_000_000)))
self.assertFalse(rt.override)
def test_blinker_only_suppresses_outside_curve(self):
rt = Runtime(enabled=True)
self.assertIsNotNone(rt.step(frame(1, blinker=True)).ceiling_mps)
outside = frame(2, blinker=True, measured_curvature=0.0)
self.assertIsNone(rt.step(outside).ceiling_mps)
def test_stale_future_duplicate_and_gap_release(self):
rt = Runtime(enabled=True)
self.assertIsNotNone(rt.step(frame(1)).ceiling_mps)
self.assertIsNone(rt.step(frame(2, model_ns=BASE + 50_000_000)).ceiling_mps)
self.assertIsNone(rt.step(frame(3, model_ns=BASE + 500_000_000)).ceiling_mps)
self.assertIsNotNone(rt.step(frame(4)).ceiling_mps)
self.assertIsNotNone(rt.step(frame(20)).ceiling_mps) # discontinuity resets filter, then fresh frame can rearm
def test_isolated_stale_frame_holds_target_without_ceiling(self):
rt = Runtime(enabled=True)
arm_selected(rt)
held = rt.filter.target
assert held is not None
stale = frame(27, inputs_fresh=False)
self.assertIsNone(rt.step(stale).ceiling_mps)
rt.confirm_applied(stale.now_ns, applied=False)
self.assertTrue(rt.was_controlling)
self.assertTrue(rt.glow)
self.assertEqual(rt.filter.target, held)
resumed = rt.step(frame(28)).ceiling_mps
assert resumed is not None
self.assertLessEqual(resumed, held)
def test_stale_hold_publishes_valid_glowing_status(self):
rt = Runtime(enabled=True)
arm_selected(rt)
stale = frame(27, inputs_fresh=False)
result = rt.step(stale)
rt.confirm_applied(stale.now_ns, applied=False)
planner = NS(last_curve_ceiling_applied=False, last_curve_ceiling_status='absent')
event = StatusPublisher().attach(None, NS(runtime=rt), result, planner, now_ns=stale.now_ns,
model_ns=frame(26).now_ns, persistence_status='idle')
status = observation(event.slcState, stale.now_ns)
assert status is not None
self.assertTrue(status.glow)
self.assertFalse(status.controlling)
def test_stale_hold_expires_after_max_step(self):
rt = Runtime(enabled=True)
arm_selected(rt)
for n in range(27, 31):
item = frame(n, inputs_fresh=False)
rt.step(item)
rt.confirm_applied(item.now_ns, applied=False)
self.assertIsNotNone(rt.filter.target)
item = frame(31, inputs_fresh=False)
rt.step(item)
rt.confirm_applied(item.now_ns, applied=False)
self.assertIsNone(rt.filter.target)
self.assertFalse(rt.glow)
self.assertFalse(rt.was_controlling)
def test_gas_override_during_stale_hold_still_latches(self):
rt = Runtime(enabled=True)
arm_selected(rt)
stale = frame(27, inputs_fresh=False, gas_pressed=True)
rt.step(stale)
rt.confirm_applied(stale.now_ns, applied=False)
self.assertIsNone(rt.step(frame(28, gas_pressed=True)).ceiling_mps)
self.assertTrue(rt.override)
def test_invalid_document_never_learns_or_persists(self):
rt = Runtime({'version': 1, 'buckets': {'nan': {'average': 2.0, 'count': 1}}}, enabled=True)
self.assertFalse(rt.document_valid)
for n in range(1, 70):
result = rt.step(frame(n, long_active=False))
self.assertIsNone(result.ceiling_mps)
self.assertIsNone(result.dirty_revision)
self.assertEqual(rt.curve.revision, 0)
def test_stale_settle_resets_and_disabled_feedback_stays_clean(self):
rt = Runtime(enabled=True)
for n in range(1, 35):
rt.step(frame(n, long_active=False))
self.assertEqual(rt.curve.revision, 0)
rt.step(frame(35, inputs_fresh=False, long_active=False))
for n in range(36, 68):
rt.step(frame(n, long_active=False))
self.assertEqual(rt.curve.revision, 0)
rt.enabled = False
rt.step(frame(68, brake_pressed=True, long_active=False))
self.assertEqual(rt.curve.revision, 0)
def test_disable_during_active_curve_does_not_nudge(self):
rt = Runtime(enabled=True)
arm_selected(rt)
rt.enabled = False
result = rt.step(frame(27, brake_pressed=True, long_active=False))
self.assertIsNone(result.ceiling_mps)
self.assertEqual(rt.curve.revision, 0)
self.assertFalse(rt.override)
def test_sLC_confirmation_does_not_nudge(self):
rt = Runtime(enabled=True)
rt.step(frame(1))
event = DriverEvent('card', 1, BASE + 50_000_000, 'confirmationAccept', 'accel', BASE + 50_000_000)
rt.step(frame(2), event=event)
self.assertEqual(rt.curve.revision, 0)
self.assertEqual(rt.watch_until_ns, 0)
def test_brake_edge_nudges_once_but_transport_loss_does_not(self):
rt = Runtime(enabled=True)
arm_selected(rt)
rt.step(frame(27, brake_pressed=True, long_active=False))
self.assertEqual(rt.curve.revision, 1)
rt.step(frame(28, brake_pressed=True, long_active=False))
self.assertEqual(rt.curve.revision, 1)
rt2 = Runtime(enabled=True)
arm_selected(rt2)
rt2.step(frame(27, inputs_fresh=False, long_active=False, brake_pressed=True))
self.assertEqual(rt2.curve.revision, 0)
def test_training_requires_driver_ownership_no_lead_and_quiet(self):
rt = Runtime(enabled=True)
for n in range(1, 150):
rt.step(frame(n, long_active=False))
self.assertGreater(rt.curve.revision, 0)
revision = rt.curve.revision
for n in range(150, 200):
rt.step(frame(n, long_active=True))
self.assertEqual(rt.curve.revision, revision)
rt2 = Runtime(enabled=True, no_lead=True)
self.assertIsNone(rt2.step(frame(1, following_lead=None)).ceiling_mps)
self.assertIsNone(rt2.step(frame(2, following_lead=True)).ceiling_mps)
self.assertIsNotNone(rt2.step(frame(3, following_lead=False)).ceiling_mps)
def test_learning_holds_after_radar_lead_dropout(self):
rt = Runtime(enabled=True)
for n in range(1, 130):
rt.step(frame(n, long_active=False, tracking_lead=True))
self.assertEqual(rt.curve.revision, 0)
for n in range(130, 145):
rt.step(frame(n, long_active=False, tracking_lead=False))
self.assertEqual(rt.curve.revision, 0)
for n in range(145, 210):
rt.step(frame(n, long_active=False, tracking_lead=False))
self.assertGreater(rt.curve.revision, 0)
def test_filtered_tracking_and_following_have_separate_authority(self):
rt = Runtime(enabled=True, no_lead=True)
# A tracked but distant lead does not block the NoLead control gate;
# it still blocks manual training.
for n in range(1, 80):
rt.step(frame(n, long_active=False, tracking_lead=True, following_lead=False))
self.assertEqual(rt.curve.revision, 0)
self.assertIsNotNone(rt.step(frame(80, tracking_lead=True, following_lead=False)).ceiling_mps)
self.assertIsNone(rt.step(frame(81, tracking_lead=False, following_lead=True)).ceiling_mps)
self.assertIsNone(rt.step(frame(82, tracking_lead=False, following_lead=None)).ceiling_mps)
def test_drive_change_resets_event_and_target(self):
rt = Runtime(enabled=True)
rt.step(frame(1))
self.assertIsNotNone(rt.filter.target)
result = rt.step(frame(2, drive_id=2, inputs_fresh=False))
self.assertIsNone(result.ceiling_mps)
self.assertIsNone(rt.filter.target)
old = DriverEvent('old-card', 1, BASE + 50_000_000, 'curveAccelPress', 'accel', BASE + 50_000_000)
rt.step(frame(3, drive_id=2), event=old)
self.assertEqual(rt.event_session, '')
if __name__ == '__main__':
unittest.main()