From b29a2ea4f842cf15400977a42c8e299ab872c4e2 Mon Sep 17 00:00:00 2001 From: firestar5683 <168790843+firestar5683@users.noreply.github.com> Date: Fri, 6 Nov 2026 05:19:36 +0000 Subject: [PATCH] Fit offroad tune strength and show completed-drive measurements --- openpilot/starpilot/flm/controller_replay.py | 7 +- .../starpilot/flm/offroad_baseline_windows.py | 7 + openpilot/starpilot/flm/offroad_checks.py | 66 +++- openpilot/starpilot/flm/offroad_daemon.py | 6 + openpilot/starpilot/flm/offroad_processor.py | 139 ++++++--- openpilot/starpilot/flm/offroad_worker.py | 151 +++++---- .../flm/tests/test_offroad_backend.py | 288 ++++++++++++++++++ .../tests/test_flm_tracking_results.mjs | 25 ++ .../galaxy/web/js/flm-training-results.js | 28 ++ openpilot/starpilot/galaxy/web/js/flm.js | 11 +- prebuilt | 0 prebuilt.json | 1 - 12 files changed, 617 insertions(+), 112 deletions(-) delete mode 100644 prebuilt delete mode 100644 prebuilt.json diff --git a/openpilot/starpilot/flm/controller_replay.py b/openpilot/starpilot/flm/controller_replay.py index b885d1a544..22d3af1a5e 100644 --- a/openpilot/starpilot/flm/controller_replay.py +++ b/openpilot/starpilot/flm/controller_replay.py @@ -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 diff --git a/openpilot/starpilot/flm/offroad_baseline_windows.py b/openpilot/starpilot/flm/offroad_baseline_windows.py index b6c5e67df9..34481880cc 100644 --- a/openpilot/starpilot/flm/offroad_baseline_windows.py +++ b/openpilot/starpilot/flm/offroad_baseline_windows.py @@ -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 = [] diff --git a/openpilot/starpilot/flm/offroad_checks.py b/openpilot/starpilot/flm/offroad_checks.py index 93ba816c73..877f888cd9 100644 --- a/openpilot/starpilot/flm/offroad_checks.py +++ b/openpilot/starpilot/flm/offroad_checks.py @@ -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): diff --git a/openpilot/starpilot/flm/offroad_daemon.py b/openpilot/starpilot/flm/offroad_daemon.py index 1dd5bf5eac..2985fd89a8 100644 --- a/openpilot/starpilot/flm/offroad_daemon.py +++ b/openpilot/starpilot/flm/offroad_daemon.py @@ -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)) diff --git a/openpilot/starpilot/flm/offroad_processor.py b/openpilot/starpilot/flm/offroad_processor.py index ccf16fd068..73dcb844cd 100644 --- a/openpilot/starpilot/flm/offroad_processor.py +++ b/openpilot/starpilot/flm/offroad_processor.py @@ -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) + ) diff --git a/openpilot/starpilot/flm/offroad_worker.py b/openpilot/starpilot/flm/offroad_worker.py index d9461b7ac4..0a727150e2 100644 --- a/openpilot/starpilot/flm/offroad_worker.py +++ b/openpilot/starpilot/flm/offroad_worker.py @@ -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'] diff --git a/openpilot/starpilot/flm/tests/test_offroad_backend.py b/openpilot/starpilot/flm/tests/test_offroad_backend.py index f2cc30376b..363eecd56a 100644 --- a/openpilot/starpilot/flm/tests/test_offroad_backend.py +++ b/openpilot/starpilot/flm/tests/test_offroad_backend.py @@ -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 diff --git a/openpilot/starpilot/galaxy/tests/test_flm_tracking_results.mjs b/openpilot/starpilot/galaxy/tests/test_flm_tracking_results.mjs index e04267a462..d25c0a9d93 100644 --- a/openpilot/starpilot/galaxy/tests/test_flm_tracking_results.mjs +++ b/openpilot/starpilot/galaxy/tests/test_flm_tracking_results.mjs @@ -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') diff --git a/openpilot/starpilot/galaxy/web/js/flm-training-results.js b/openpilot/starpilot/galaxy/web/js/flm-training-results.js index 691459bd04..8721647b92 100644 --- a/openpilot/starpilot/galaxy/web/js/flm-training-results.js +++ b/openpilot/starpilot/galaxy/web/js/flm-training-results.js @@ -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 diff --git a/openpilot/starpilot/galaxy/web/js/flm.js b/openpilot/starpilot/galaxy/web/js/flm.js index e99cbf5055..c48eaa38de 100644 --- a/openpilot/starpilot/galaxy/web/js/flm.js +++ b/openpilot/starpilot/galaxy/web/js/flm.js @@ -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 = {
Applied parameters{{ amount(appliedCount) }} / 30

{{ appliedCount === null ? 'Requires current controller evidence' : 'Nonzero corrections acknowledged now' }}

-
Accepted corrections{{ amount(acceptedCount) }}

{{ acceptedCount === null ? 'No accepted map reported' : 'Reported accepted map · saving is separate' }}

+
{{ offroad ? 'Saved adjustments' : 'Accepted corrections' }}{{ amount(acceptedCount) }}

{{ offroad ? 'Saved for the next drive · current application is separate' : acceptedCount === null ? 'No accepted map reported' : 'Reported accepted map · saving is separate' }}

Steering measurements{{ amount(measurements) }}

{{ trackingProgress ? 'Retained observations, before quality checks' : tracking ? 'No capture count reported' : 'Samples retained in the learner report' }}

STEERING RESPONSE

Requested meets measured

{{ trackingPlot ? 'Completed window' : offroad ? 'Recorded drive' : 'Waiting for samples' }}
-

{{ amount(offroad.samples) }} recorded steering observations

{{ learningReason(offroad.reason) }}

The recorded-drive analysis is separate from the live, delay-paired response graph.

+ +

{{ amount(offroad.samples) }} recorded steering observations

{{ learningReason(offroad.reason) }}

No recorded response series was included in this report.

Real measurements go here

A completed steering window will show the requested response beside what the car actually did.

{{ offroad ? 'RECORDED-DRIVE ANALYSIS' : live ? 'THE CURRENT EXPERIMENT' : 'LAST REPORTED EXPERIMENT' }}

{{ offroad ? 'Next-drive tuning' : tracking?.acknowledged && tracking.status === 'trial_active' ? 'Trial in progress' : 'Trial results' }}

{{ learningReason(tracking.reason) }}

{{ learningReason(offroad.reason) }}

Waiting for driving samples. Auto Learn compares steering error before and during a trial.

+ -
Before{{ measurementText(tracking?.baselineErrorRms ?? null) }}
Trial{{ measurementText(tracking?.futureErrorRms ?? null) }}

Tracking error RMS · m/s² · lower is better

+
{{ offroad ? 'Recorded baseline' : 'Before' }}{{ measurementText(offroad?.observed?.errorRms ?? tracking?.baselineErrorRms ?? null) }}
Trial{{ offroad ? 'Awaiting next-drive measurement' : measurementText(tracking?.futureErrorRms ?? null) }}

Tracking error RMS · {{ offroad?.observed?.units ?? 'm/s²' }} · lower is better

{{ Math.abs(improvement).toFixed(1) }}% {{ improvement >= 0 ? 'lower' : 'higher' }} observed error{{ state.key === 'confirmed' ? 'Comparison passed; controller acknowledged' : 'Observation so far, not a confirmed improvement' }}

Comparable trial windows{{ quota.completed }} / {{ quota.required }}

Changes that make steering worse are rolled back. Accepted and saved values are shown separately.

diff --git a/prebuilt b/prebuilt deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/prebuilt.json b/prebuilt.json deleted file mode 100644 index 4e60eddae3..0000000000 --- a/prebuilt.json +++ /dev/null @@ -1 +0,0 @@ -{"protected_inventory_sha256": "634294450d5ee27ffd4369f4241cd61083c4c4244909ba9daff78b215b9049cb", "schema_version": 1}