Files
StarPilot/openpilot/starpilot/flm/tests/test_automatic_learning_wire.py
T
firestar5683 abb9646499 FLM Calibration Lifecycle
Keep bounded automatic learning collection through valid live calibration refinements. Retire generations on resets or invalid calibration, preserve live physical transforms and existing controller tunes, and validate fitted coefficient bounds before publication. Exercise the current controller and historical wire paths.
2026-10-16 09:47:39 -05:00

219 lines
10 KiB
Python

"""Historical CarParams wire stays readable and cannot masquerade as a fit."""
from contextlib import contextmanager
from pathlib import Path
from tempfile import TemporaryDirectory
import unittest
import capnp
from openpilot.cereal import custom, log, messaging
from openpilot.starpilot.flm.learning_source import SERVICE
from tools.ci.custom_schema_contract import describe, validate
NOW = 400_000_000_000
# Exact occupied prefix from c2109496a72e85ca702ecef7aff53ff45adcfb8c,
# cereal/custom.capnp. No learner pointer existed in these recordings.
HISTORICAL_CAR_PARAMS = """@0xb526ba661d550a59;
struct StarPilotCarParams @0xaedffd8f31e7b55d {
alternativeExperience @0 :Int16;
canUsePedal @1 :Bool;
canUseSDSU @2 :Bool;
flags @3 :UInt32;
isHDA2 @4 :Bool;
openpilotLongitudinalControlDisabled @5 :Bool;
safetyConfigs @6 :List(SafetyConfig);
pcmCruiseSpeed @7 :Bool = true;
redneckCruiseAvailable @8 :Bool;
struct SafetyConfig {
safetyParam @0 :UInt16;
}
}
"""
# Exact transient c204 payload: incompatible with the older occupied prefix.
TRANSIENT_FLAT_LEARNER = """@0xb526ba661d550a59;
struct AutoSteeringLearning @0xaedffd8f31e7b55d {
version @0 :UInt16;
status @1 :Status;
reason @2 :Text;
identity @3 :Identity;
session @4 :Text;
sequence @5 :UInt64;
observedMonoTime @6 :UInt64; # MONOTONIC, separate from Event BOOTTIME.
validUntilMonoTime @7 :UInt64;
sourceWindowStartMonoTime @8 :UInt64;
sourceWindowEndMonoTime @9 :UInt64;
family @10 :Family;
latAccelFactor @11 :Float64;
acceptancePolicyId @12 :Text;
trainWindows @13 :UInt32;
heldoutWindows @14 :UInt32;
trainBaselineLoss @15 :Float64;
trainCandidateLoss @16 :Float64;
heldoutBaselineLoss @17 :Float64;
heldoutCandidateLoss @18 :Float64;
enum Status { unavailable @0; collecting @1; candidate @2; rejected @3; qualifiedFit @4; }
enum Family { none @0; linearFactor @1; }
struct Identity {
vehicle @0 :Text;
brand @1 :Text;
controller @2 :Text;
policy @3 :Text;
profile @4 :Text;
conversionVersion @5 :Text;
firmwareSha256 @6 :Text;
basis @7 :List(Float64);
baselineSha256 @8 :Text;
preferenceSha256 @9 :Text;
resetGeneration @10 :Text;
controllerRevision @11 :Text;
}
}
"""
@contextmanager
def historical_car_params(source=HISTORICAL_CAR_PARAMS):
with TemporaryDirectory() as directory:
path = Path(directory) / 'historical.capnp'
path.write_text(source)
yield capnp.SchemaParser().load(str(path))
class TestAutomaticLearningWire(unittest.TestCase):
def test_historical_car_params_prefix_defaults_and_nested_layout_survive(self):
with historical_car_params() as historical:
old_layout = describe(historical, ['StarPilotCarParams'])
current_layout = describe(custom, ['StarPilotCarParams'])
self.assertEqual(validate(current_layout, old_layout), [])
self.assertEqual(set(old_layout['nodes'][str(0xaedffd8f31e7b55d)]['fields']),
{str(i) for i in range(9)})
field = log.Event.schema.fields[SERVICE].proto
self.assertEqual(field.ordinal.explicit, 108)
self.assertEqual(field.slot.type.struct.typeId, 0xaedffd8f31e7b55d)
self.assertEqual(custom.StarPilotCarParams.schema.fields['learning'].proto.ordinal.explicit, 9)
values = {'alternativeExperience': -123, 'canUsePedal': True, 'canUseSDSU': True,
'flags': 0xa5a51234, 'isHDA2': True, 'openpilotLongitudinalControlDisabled': True,
'safetyConfigs': [{'safetyParam': 0x5a5a}, {'safetyParam': 0xffff}],
'pcmCruiseSpeed': False, 'redneckCruiseAvailable': True}
old = historical.StarPilotCarParams.new_message(**values)
with custom.StarPilotCarParams.from_bytes(old.to_bytes()) as current:
self.assertEqual({key: value for key, value in current.to_dict().items() if key != 'learning'}, values)
self.assertEqual(current.learning.version, 0)
self.assertEqual(str(current.learning.status), 'unavailable')
self.assertEqual(current.learning.session, '')
self.assertEqual(current.learning.latAccelFactor, 0.)
with custom.StarPilotCarParams.from_bytes(historical.StarPilotCarParams.new_message().to_bytes()) as defaults:
self.assertTrue(defaults.pcmCruiseSpeed)
self.assertEqual(defaults.learning.version, 0)
new = custom.StarPilotCarParams.new_message(**values)
new.learning.version = 1
new.learning.status = 'qualifiedFit'
new.learning.latAccelFactor = 3.25
with historical.StarPilotCarParams.from_bytes(new.to_bytes()) as prior:
self.assertEqual(prior.to_dict(), values)
def test_real_controls_treats_valid_historical_payload_as_absent_learning(self):
from opendbc.car.car_helpers import interfaces
from opendbc.car.toyota.values import CAR
from openpilot.starpilot.flm.tests.test_registered_controls_global import controls_for, frame
cp = interfaces[CAR.TOYOTA_COROLLA_TSS2].get_non_essential_params(CAR.TOYOTA_COROLLA_TSS2)
with historical_car_params() as historical, controls_for(cp, 'standard', True) as controls:
self.assertIsNotNone(controls.global_learning.descriptor)
cp_before = controls.CP.as_builder().to_bytes()
native_factor = controls.LaC.torque_params.latAccelFactor
native_tune = controls.LaC.torque_params.to_dict()
old = historical.StarPilotCarParams.new_message(
alternativeExperience=123, flags=0xffffffff, canUsePedal=True,
safetyConfigs=[{'safetyParam': 65535}], redneckCruiseAvailable=True)
old_bytes = old.to_bytes()
readers = []
for tick in range(100):
now = NOW + tick * 10_000_000
if tick % 25 == 0:
event = messaging.new_message(SERVICE, valid=True)
with custom.StarPilotCarParams.from_bytes(old_bytes) as envelope:
event.autoSteeringLearning = envelope
reader = messaging.log_from_bytes(event.to_bytes())
readers.append(reader)
controls.sm.update_msgs(now / 1e9, [reader])
command, _ = frame(controls, now, tick, collecting=False)
self.assertTrue(command.latActive)
self.assertIsNone(controls.torque_host)
self.assertIsNotNone(controls.global_learning)
self.assertIsNone(controls.global_learning.profile)
self.assertIsNone(controls.global_learning.validation)
self.assertIsNone(controls.global_learning.outcome)
self.assertEqual(controls.global_learning.applied_coordinates, (0.0, 0.0))
self.assertEqual(controls.LaC.torque_params.latAccelFactor, native_factor)
self.assertEqual(controls.LaC.torque_params.to_dict(), native_tune)
self.assertTrue(controls.sm.all_checks([SERVICE]))
self.assertFalse(controls.sm.simulation)
self.assertEqual(len(readers), 4)
self.assertEqual(controls.global_learning.session, '')
self.assertEqual(controls.CP.as_builder().to_bytes(), cp_before)
def test_transient_flat_learner_bytes_cannot_be_admitted_as_nested_fit(self):
from opendbc.car.car_helpers import interfaces
from opendbc.car.toyota.values import CAR
from openpilot.starpilot.flm.learning_source import AutomaticLearningConsumer, identity_for
from openpilot.starpilot.flm.tests.test_registered_controls_global import controls_for, frame
cp = interfaces[CAR.TOYOTA_COROLLA_TSS2].get_non_essential_params(CAR.TOYOTA_COROLLA_TSS2)
with historical_car_params(TRANSIENT_FLAT_LEARNER) as historical, controls_for(cp, 'standard', True) as controls:
self.assertIsNotNone(controls.global_learning.descriptor)
cp_before = controls.CP.as_builder().to_bytes()
native_factor = controls.LaC.torque_params.latAccelFactor
native_tune = controls.LaC.torque_params.to_dict()
legacy = AutomaticLearningConsumer(controls.CP, interfaces[cp.carFingerprint](controls.CP), 'standard', controls.params)
self.assertTrue(legacy.supported)
identity, enabled, manual = identity_for(controls.CP, 'standard', controls.params, legacy.conversion)
self.assertTrue(enabled)
self.assertFalse(manual)
self.assertIsNotNone(identity)
old = historical.AutoSteeringLearning.new_message(
version=1, status='qualifiedFit', identity=identity, session='transient-c204', sequence=1,
observedMonoTime=NOW, validUntilMonoTime=NOW + 500_000_000,
sourceWindowStartMonoTime=NOW - 10_000_000_000,
sourceWindowEndMonoTime=NOW - 1_000_000_000,
family='linearFactor', latAccelFactor=native_factor * 1.05,
acceptancePolicyId=next(iter(legacy.policies)),
trainWindows=10, heldoutWindows=8, trainBaselineLoss=.1, trainCandidateLoss=.09,
heldoutBaselineLoss=.1, heldoutCandidateLoss=.09)
# Use the genuinely admitted policy, so only the changed wire layout denies the fit.
self.assertEqual(str(old.status), 'qualifiedFit')
self.assertEqual(old.identity.to_dict(), identity)
old_bytes = old.to_bytes()
readers = []
for tick in range(50):
now = NOW + tick * 10_000_000
if tick % 25 == 0:
event = messaging.new_message(SERVICE, valid=True)
with custom.StarPilotCarParams.from_bytes(old_bytes) as envelope:
self.assertEqual(envelope.learning.version, 0)
event.autoSteeringLearning = envelope
reader = messaging.log_from_bytes(event.to_bytes())
readers.append(reader)
controls.sm.update_msgs(now / 1e9, [reader])
command, _ = frame(controls, now, tick, collecting=False)
self.assertTrue(command.latActive)
self.assertIsNone(controls.torque_host)
self.assertIsNotNone(controls.global_learning)
self.assertIsNone(controls.global_learning.profile)
self.assertIsNone(controls.global_learning.validation)
self.assertIsNone(controls.global_learning.outcome)
self.assertEqual(controls.global_learning.applied_coordinates, (0.0, 0.0))
self.assertEqual(controls.LaC.torque_params.latAccelFactor, native_factor)
self.assertEqual(controls.LaC.torque_params.to_dict(), native_tune)
self.assertTrue(controls.sm.all_checks([SERVICE]))
self.assertEqual(len(readers), 2)
self.assertEqual(controls.global_learning.session, '')
self.assertEqual(controls.CP.as_builder().to_bytes(), cp_before)