mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-11-08 05:23:41 +08:00
abb9646499
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.
219 lines
10 KiB
Python
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)
|