mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-11-14 16:33:59 +08:00
Fit offroad tune strength and show completed-drive measurements
This commit is contained in:
@@ -520,7 +520,12 @@ def _run(
|
||||
if values['active'] and not cs.steeringPressed and not values['safety_limited'] and source.calls != 1:
|
||||
raise ValueError('actual selected stage hook absent or duplicated')
|
||||
if effect_source is not None and values['active'] and not cs.steeringPressed and not values['curvature_limited']:
|
||||
if effect_source.stage_calls != 1 or effect_source.friction_calls != 1:
|
||||
from openpilot.starpilot.flm.selected_effects import bounded_profile_on_limited_frame
|
||||
|
||||
# Route profiles retain the real safety-limit inhibition. Zero hooks are
|
||||
# required there; admitted limited-profile replay still requires both once.
|
||||
expected_calls = 0 if values['safety_limited'] and not bounded_profile_on_limited_frame(controller) else 1
|
||||
if effect_source.stage_calls != expected_calls or effect_source.friction_calls != expected_calls:
|
||||
raise ValueError('actual selected effect hook absent or duplicated')
|
||||
return float(output), log
|
||||
|
||||
|
||||
@@ -28,6 +28,13 @@ def stable_proposal_windows(continuous, observed):
|
||||
return selected, grades
|
||||
|
||||
|
||||
def tracking_proposal_windows(continuous, observed):
|
||||
# Two-second statistics use the same known-zero observations as the Stable
|
||||
# reader. These children supply no missing controller/native evidence.
|
||||
children = {key: [child for parent in parents for child in whole_windows(parent.points)] for key, parents in observed.items()}
|
||||
return stable_proposal_windows(continuous, children)
|
||||
|
||||
|
||||
def baseline_windows(points, timeline, metadata_for):
|
||||
times = tuple(t for t, _ in timeline)
|
||||
result = []
|
||||
|
||||
@@ -11,9 +11,59 @@ def qualify(assessment, group, cohort, values, root, *, permitted, require_chang
|
||||
roles = cohort.get('qualification_roles', cohort)
|
||||
ids = tuple(roles['training']['windows']) + tuple(roles['heldout']['windows'])
|
||||
original = group['qualification_original_windows'] if 'qualification_roles' in cohort else group['selected_original_windows']
|
||||
if not all(3 <= len(roles[role]['windows']) <= 12 for role in ('training', 'heldout')) or len(set(ids)) != len(ids):
|
||||
raise ValueError('Disjoint original training and held-out windows required')
|
||||
results = _original_results(assessment, original, ids, values, root, permitted=permitted)
|
||||
return {
|
||||
'qualified': qualified_originals(results, roles, require_change=require_change),
|
||||
'policy': 'every_preselected_window_controller_and_registered_encoder_v1',
|
||||
'windows': results,
|
||||
'physical_improvement_proven': False,
|
||||
'current_permission_provided': False,
|
||||
'requires_changed_emission': require_change,
|
||||
'changed_emission_scope': 'both_preselected_roles' if require_change else 'observed_original_map',
|
||||
}
|
||||
|
||||
|
||||
def fit_training_step(assessment, group, cohort, proposal, previous, root, *, permitted):
|
||||
"""Fit deterministic actuator feasibility on training only; never retry held-out."""
|
||||
roles = cohort.get('qualification_roles', cohort)
|
||||
train, heldout = (tuple(roles[role]['windows']) for role in ('training', 'heldout'))
|
||||
if not 3 <= len(train) <= 12 or not 3 <= len(heldout) <= 12 or len(set(train + heldout)) != len(train + heldout):
|
||||
raise ValueError('Original disjoint training and held-out windows required')
|
||||
original = group['qualification_original_windows'] if 'qualification_roles' in cohort else group['selected_original_windows']
|
||||
if set(previous) != set(proposal['values']):
|
||||
raise ValueError('Exact original full-map prior required')
|
||||
attempts = []
|
||||
for fraction in (1.0, 0.8, 0.6, 0.4, 0.2):
|
||||
values = (
|
||||
dict(proposal['values']) if fraction == 1 else {name: previous[name] + fraction * (value - previous[name]) for name, value in proposal['values'].items()}
|
||||
)
|
||||
checked = _original_results(assessment, original, train, values, root, permitted=permitted)
|
||||
feasible = len(checked) == len(train) and all(row['qualified'] for row in checked)
|
||||
attempts.append({'fraction': fraction, 'feasible': feasible, 'original_training_windows': checked})
|
||||
if feasible:
|
||||
fitted = dict(proposal, values=values, delta=proposal['delta'] * fraction)
|
||||
return fitted, {
|
||||
'policy': 'training_only_native_feasibility_v1',
|
||||
'training_windows': train,
|
||||
'heldout_windows_not_evaluated': heldout,
|
||||
'attempts': attempts,
|
||||
'selected_fraction': fraction,
|
||||
}
|
||||
return None, {
|
||||
'policy': 'training_only_native_feasibility_v1',
|
||||
'training_windows': train,
|
||||
'heldout_windows_not_evaluated': heldout,
|
||||
'attempts': attempts,
|
||||
'selected_fraction': None,
|
||||
}
|
||||
|
||||
|
||||
def _original_results(assessment, original, ids, values, root, *, permitted):
|
||||
spans = {w['identity']: w for w in original if w['identity'] in ids}
|
||||
if not 6 <= len(ids) <= 24 or len(set(ids)) != len(ids) or set(spans) != set(ids):
|
||||
raise ValueError('Every original training and held-out window required')
|
||||
if not 3 <= len(ids) <= 24 or len(set(ids)) != len(ids) or set(spans) != set(ids):
|
||||
raise ValueError('Every preselected original window required')
|
||||
from openpilot.starpilot.flm.learning_wire import read_control, read_applied
|
||||
import hashlib
|
||||
|
||||
@@ -55,6 +105,8 @@ def qualify(assessment, group, cohort, values, root, *, permitted, require_chang
|
||||
missing.append(int(event.logMonoTime))
|
||||
if not current() or not permitted():
|
||||
raise ValueError('Original input changed during checks')
|
||||
# Parsed evidence owns primitives; release segment readers before replay.
|
||||
del events, raw
|
||||
for wid, w in sorted(spans.items()):
|
||||
if w['segment'] != number:
|
||||
continue
|
||||
@@ -88,15 +140,7 @@ def qualify(assessment, group, cohort, values, root, *, permitted, require_chang
|
||||
controls = []
|
||||
outputs = []
|
||||
missing = []
|
||||
return {
|
||||
'qualified': qualified_originals(results, roles, require_change=require_change),
|
||||
'policy': 'every_preselected_window_controller_and_registered_encoder_v1',
|
||||
'windows': results,
|
||||
'physical_improvement_proven': False,
|
||||
'current_permission_provided': False,
|
||||
'requires_changed_emission': require_change,
|
||||
'changed_emission_scope': 'both_preselected_roles' if require_change else 'observed_original_map',
|
||||
}
|
||||
return results
|
||||
|
||||
|
||||
def qualified_originals(results, cohort, *, require_change):
|
||||
|
||||
@@ -60,6 +60,12 @@ class StatusReport:
|
||||
details['Controller eligibility refusals'] = count('controller_fit_refusals')
|
||||
details['Automatic configuration changed'] = bool(value.get('configuration_changed', False))
|
||||
details['Analysis decision'] = value['reason']
|
||||
details['Saved route adjustment count'] = value.get('saved_adjustments')
|
||||
details['Recorded steering response'] = value.get('recorded_tracking')
|
||||
checks = value.get('qualification_summary')
|
||||
if checks is not None:
|
||||
details['Controller and encoder check windows'] = [checks['passed'], checks['required']]
|
||||
details['Automatic tune refusal reasons'] = [list(row) for row in checks['refusals']]
|
||||
suggestions = value.get('suggestions', ())
|
||||
details['Recorded route suggestions'] = [[s['parameter'], s['delta'], s['whole_windows'], s['qualification']] for s in suggestions[:16]]
|
||||
details['Recorded suggestion count'] = value.get('suggestion_count', len(suggestions))
|
||||
|
||||
@@ -26,6 +26,7 @@ class CompletedAssessment:
|
||||
quality_stop_markers: tuple = ()
|
||||
diagnostic_groups: tuple = ()
|
||||
stable_groups: tuple = ()
|
||||
recorded_tracking: dict | None = None
|
||||
|
||||
def report(self):
|
||||
return {
|
||||
@@ -38,6 +39,7 @@ class CompletedAssessment:
|
||||
'quality_stop_markers': list(self.quality_stop_markers),
|
||||
'diagnostic_groups': list(self.diagnostic_groups),
|
||||
'stable_groups': list(self.stable_groups),
|
||||
'recorded_tracking': self.recorded_tracking,
|
||||
'source_role': 'Historical diagnostic alignment, not a statement of actual Controls consumption or current permission.',
|
||||
}
|
||||
|
||||
@@ -62,6 +64,82 @@ def window_demand(window):
|
||||
}
|
||||
|
||||
|
||||
def _separate_qualification(report, continuous):
|
||||
qualification = {}
|
||||
for c in report['cohorts']:
|
||||
native = [w for w in continuous if cohort(w) == c['cohort']]
|
||||
chosen = _spread(sorted(native, key=lambda w: (w.points[0].segment, w.points[0].mono_ns)), MAX_COHORT_WINDOWS)
|
||||
c['qualification_roles'] = {role: {'windows': tuple(w.identity for w in chosen[offset::2])} for role, offset in (('training', 0), ('heldout', 1))}
|
||||
qualification.update((w.identity, w) for w in chosen)
|
||||
report['qualification_original_windows'] = [
|
||||
{
|
||||
'identity': w.identity,
|
||||
'segment': w.points[0].segment,
|
||||
'start_ns': w.points[0].mono_ns,
|
||||
'end_ns': w.points[-1].mono_ns,
|
||||
'metrics': tracking_metrics(w.points),
|
||||
'demand': window_demand(w),
|
||||
}
|
||||
for w in qualification.values()
|
||||
]
|
||||
|
||||
|
||||
def _recorded_tracking(windows):
|
||||
"""Measured route summary and one input-ordered window, never a trial result."""
|
||||
import math
|
||||
|
||||
ordered = sorted((w for rows in windows.values() for w in rows), key=lambda w: (w.points[0].segment, w.points[0].mono_ns))
|
||||
if not ordered or len({w.domain for w in ordered}) != 1:
|
||||
return None
|
||||
samples = sum(len(w.points) for w in ordered)
|
||||
squared_error = sum((p.desired - p.actual) ** 2 for w in ordered for p in w.points)
|
||||
first = ordered[0]
|
||||
return {
|
||||
'version': 1,
|
||||
'domain': first.domain,
|
||||
'samples': samples,
|
||||
'windows': len(ordered),
|
||||
'error_rms': math.sqrt(squared_error / samples),
|
||||
'route': first.points[0].route,
|
||||
'segment': first.points[0].segment,
|
||||
'source_start_ns': first.points[0].mono_ns,
|
||||
'source_end_ns': first.points[-1].mono_ns,
|
||||
'points': [[p.mono_ns, p.desired, p.actual] for p in _spread(first.points, 64)],
|
||||
'sampling': 'input_ordered_original_endpoints_no_interpolation',
|
||||
}
|
||||
|
||||
|
||||
def _control_eligible(control, stamp):
|
||||
return bool(
|
||||
control is not None
|
||||
and abs(control.control_ns - stamp) <= 30_000_000
|
||||
and control.enabled
|
||||
and control.active
|
||||
and control.valid
|
||||
and not (control.manual or control.override or control.fault or control.saturated or control.limited)
|
||||
and 0 < control.state_ns <= control.control_ns <= control.state_ns + 100_000_000
|
||||
and 0 < control.source_receipt_ns <= control.control_ns <= control.source_receipt_ns + 500_000_000
|
||||
and (
|
||||
not control.native_required
|
||||
or control.native_allowed
|
||||
and 0 < control.native_observed_ns <= control.control_ns <= control.native_expires_ns <= control.native_observed_ns + 200_000_000
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def continuous_control_window(window, timeline, metadata_for, expected):
|
||||
"""Check ALL original envelopes, including those between tracking samples."""
|
||||
start, end = window.points[0].mono_ns, window.points[-1].mono_ns
|
||||
rows = [(stamp, control) for stamp, control in timeline if start - 30_000_000 <= stamp <= end + 30_000_000]
|
||||
return bool(
|
||||
rows
|
||||
and rows[0][0] <= start
|
||||
and rows[-1][0] >= end
|
||||
and all(b[0] - a[0] <= 50_000_000 for a, b in zip(rows, rows[1:], strict=False))
|
||||
and all(_control_eligible(control, stamp) and metadata_for(control, window.domain) == expected for stamp, control in rows)
|
||||
)
|
||||
|
||||
|
||||
def analyze_latest(params, root, previous, *, permitted):
|
||||
from openpilot.starpilot.flm.offroad_control_metadata import read_control_metadata as read_control
|
||||
|
||||
@@ -82,6 +160,7 @@ def analyze_latest(params, root, previous, *, permitted):
|
||||
'original_applied_envelopes': 0,
|
||||
'controller_fit_refusals': 0,
|
||||
'omitted_control_metadata': 0,
|
||||
'whole_control_window_refusals': 0,
|
||||
}
|
||||
quality_stops = []
|
||||
|
||||
@@ -197,21 +276,7 @@ def analyze_latest(params, root, previous, *, permitted):
|
||||
candidates = tuple(i for i in (at - 1, at) if 0 <= i < len(timeline))
|
||||
nearest = min(candidates, key=lambda i: abs(times[i] - point.mono_ns)) if candidates else None
|
||||
control = None if nearest is None else timeline[nearest][1]
|
||||
okay = bool(
|
||||
control is not None
|
||||
and abs(control.control_ns - point.mono_ns) <= 30_000_000
|
||||
and control.enabled
|
||||
and control.active
|
||||
and control.valid
|
||||
and not (control.manual or control.override or control.fault or control.saturated or control.limited)
|
||||
and 0 < control.state_ns <= control.control_ns <= control.state_ns + 100_000_000
|
||||
and 0 < control.source_receipt_ns <= control.control_ns <= control.source_receipt_ns + 500_000_000
|
||||
and (
|
||||
not control.native_required
|
||||
or control.native_allowed
|
||||
and 0 < control.native_observed_ns <= control.control_ns <= control.native_expires_ns <= control.native_observed_ns + 200_000_000
|
||||
)
|
||||
)
|
||||
okay = _control_eligible(control, point.mono_ns)
|
||||
if not okay:
|
||||
counts['controller_fit_refusals'] += 1
|
||||
run_context = None
|
||||
@@ -231,10 +296,12 @@ def analyze_latest(params, root, previous, *, permitted):
|
||||
run_context = context
|
||||
for context, runs in selected.items():
|
||||
for qualified in runs:
|
||||
windows.setdefault(context, []).extend(whole_windows(tuple(qualified)))
|
||||
# The original Stable chatter window needs genuine multi-second
|
||||
# context. Keep the existing two-second path and fast gross guard.
|
||||
stable_windows.setdefault(context, []).extend(whole_windows(tuple(qualified), duration_ns=6_000_000_000))
|
||||
for duration, destination in ((2_000_000_000, windows), (6_000_000_000, stable_windows)):
|
||||
for window in whole_windows(tuple(qualified), duration_ns=duration):
|
||||
if continuous_control_window(window, timeline, metadata_for, context_metadata[context]):
|
||||
destination.setdefault(context, []).append(window)
|
||||
else:
|
||||
counts['whole_control_window_refusals'] += 1
|
||||
|
||||
if sum(map(len, windows.values())) + sum(map(len, stable_windows.values())) + sum(map(len, baseline_tracking.values())) > MAX_WINDOWS:
|
||||
raise ValueError('Whole-drive tracking window bound exceeded')
|
||||
@@ -242,17 +309,24 @@ def analyze_latest(params, root, previous, *, permitted):
|
||||
receipt = walk_completed_route(root, route, permitted=permitted, consume=consume)
|
||||
if not permitted():
|
||||
raise ValueError('Parked authority lost after analysis')
|
||||
from openpilot.starpilot.flm.offroad_baseline_windows import stable_proposal_windows
|
||||
from openpilot.starpilot.flm.offroad_baseline_windows import stable_proposal_windows, tracking_proposal_windows
|
||||
|
||||
proposal_windows, proposal_grades = tracking_proposal_windows(windows, baseline_tracking)
|
||||
stable_windows, stable_grades = stable_proposal_windows(stable_windows, baseline_tracking)
|
||||
if sum(map(len, proposal_windows.values())) + sum(map(len, stable_windows.values())) > MAX_WINDOWS:
|
||||
raise ValueError('Whole-drive proposal window bound exceeded')
|
||||
counts['qualified_whole_windows'] = sum(map(len, windows.values()))
|
||||
counts['proposal_tracking_whole_windows'] = sum(map(len, proposal_windows.values()))
|
||||
prior = previous(state['cp']) if callable(previous) else previous
|
||||
amplitude_prior = {name: prior[name] for name in AMPLITUDE_NAMES}
|
||||
reports = []
|
||||
for key, values in sorted(windows.items()):
|
||||
for key, values in sorted(proposal_windows.items()):
|
||||
if not values:
|
||||
continue
|
||||
report = analyze(tuple(values), amplitude_prior, permitted=permitted)
|
||||
if key in proposal_grades:
|
||||
report['tracking_source_grade'] = proposal_grades[key]
|
||||
_separate_qualification(report, windows.get(key, ()))
|
||||
selected_ids = {wid for cohort_row in report['cohorts'] for role in ('training', 'heldout') for wid in cohort_row.get(role, {}).get('windows', ())}
|
||||
report.update(
|
||||
original_context_sha256=key,
|
||||
@@ -296,25 +370,8 @@ def analyze_latest(params, root, previous, *, permitted):
|
||||
report = analyze_stable_windows(tuple(values), permitted=permitted)
|
||||
grade = stable_grades.get(key, 'continuous_original_control_metadata')
|
||||
report['tracking_source_grade'] = grade
|
||||
qualification = []
|
||||
if grade == 'bookended_zero_route_tracking':
|
||||
for c in report['cohorts']:
|
||||
native = [w for w in windows.get(key, ()) if cohort(w) == c['cohort']]
|
||||
chosen = _spread(sorted(native, key=lambda w: (w.points[0].segment, w.points[0].mono_ns)), MAX_COHORT_WINDOWS)
|
||||
c['qualification_roles'] = {role: {'windows': tuple(w.identity for w in chosen[offset::2])} for role, offset in (('training', 0), ('heldout', 1))}
|
||||
qualification.extend(chosen)
|
||||
unique = {w.identity: w for w in qualification}
|
||||
report['qualification_original_windows'] = [
|
||||
{
|
||||
'identity': w.identity,
|
||||
'segment': w.points[0].segment,
|
||||
'start_ns': w.points[0].mono_ns,
|
||||
'end_ns': w.points[-1].mono_ns,
|
||||
'metrics': tracking_metrics(w.points),
|
||||
'demand': window_demand(w),
|
||||
}
|
||||
for w in unique.values()
|
||||
]
|
||||
_separate_qualification(report, windows.get(key, ()))
|
||||
selected = {wid for c in report['cohorts'] for role in ('training', 'heldout') for wid in c.get(role, {}).get('windows', ())}
|
||||
report.update(
|
||||
original_context_sha256=key,
|
||||
@@ -349,4 +406,6 @@ def analyze_latest(params, root, previous, *, permitted):
|
||||
}
|
||||
)
|
||||
counts['diagnostic_whole_windows'] = sum(map(len, raw_windows.values()))
|
||||
return CompletedAssessment(receipt, reports, counts, state['cp'], (), route, tuple(quality_stops), tuple(diagnostic), tuple(stable))
|
||||
return CompletedAssessment(
|
||||
receipt, reports, counts, state['cp'], (), route, tuple(quality_stops), tuple(diagnostic), tuple(stable), _recorded_tracking(proposal_windows)
|
||||
)
|
||||
|
||||
@@ -165,6 +165,23 @@ def assess_next_drive(trial, assessment, scope):
|
||||
}
|
||||
|
||||
|
||||
def qualification_summary(checks):
|
||||
refusals = {}
|
||||
for window in checks['windows']:
|
||||
if window['qualified']:
|
||||
continue
|
||||
check = window.get('numerical') or {}
|
||||
if check.get('qualified'):
|
||||
check = window.get('registered_encoder') or {}
|
||||
reason = window.get('reason') or check.get('detail') or check.get('reason') or 'original_window_not_qualified'
|
||||
refusals[reason] = refusals.get(reason, 0) + 1
|
||||
return {
|
||||
'passed': sum(w['qualified'] for w in checks['windows']),
|
||||
'required': len(checks['windows']),
|
||||
'refusals': sorted(refusals.items()),
|
||||
}
|
||||
|
||||
|
||||
class OffroadWorker:
|
||||
def __init__(
|
||||
self, params, log_root, cache, *, permitted, assessor=analyze_latest, checker=qualify, scope_for=_current_scope, on_analysis=None, on_assessment=None
|
||||
@@ -310,6 +327,8 @@ class OffroadWorker:
|
||||
if metadata['route_scope'] not in ('', scope.key()):
|
||||
continue
|
||||
for cohort in group['cohorts']:
|
||||
if 'qualification_roles' in cohort and any(len(cohort['qualification_roles'][role]['windows']) < 3 for role in ('training', 'heldout')):
|
||||
continue
|
||||
proposal = cohort.get('proposal')
|
||||
if proposal is not None:
|
||||
proposal = dict(proposal, values=dict(current, **proposal['values']))
|
||||
@@ -348,64 +367,81 @@ class OffroadWorker:
|
||||
if blocked:
|
||||
reason = 'same_rejected_change_requires_different_evidence_or_reset'
|
||||
else:
|
||||
checked = self.checker(assessment, group, cohort, proposal['values'], self.root, permitted=self.permitted)
|
||||
if checked['qualified']:
|
||||
record = {
|
||||
'kind': 'next_drive_trial',
|
||||
'policy': POLICY,
|
||||
'assessment': report,
|
||||
'parameter': proposal['parameter'],
|
||||
'delta': proposal['delta'],
|
||||
'values': proposal['values'],
|
||||
'previous': current,
|
||||
'cohort': cohort['cohort'],
|
||||
'original_metadata': group['original_metadata'],
|
||||
'before': {role: cohort[role] for role in ('training', 'heldout')},
|
||||
'all_original_window_checks': checked,
|
||||
'confirmed_improvement': False,
|
||||
'proposal_policy': proposal.get('policy', 'observed_tracking_v1'),
|
||||
'stable_profile_proof': cohort.get('stable_profile_proof'),
|
||||
'gross_policy': 'fixed_input_matched_2s_v1' if cohort.get('stable_profile_proof') is not None else 'original_2s_v1',
|
||||
'gross_baseline': [
|
||||
(w.get('gross_reference') if cohort.get('stable_profile_proof') is not None else w)
|
||||
for w in group['selected_original_windows']
|
||||
if w['identity'] in set(cohort['training']['windows']) | set(cohort['heldout']['windows'])
|
||||
],
|
||||
}
|
||||
if 'qualification_roles' in cohort:
|
||||
roles = cohort['qualification_roles']
|
||||
ids = set(roles['training']['windows']) | set(roles['heldout']['windows'])
|
||||
record.update(
|
||||
gross_policy='separate_original_2s_v1',
|
||||
gross_original_roles=roles,
|
||||
tracking_source_grade=group['tracking_source_grade'],
|
||||
gross_baseline=[w for w in group['qualification_original_windows'] if w['identity'] in ids],
|
||||
)
|
||||
from openpilot.starpilot.flm.offroad_quality import TrialGuard
|
||||
feasible = True
|
||||
if self.checker is qualify:
|
||||
from openpilot.starpilot.flm.offroad_checks import fit_training_step
|
||||
|
||||
if TrialGuard(scope.key(), proposal['values'], record).stopped is not None:
|
||||
raise ValueError('Original onroad gross-guard baseline unavailable')
|
||||
digest = self.cache.put(record)
|
||||
new = {
|
||||
'scope': scope.document(),
|
||||
'values': proposal['values'],
|
||||
'previous': current,
|
||||
'route_sha256s': [assessment.receipt.source_sha256],
|
||||
'source_start_ns': assessment.receipt.original_start_ns,
|
||||
'source_end_ns': assessment.receipt.original_end_ns,
|
||||
'completed_wall_ns': assessment.receipt.completed_wall_ns,
|
||||
'analysis_sha256': digest,
|
||||
'sequence': 1 if row is None else row['sequence'] + 1,
|
||||
'state': 'ready_for_next_drive',
|
||||
}
|
||||
saved = RouteStorage(self.params).save(new, expected=expected, authorized=authorized)
|
||||
if not saved:
|
||||
raise ValueError('Next-drive tune compare-and-swap refused')
|
||||
status = 'ready_for_next_drive'
|
||||
reason = 'bounded_trial_configured_for_next_drive'
|
||||
fitted, fit_proof = fit_training_step(assessment, group, cohort, proposal, current, self.root, permitted=self.permitted)
|
||||
report['native_training_feasibility'] = fit_proof
|
||||
feasible = fitted is not None
|
||||
if fitted is not None:
|
||||
proposal = fitted
|
||||
offered = {'values': proposal['values'], 'parameter': proposal['parameter'], 'delta': proposal['delta']}
|
||||
if row is not None and row['state'] == 'rolled_back':
|
||||
trial = trial_document(self.cache, row['analysis_sha256'])
|
||||
feasible = not (trial['parameter'] == proposal['parameter'] and trial['delta'] == proposal['delta'])
|
||||
if not feasible:
|
||||
reason = 'no_new_native_feasible_training_step'
|
||||
else:
|
||||
checked = self.checker(assessment, group, cohort, proposal['values'], self.root, permitted=self.permitted)
|
||||
report['all_original_window_checks'] = checked
|
||||
reason = 'original_controller_or_registered_encoder_check_refused'
|
||||
if checked['qualified']:
|
||||
record = {
|
||||
'kind': 'next_drive_trial',
|
||||
'policy': POLICY,
|
||||
'assessment': report,
|
||||
'parameter': proposal['parameter'],
|
||||
'delta': proposal['delta'],
|
||||
'values': proposal['values'],
|
||||
'previous': current,
|
||||
'cohort': cohort['cohort'],
|
||||
'original_metadata': group['original_metadata'],
|
||||
'before': {role: cohort[role] for role in ('training', 'heldout')},
|
||||
'all_original_window_checks': checked,
|
||||
'confirmed_improvement': False,
|
||||
'proposal_policy': proposal.get('policy', 'observed_tracking_v1'),
|
||||
'stable_profile_proof': cohort.get('stable_profile_proof'),
|
||||
'native_training_feasibility': report.get('native_training_feasibility'),
|
||||
'gross_policy': 'fixed_input_matched_2s_v1' if cohort.get('stable_profile_proof') is not None else 'original_2s_v1',
|
||||
'gross_baseline': [
|
||||
(w.get('gross_reference') if cohort.get('stable_profile_proof') is not None else w)
|
||||
for w in group['selected_original_windows']
|
||||
if w['identity'] in set(cohort['training']['windows']) | set(cohort['heldout']['windows'])
|
||||
],
|
||||
}
|
||||
if 'qualification_roles' in cohort:
|
||||
roles = cohort['qualification_roles']
|
||||
ids = set(roles['training']['windows']) | set(roles['heldout']['windows'])
|
||||
record.update(
|
||||
gross_policy='separate_original_2s_v1',
|
||||
gross_original_roles=roles,
|
||||
tracking_source_grade=group['tracking_source_grade'],
|
||||
gross_baseline=[w for w in group['qualification_original_windows'] if w['identity'] in ids],
|
||||
)
|
||||
from openpilot.starpilot.flm.offroad_quality import TrialGuard
|
||||
|
||||
if TrialGuard(scope.key(), proposal['values'], record).stopped is not None:
|
||||
raise ValueError('Original onroad gross-guard baseline unavailable')
|
||||
digest = self.cache.put(record)
|
||||
new = {
|
||||
'scope': scope.document(),
|
||||
'values': proposal['values'],
|
||||
'previous': current,
|
||||
'route_sha256s': [assessment.receipt.source_sha256],
|
||||
'source_start_ns': assessment.receipt.original_start_ns,
|
||||
'source_end_ns': assessment.receipt.original_end_ns,
|
||||
'completed_wall_ns': assessment.receipt.completed_wall_ns,
|
||||
'analysis_sha256': digest,
|
||||
'sequence': 1 if row is None else row['sequence'] + 1,
|
||||
'state': 'ready_for_next_drive',
|
||||
}
|
||||
saved = RouteStorage(self.params).save(new, expected=expected, authorized=authorized)
|
||||
if not saved:
|
||||
raise ValueError('Next-drive tune compare-and-swap refused')
|
||||
status = 'ready_for_next_drive'
|
||||
reason = 'bounded_trial_configured_for_next_drive'
|
||||
else:
|
||||
reason = 'original_controller_or_registered_encoder_check_refused'
|
||||
diagnostics = [(g, c, c['proposal']) for g in assessment.diagnostic_groups for c in g['cohorts'] if c.get('proposal') is not None]
|
||||
diagnostics.sort(key=lambda v: (-v[1]['whole_windows'], v[1]['cohort'], v[0]['original_tracking_context_sha256']))
|
||||
if offered is None:
|
||||
@@ -437,6 +473,7 @@ class OffroadWorker:
|
||||
'assessment_sha256': self.cache.put({'kind': 'completed_assessment', 'assessment': report}),
|
||||
'source_sha256': assessment.receipt.source_sha256,
|
||||
'vehicle': str(assessment.cp.carFingerprint),
|
||||
'recorded_tracking': report.get('recorded_tracking'),
|
||||
'suggestion_count': len(diagnostics),
|
||||
'suggestions': [
|
||||
{'parameter': v['parameter'], 'delta': v['delta'], 'whole_windows': c['whole_windows'], 'qualification': 'controller_native_checks_not_passed'}
|
||||
@@ -445,10 +482,14 @@ class OffroadWorker:
|
||||
}
|
||||
if offered is not None:
|
||||
result['suggestion'] = offered
|
||||
checks = report.get('all_original_window_checks')
|
||||
if checks is not None:
|
||||
result['qualification_summary'] = qualification_summary(checks)
|
||||
current_raw, readable = read_saved(self.params, KEY, MAX_BYTES)
|
||||
if not readable:
|
||||
raise ValueError('Saved route configuration unreadable after assessment')
|
||||
configured = decode(current_raw).get(NAMESPACE, {}).get(scope.key())
|
||||
result['saved_adjustments'] = 0 if configured is None else sum(value != 0 for value in configured['values'].values())
|
||||
if configured is not None:
|
||||
result['configured_values'] = dict(configured['values'])
|
||||
result['configuration_state'] = configured['state']
|
||||
|
||||
@@ -135,6 +135,67 @@ class TestOffroadCache(unittest.TestCase):
|
||||
|
||||
|
||||
class TestOffroadSurface(unittest.TestCase):
|
||||
def test_real_controller_route_replay_preserves_limiter_and_requires_exact_hooks(self):
|
||||
from types import SimpleNamespace
|
||||
from opendbc.car import structs
|
||||
from opendbc.car.car_helpers import interfaces
|
||||
from opendbc.car.hyundai.values import CAR
|
||||
from openpilot.starpilot.flm.controller_replay import SelectedControllerFactory, capture_snapshot, restore_snapshot, frame_input, _run
|
||||
from openpilot.starpilot.flm.offroad_surface import replay_source
|
||||
from openpilot.starpilot.flm.torque_adapter import Objective
|
||||
|
||||
cp = interfaces[CAR.HYUNDAI_IONIQ_6].get_non_essential_params(CAR.HYUNDAI_IONIQ_6).as_reader()
|
||||
controller, vm = SelectedControllerFactory(cp, 'starpilot').create()
|
||||
cs = structs.CarState.new_message(vEgo=25.0, steeringAngleDeg=0.2, gearShifter='drive')
|
||||
snapshot = capture_snapshot(controller, vm)
|
||||
objective = Objective('feedforward', 'high', 'steady')
|
||||
neutral = (0.0,) * len(objective.parameters)
|
||||
original_update = controller.update
|
||||
for limited in (False, True):
|
||||
with self.subTest(limited=limited):
|
||||
inputs = frame_input(
|
||||
cs,
|
||||
vm,
|
||||
SimpleNamespace(angleOffsetDeg=0.0, roll=0.0),
|
||||
active=True,
|
||||
safety_limited=limited,
|
||||
desired_curvature=0.0001,
|
||||
curvature_limited=False,
|
||||
lat_delay=0.1,
|
||||
) + (('control_frame', 1.0),)
|
||||
restore_snapshot(controller, vm, snapshot)
|
||||
baseline, _ = _run(controller, vm, inputs, objective, neutral)
|
||||
restore_snapshot(controller, vm, snapshot)
|
||||
source = replay_source(values(0.005), 'b' * 64)
|
||||
candidate, _ = _run(controller, vm, inputs, objective, neutral, effect_trial=source)
|
||||
self.assertEqual((source.stage_calls, source.friction_calls), (0, 0) if limited else (1, 1))
|
||||
if limited:
|
||||
self.assertEqual(candidate, baseline, 'The actual limiter inhibits all route effects')
|
||||
else:
|
||||
|
||||
def omit_effect(*args):
|
||||
actual = controller.automatic_flm_effects
|
||||
controller.automatic_flm_effects = None
|
||||
try:
|
||||
return original_update(*args)
|
||||
finally:
|
||||
controller.automatic_flm_effects = actual
|
||||
|
||||
restore_snapshot(controller, vm, snapshot)
|
||||
with patch.object(controller, 'update', side_effect=omit_effect), self.assertRaisesRegex(ValueError, 'selected effect hook'):
|
||||
_run(controller, vm, inputs, objective, neutral, effect_trial=replay_source(values(), 'b' * 64))
|
||||
|
||||
def duplicate_effect(*args):
|
||||
result = original_update(*args)
|
||||
effect = controller.automatic_flm_effects.source
|
||||
effect.stage_calls += 1
|
||||
effect.friction_calls += 1
|
||||
return result
|
||||
|
||||
restore_snapshot(controller, vm, snapshot)
|
||||
with patch.object(controller, 'update', side_effect=duplicate_effect), self.assertRaisesRegex(ValueError, 'selected effect hook'):
|
||||
_run(controller, vm, inputs, objective, neutral, effect_trial=replay_source(values(), 'b' * 64))
|
||||
|
||||
def test_zero_curves_preserve_canonical_and_interpolation_is_relative(self):
|
||||
for speed, expected in ((0.0, 0.0), (2.5, 0.005), (5.0, 0.01), (10.0, 0.02), (20.0, 0.035), (30.0, 0.04)):
|
||||
with self.subTest(speed=speed):
|
||||
@@ -218,12 +279,40 @@ class TestOffroadStore(unittest.TestCase):
|
||||
class TestOffroadReport(unittest.TestCase):
|
||||
def test_completed_drive_reports_every_candidate_without_claiming_application(self):
|
||||
from openpilot.starpilot.flm.training_report import read_report
|
||||
from openpilot.starpilot.flm.offroad_worker import qualification_summary
|
||||
|
||||
checks = {
|
||||
'windows': [{'qualified': True}] * 22
|
||||
+ [
|
||||
{'qualified': False, 'numerical': {'qualified': False, 'reason': 'original_scored_frame_not_eligible'}},
|
||||
{
|
||||
'qualified': False,
|
||||
'numerical': {'qualified': True},
|
||||
'registered_encoder': {'qualified': False, 'detail': 'candidate changed an actual limiter/support'},
|
||||
},
|
||||
]
|
||||
}
|
||||
summary = qualification_summary(checks)
|
||||
self.assertEqual((summary['passed'], summary['required']), (22, 24))
|
||||
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
path = Path(directory) / 'report.json'
|
||||
report = StatusReport(path=path, clock_ns=lambda: 10_000_000_000)
|
||||
names = ['highway_center_taper_max', 'turn_in_boost_left', 'unwind_taper_left', 'turn_in_boost_right', 'unwind_taper_right']
|
||||
suggestions = [{'parameter': name, 'delta': 0.005, 'whole_windows': 9, 'qualification': 'controller_native_checks_not_passed'} for name in names]
|
||||
recorded = {
|
||||
'version': 1,
|
||||
'domain': 'lateral_acceleration',
|
||||
'samples': 2000,
|
||||
'windows': 10,
|
||||
'error_rms': 0.12,
|
||||
'route': 'declared-completed-drive',
|
||||
'segment': 0,
|
||||
'source_start_ns': 1_000_000_000,
|
||||
'source_end_ns': 2_900_000_000,
|
||||
'points': [[1_000_000_000 + i * 100_000_000, 0.2, 0.08] for i in range(20)],
|
||||
'sampling': 'input_ordered_original_endpoints_no_interpolation',
|
||||
}
|
||||
report.set(
|
||||
{
|
||||
'status': 'collecting',
|
||||
@@ -233,6 +322,9 @@ class TestOffroadReport(unittest.TestCase):
|
||||
'suggestions': suggestions,
|
||||
'suggestion_count': len(suggestions),
|
||||
'configuration_changed': False,
|
||||
'recorded_tracking': recorded,
|
||||
'saved_adjustments': 0,
|
||||
'qualification_summary': summary,
|
||||
}
|
||||
)
|
||||
report.publish()
|
||||
@@ -244,6 +336,12 @@ class TestOffroadReport(unittest.TestCase):
|
||||
self.assertFalse(current['details']['Automatic configuration changed'])
|
||||
self.assertFalse(current['application']['applied'])
|
||||
self.assertFalse(current['details']['Physical road improvement proven'])
|
||||
self.assertEqual(current['details']['Recorded steering response'], recorded)
|
||||
self.assertEqual(current['details']['Saved route adjustment count'], 0)
|
||||
self.assertEqual(current['details']['Controller and encoder check windows'], [22, 24])
|
||||
self.assertEqual(
|
||||
current['details']['Automatic tune refusal reasons'], [['candidate changed an actual limiter/support', 1], ['original_scored_frame_not_eligible', 1]]
|
||||
)
|
||||
|
||||
def test_typed_current_application_and_completed_analysis_remain_distinct(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
@@ -300,6 +398,50 @@ class TestOffroadReport(unittest.TestCase):
|
||||
|
||||
|
||||
class TestOffroadRouteCoverage(unittest.TestCase):
|
||||
def test_native_windows_veto_ineligible_envelopes_between_tracking_samples(self):
|
||||
from types import SimpleNamespace
|
||||
from openpilot.starpilot.flm.offroad_processor import continuous_control_window
|
||||
|
||||
window = TestOffroadTrackingPolicy.windows('declared-envelope-coverage', 1.0)[0]
|
||||
start, end = window.points[0].mono_ns, window.points[-1].mono_ns
|
||||
metadata = {'scope': 'declared-source'}
|
||||
timeline = []
|
||||
for stamp in range(start - 30_000_000, end + 30_000_001, 5_000_000):
|
||||
control = SimpleNamespace(
|
||||
control_ns=stamp,
|
||||
state_ns=stamp,
|
||||
source_receipt_ns=stamp,
|
||||
enabled=True,
|
||||
active=True,
|
||||
valid=True,
|
||||
manual=False,
|
||||
override=False,
|
||||
fault=False,
|
||||
saturated=False,
|
||||
limited=False,
|
||||
native_required=True,
|
||||
native_allowed=True,
|
||||
native_observed_ns=stamp - 1_000_000,
|
||||
native_expires_ns=stamp + 100_000_000,
|
||||
metadata=metadata,
|
||||
)
|
||||
timeline.append((stamp, control))
|
||||
|
||||
def reader(control, domain):
|
||||
return control.metadata
|
||||
|
||||
self.assertTrue(continuous_control_window(window, timeline, reader, metadata))
|
||||
index = next(i for i, (stamp, _) in enumerate(timeline) if stamp == start + 5_000_000)
|
||||
control = timeline[index][1]
|
||||
for changes in ({'limited': True}, {'native_allowed': False}, {'metadata': {'scope': 'changed'}}):
|
||||
with self.subTest(changes=changes):
|
||||
changed = list(timeline)
|
||||
changed[index] = (timeline[index][0], SimpleNamespace(**(vars(control) | changes)))
|
||||
self.assertFalse(continuous_control_window(window, changed, reader, metadata))
|
||||
absent: list[tuple[int, SimpleNamespace | None]] = list(timeline)
|
||||
absent[index] = (timeline[index][0], None)
|
||||
self.assertFalse(continuous_control_window(window, absent, reader, metadata))
|
||||
|
||||
def test_actual_aggregation_keeps_classifier_and_separate_qualification_windows(self):
|
||||
import math
|
||||
from types import SimpleNamespace
|
||||
@@ -365,8 +507,42 @@ class TestOffroadRouteCoverage(unittest.TestCase):
|
||||
self.assertEqual(len(roles['training']['windows']), 3)
|
||||
self.assertEqual(len(roles['heldout']['windows']), 3)
|
||||
self.assertFalse(set(roles['training']['windows']) & set(roles['heldout']['windows']))
|
||||
primary = assessment.groups[0]
|
||||
self.assertEqual(primary['tracking_source_grade'], 'bookended_zero_route_tracking')
|
||||
self.assertEqual(primary['cohorts'][0]['whole_windows'], 18)
|
||||
self.assertEqual({w['identity'] for w in primary['qualification_original_windows']}, {w.identity for w in continuous})
|
||||
self.assertEqual(assessment.counts['qualified_whole_windows'], 6)
|
||||
self.assertEqual(assessment.counts['proposal_tracking_whole_windows'], 18)
|
||||
recorded = assessment.recorded_tracking
|
||||
self.assertIsNotNone(recorded)
|
||||
assert recorded is not None
|
||||
self.assertEqual((recorded['samples'], recorded['windows']), (3600, 18))
|
||||
self.assertEqual(len(recorded['points']), 64)
|
||||
self.assertEqual(recorded['points'][0], [observed[0].points[0].mono_ns, observed[0].points[0].desired, observed[0].points[0].actual])
|
||||
self.assertEqual(recorded['source_end_ns'], observed[0].points[199].mono_ns)
|
||||
self.assertEqual(recorded['sampling'], 'input_ordered_original_endpoints_no_interpolation')
|
||||
self.assertFalse(assessment.report()['automatically_saved'])
|
||||
|
||||
def test_primary_statistics_keep_original_children_and_never_supply_qualification(self):
|
||||
from openpilot.starpilot.flm.offroad_baseline_windows import tracking_proposal_windows
|
||||
from openpilot.starpilot.flm.offroad_evidence import RouteWindow, whole_windows, window_identity
|
||||
|
||||
original = TestOffroadTrackingPolicy.windows('declared-source', 0.9)[0]
|
||||
short = replace(original, points=original.points[:200], identity='')
|
||||
short = replace(short, identity=window_identity(short.points))
|
||||
first = short.points[0].mono_ns
|
||||
points = tuple(replace(p, mono_ns=p.mono_ns + block * 2_000_000_000) for block in range(3) for p in short.points)
|
||||
parent = RouteWindow(window_identity(points), points)
|
||||
self.assertEqual(parent.points[0].mono_ns, first)
|
||||
expected = whole_windows(parent.points)
|
||||
continuous = {'nonzero': (short,)}
|
||||
selected, grades = tracking_proposal_windows(continuous, {'known-zero': (parent,)})
|
||||
self.assertEqual(selected['known-zero'], list(expected))
|
||||
self.assertEqual(tuple(p for w in selected['known-zero'] for p in w.points), points)
|
||||
self.assertEqual(selected['nonzero'], (short,))
|
||||
self.assertEqual(continuous, {'nonzero': (short,)})
|
||||
self.assertEqual(grades, {'known-zero': 'bookended_zero_route_tracking'})
|
||||
|
||||
def test_continuous_center_does_not_hide_other_baseline_maneuvers(self):
|
||||
from openpilot.starpilot.flm.offroad_baseline_windows import stable_proposal_windows
|
||||
|
||||
@@ -431,6 +607,118 @@ class TestOffroadRouteCoverage(unittest.TestCase):
|
||||
|
||||
|
||||
class TestOffroadTrackingPolicy(unittest.TestCase):
|
||||
def test_saved_unit_configuration_reports_successful_original_checks(self):
|
||||
from types import SimpleNamespace
|
||||
from openpilot.starpilot.flm.offroad_worker import OffroadWorker
|
||||
from openpilot.starpilot.flm.offroad_processor import CompletedAssessment
|
||||
from openpilot.starpilot.flm.offroad_route import RouteReceipt
|
||||
|
||||
declared = trial()
|
||||
roles = declared['before']
|
||||
baseline = declared['gross_baseline']
|
||||
proposed = {'values': values(0.005), 'parameter': 'center_taper_max', 'delta': 0.005}
|
||||
cohort = dict(roles, cohort='declared-positive-report', whole_windows=6, proposal=proposed, qualification_roles=roles)
|
||||
metadata = {'route_values': tuple(values().items()), 'route_revision': REVISION, 'controller_mode': 'starpilot', 'route_scope': scope().key()}
|
||||
group = {
|
||||
'original_metadata': metadata,
|
||||
'original_context_sha256': 'f' * 64,
|
||||
'cohorts': [cohort],
|
||||
'qualification_original_windows': baseline,
|
||||
'selected_original_windows': baseline,
|
||||
'tracking_source_grade': 'declared-unit-only',
|
||||
}
|
||||
cp = SimpleNamespace(carFingerprint='declared-torque-unit')
|
||||
receipt = RouteReceipt('declared-positive-report', 'd' * 64, (), 'a' * 64, 1, 2, 3, ())
|
||||
inventory = {'routeId': receipt.route_id}
|
||||
assessment = CompletedAssessment(receipt, (group,), {}, cp, (), inventory)
|
||||
|
||||
def assessed(params, root, previous, *, permitted):
|
||||
previous(cp)
|
||||
return assessment
|
||||
|
||||
def declared_checks(actual, original, ids, trial_values, root, *, permitted):
|
||||
# Declared protocol responses only, not a native qualification claim.
|
||||
return [
|
||||
{
|
||||
'window': wid,
|
||||
'qualified': True,
|
||||
'numerical': {'qualified': True, 'changed_frames': 1},
|
||||
'registered_encoder': {'qualified': True, 'candidate_changed_emissions': 1},
|
||||
}
|
||||
for wid in ids
|
||||
]
|
||||
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
params = Params(directory)
|
||||
with (
|
||||
patch('openpilot.starpilot.flm.offroad_worker.latest_route', return_value=inventory),
|
||||
patch('openpilot.starpilot.flm.offroad_checks._original_results', new=declared_checks),
|
||||
):
|
||||
worker = OffroadWorker(params, directory, Path(directory) / 'cache', permitted=lambda: True, assessor=assessed, scope_for=lambda cp, params: scope())
|
||||
result = worker.run_once()
|
||||
self.assertTrue(result['configuration_changed'])
|
||||
self.assertEqual(result['qualification_summary'], {'passed': 6, 'required': 6, 'refusals': []})
|
||||
self.assertEqual(result['saved_adjustments'], 1)
|
||||
self.assertFalse(result['actual_application_proven'])
|
||||
|
||||
def test_training_feasibility_is_bounded_and_heldout_refusal_never_retries(self):
|
||||
from types import SimpleNamespace
|
||||
from openpilot.starpilot.flm.offroad_checks import fit_training_step
|
||||
from openpilot.starpilot.flm.offroad_processor import CompletedAssessment
|
||||
from openpilot.starpilot.flm.offroad_route import RouteReceipt
|
||||
from openpilot.starpilot.flm.offroad_worker import OffroadWorker
|
||||
|
||||
train, heldout = tuple(str(i) * 64 for i in range(1, 4)), tuple(str(i) * 64 for i in range(4, 7))
|
||||
roles = {'training': {'windows': train}, 'heldout': {'windows': heldout}}
|
||||
proposed = values(0.005)
|
||||
proposal = {'values': proposed, 'parameter': 'center_taper_max', 'delta': 0.005}
|
||||
cohort = {'cohort': 'declared-training', 'whole_windows': 6, 'proposal': proposal, 'qualification_roles': roles}
|
||||
metadata = {'route_values': tuple(values().items()), 'route_revision': REVISION, 'controller_mode': 'starpilot', 'route_scope': scope().key()}
|
||||
group = {'original_metadata': metadata, 'original_context_sha256': 'f' * 64, 'cohorts': [cohort], 'qualification_original_windows': []}
|
||||
cp = SimpleNamespace(carFingerprint='declared-torque-unit')
|
||||
receipt = RouteReceipt('declared-training-drive', 'd' * 64, (), 'a' * 64, 1, 2, 3, ())
|
||||
inventory = {'routeId': 'declared-training-drive'}
|
||||
assessment = CompletedAssessment(receipt, (group,), {}, cp, (), inventory)
|
||||
calls = []
|
||||
|
||||
def original_checks(actual, original, ids, trial_values, root, *, permitted):
|
||||
self.assertTrue(permitted())
|
||||
self.assertIs(actual, assessment)
|
||||
amount = trial_values['center_taper_max']
|
||||
calls.append((tuple(ids), amount))
|
||||
self.assertTrue(set(ids) in (set(train), set(train + heldout)))
|
||||
return [{'window': wid, 'qualified': amount <= 0.004 and wid not in heldout} for wid in ids]
|
||||
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
params = Params(directory)
|
||||
|
||||
def assessed(params, root, previous, *, permitted):
|
||||
previous(cp)
|
||||
return assessment
|
||||
|
||||
with (
|
||||
patch('openpilot.starpilot.flm.offroad_worker.latest_route', return_value=inventory),
|
||||
patch('openpilot.starpilot.flm.offroad_checks._original_results', new=original_checks),
|
||||
):
|
||||
worker = OffroadWorker(params, directory, Path(directory) / 'cache', permitted=lambda: True, assessor=assessed, scope_for=lambda cp, params: scope())
|
||||
result = worker.run_once()
|
||||
self.assertEqual(calls, [(train, 0.005), (train, 0.004), (train + heldout, 0.004)])
|
||||
self.assertFalse(result['configuration_changed'])
|
||||
self.assertEqual(result['reason'], 'original_controller_or_registered_encoder_check_refused')
|
||||
self.assertEqual(read_saved(params, KEY, MAX_BYTES), (None, True))
|
||||
|
||||
calls.clear()
|
||||
|
||||
def infeasible(actual, original, ids, trial_values, root, *, permitted):
|
||||
calls.append(tuple(ids))
|
||||
return [{'window': wid, 'qualified': False} for wid in ids]
|
||||
|
||||
with patch('openpilot.starpilot.flm.offroad_checks._original_results', new=infeasible):
|
||||
fitted, proof = fit_training_step(assessment, group, cohort, proposal, values(), '.', permitted=lambda: True)
|
||||
self.assertIsNone(fitted)
|
||||
self.assertEqual(calls, [train] * 5)
|
||||
self.assertIsNone(proof['selected_fraction'])
|
||||
|
||||
@staticmethod
|
||||
def windows(prefix, gain, amplitude=1.0):
|
||||
import math
|
||||
|
||||
@@ -241,6 +241,31 @@ for(const label of ['Highway center taper','Left turn-in boost','Left unwind tap
|
||||
assert(!offroadText.includes('Waiting for driving samples'))
|
||||
assert(!offroadText.includes('Real measurements go here'))
|
||||
assert.equal(ctx.currentApplied,false)
|
||||
const measuredWindow={version:1,domain:'lateral_acceleration',samples:2000,windows:10,error_rms:.12,route:'declared-drive',segment:0,
|
||||
source_start_ns:1e9,source_end_ns:2.9e9,points:Array.from({length:20},(_,i)=>[1e9+i*1e8,.2,.08]),
|
||||
sampling:'input_ordered_original_endpoints_no_interpolation'}
|
||||
ctx.report.current.details['Recorded steering response']=measuredWindow
|
||||
ctx.report.current.details['Saved route adjustment count']=0
|
||||
ctx.report.current.details['Controller and encoder check windows']=[22,24]
|
||||
ctx.report.current.details['Automatic tune refusal reasons']=[['original_scored_frame_not_eligible',1],['candidate changed an actual limiter/support',1]]
|
||||
assert.equal(ctx.offroad.observed.errorRms,.12)
|
||||
assert.equal(ctx.recordedTrackingChart.valid,true)
|
||||
assert(ctx.recordedTrackingChart.paths.every(path=>path.length===1),'Both original series render')
|
||||
const measuredText=text(render.call(ctx,ctx,[]))
|
||||
assert(measuredText.includes('Recorded baseline') && measuredText.includes(measurementText(.12)))
|
||||
assert(measuredText.includes('Saved adjustments') && measuredText.includes('Awaiting next-drive measurement'))
|
||||
assert(measuredText.includes('No interpolation or delay alignment'))
|
||||
assert(measuredText.includes('22 / 24 original controller and encoder windows passed'))
|
||||
assert(measuredText.includes('candidate triggered an encoder limiter'))
|
||||
assert.equal(ctx.currentApplied,false)
|
||||
for(const changed of [{points:measuredWindow.points.toReversed()},{error_rms:NaN},{domain:'torque'},
|
||||
{source_end_ns:measuredWindow.source_end_ns+1}]){
|
||||
ctx.report.current.details['Recorded steering response']={...measuredWindow,...changed}
|
||||
assert.equal(ctx.offroad.observed,null,'Malformed recorded data cannot populate the graph')
|
||||
}
|
||||
ctx.report.current.details['Recorded steering response']={...measuredWindow,domain:'steering_angle'}
|
||||
assert.equal(ctx.offroad.observed.units,'degrees','PID observations retain their angle units')
|
||||
ctx.report.current.details['Recorded steering response']=measuredWindow
|
||||
ctx.report.current.details['Saved route configuration state']='ready_for_next_drive'
|
||||
assert.equal(ctx.state.title,'Tune saved for the next drive')
|
||||
assert.equal(ctx.currentApplied,false,'Next-drive saving never fabricates live application')
|
||||
|
||||
@@ -160,7 +160,11 @@ const REASONS = {
|
||||
analyzing_last_completed_drive: "Analyzing the last completed drive while parked.",
|
||||
checking_recorded_steering_recommendations: "Checking the recorded-drive recommendations against the original controller and steering outputs.",
|
||||
recorded_tracking_suggestion_not_yet_qualified_for_automatic_application: "Recommendations were found, but none has passed the controller and steering-output checks required for automatic saving.",
|
||||
original_controller_or_registered_encoder_check_refused: "A tune was suggested, but at least one original controller or encoder check refused it. Your saved tune was kept.",
|
||||
original_scored_frame_not_eligible: "An original frame was inactive, limited or otherwise ineligible.",
|
||||
"candidate changed an actual limiter/support": "The candidate triggered an encoder limiter that the recorded request did not.",
|
||||
original_controller_or_registered_encoder_check_refused: "The recommendation failed an original controller or steering-output check. The saved tune was kept.",
|
||||
no_new_native_feasible_training_step: "No bounded training step passed the original steering-output checks. The saved tune was kept.",
|
||||
no_consistent_adjustment_in_completed_drive: "The independent parts of this drive did not support the same adjustment. The saved tune was kept.",
|
||||
more_independent_eligible_tracking_required: "The drive was analyzed. More complete independent steering windows are needed before saving a tune.",
|
||||
bounded_trial_configured_for_next_drive: "A bounded, reversible tune was saved automatically for the next drive. Its road improvement is not yet confirmed.",
|
||||
@@ -207,13 +211,37 @@ export function offroadView(report) {
|
||||
if (!Array.isArray(rows) || rows.length > 16 || rows.some(row => !Array.isArray(row) || row.length !== 4 ||
|
||||
!text(row[0], 500) || typeof row[1] !== 'number' || !Number.isFinite(row[1]) || Math.abs(row[1]) > .25 ||
|
||||
!stamp(row[2]) || row[3] !== 'controller_native_checks_not_passed')) return null
|
||||
const observed = recordedSteeringView(details['Recorded steering response'])
|
||||
const savedCount = details['Saved route adjustment count']
|
||||
const checked = details['Controller and encoder check windows'], refused = details['Automatic tune refusal reasons']
|
||||
const checks = Array.isArray(checked) && checked.length === 2 && checked.every(stamp) && checked[0] <= checked[1] && checked[1] <= 24 &&
|
||||
Array.isArray(refused) && refused.length <= 24 && refused.every(r => Array.isArray(r) && r.length === 2 && text(r[0]) && stamp(r[1])) ?
|
||||
{ passed: checked[0], required: checked[1], refusals: refused.map(([reason, count]) => ({ reason, count })) } : null
|
||||
return { route: details['Completed drive analyzed'], samples: details['Recorded steering observations'],
|
||||
reason: details['Analysis decision'], saved: details['Automatic configuration changed'] === true,
|
||||
configurationState: details['Saved route configuration state'] ?? null,
|
||||
observed, checks, savedCount: stamp(savedCount) && savedCount <= 35 ? savedCount : null,
|
||||
suggestions: rows.map(([parameter, delta, windows, qualification]) => ({ parameter, delta, windows, qualification })),
|
||||
moreSuggestions: details['More recorded suggestions omitted'] === true }
|
||||
}
|
||||
|
||||
export function recordedSteeringView(value) {
|
||||
if (!record(value) || value.version !== 1 || !['lateral_acceleration', 'steering_angle'].includes(value.domain) ||
|
||||
!stamp(value.samples) || value.samples < 20 || !stamp(value.windows) || value.windows < 1 ||
|
||||
!Number.isFinite(value.error_rms) || value.error_rms < 0 || !text(value.route, 180) ||
|
||||
!stamp(value.segment) || !stamp(value.source_start_ns) || !stamp(value.source_end_ns) ||
|
||||
value.source_start_ns <= 0 || value.source_end_ns <= value.source_start_ns ||
|
||||
value.sampling !== 'input_ordered_original_endpoints_no_interpolation' ||
|
||||
!Array.isArray(value.points) || value.points.length < 20 || value.points.length > 64 ||
|
||||
value.points.some((r, i) => !Array.isArray(r) || r.length !== 3 || !stamp(r[0]) ||
|
||||
r[0] < value.source_start_ns || r[0] > value.source_end_ns || i > 0 && r[0] <= value.points[i - 1][0] ||
|
||||
!r.slice(1).every(v => Number.isFinite(v) && Math.abs(v) <= 10000)) ||
|
||||
value.points[0][0] !== value.source_start_ns || value.points.at(-1)[0] !== value.source_end_ns) return null
|
||||
return { errorRms: value.error_rms, samples: value.samples, windows: value.windows, route: value.route,
|
||||
segment: value.segment, units: value.domain === 'lateral_acceleration' ? 'm/s²' : 'degrees',
|
||||
points: value.points.map(([ns, desired, actual]) => ({ at: (ns - value.source_start_ns) / 1e6, desired, actual })) }
|
||||
}
|
||||
|
||||
export function tunerStatus(report, live = false, choiceLive = false, error = "", reportFresh = live) {
|
||||
const preference = choiceLive && validLearningPreference(report?.preference) ? report.preference : null
|
||||
const current = report?.current
|
||||
|
||||
@@ -74,12 +74,13 @@ export const FlmPage = {
|
||||
measurements() { return this.tracking ? this.trackingProgress?.capturedPoses ?? null : this.current?.progress?.samples ?? null },
|
||||
trackingPlot() { return trackingPlotView(this.report, this.now - this.receivedAt) },
|
||||
offroad() { return offroadView(this.report) },
|
||||
recordedTrackingChart() { return chartGeometry(this.offroad?.observed?.points || [], ['desired', 'actual']) },
|
||||
pairedTrackingChart() { return chartGeometry(this.trackingPlot?.points || [], ["desiredForceAccel", "measuredForceAccel"]) },
|
||||
pairedErrorChart() { return chartGeometry(this.trackingPlot?.points || [], ["errorForceAccel"]) },
|
||||
details() { return Object.entries(this.current?.details || {}) },
|
||||
reportAge() { return this.current ? this.current.ageSeconds + Math.max(0, this.now - this.receivedAt) / 1000 : null },
|
||||
appliedCount() { return this.currentApplied && this.rows.every(row => row.applied !== null) ? this.rows.filter(row => row.applied !== 0).length : null },
|
||||
acceptedCount() { return this.trackingValues ? this.rows.filter(row => row.accepted !== null && row.accepted !== 0).length : null },
|
||||
acceptedCount() { return this.offroad ? this.offroad.savedCount : this.trackingValues ? this.rows.filter(row => row.accepted !== null && row.accepted !== 0).length : null },
|
||||
groupRows() { return this.rows.filter(row => this.selectedGroup.names.includes(row.name)) },
|
||||
selectedGroup() { return this.groups.find(group => group.id === this.group) || this.groups[0] },
|
||||
historyRows() { return this.showAllHistory ? this.report?.history || [] : (this.report?.history || []).slice(0, 6) },
|
||||
@@ -180,17 +181,19 @@ export const FlmPage = {
|
||||
</nav>
|
||||
<section v-if="view === 'overview'" id="tuner-panel-overview" role="tabpanel" aria-labelledby="tuner-tab-overview" class="gx-tuner__panel">
|
||||
<div class="gx-tuner__metrics"><div class="gx-card gx-tuner__metric"><span>Applied parameters</span><strong>{{ amount(appliedCount) }}<small v-if="appliedCount !== null"> / 30</small></strong><p>{{ appliedCount === null ? 'Requires current controller evidence' : 'Nonzero corrections acknowledged now' }}</p></div>
|
||||
<div class="gx-card gx-tuner__metric"><span>Accepted corrections</span><strong>{{ amount(acceptedCount) }}</strong><p>{{ acceptedCount === null ? 'No accepted map reported' : 'Reported accepted map · saving is separate' }}</p></div>
|
||||
<div class="gx-card gx-tuner__metric"><span>{{ offroad ? 'Saved adjustments' : 'Accepted corrections' }}</span><strong>{{ amount(acceptedCount) }}</strong><p>{{ offroad ? 'Saved for the next drive · current application is separate' : acceptedCount === null ? 'No accepted map reported' : 'Reported accepted map · saving is separate' }}</p></div>
|
||||
<div class="gx-card gx-tuner__metric"><span>Steering measurements</span><strong>{{ amount(measurements) }}</strong><p>{{ trackingProgress ? 'Retained observations, before quality checks' : tracking ? 'No capture count reported' : 'Samples retained in the learner report' }}</p></div></div>
|
||||
<div class="gx-tuner__overview-grid"><section class="gx-card gx-tuner__section gx-tuner__response"><div class="gx-tuner__section-heading"><div><p class="gx-tuner__eyebrow">STEERING RESPONSE</p><h3>Requested meets measured</h3></div><span class="gx-tuner__pill">{{ trackingPlot ? 'Completed window' : offroad ? 'Recorded drive' : 'Waiting for samples' }}</span></div>
|
||||
<template v-if="trackingPlot"><PlotGraph :chart="pairedTrackingChart" title="Requested and measured steering acceleration, paired using the learner's measured delay" /><div class="gx-tuner__legend"><span class="gx-tuner__requested">Requested</span><span class="gx-tuner__measured">Measured</span><small>m/s² · delay aligned</small></div><p class="gx-tuner__caption">{{ trackingPlot.totalEndpoints }} measured endpoints · {{ trackingPlot.age.toFixed(1) }}s since the last pose</p></template>
|
||||
<div v-else-if="offroad" class="gx-tuner__empty"><i class="bi bi-activity" aria-hidden="true"></i><h4>{{ amount(offroad.samples) }} recorded steering observations</h4><p>{{ learningReason(offroad.reason) }}</p><p class="gx-tuner__caption">The recorded-drive analysis is separate from the live, delay-paired response graph.</p></div>
|
||||
<template v-else-if="offroad?.observed"><PlotGraph :chart="recordedTrackingChart" title="Recorded requested and measured steering" /><div class="gx-tuner__legend"><span class="gx-tuner__requested">Requested</span><span class="gx-tuner__measured">Measured</span><small>{{ offroad.observed.units }} · original source times</small></div><p class="gx-tuner__caption">Drive {{ offroad.observed.route }} · segment {{ offroad.observed.segment }} · one input-ordered recorded window. No interpolation or delay alignment. Baseline statistics use {{ amount(offroad.observed.windows) }} windows.</p></template>
|
||||
<div v-else-if="offroad" class="gx-tuner__empty"><i class="bi bi-activity" aria-hidden="true"></i><h4>{{ amount(offroad.samples) }} recorded steering observations</h4><p>{{ learningReason(offroad.reason) }}</p><p class="gx-tuner__caption">No recorded response series was included in this report.</p></div>
|
||||
<div v-else class="gx-tuner__empty"><i class="bi bi-activity" aria-hidden="true"></i><h4>Real measurements go here</h4><p>A completed steering window will show the requested response beside what the car actually did.</p></div>
|
||||
</section>
|
||||
<section class="gx-card gx-tuner__section"><div class="gx-tuner__section-heading"><div><p class="gx-tuner__eyebrow">{{ offroad ? 'RECORDED-DRIVE ANALYSIS' : live ? 'THE CURRENT EXPERIMENT' : 'LAST REPORTED EXPERIMENT' }}</p><h3>{{ offroad ? 'Next-drive tuning' : tracking?.acknowledged && tracking.status === 'trial_active' ? 'Trial in progress' : 'Trial results' }}</h3></div></div>
|
||||
<p v-if="tracking">{{ learningReason(tracking.reason) }}</p><p v-else-if="offroad">{{ learningReason(offroad.reason) }}</p><p v-else>Waiting for driving samples. Auto Learn compares steering error before and during a trial.</p>
|
||||
<template v-if="offroad?.checks"><p>{{ offroad.checks.passed }} / {{ offroad.checks.required }} original controller and encoder windows passed.</p><p v-for="(refusal, index) in offroad.checks.refusals" :key="index">{{ refusal.count }} window(s): {{ learningReason(refusal.reason) }}</p></template>
|
||||
<template v-if="offroad"><ul v-if="offroad.suggestions.length"><li v-for="(suggestion, index) in offroad.suggestions" :key="index"><strong>{{ parameterLabel(suggestion.parameter) }} {{ coordinateText(suggestion.delta) }}</strong> · {{ amount(suggestion.windows) }} recorded windows · not approved for automatic saving</li></ul><p v-if="offroad.moreSuggestions" class="gx-tuner__caption">Additional candidates are retained in the device's full assessment.</p><p class="gx-tuner__caption">{{ offroad.configurationState === 'ready_for_next_drive' ? 'A separate qualified tune is saved for the next drive. Current application still requires controller evidence.' : 'Your saved tune is unchanged unless the controller and steering-output checks pass.' }}</p></template>
|
||||
<div class="gx-tuner__comparison"><div><span>Before</span><strong>{{ measurementText(tracking?.baselineErrorRms ?? null) }}</strong></div><i class="bi bi-arrow-right" aria-hidden="true"></i><div><span>Trial</span><strong>{{ measurementText(tracking?.futureErrorRms ?? null) }}</strong></div></div><p class="gx-tuner__caption">Tracking error RMS · m/s² · lower is better</p>
|
||||
<div class="gx-tuner__comparison"><div><span>{{ offroad ? 'Recorded baseline' : 'Before' }}</span><strong>{{ measurementText(offroad?.observed?.errorRms ?? tracking?.baselineErrorRms ?? null) }}</strong></div><i class="bi bi-arrow-right" aria-hidden="true"></i><div><span>Trial</span><strong>{{ offroad ? 'Awaiting next-drive measurement' : measurementText(tracking?.futureErrorRms ?? null) }}</strong></div></div><p class="gx-tuner__caption">Tracking error RMS · {{ offroad?.observed?.units ?? 'm/s²' }} · lower is better</p>
|
||||
<p v-if="improvement !== null" class="gx-tuner__observed">{{ Math.abs(improvement).toFixed(1) }}% {{ improvement >= 0 ? 'lower' : 'higher' }} observed error<small>{{ state.key === 'confirmed' ? 'Comparison passed; controller acknowledged' : 'Observation so far, not a confirmed improvement' }}</small></p>
|
||||
<div v-if="quota" class="gx-tuner__quota"><div><span>Comparable trial windows</span><strong>{{ quota.completed }} / {{ quota.required }}</strong></div><div class="gx-tuner__progress" role="progressbar" aria-label="Comparable trial windows" :aria-valuenow="quota.completed" :aria-valuemax="quota.required" aria-valuemin="0"><span :style="{ width: quota.percent + '%' }"></span></div></div>
|
||||
<p class="gx-tuner__caption">Changes that make steering worse are rolled back. Accepted and saved values are shown separately.</p>
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
{"protected_inventory_sha256": "634294450d5ee27ffd4369f4241cd61083c4c4244909ba9daff78b215b9049cb", "schema_version": 1}
|
||||
Reference in New Issue
Block a user