mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-11-08 13:33:42 +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>
141 lines
6.7 KiB
Python
141 lines
6.7 KiB
Python
from dataclasses import replace
|
|
|
|
import pytest
|
|
|
|
from openpilot.starpilot.flm.adaptive_profile import Block, Parameter, ProfileOwner, ReplayValidation, CurrentAdmission, readmit_profile, Sample, evaluate
|
|
|
|
IDENTITY = 'a' * 64
|
|
PARAMETERS = (Parameter('ff_gain', 'stage1', -.03, .03, .03, .005),)
|
|
|
|
|
|
def blocks():
|
|
result = []
|
|
for index in range(8):
|
|
rows = tuple(Sample(1_000_000_000 + (index * 40 + j) * 10_000_000,
|
|
990_000_000 + (index * 40 + j) * 10_000_000,
|
|
995_000_000 + (index * 40 + j) * 10_000_000,
|
|
(1. + j / 40,), .02 * (1. + j / 40), 1.) for j in range(40))
|
|
result.append(Block(IDENTITY, str(index), 'left' if index % 2 else 'right', 'stage1', 'plant1', rows))
|
|
return tuple(result)
|
|
|
|
|
|
def fit(data=None, parameters=PARAMETERS, **kwargs):
|
|
return evaluate(IDENTITY, parameters, blocks() if data is None else data, now_ns=5_000_000_000,
|
|
required_cohorts=('left', 'right'), response_revision='plant1', **kwargs)
|
|
|
|
|
|
def test_identified_plant_and_original_expiry():
|
|
profile = fit().profile
|
|
assert profile is not None
|
|
assert 0 < profile.coefficients[0] < .02
|
|
assert profile.candidate_loss < profile.baseline_loss
|
|
assert profile.expires_ns == profile.observed_ns + 500_000_000
|
|
assert set(profile.train_ids).isdisjoint(profile.heldout_ids)
|
|
|
|
|
|
@pytest.mark.parametrize('field', ['override', 'fault', 'saturated', 'model_wobble', 'controller_chatter'])
|
|
def test_ineligible_response_never_trains(field):
|
|
data = list(blocks())
|
|
rows = list(data[0].samples)
|
|
rows[0] = replace(rows[0], **{field: True})
|
|
data[0] = replace(data[0], samples=tuple(rows))
|
|
assert fit(tuple(data)).reason == 'evidence'
|
|
|
|
|
|
def test_wrong_physical_model_and_missing_cohort():
|
|
data = tuple(replace(block, response_revision='unqualified') for block in blocks())
|
|
assert fit(data).reason == 'provenance'
|
|
data = tuple(replace(block, cohort='left') for block in blocks())
|
|
assert fit(data).reason == 'independent_cohorts'
|
|
|
|
|
|
def test_heldout_regression_rejected():
|
|
data = list(blocks())
|
|
for index in range(4, 8):
|
|
data[index] = replace(data[index], samples=tuple(replace(row, residual_target=-row.residual_target) for row in data[index].samples))
|
|
assert fit(tuple(data)).status == 'rejected'
|
|
|
|
|
|
@pytest.mark.parametrize('epsilon', [0., 1e-9])
|
|
def test_confounded_basis_rejected(epsilon):
|
|
parameters = PARAMETERS + (replace(PARAMETERS[0], name='friction'),)
|
|
data = tuple(replace(block, samples=tuple(replace(row, features=(row.features[0], row.features[0] + epsilon))
|
|
for row in block.samples)) for block in blocks())
|
|
assert fit(data, parameters).reason == 'confounded_stage'
|
|
|
|
|
|
def test_gradual_adoption_same_sequence_mutation_and_exact_fallback():
|
|
profile = fit().profile
|
|
assert profile is not None
|
|
owner = ProfileOwner(IDENTITY, PARAMETERS, baseline_revision="baseline1", physical_model_revision="plant1")
|
|
kwargs = {"active": True, "enabled": True, "manual": False, "healthy": True,
|
|
"replay_validation": ReplayValidation(profile, "baseline1", "plant1", True, True, .001)}
|
|
first = owner.tick(profile, now_ns=5_000_000_000, **kwargs)
|
|
assert first == (.00005,)
|
|
changed = replace(profile, coefficients=(.01,))
|
|
changed_validation = replace(kwargs["replay_validation"], profile=changed)
|
|
assert owner.tick(changed, now_ns=5_010_000_000, **(kwargs | {"replay_validation": changed_validation})) == (0.,)
|
|
assert owner.tick(profile, now_ns=5_020_000_000, **kwargs) == (.00005,)
|
|
assert owner.tick(profile, now_ns=5_030_000_000, **(kwargs | {'manual': True})) == (0.,)
|
|
assert owner.tick(profile, now_ns=5_040_000_000, **(kwargs | {'active': False})) == (0.,)
|
|
assert owner.tick(profile, now_ns=5_550_000_001, **kwargs) == (0.,)
|
|
|
|
|
|
def test_live_revalidation_adopts_for_six_seconds_then_stale_withdraws():
|
|
fit_profile = fit().profile
|
|
assert fit_profile is not None
|
|
validation = ReplayValidation(fit_profile, 'baseline', 'physical', True, True, .001)
|
|
owner = ProfileOwner(IDENTITY, PARAMETERS, baseline_revision="baseline", physical_model_revision="physical")
|
|
profile, replay = fit_profile, validation
|
|
for tick in range(601):
|
|
now = 5_000_000_000 + tick * 10_000_000
|
|
if tick % 25 == 0:
|
|
admission = CurrentAdmission(IDENTITY, 'baseline', 'physical', now, True, True, False, True)
|
|
renewed = readmit_profile(fit_profile, validation, admission, now_ns=now, sequence=tick + 2)
|
|
assert renewed is not None
|
|
profile, replay = renewed
|
|
value = owner.tick(profile, now_ns=now, active=True, enabled=True, manual=False, healthy=True, replay_validation=replay)
|
|
assert value == profile.coefficients
|
|
stale = replace(admission, source_ns=now - 100_000_001)
|
|
assert readmit_profile(fit_profile, validation, stale, now_ns=now, sequence=1000) is None
|
|
assert fit_profile.expires_ns == 5_500_000_000
|
|
owner.last_ns = profile.expires_ns - 1
|
|
assert owner.tick(profile, now_ns=profile.expires_ns + 1, active=True, enabled=True, manual=False,
|
|
healthy=True, replay_validation=replay) == (0.,)
|
|
|
|
|
|
@pytest.mark.parametrize('field', ['baseline_revision', 'physical_model_revision'])
|
|
def test_owner_rejects_wrong_current_revision(field):
|
|
profile = fit().profile
|
|
assert profile is not None
|
|
owner = ProfileOwner(IDENTITY, PARAMETERS, baseline_revision='baseline', physical_model_revision='physical')
|
|
validation = ReplayValidation(profile, 'baseline', 'physical', True, True, .001)
|
|
assert owner.tick(profile, now_ns=5_000_000_000, active=True, enabled=True, manual=False,
|
|
healthy=True, replay_validation=replace(validation, **{field: 'different'})) == (0.,)
|
|
|
|
|
|
def test_historical_prior_regularizes_only_fresh_fit():
|
|
neutral = fit().profile
|
|
informed = fit(prior=(.02,)).profile
|
|
assert neutral is not None and informed is not None
|
|
assert abs(informed.coefficients[0] - .02) < abs(neutral.coefficients[0] - .02)
|
|
assert informed.train_ids == neutral.train_ids
|
|
assert informed.heldout_ids == neutral.heldout_ids
|
|
assert fit((), prior=(.02,)).profile is None
|
|
assert fit(prior=None) == fit()
|
|
|
|
|
|
@pytest.mark.parametrize('prior', [(.04,), (float('nan'),), (), [0.], (True,)])
|
|
def test_bad_historical_prior_cannot_admit(prior):
|
|
assert fit(prior=prior).reason == 'historical_prior'
|
|
|
|
|
|
def test_prior_cannot_bypass_fresh_override_or_heldout_regression():
|
|
data = list(blocks())
|
|
data[0] = replace(data[0], samples=tuple(replace(row, override=True) for row in data[0].samples))
|
|
assert fit(tuple(data), prior=(.02,)).reason == 'evidence'
|
|
data = list(blocks())
|
|
for index in range(4, 8):
|
|
data[index] = replace(data[index], samples=tuple(replace(row, residual_target=-row.residual_target) for row in data[index].samples))
|
|
assert fit(tuple(data), prior=(.02,)).status == 'rejected'
|