Report live steering trials without legacy counter errors

This commit is contained in:
firestar5683
2026-11-03 01:07:00 -06:00
parent 3924bd2e39
commit 10d170e799
9 changed files with 174 additions and 19 deletions
+6 -2
View File
@@ -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.
+6 -3
View File
@@ -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)}
@@ -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
@@ -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))
@@ -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')
@@ -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')
@@ -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) }
}
+10 -7
View File
@@ -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 = {
<div class="gx-tuner__hero-icon" aria-hidden="true"><i class="bi bi-sliders2"></i></div>
<div class="gx-tuner__hero-copy"><div class="gx-tuner__eyebrow"><span class="gx-tuner__status-dot"></span>{{ loading && !report ? 'CONNECTING' : state.key === 'off' ? 'DISABLED' : choiceLive || live ? 'DEVICE CONNECTED' : 'WAITING FOR DATA' }}</div>
<h3 role="status">{{ loading && !report ? 'Reading your tuner…' : state.title }}</h3><p>{{ state.description }}</p>
<p v-if="tracking?.changedParameters && live" class="gx-tuner__current-job"><i class="bi bi-crosshair" aria-hidden="true"></i>{{ tracking.changedParameters }}</p>
<p v-if="tracking?.changedParameters && live" class="gx-tuner__current-job"><i class="bi bi-crosshair" aria-hidden="true"></i>{{ tracking.names.map(parameterLabel).join(' · ') }}</p>
</div>
<div class="gx-tuner__hero-meta"><span>{{ currentApplied ? 'Controller acknowledged' : live ? 'No correction acknowledged' : 'Current application unknown' }}</span><small>{{ reportAge === null ? 'No learner report yet' : 'Learner report ' + reportAge.toFixed(1) + 's old' }}</small></div>
</section>
@@ -145,13 +148,13 @@ export const FlmPage = {
<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>Driving samples</span><strong>{{ amount(current?.progress?.samples) }}</strong><p>Samples retained in the learner report</p></div></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' : '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 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">{{ live ? 'THE CURRENT EXPERIMENT' : 'LAST REPORTED EXPERIMENT' }}</p><h3>{{ tracking?.acknowledged && tracking.status === 'trial_active' ? 'Does the correction help?' : 'One change. A fair comparison.' }}</h3></div></div>
<p v-if="tracking">{{ readable(tracking.reason) }}</p><p v-else>Comparable driving windows establish the baseline before a small correction can be evaluated.</p>
<p v-if="tracking">{{ learningReason(tracking.reason) }}</p><p v-else>Comparable driving windows establish the baseline before a small correction can be evaluated.</p>
<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>
<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>
@@ -167,11 +170,11 @@ export const FlmPage = {
</section></div>
</section>
<section v-if="view === 'activity'" id="tuner-panel-activity" role="tabpanel" aria-labelledby="tuner-tab-activity" class="gx-tuner__panel"><section class="gx-card gx-tuner__section"><div class="gx-tuner__section-heading"><div><p class="gx-tuner__eyebrow">LEARNING ACTIVITY</p><h3>What the tuner found</h3></div><span class="gx-tuner__pill">Reported observations</span></div>
<ol v-if="historyRows.length" class="gx-tuner__timeline"><li v-for="(row, index) in historyRows" :key="index"><span class="gx-tuner__timeline-dot" :class="{ 'gx-tuner__timeline-dot--applied': row.applied }"></span><div><strong>{{ readable(row.status) }}</strong><p>{{ readable(row.reason) }}</p><small>{{ row.time }}<template v-if="row.applied"> · Applied at that time</template></small></div></li></ol>
<ol v-if="historyRows.length" class="gx-tuner__timeline"><li v-for="(row, index) in historyRows" :key="index"><span class="gx-tuner__timeline-dot" :class="{ 'gx-tuner__timeline-dot--applied': row.applied }"></span><div><strong>{{ historyTitle(row.status) }}</strong><p>{{ learningReason(row.reason) }}</p><small>{{ row.time }}<template v-if="row.applied"> · Applied at that time</template></small></div></li></ol>
<div v-else class="gx-tuner__empty"><i class="bi bi-clock-history" aria-hidden="true"></i><h4>No findings reported yet</h4><p>This will show real collection, trial and rollback events as the learner reports them.</p></div>
<button v-if="(report?.history.length || 0) > 6" type="button" class="gx-btn gx-btn--tonal" @click="showAllHistory = !showAllHistory">{{ showAllHistory ? 'Show recent activity' : 'Show all reported activity' }}</button>
</section>
<details class="gx-card gx-tuner__section gx-tuner__disclosure"><summary>Diagnostics &amp; source details</summary><dl class="gx-tuner__diagnostics"><dt>Reported state</dt><dd>{{ readable(current?.status) }}</dd><dt>Reason</dt><dd>{{ readable(current?.reason) }}</dd><dt>Retained blocks</dt><dd>{{ amount(current?.progress?.blocks) }}</dd><template v-if="current?.fit"><dt>Latest fit</dt><dd>{{ readable(current.fit.status) }} · {{ readable(current.fit.reason) }}</dd><dt>Training / held-out windows</dt><dd>{{ amount(current.fit.trainWindows) }} / {{ amount(current.fit.heldoutWindows) }}</dd></template><template v-if="losses"><dt>Baseline / candidate loss</dt><dd>{{ losses.baseline.toPrecision(5) }} / {{ losses.learned.toPrecision(5) }}</dd></template><template v-for="[key, value] in details" :key="key"><dt>{{ readable(key) }}</dt><dd>{{ detailText(value) }}</dd></template></dl>
<details class="gx-card gx-tuner__section gx-tuner__disclosure"><summary>Diagnostics &amp; source details</summary><dl class="gx-tuner__diagnostics"><template v-if="tracking"><dt>Live learner state</dt><dd>{{ tracking.status }}</dd><dt>Live learner reason</dt><dd>{{ tracking.reason }}</dd></template><template v-if="trackingProgress"><dt>Paired inputs retained</dt><dd>{{ amount(trackingProgress.readyPairs) }}</dd></template><dt>Legacy collector state</dt><dd>{{ readable(current?.status) }}</dd><dt>Legacy collector reason</dt><dd>{{ readable(current?.reason) }}</dd><dt>Legacy retained blocks</dt><dd>{{ amount(current?.progress?.blocks) }}</dd><template v-if="current?.fit"><dt>Latest legacy fit</dt><dd>{{ readable(current.fit.status) }} · {{ readable(current.fit.reason) }}</dd><dt>Training / held-out windows</dt><dd>{{ amount(current.fit.trainWindows) }} / {{ amount(current.fit.heldoutWindows) }}</dd></template><template v-if="losses"><dt>Baseline / candidate loss</dt><dd>{{ losses.baseline.toPrecision(5) }} / {{ losses.learned.toPrecision(5) }}</dd></template><template v-for="[key, value] in details" :key="key"><dt>{{ detailLabel(key) }}</dt><dd>{{ detailText(value) }}</dd></template></dl>
<template v-if="trackingPlot"><h4>Delay-paired error</h4><PlotGraph :chart="pairedErrorChart" title="Delay-paired steering error" /><p class="gx-tuner__caption">Episode {{ trackingPlot.episodeId }} · {{ trackingPlot.sampled ? 'Selected' : 'All' }} original endpoints. No interpolation.</p><p class="gx-tuner__caption">Control / applied output / pose: {{ sourceSeconds(trackingPlot.points.at(-1).controlMonoTime) }} / {{ sourceSeconds(trackingPlot.points.at(-1).applyMonoTime) }} / {{ sourceSeconds(trackingPlot.points.at(-1).poseMonoTime) }} monotonic seconds.</p></template>
</details>
</section>
+1 -1
View File
@@ -1 +1 @@
{"protected_inventory_sha256": "bbc66f14ace8a765de855992d1eea2c26dc6aac72c1e07815711ccc03d1915e9", "schema_version": 1}
{"protected_inventory_sha256": "c0c26f431f8ae3af2e4a5995cd4c6078829bd58da381c698866f77ac7cafda4a", "schema_version": 1}