mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-11-06 04:23:42 +08:00
Report live steering trials without legacy counter errors
This commit is contained in:
@@ -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.
|
||||
|
||||
|
||||
@@ -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) }
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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 & 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 & 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
@@ -1 +1 @@
|
||||
{"protected_inventory_sha256": "bbc66f14ace8a765de855992d1eea2c26dc6aac72c1e07815711ccc03d1915e9", "schema_version": 1}
|
||||
{"protected_inventory_sha256": "c0c26f431f8ae3af2e4a5995cd4c6078829bd58da381c698866f77ac7cafda4a", "schema_version": 1}
|
||||
|
||||
Reference in New Issue
Block a user