mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-11-08 05:23:41 +08:00
200632aabd
Learn bounded feed-forward corrections on eligible torque controllers behind Use StarPilot Auto Learn, preserving installed tunes and native safety limits. Co-authored-by: Danny <dannyirwin31@yahoo.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: 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: Intelli <contact@intelli.software> Co-authored-by: haraschax <6804392+haraschax@users.noreply.github.com> Co-authored-by: pharmacomaniac <blittle65@gmail.com> Co-authored-by: whoisdomi <dcantu83@gmail.com>
221 lines
9.9 KiB
Python
221 lines
9.9 KiB
Python
"""Source receipt leaf decoding and unchanged generic fallback contracts."""
|
|
import math
|
|
import struct
|
|
from types import SimpleNamespace
|
|
from typing import TypedDict
|
|
import unittest
|
|
|
|
from capnp.lib.capnp import KjException, _DynamicStructReader
|
|
from openpilot.cereal import custom, log, messaging
|
|
from openpilot.starpilot.flm import learning_wire
|
|
from openpilot.starpilot.flm.runtime_adapter_registry import SourceState
|
|
|
|
NOW = 10_000_000_000
|
|
CP = 'c' * 64
|
|
|
|
|
|
class ReceiptFields(TypedDict):
|
|
version: int
|
|
enabled: bool
|
|
manual: bool
|
|
resetGeneration: str
|
|
sourceEpoch: str
|
|
observedMonoTime: int
|
|
validUntilMonoTime: int
|
|
cpSha256: str
|
|
calibrationBasis: list[float]
|
|
|
|
|
|
FIELDS: ReceiptFields = {'version': 1, 'enabled': True, 'manual': False, 'resetGeneration': 'reset-1', 'sourceEpoch': 'b' * 64,
|
|
'observedMonoTime': NOW, 'validUntilMonoTime': NOW + 500_000_000, 'cpSha256': CP,
|
|
'calibrationBasis': [0.0, 0.0, 0.0]}
|
|
VALID_LOG = [
|
|
'event.valid', 'event.autoSteeringLearning', 'outer.learning', 'learning.sourceReceipt',
|
|
'row.version', 'row.cpSha256', 'row.cpSha256', 'row.observedMonoTime',
|
|
'row.validUntilMonoTime', 'row.observedMonoTime', 'row.enabled', 'row.sourceEpoch',
|
|
'row.resetGeneration', 'row.resetGeneration', 'row.calibrationBasis', 'row.calibrationBasis',
|
|
'row.enabled', 'row.manual', 'row.resetGeneration', 'row.sourceEpoch', 'row.observedMonoTime',
|
|
'row.validUntilMonoTime', 'row.cpSha256', 'row.calibrationBasis',
|
|
]
|
|
|
|
|
|
def native(*, reader=True, **changes):
|
|
event = messaging.new_message('autoSteeringLearning', valid=True, logMonoTime=NOW)
|
|
event.autoSteeringLearning.learning.sourceReceipt = {**FIELDS, **changes}
|
|
return event.as_reader() if reader else event
|
|
|
|
|
|
def envelope(row, valid=True):
|
|
return SimpleNamespace(valid=valid, autoSteeringLearning=SimpleNamespace(learning=SimpleNamespace(sourceReceipt=row)))
|
|
|
|
|
|
def expected(**changes):
|
|
row: ReceiptFields = {**FIELDS, **changes}
|
|
return SourceState(row['enabled'], row['manual'], row['resetGeneration'], row['sourceEpoch'],
|
|
row['observedMonoTime'], row['validUntilMonoTime'], row['cpSha256'], tuple(row['calibrationBasis']))
|
|
|
|
|
|
def signature(value):
|
|
if value is None:
|
|
return None
|
|
return (type(value), value.enabled, value.manual, value.reset, value.source_epoch,
|
|
value.observed_ns, value.expires_ns, value.cp_sha256,
|
|
tuple(struct.pack('!d', item) for item in value.calibration))
|
|
|
|
|
|
class Logged:
|
|
def __init__(self, values, accesses, label):
|
|
self.values, self.accesses, self.label = values, accesses, label
|
|
|
|
def __getattr__(self, name):
|
|
self.accesses.append(self.label + '.' + name)
|
|
if name not in self.values:
|
|
raise AttributeError(name)
|
|
value = self.values[name]
|
|
if isinstance(value, Exception):
|
|
raise value
|
|
return value() if callable(value) else value
|
|
|
|
|
|
def traced(accesses, *, valid=True, **changes):
|
|
row = Logged({**FIELDS, **changes}, accesses, 'row')
|
|
learning = Logged({'sourceReceipt': row}, accesses, 'learning')
|
|
outer = Logged({'learning': learning}, accesses, 'outer')
|
|
return Logged({'valid': valid, 'autoSteeringLearning': outer}, accesses, 'event')
|
|
|
|
|
|
class TestSourceReceipt(unittest.TestCase):
|
|
def assert_receipt(self, changes, wanted, *, now=NOW, cp=CP):
|
|
builder = native(reader=False, **changes)
|
|
# Builders retain the original generic body; both paths must meet an explicit result.
|
|
for event in (builder.as_reader(), builder):
|
|
value = learning_wire.read_source_receipt(event, now_ns=now, cp_sha256=cp)
|
|
self.assertEqual(signature(value), signature(wanted))
|
|
return value
|
|
|
|
def test_native_schemas_and_enabled_manual_flags(self):
|
|
event = native()
|
|
row = event.autoSteeringLearning.learning.sourceReceipt
|
|
self.assertIs(type(row), _DynamicStructReader)
|
|
self.assertEqual(event.schema, log.Event.schema)
|
|
self.assertEqual(event.autoSteeringLearning.schema, custom.StarPilotCarParams.schema)
|
|
self.assertEqual(row.schema, custom.AutomaticLearningSourceReceipt.schema)
|
|
for enabled, manual in ((True, False), (True, True), (False, False), (False, True)):
|
|
with self.subTest(enabled=enabled, manual=manual):
|
|
changes = {'enabled': enabled, 'manual': manual}
|
|
self.assert_receipt(changes, expected(**changes))
|
|
|
|
def test_version_identity_reset_and_time_boundaries(self):
|
|
rows = (
|
|
{'version': 0}, {'version': 2}, {'cpSha256': 'd' * 64}, {'cpSha256': 'invalid'},
|
|
{'sourceEpoch': ''}, {'sourceEpoch': 'B' * 64}, {'resetGeneration': ''},
|
|
{'resetGeneration': 'r' * 65}, {'enabled': False, 'resetGeneration': 'r' * 65},
|
|
{'observedMonoTime': 0}, {'observedMonoTime': NOW + 1},
|
|
{'validUntilMonoTime': NOW - 1}, {'validUntilMonoTime': NOW + 500_000_001},
|
|
)
|
|
for changes in rows:
|
|
with self.subTest(changes=changes):
|
|
self.assert_receipt(changes, None)
|
|
for changes in ({'enabled': False, 'sourceEpoch': '', 'resetGeneration': ''},
|
|
{'resetGeneration': 'r' * 64}, {'validUntilMonoTime': NOW}):
|
|
self.assert_receipt(changes, expected(**changes))
|
|
for now, wanted in ((NOW - 1, None), (NOW, expected()),
|
|
(NOW + 500_000_000, expected()), (NOW + 500_000_001, None)):
|
|
with self.subTest(now=now):
|
|
self.assert_receipt({}, wanted, now=now)
|
|
|
|
def test_calibration_lengths_nonfinite_and_signed_zero(self):
|
|
for basis in ([], [0.0], [0.0, 0.0], [0.0] * 4, [math.nan, 0.0, 0.0],
|
|
[0.0, math.inf, 0.0], [0.0, 0.0, -math.inf]):
|
|
with self.subTest(basis=basis):
|
|
self.assert_receipt({'calibrationBasis': basis}, None)
|
|
changes = {'calibrationBasis': [-0.0, 0.0, -0.0]}
|
|
self.assert_receipt(changes, expected(**changes))
|
|
|
|
def test_borrowed_reader_mutations_and_fresh_result_objects(self):
|
|
builder = native(reader=False)
|
|
borrowed = builder.as_reader()
|
|
first = learning_wire.read_source_receipt(borrowed, now_ns=NOW, cp_sha256=CP)
|
|
second = learning_wire.read_source_receipt(borrowed, now_ns=NOW, cp_sha256=CP)
|
|
self.assertEqual(signature(first), signature(expected()))
|
|
self.assertEqual(signature(second), signature(expected()))
|
|
self.assertIsNot(first, second)
|
|
rows = (('enabled', False), ('manual', True), ('version', 0), ('cpSha256', 'd' * 64),
|
|
('sourceEpoch', 'e' * 64), ('resetGeneration', 'reset-2'),
|
|
('observedMonoTime', NOW - 1), ('validUntilMonoTime', NOW + 1),
|
|
('calibrationBasis', [-0.0, 0.25, -0.5]))
|
|
for field, value in rows:
|
|
with self.subTest(field=field):
|
|
builder.autoSteeringLearning.learning.sourceReceipt = FIELDS
|
|
setattr(builder.autoSteeringLearning.learning.sourceReceipt, field, value)
|
|
wanted = None if field in ('version', 'cpSha256', 'observedMonoTime') else expected(**{field: value})
|
|
actual = learning_wire.read_source_receipt(borrowed, now_ns=NOW, cp_sha256=CP)
|
|
self.assertEqual(signature(actual), signature(wanted))
|
|
builder.autoSteeringLearning.learning.sourceReceipt = FIELDS
|
|
self.assertEqual(signature(learning_wire.read_source_receipt(borrowed, now_ns=NOW, cp_sha256=CP)), signature(expected()))
|
|
|
|
def test_custom_schema_spoof_and_wrong_native_schema_fallback(self):
|
|
row = SimpleNamespace(**FIELDS, schema=custom.AutomaticLearningSourceReceipt.schema)
|
|
actual = learning_wire.read_source_receipt(envelope(row), now_ns=NOW, cp_sha256=CP)
|
|
self.assertEqual(signature(actual), signature(expected()))
|
|
wrong = messaging.new_message('carState').as_reader().carState
|
|
with self.assertRaises((AttributeError, ValueError, KjException)):
|
|
learning_wire.read_source_receipt(envelope(wrong), now_ns=NOW, cp_sha256=CP)
|
|
with self.assertRaises(AttributeError):
|
|
learning_wire.read_source_receipt(envelope(None), now_ns=NOW, cp_sha256=CP)
|
|
|
|
def test_custom_getter_short_circuit_order_and_exception(self):
|
|
cases = (({'valid': False}, 1), ({'version': 0}, 5), ({'cpSha256': 'd' * 64}, 6),
|
|
({'observedMonoTime': NOW + 1}, 8), ({'sourceEpoch': ''}, 12),
|
|
({'resetGeneration': ''}, 13), ({'calibrationBasis': []}, 15))
|
|
for changes, length in cases:
|
|
with self.subTest(changes=changes):
|
|
accesses = []
|
|
self.assertIsNone(learning_wire.read_source_receipt(traced(accesses, **changes), now_ns=NOW, cp_sha256=CP))
|
|
self.assertEqual(accesses, VALID_LOG[:length])
|
|
accesses = []
|
|
with self.assertRaisesRegex(RuntimeError, '^manual getter$'):
|
|
learning_wire.read_source_receipt(traced(accesses, manual=RuntimeError('manual getter')), now_ns=NOW, cp_sha256=CP)
|
|
self.assertEqual(accesses, VALID_LOG[:18])
|
|
|
|
def test_stateful_custom_field_keeps_original_repeated_reads(self):
|
|
accesses = []
|
|
values = iter((CP, 'd' * 64, 'e' * 64))
|
|
event = traced(accesses, cpSha256=lambda: next(values))
|
|
actual = learning_wire.read_source_receipt(event, now_ns=NOW, cp_sha256=CP)
|
|
self.assertEqual(signature(actual), signature(expected(cpSha256='e' * 64)))
|
|
self.assertEqual(accesses, VALID_LOG)
|
|
|
|
def test_nonbuiltin_clock_and_cp_arguments(self):
|
|
class IntSubclass(int):
|
|
pass
|
|
|
|
class StrSubclass(str):
|
|
pass
|
|
|
|
class Clock:
|
|
def __init__(self, accesses):
|
|
self.accesses = accesses
|
|
|
|
def __ge__(self, other):
|
|
self.accesses.append(('ge', other))
|
|
return NOW >= other
|
|
|
|
def __le__(self, other):
|
|
self.accesses.append(('le', other))
|
|
return NOW <= other
|
|
|
|
for now, cp, wanted in ((float(NOW), CP, expected()), (IntSubclass(NOW), CP, expected()),
|
|
(True, CP, None), (0.0, CP, None), (-0.0, CP, None), (math.nan, CP, None),
|
|
(NOW, StrSubclass(CP), expected()), (NOW, None, None)):
|
|
with self.subTest(now=now, cp=cp):
|
|
self.assert_receipt({}, wanted, now=now, cp=cp)
|
|
accesses = []
|
|
value = learning_wire.read_source_receipt(native(), now_ns=Clock(accesses), cp_sha256=CP)
|
|
self.assertEqual(signature(value), signature(expected()))
|
|
self.assertEqual(accesses, [('ge', NOW), ('le', NOW + 500_000_000)])
|
|
|
|
|
|
if __name__ == '__main__':
|
|
unittest.main()
|