diff --git a/openpilot/starpilot/flm/README.md b/openpilot/starpilot/flm/README.md index b4d7adf0b1..282f5de61a 100644 --- a/openpilot/starpilot/flm/README.md +++ b/openpilot/starpilot/flm/README.md @@ -1,7 +1,11 @@ # StarPilot steering learning -Galaxy’s Auto Learn results at `/tuning/flm` read the existing bounded training -report and live plots. `/tuning` and `/tuning/plots` show Live Plots directly. +Galaxy’s StarPilot Tuner at `/tuning` shows Auto Learn status, grouped full30 +correction coordinates, and reported learning activity. `/tuning/flm` remains an +alias; `/tuning/plots` opens the separate live control plots. The saved vehicle +choice is read independently of the optional learner report, so Off remains +visible when the learner is stopped. Historical reports, current controller +acknowledgement, accepted corrections and disk saving are separate states. Recorded-log analysis, manual GM trial generation, their worker and CLI have been removed. Shared log readers remain for route summaries and downloads. diff --git a/openpilot/starpilot/flm/training_report.py b/openpilot/starpilot/flm/training_report.py index 14661da146..6c85f90ed7 100644 --- a/openpilot/starpilot/flm/training_report.py +++ b/openpilot/starpilot/flm/training_report.py @@ -344,10 +344,13 @@ class TrainingReporter: live = getattr(collector, 'live_tracking', None) if collector is not None else None if live is not None: current.update(tracking_objects(live.report(now_ns), fresh=True)) - state = (status, reason, application['applied'], None if fit is None else (fit['status'], fit['reason'])) + tracking = current.get('tracking') + history_status, history_reason = (tracking['status'], tracking['reason']) if tracking is not None else (status, reason) + state = (history_status, history_reason, application['applied'], None if tracking is None else tracking['trialId'], + None if fit is None else (fit['status'], fit['reason'])) if state != self.last_state: - self.history.append({'time': f'{(now_ns - self.started) / 1e9:.1f} s', 'status': status, - 'reason': reason, 'applied': application['applied']}) + self.history.append({'time': f'{(now_ns - self.started) / 1e9:.1f} s', 'status': history_status, + 'reason': history_reason, 'applied': application['applied']}) self.last_state = state payload = {'version': 1, 'observedMonoTime': now_ns, 'vehicle': self.vehicle, 'session': '' if collector is None else collector.session, 'current': current, 'history': list(self.history)} diff --git a/openpilot/starpilot/galaxy/test_flm_training_results.py b/openpilot/starpilot/galaxy/test_flm_training_results.py index 63720351ab..8cfe0b72b0 100644 --- a/openpilot/starpilot/galaxy/test_flm_training_results.py +++ b/openpilot/starpilot/galaxy/test_flm_training_results.py @@ -205,3 +205,42 @@ def test_mode_or_choice_change_during_read_is_unknown(tmp_path, monkeypatch, key monkeypatch.setattr(saved_source, 'read_saved', changed) actual = FlmTrainingResults(tmp_path / 'missing', params=params).snapshot()['preference'] assert actual['valid'] is False and actual['enabled'] is None + + +def tracking_report(status='trial_pending', trial='a' * 64): + return {'version': 1, 'status': status, 'reason': 'awaiting_current_application', 'group': 'center', + 'names': ['center_taper_max'], 'trialId': trial, 'phase': 'trial', 'requiredEpisodes': 3, + 'beforeEpisodes': 3, 'futureEpisodes': 0, 'modelStatus': 'valid', 'lagMs': 170., + 'baselineErrorRms': .1, 'futureErrorRms': None, 'baselineReversalHz': .2, 'futureReversalHz': None, + 'applicationAcknowledged': False, 'bindingCurrent': True, + 'applicationSettledMonoTime': None, 'lastCorrectionMonoTime': None} + + +def test_live_trial_history_does_not_use_legacy_fitter_state(tmp_path): + from types import SimpleNamespace as NS + from openpilot.starpilot.flm.training_report import TrainingReporter + path = tmp_path / 'live.json' + state = tracking_report() + live = NS(latest_offer=None, report=lambda _now: {'tracking': dict(state)}) + collector = NS(adapter=NS(raw_collection=False, event_collection=False), blocks=(), points=(), sealed=False, + ready=(None,) * 128, live_tracking=live, original_source_capture=NS(rows=(None,) * 1731), + session='live-unit', training_interruptions={}, dropped=0, reason='no_current_learning_evidence') + message = NS(autoSteeringLearning=NS(learning=NS(status='collecting', reason='legacy'))) + source = NS(enabled=True, calibration_generation='', observed_ns=NOW) + reporter = TrainingReporter('HYUNDAI_IONIQ_6', path=path) + def publish(now): + reporter.publish(now_ns=now, message=message, collector=collector, source=source, objective=None, + pending=False, native_current=False) + return FlmTrainingResults(path, clock=lambda: now).snapshot() + + first = publish(NOW) + assert first['current']['progress']['samples'] == 0 # Original legacy contract is preserved. + assert first['history'][-1]['status'] == 'trial_pending' + assert not first['current']['application']['applied'] # Telemetry never creates an acknowledgement. + state['trialId'] = 'b' * 64 + second = publish(NOW + 1_000_000_000) + assert len(second['history']) == 2 # Same status, distinct original trial. + state.update(status='rollback', phase='rollback', reason='trial_future_support_timeout') + final = publish(NOW + 2_000_000_000) + assert final['history'][-1]['status'] == 'rollback' + assert final['current']['details']['Fit attempts'] == 0 diff --git a/openpilot/starpilot/galaxy/tests/test_flm.mjs b/openpilot/starpilot/galaxy/tests/test_flm.mjs index 16076bb5f4..249e7184f2 100644 --- a/openpilot/starpilot/galaxy/tests/test_flm.mjs +++ b/openpilot/starpilot/galaxy/tests/test_flm.mjs @@ -2,7 +2,7 @@ import assert from "node:assert/strict" import { readFileSync } from "node:fs" import { compile } from "../web/vendor/vue/vue.esm-browser.js" import { FlmPage } from "../web/js/flm.js" -import { SURFACE_NAMES, PID_UNSUPPORTED, fullCoordinates, surfaceRows, validTrainingResults, coordinateText, SURFACE_GROUPS, validLearningPreference, tunerStatus } from "../web/js/flm-training-results.js" +import { SURFACE_NAMES, PID_UNSUPPORTED, fullCoordinates, surfaceRows, validTrainingResults, coordinateText, SURFACE_GROUPS, validLearningPreference, tunerStatus, learningReason } from "../web/js/flm-training-results.js" const report = () => ({ version: 1, history: [], current: { fresh: true, ageSeconds: .1, enabled: true, status: "collecting", reason: "capturing_steering_evidence", progress: { samples: 120, blocks: 3, requiredBlocks: 16 }, @@ -77,6 +77,10 @@ assert.equal(tunerStatus({ ...value, preference: choice }, false, true).key, 'pa assert.equal(tunerStatus({ ...value, preference: { ...choice, enabled: false } }, true, true).key, 'off') assert.equal(tunerStatus({ ...value, preference: choice }, false, false).key, 'stale') assert.equal(tunerStatus({ ...value, preference: choice }, true, true, 'offline').key, 'offline') +const inactive = report(); inactive.current.reason = 'controller_collection_required' +assert.equal(tunerStatus(inactive, true).title, 'Waiting for active steering') +assert.match(learningReason('stale_control'), /arrived late/) +assert.equal(learningReason('unknown_internal_reason'), "The device's detailed report is available in Activity.") for (const invalid of [null, {}, { ...choice, version: true }, { ...choice, valid: false, enabled: true }, { ...choice, enabled: 'yes' }, { ...choice, parked: 1 }, { ...choice, vehicle: 'x'.repeat(257) }]) { assert(!validLearningPreference(invalid)) diff --git a/openpilot/starpilot/galaxy/tests/test_flm_tracking_results.mjs b/openpilot/starpilot/galaxy/tests/test_flm_tracking_results.mjs index 8034704117..9f58aac0f7 100644 --- a/openpilot/starpilot/galaxy/tests/test_flm_tracking_results.mjs +++ b/openpilot/starpilot/galaxy/tests/test_flm_tracking_results.mjs @@ -194,6 +194,15 @@ assert(!liveText.toLowerCase().includes('invalid applied output'), 'Live learner assert(!liveText.includes('Collection & fitting'), 'Legacy fitter cards must not describe the live empirical learner') delete ctx.report.current.tracking const legacyText = text(render.call(ctx,ctx,[])) -assert(legacyText.toLowerCase().includes('invalid applied output')) +assert(legacyText.includes('The reported steering output could not be verified')) assert(legacyText.includes('Collecting driving samples')) console.log('Actual Vue render keeps live learner progress separate and preserves legacy report fallback') + +const countsReport=report();countsReport.current.details['GLOBAL original source capture']={poses:1731,readyPairs:128} +const progressContext={...FlmPage.data(),report:countsReport} +for(const [name,get] of Object.entries(FlmPage.computed))Object.defineProperty(progressContext,name,{get:()=>get.call(progressContext)}) +assert.equal(progressContext.measurements,1731,'Live source observations are independent of legacy fitter counts') +countsReport.current.details['GLOBAL original source capture'].poses=-1 +assert.equal(progressContext.trackingProgress,null,'Invalid original counts stay unknown') +assert.equal(progressContext.measurements,null,'Missing live counts cannot be replaced with legacy zero') +console.log('Live pose count uses existing producer data, without extra telemetry or an eligibility claim') diff --git a/openpilot/starpilot/galaxy/tests/test_flm_training_results.mjs b/openpilot/starpilot/galaxy/tests/test_flm_training_results.mjs index 1a70faa331..7ecb43a1c0 100644 --- a/openpilot/starpilot/galaxy/tests/test_flm_training_results.mjs +++ b/openpilot/starpilot/galaxy/tests/test_flm_training_results.mjs @@ -24,3 +24,29 @@ for (const mutate of [value => value.current.progress.blocks = -1, const value = report(); mutate(value); assert(!validTrainingResults(value)) } console.log("Training wire validation, bounded details and stale/no-data labels passed") + +// The live producer's full30 flat acknowledgement and nested refusal schema +// must not be rejected as generic oversized/deep diagnostic objects. +const receiver = Object.fromEntries(['deviceMotion','extrinsicsCalibration','carState','driverMonitoringState'].map(name => [name, + {seen:true,alive:true,valid:true,freqOk:false,sourceEventMonoTime:1,receivedMonoTime:2}])) +const snapshot = {reason:'receiver_health',observedMonoTime:3,readyPairs:4,currentNative:true, + controlMonoTime:1,applyMonoTime:2,originalNativeObservedMonoTime:1,originalNativeValidUntilMonoTime:3,receiver} +const sourceRefusals = {publisherRefusal:'unknown_not_transmitted',scope:'learner receiver only; not the Controls publisher admission reason', + counts:{receiver_health:1},lastRefusal:{reason:'receiver_health',observedMonoTime:3},lastException:null, + firstRefusal:snapshot,firstReadyPairRefusal:snapshot} +const actualShape=report();actualShape.current.details['GLOBAL source observation refusals']=sourceRefusals +actualShape.current.details['Applied correction coordinates']=Array(30).fill(0) +assert(validTrainingResults(actualShape)) +for(const mutate of [v=>v.current.details['Applied correction coordinates'].pop(), + v=>v.current.details['Applied correction coordinates'].push(0), + v=>v.current.details['Applied correction coordinates'][0]=.251, + v=>v.current.details['Applied correction coordinates'][0]=true, + v=>v.current.details['GLOBAL source observation refusals'].firstRefusal.receiver.carState.freqOk=1, + v=>v.current.details['GLOBAL source observation refusals'].counts.receiver_health=-1, + v=>v.current.details['GLOBAL source observation refusals'].firstRefusal.receiver.unknown={}, + v=>v.current.details['GLOBAL source observation refusals'].firstRefusal.unexpected=0]) { + const invalid=structuredClone(actualShape);mutate(invalid);assert(!validTrainingResults(invalid)) +} +const alias=report();alias.current.details['GLOBAL source observation refusals ']=sourceRefusals +assert(!validTrainingResults(alias),'No generic nesting or alias relaxation') +console.log('Exact live full30 acknowledgement and bounded source-refusal telemetry accepted; malformed and unknown fields refused') diff --git a/openpilot/starpilot/galaxy/web/js/flm-training-results.js b/openpilot/starpilot/galaxy/web/js/flm-training-results.js index c1c10d147f..1912b8a166 100644 --- a/openpilot/starpilot/galaxy/web/js/flm-training-results.js +++ b/openpilot/starpilot/galaxy/web/js/flm-training-results.js @@ -1,5 +1,32 @@ const text = (value, limit = 240) => typeof value === "string" && value.length <= limit const count = value => value === null || Number.isSafeInteger(value) && value >= 0 +const record = value => value !== null && typeof value === 'object' && !Array.isArray(value) +const keysAre = (value, keys) => record(value) && Object.keys(value).length === keys.length && keys.every(key => Object.hasOwn(value, key)) +const stamp = value => Number.isSafeInteger(value) && value >= 0 +const REFUSAL_SERVICES = ['deviceMotion', 'extrinsicsCalibration', 'carState', 'driverMonitoringState'] +function refusalSnapshot(value) { + return value === null || keysAre(value, ['reason', 'observedMonoTime', 'readyPairs', 'currentNative', 'controlMonoTime', + 'applyMonoTime', 'originalNativeObservedMonoTime', 'originalNativeValidUntilMonoTime', 'receiver']) && + text(value.reason) && stamp(value.observedMonoTime) && stamp(value.readyPairs) && typeof value.currentNative === 'boolean' && + ['controlMonoTime', 'applyMonoTime', 'originalNativeObservedMonoTime', 'originalNativeValidUntilMonoTime'].every(key => value[key] === null || stamp(value[key])) && + keysAre(value.receiver, REFUSAL_SERVICES) && REFUSAL_SERVICES.every(service => { + const row = value.receiver[service] + return keysAre(row, ['seen', 'alive', 'valid', 'freqOk', 'sourceEventMonoTime', 'receivedMonoTime']) && + ['seen', 'alive', 'valid', 'freqOk'].every(key => typeof row[key] === 'boolean') && + stamp(row.sourceEventMonoTime) && stamp(row.receivedMonoTime) + }) +} +export function validSourceRefusals(value) { + return keysAre(value, ['publisherRefusal', 'scope', 'counts', 'lastRefusal', 'lastException', 'firstRefusal', 'firstReadyPairRefusal']) && + value.publisherRefusal === 'unknown_not_transmitted' && + value.scope === 'learner receiver only; not the Controls publisher admission reason' && + record(value.counts) && Object.keys(value.counts).length <= 24 && + Object.entries(value.counts).every(([key, amount]) => text(key, 96) && stamp(amount)) && + (value.lastRefusal === null || keysAre(value.lastRefusal, ['reason', 'observedMonoTime']) && text(value.lastRefusal.reason) && stamp(value.lastRefusal.observedMonoTime)) && + (value.lastException === null || keysAre(value.lastException, ['observedMonoTime', 'type', 'message']) && + stamp(value.lastException.observedMonoTime) && text(value.lastException.type, 96) && text(value.lastException.message, 192)) && + refusalSnapshot(value.firstRefusal) && refusalSnapshot(value.firstReadyPairRefusal) +} const detailValue = (value, depth = 0) => value === null || typeof value === "boolean" || typeof value === "number" && Number.isFinite(value) || text(value, 500) || depth < 2 && (Array.isArray(value) ? value.length <= 16 && value.every(item => detailValue(item, depth + 1)) : @@ -22,8 +49,10 @@ export function validTrainingResults(value) { if (row.trackingPlot !== undefined && row.trackingPlot !== null && !validTrackingPlot(row.trackingPlot)) return false return row.details === undefined || row.details && typeof row.details === "object" && !Array.isArray(row.details) && Object.keys(row.details).length <= 64 && Object.entries(row.details).every(([key, item]) => text(key, 80) && - (["Full surface coordinates", "Actual full surface coordinates"].includes(key) ? - fullCoordinates(item) !== null : detailValue(item))) + (["Full surface coordinates", "Actual full surface coordinates"].includes(key) ? fullCoordinates(item) !== null : + key === 'Applied correction coordinates' && Array.isArray(item) && item.length === 30 ? + item.every(number => typeof number === 'number' && Number.isFinite(number) && Math.abs(number) <= .25) : + key === 'GLOBAL source observation refusals' ? validSourceRefusals(item) : detailValue(item))) } const labels = { collecting: "Gathering driving examples", unavailable: "Waiting for usable driving data", qualifiedFit: "Fit passed validation", qualified: "Fit passed validation", rejected: "Fit did not pass validation", @@ -127,6 +156,40 @@ export const SURFACE_GROUPS = Object.freeze([ { id: "speed", label: "Speed ranges", icon: "bi-sliders", description: "Where turn-in and unwind shaping take effect.", names: ["curvy_speed_min", "curvy_speed_max", "curvy_turn_in_trim_speed_min", "curvy_turn_in_trim_speed_max"] }, ]) +const REASONS = { + controller_collection_required: "Waiting for active steering before collecting samples.", + fresh_physical_residual_window_required: "Checking the car's current steering response.", + qualified_tracking_episodes_required: "Collecting complete, comparable steering windows.", + uncensored_independent_comparison_baseline_required: "Measuring comparable driving before testing a correction.", + qualified_consistent_hook_sensitivity_required: "Collecting repeated examples before choosing an adjustment.", + stale_control: "Steering samples arrived late. Fresh data is required before learning can continue.", + stale_applied_output: "Applied steering data arrived late. Those samples cannot be used.", + invalid_applied_output: "The reported steering output could not be verified.", + independent_baseline_trial_power_insufficient: "More comparable driving is needed to distinguish a correction from normal variation.", + fresh_selected_native_probe_required: "Checking the proposed adjustment against the current steering commands.", + genuine_native_sensitivity_unavailable: "The effect of this adjustment has not been verified yet.", + consistent_later_tracking_improved: "Later comparable driving showed lower steering error.", + independent_future_tracking_improved: "Independent trial windows showed lower steering error.", + matched_tracking_not_consistently_improved: "The trial did not show a consistent improvement.", + trial_tracking_or_oscillation_worse: "The trial increased steering error or oscillation, so it was rolled back.", + trial_oscillation_batch_worse: "Comparable trial windows showed more oscillation, so the correction was rolled back.", + predeclared_independent_future_look_required: "Collecting the next planned set of comparison windows.", + prior_accepted_baseline_restored: "The controller returned to its prior baseline.", + scoring_later_matched_whole_episodes: "Comparing new steering windows with the baseline.", + accepted_target_currently_acknowledged: "The controller acknowledged the accepted correction.", +} +export const learningReason = reason => Object.hasOwn(REASONS, reason) ? REASONS[reason] : "The device's detailed report is available in Activity." +export function sourceCaptureCounts(report) { + const value = report?.current?.details?.['GLOBAL original source capture'] + return validTrackingResults(report?.current?.tracking) && value && + ['poses', 'readyPairs'].every(key => Number.isSafeInteger(value[key]) && value[key] >= 0) ? + { capturedPoses: value.poses, readyPairs: value.readyPairs } : null +} +const HISTORY_LABELS = { collecting: 'Collecting samples', calibrating: 'Measuring response', + not_identifiable: 'More evidence needed', ready: 'Adjustment selected', trial_pending: 'Trial offered', + trial_active: 'Evaluating a trial', confirmed: 'Correction confirmed', rollback: 'Returning to baseline', paused: 'Learning paused' } +export const historyTitle = status => Object.hasOwn(HISTORY_LABELS, status) ? HISTORY_LABELS[status] : readable(status) + export function tunerStatus(report, live = false, choiceLive = false, error = "") { const preference = choiceLive && validLearningPreference(report?.preference) ? report.preference : null const current = report?.current @@ -138,12 +201,16 @@ export function tunerStatus(report, live = false, choiceLive = false, error = "" if (!current) return { key: "waiting", tone: "muted", title: "Waiting for the learner", description: preference?.enabled ? "Auto Learn is on, but no learner report has arrived yet." : preference ? "The device has not identified a current vehicle and Auto Learn setting." : "Connect to your device to see its learning activity and steering tune." } if (!live) return { key: "stale", tone: "muted", title: "No current learner report", description: "These are earlier observations. They do not establish a correction being applied now." } const tracking = trackingView(report, live) + if ((tracking?.reason || current.reason) === "controller_collection_required") + return { key: "waiting", tone: "muted", title: "Waiting for active steering", description: learningReason("controller_collection_required") } + if (tracking && !tracking.bindingCurrent) + return { key: "paused", tone: "warning", title: "Learning paused", description: "The steering context changed. A fresh qualified window is required." } if (tracking?.acknowledged && tracking.status === "confirmed") return { key: "confirmed", tone: "success", title: "Correction applied", description: "The comparison passed and the controller acknowledged this correction. Disk saving is a separate result." } if (tracking?.acknowledged && tracking.status === "trial_active") return { key: "testing", tone: "active", title: "Testing a small correction", description: "The controller acknowledged the trial. Comparable new driving windows decide whether to keep it." } if (tracking?.status === "trial_pending") return { key: "pending", tone: "active", title: "Waiting for the controller", description: "A trial was offered. It has not yet been acknowledged as applied." } if (tracking?.status === "rollback") return { key: "rollback", tone: "warning", title: "Returning to the baseline", description: tracking.acknowledged ? "The controller acknowledged the rollback." : "A rollback was requested. Current application is not yet acknowledged." } - if (tracking?.status === "paused" || current.status === "unavailable") return { key: "paused", tone: "warning", title: "Learning paused", description: readable(tracking?.reason || current.reason) } - return { key: "collecting", tone: "active", title: tracking?.status === "calibrating" ? "Measuring steering response" : "Collecting driving samples", description: readable(tracking?.reason || current.reason) } + if (tracking?.status === "paused" || current.status === "unavailable") return { key: "paused", tone: "warning", title: "Learning paused", description: learningReason(tracking?.reason || current.reason) } + return { key: "collecting", tone: "active", title: tracking?.status === "calibrating" ? "Measuring steering response" : "Collecting driving samples", description: learningReason(tracking?.reason || current.reason) } } diff --git a/openpilot/starpilot/galaxy/web/js/flm.js b/openpilot/starpilot/galaxy/web/js/flm.js index fe3c81f707..6a06d600e9 100644 --- a/openpilot/starpilot/galaxy/web/js/flm.js +++ b/openpilot/starpilot/galaxy/web/js/flm.js @@ -3,7 +3,7 @@ import { GxNotice } from "./notice.js" import { PollTimer, connectionError } from "./polling.js" import { requestJson } from "./startup.js" import { PlotGraph, chartGeometry } from "./plots.js" -import { validTrainingResults, readable, detailText, surfaceRows, coordinateText, trackingView, measurementText, validTrackingValues, trackingPlotView, sourceSeconds, validLearningPreference, SURFACE_GROUPS, tunerStatus } from "./flm-training-results.js" +import { validTrainingResults, readable, detailText, surfaceRows, coordinateText, trackingView, measurementText, validTrackingValues, trackingPlotView, sourceSeconds, validLearningPreference, SURFACE_GROUPS, tunerStatus, learningReason, sourceCaptureCounts, historyTitle } from "./flm-training-results.js" const VIEWS = ["overview", "tune", "activity"] const PARAMETER_LABELS = { @@ -59,6 +59,8 @@ export const FlmPage = { rows() { return surfaceRows(this.report, this.live) }, tracking() { return trackingView(this.report, this.live) }, trackingValues() { return validTrackingValues(this.current?.trackingValues) ? this.current.trackingValues : null }, + trackingProgress() { return sourceCaptureCounts(this.report) }, + measurements() { return this.tracking ? this.trackingProgress?.capturedPoses ?? null : this.current?.progress?.samples ?? null }, trackingPlot() { return trackingPlotView(this.report, this.now - this.receivedAt) }, pairedTrackingChart() { return chartGeometry(this.trackingPlot?.points || [], ["desiredForceAccel", "measuredForceAccel"]) }, pairedErrorChart() { return chartGeometry(this.trackingPlot?.points || [], ["errorForceAccel"]) }, @@ -84,7 +86,8 @@ export const FlmPage = { }, }, methods: { - readable, detailText, coordinateText, measurementText, sourceSeconds, + readable, detailText, coordinateText, measurementText, sourceSeconds, learningReason, historyTitle, + detailLabel(key) { return ['Fit attempts', 'Fit results', 'Fit in progress', 'Submitted fit jobs (reporter lifetime)'].includes(key) ? 'Legacy fitter · ' + key : readable(key) }, amount(value) { return value === null || value === undefined ? "—" : value.toLocaleString() }, parameterLabel(name) { return PARAMETER_LABELS[name] || readable(name) }, groupChanges(group) { @@ -134,7 +137,7 @@ export const FlmPage = {
{{ state.description }}
-{{ tracking.changedParameters }}
+{{ tracking.names.map(parameterLabel).join(' · ') }}
{{ appliedCount === null ? 'Requires current controller evidence' : 'Nonzero corrections acknowledged now' }}
{{ acceptedCount === null ? 'No accepted map reported' : 'Reported accepted map · saving is separate' }}
Samples retained in the learner report
{{ trackingProgress ? 'Retained observations, before quality checks' : tracking ? 'No capture count reported' : 'Samples retained in the learner report' }}
STEERING RESPONSE
{{ trackingPlot.totalEndpoints }} measured endpoints · {{ trackingPlot.age.toFixed(1) }}s since the last pose
A completed steering window will show the requested response beside what the car actually did.
{{ live ? 'THE CURRENT EXPERIMENT' : 'LAST REPORTED EXPERIMENT' }}
{{ readable(tracking.reason) }}
Comparable driving windows establish the baseline before a small correction can be evaluated.
+{{ learningReason(tracking.reason) }}
Comparable driving windows establish the baseline before a small correction can be evaluated.
Tracking error RMS · 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' }}
LEARNING ACTIVITY
{{ readable(row.reason) }}
{{ row.time }} · Applied at that time{{ learningReason(row.reason) }}
{{ row.time }} · Applied at that timeThis will show real collection, trial and rollback events as the learner reports them.
Episode {{ trackingPlot.episodeId }} · {{ trackingPlot.sampled ? 'Selected' : 'All' }} original endpoints. No interpolation.
Control / applied output / pose: {{ sourceSeconds(trackingPlot.points.at(-1).controlMonoTime) }} / {{ sourceSeconds(trackingPlot.points.at(-1).applyMonoTime) }} / {{ sourceSeconds(trackingPlot.points.at(-1).poseMonoTime) }} monotonic seconds.