Skip to content

fix(dynamics): overhaul steer-balance algo to trust slip angle over yaw#60

Open
Snazzie wants to merge 6 commits intomainfrom
fix/steer-balance-algo
Open

fix(dynamics): overhaul steer-balance algo to trust slip angle over yaw#60
Snazzie wants to merge 6 commits intomainfrom
fix/steer-balance-algo

Conversation

@Snazzie
Copy link
Copy Markdown
Collaborator

@Snazzie Snazzie commented Apr 20, 2026

Summary

  • Slip angle gates removed: yaw signal is now gated on latG floor alone; slip angle (a direct lateral tire measurement) always contributes regardless of latG
  • Signal conflict resolution: when slip angle and yaw rate disagree on direction, slip angle wins — yaw is unreliable at high speed (path yaw rate Ay/V → 0) and under diff torque
  • Wheelspin guard: when |uSlip| < 0.15 (tires reporting balanced), yaw spikes from rear wheelspin/diff torque are ignored
  • Steering overlay fix: now uses the user-configured lock (consistent with data panel)
  • Throttle/brake colors: now trigger at any non-zero value (not 50%/10% thresholds)
  • Signal breakdown visualization: tooltip now shows slip Δ and yaw as separate signal bars, with reliability fade at high speed and CONFLICT/AGREE badge

Bugs fixed

Scenario Before After
0.23g lateral, front 8.5° vs rear 3.4° Neutral (gated) Understeer
263 km/h, yaw spike, front > rear slip Oversteer (yaw dominates) Understeer
Full throttle, RR wheelspin, balanced slip angles Oscillates Oversteer/Neutral Neutral
Trail braking, front slip > rear Neutral (latG gate) Understeer

Test plan

  • bun run test test/steer-balance.test.ts — 9 scenario snapshot tests all pass
  • Check balance label in analyse panel on a real lap across braking zones, fast corners, full-throttle corners

🤖 Generated with Claude Code

Snazzie and others added 6 commits April 18, 2026 22:19
Why: the analyst route previously bypassed Mastra (direct Gemini/OpenAI
fetch) so agent tools could never fire. F1 tuning suggestions had no
grounding in what fast drivers actually run.

Route migration:
- POST /api/laps/:id/analyse now calls lapAnalystAgent.generate via the
  dev/prod agent split in server/ai/agents.ts (tree-shakes DuckDB out of
  the prod binary, same pattern as the chat flow)
- Keystore → env bridge for Gemini/OpenAI/local providers
- generate(..., { maxSteps: 5 }) so tool rounds are allowed

F1 setup comparison tool:
- mastra/tools/f1-setup-compare.ts exposes compare-f1-setup-to-catalog:
  input { lapId }, output { currentSetup, references[5] with per-field
  delta } sourced from shared/tunes/f1-25/f1laps/
- server/ai/f1-setup-catalog.ts bundles all 24 track setup JSONs at
  import time so the Mastra dev bundler doesn't readFileSync-fail
- normalizePacketSetup remaps packet keys (onThrottle) onto the
  catalog shape (diffOnThrottle) so diffs line up
- Falls back to scanning lap.telemetry for f1.setup when the carSetup
  column is null on older laps
- getLapById now returns carSetup (was SELECTed but dropped from the
  return object); hasTune accounts for F1 carSetup presence
- Lap Analyst + Lap Chat agents register the tool; F1 adapter prompt
  instructs the model to call it before filling in tuning[]

Shared analyst output schema:
- server/ai/schemas.ts extracts AnalystOutputSchema (verdict, pace,
  handling, corners, technique, setup, tuning) as the single source
  of truth
- FM, F1, ACC, and AC Evo adapter prompts render their JSON shape
  via renderAnalystSchemaForPrompt() — ACC/AC-Evo previously emitted
  free-form prose

Deterministic evaluators:
- 6 scorers in mastra/evals/scorers/: output-shape, corner-coverage,
  numeric-grounding, unit-consistency, compare-directionality,
  chat-freeform-shape
- Eval-only Gemini 3 Flash agent factory (mastra/evals/eval-agents.ts)
  so eval runs are reproducible regardless of user settings
- test/ai-quality.test.ts gate + test/ai-fixtures scaffold
  (3 lap fixtures + 1 compare pair; packet zips provided by the
  contributor via laps:export)
- scripts/ai-baseline.ts for SHA-tagged score snapshots
- ai-quality CI job in build-test.yml runs bun run test:ai

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Continues the first mastra-backed analyst commit: tighter prompt
discipline, a unified setup schema across games, F1 tool-call wiring,
and a live status UI in the chat panels.

Prompt & schema:
- Unified AnalystOutputSchema (single setup[]) so TuneBars render
  across FM, F1, ACC, AC Evo
- DISCIPLINE block in lap-analyst: step caps, ranked reference
  citations, data-cited symptoms, corner whitelist / Tn fallback
- Session Type from F1 packets drives ERS rules (quali vs race)
- Per-adapter THERMAL REFERENCE blocks (tyre/brake bands, health)
- Setup coverage bumped (F1 8-14, others 6-12) with category coverage

Streaming chat protocol:
- server/ai/chat-stream.ts NDJSON emitter from agent.fullStream
- Bun idle timeout bumped 120 -> 255s for slow local models
- client/src/lib/chat-stream.ts line parser
- AiPanel + CompareAiPanel show thinking / tool-call / generating
  status chips and input/output token footer
- 15s heartbeat ping keeps sockets alive pre-first-token

F1 setup tool:
- carSetup fallback scan of lap.telemetry + key normalisation
- delta coerced to Record<string,number> for zod outputSchema

UI:
- SetupSection modal (body-only scroll) shared by AnalysisDisplay
  and AiPanel
- TuneBar fix for negative-value fields (camber/toe)
- AnalyseLapHeader hides Forza tune picker for F1 25
- CompareAiPanel input-comparison segments clickable
- SessionsPage 'Compare 2 laps' button next to Delete

Defaults:
- Gemini + gemini-flash-latest across settings and fallbacks

TS:
- zod v4 SafeParseReturnType -> ReturnType<safeParse>
- AI SDK v4 usage via defensive accessor
- CompareAiPanel InputsModal receives trackSegments/onJumpToFrac
  (earlier edit was in the wrong scope)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Route local (LM Studio) through @ai-sdk/openai with createOpenAI + explicit
  .chat(id) so requests hit /v1/chat/completions, not /v1/responses.
- Disable reasoningEffort ("none") for local to stop Gemma-style reasoning
  models from burning the 8k output budget on hidden thinking.
- Switch response_format to strict json_schema (OpenAI Structured Outputs)
  so the decoder is grammar-constrained — no mid-string truncation / bad
  chars. Same schema also passed as google.responseSchema for Gemini.
- Don't cache invalid/truncated model output; drop stale empty caches on
  read so retries aren't poisoned by pre-validation runs.
- Client: safeParseAnalysis logs a window around the failing byte on JSON
  parse errors; fix ChatStreamEvent narrowing via as-unknown casts.
- Add guides/local-ai.md covering the LM Studio setup flow.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Analyse: replace plain JSON with heartbeat NDJSON (ping every 200s,
  single result at end) so slow local models don't hit Bun's 255s
  idleTimeout. Client handles ping / result / error only — no
  intermediate status UI.
- F1 25 setup ranges: correct the prompt and client TuneBar bounds to
  match the game's actual slider limits (Diff 10–100, ARB 1–41, Ride
  Height 20–50, rear tyre pressure 20.0–26.5 psi, toe 0–0.10/0.40).
- Drop all DRS mentions from the F1 analyst: prompt category guidelines,
  rules block, and the DrsActive context builder. Zone data isn't
  reliable enough to reason from, and raw activation counts aren't
  actionable.
- Metric labels: humanise snake_case/camelCase at render time and tell
  the model to emit Title Case with spaces.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Gate yaw signal on latG floor only; slip angle (lateral-only) now always contributes
- When signals conflict, use slip angle alone (yaw unreliable at high speed, Ay/V → 0)
- When slip angles are balanced (|uSlip| < 0.15), ignore yaw spikes from diff torque / wheelspin
- Fixes: neutral wrongly shown at 0.23g, oversteer wrongly shown at high speed, oscillation under full-throttle throttle
- Expose uSlip, uYaw, signalsAgree in SteerBalance for tooltip signal breakdown viz
- Steering overlay now uses configured lock (consistent with data panel)
- Throttle/brake colors now trigger at any non-zero value
- Add 9-scenario snapshot test suite (test/steer-balance.test.ts)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Slip angle is now always authoritative. Yaw can push balance further from
zero when it strongly agrees, but is discarded when the blend would move
the result closer to zero than slip alone. Validated against F1 lap 391
(28975 pkts) and ACC lap 461 (5538 pkts) — fixes ACC pkt-360-style cases
where weak yaw was washing out clear understeer below classify threshold.

Adds 4 new snapshot tests covering amplification, dilution suppression,
amplified oversteer, and conflict resolution.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant