Compare commits

...

177 Commits

Author SHA1 Message Date
Isaac Barham 7ca2df4819 Ford: experiment with earlier C1 release from model path shape
Bound the model heading request toward zero using terminal spatial
curvature at the existing preview point. Preserve C0, command limits,
slew, 20 Hz cadence, and the existing Sunnylink selection.

This changes requests before some turn peaks as well as during unwind.
Document that authority tradeoff and distinguish command-level evidence
from unverified physical PSCM release.

Validation: Ford controller/sender/safety tests, Sunnylink schema tests,
independent geometry and replay tests, 28,138 stress CAN round trips,
and 35,775 recorded cycles with 71,550 baseline/candidate CAN checks.

Assisted-by: OpenAI Codex
2026-09-08 16:37:58 -04:00
Isaac Barham 4bd841eccd Ford: verify unwind timing and reject incompatible offline replays
Compare v7 and recorded v6 unwind instructions on a5 using both common request levels and own-peak thresholds. Preserve the later C0 release and earlier C1 release in the report without claiming a physical improvement.

Exercise saturated release through the actual sender for both signs and every send phase. Require original model clocks for current replay extracts and reject current candidates in the historical unchanged-C1 damping replay. Record the complete 585-test suite and refreshed stress evidence.

Assisted-by: OpenAI Codex
2026-09-08 13:53:15 -04:00
Isaac Barham 6f83b17457 Ford: sample position and heading from one shared model point
Use the model-predicted one-second station with the existing seven-metre minimum and endpoint hold for both C0 and C1. Remove the extra yaw forecast and scalar-curvature heading reconstruction; retain C2/C3 zero, independent limits and slew, the existing toggle, and 20Hz sends. Reject scalar-only maneuver references explicitly.

Validate model clocks, geometry, CAN delivery, Panda TX acceptance, and randomized boundaries offline. Recorded-input replay does not establish improved physical tracking.

Assisted-by: OpenAI Codex
2026-09-08 13:35:07 -04:00
Isaac Barham e1cd61166c Ford: gate 20Hz selected-action cadence with shared startup selection
Snapshot the existing Sunnylink toggle into CarParamsSP so controlsd and the CAN sender agree. Preserve the v6 C0/C1 calculation at 100Hz and Panda safety unchanged. Add actual sender timing, packing, selection, and safety checks; record the a5 frozen-input replay. Physical tracking improvement remains unverified.

Assisted-by: OpenAI Codex
2026-09-08 12:56:33 -04:00
Isaac Barham c70a9ee84b Ford: validate pose state freshness and finish v6 deployment metadata
Check both motion publication and embedded filter-state ages before using
calibrated yaw. Rebuild the pose when calibration changes between motion
samples. Four regression cases demonstrate stale, future, missing-state
fallback and calibration-only pose refresh.

Keep the Sunnylink YAML source synchronized with its generated schema and
update the drive guide and source-bound validation record for v6.

Validation: 427 tests and 26 subtests pass; type, lint and settings compiler
checks pass. All 340,757 recorded cycles still match the reviewed candidate,
with 681,514 CAN round trips. Physical steering improvement remains unverified.

Assisted-by: OpenAI Codex
2026-09-08 11:45:34 -04:00
Isaac Barham d4f6403746 Ford: predict path offset from calibrated vehicle motion
Use fresh calibrated turn rate for the existing 150 ms C0 pose forecast.
Retain selected-curvature prediction when motion or calibration is unavailable,
and report the active pose source in controller diagnostics. C1, coefficient
limits, slew rates, C2/C3 zero, and the existing Sunnylink toggle are unchanged.

Validated 340,757 recorded cycles against the reviewed offline candidate,
681,514 CAN round trips, and the Ford/controller/Sunnylink test suite.
This is an experimental drive candidate, not a proven road-tracking fix.

Assisted-by: OpenAI Codex
2026-09-08 11:32:55 -04:00
Isaac Barham 72e9d94f62 Ford: remove yaw damping from selected-action controller
Remove excess-yaw C0 attenuation while retaining full path prediction,
command limits, slew and input-health gates. Valid measured yaw no longer
changes path demand. Update diagnostics and Sunnylink help for v5.

Validate with 356 tests and 26 subtests, 100% controller coverage,
280,636 recorded route cycles and 779,410 Float32/CAN round trips.
These are command checks; physical tracking improvement is not established.

Assisted-by: OpenAI Codex
2026-09-08 04:33:06 -04:00
Isaac Barham 7e63449749 Ford: use full geometric prediction within existing command limits
Remove the hand-chosen 0.15 m / 25% cap on the prediction adjustment.
Retain the available-horizon bound, nonfinite fallback, total field limits,
yaw damping, slew, input gates, two states and zero C2/C3.

Cover full predictions and geometric countersteering in regression tests.
Validate 374 tests plus 26 subtests, four-route replay and 817,346
Float32/CAN round trips. Document command changes without inferring
physical tracking performance from PSCM limits or fixed-input replay.

Assisted-by: OpenAI Codex
2026-09-07 19:19:03 -04:00
Isaac Barham 01f8d51c82 Ford: add bounded model-path prediction to selected-action controller
Predict nearby C0 from the current model path and selected curvature over
150 ms, bounded to 0.15 m and 25% of the original offset. Retain excess-yaw
damping, two slew states, C1 behavior, input gates, and zero C2/C3 under the
existing default-off Sunnylink toggle.

Validate 372 tests plus 26 subtests, four-route replay, and 817,346
Float32/CAN round trips. Record earlier command-level crossings and the
right-exit damping tradeoff without claiming improved vehicle tracking.

Assisted-by: OpenAI Codex
2026-09-07 18:21:03 -04:00
Isaac Barham 744a97d9bc Ford: damp excess-yaw offset demand in selected-action controller
Attenuate same-direction C0 when measured yaw exceeds the nonnegative
requested turn plus a deadband. Keep C1, two slew states, input gates and
zero C2/C3. Opposed planned curvature cannot amplify small yaw bias.

Segment 10 replay reduces residual exit demand while preserving peak
entry C0. This remains an experimental, physically unvalidated candidate
under the existing default-off Sunnylink toggle.

Validation: 325 tests and 26 subtests; 100% controller statement/branch
coverage; 204,946 route cycles; 628,030 Float32/CAN round trips; independent
standards/spec reviews.

Assisted-by: OpenAI Codex
2026-09-07 17:00:05 -04:00
Isaac Barham 5fc16abc76 Ford: document hiimisaac-dev installation target
Record the requested sunnypilot/sunnypilot deployment branch. The validated controller code is unchanged.

Assisted-by: OpenAI Codex
2026-09-07 12:20:35 -04:00
Isaac Barham ea1ed70c71 Ford: gate selected-action controller in Sunnylink and retire v8
Select the new controller only on the CAN FD Lightning through a default-off startup toggle. Retire v8 and its setting; disabling restores the original controller or selected observer. Preserve packing, input gates and zero C2/C3.

Fix the existing Params filtered-key buffer lifetime exposed by Sunnylink backup tests. Record 284 tests, 26 subtests and 485238 offline packing round trips; physical calibration remains unapproved.

Assisted-by: OpenAI Codex
2026-09-07 11:43:56 -04:00
Isaac Barham 7ca3c6e3b3 Ford: add offline selected-action controller and validation
Add a two-state C0/C1 core and a separate freshness/timing adapter compatible
with the existing controlsd call. Preserve reviewed endpoint holding, reject
malformed inputs, and keep production selection and safety unchanged.

Validate actual selection/limiting/publication/CAN integration, exact core
replay across 133,550 route cycles, 200,000 randomized and mirrored cycles,
field boundaries, resets and release uncertainty. Record the completed
264-test Ford suite, mutation probes and dependency/source provenance.
Physical tracking remains unvalidated; calibration_approved=false.

Assisted-by: OpenAI Codex
2026-09-07 10:40:06 -04:00
Isaac Barham c4b3c55c82 Merge sunnypilot master into hiimisaac-dev
Sync upstream 6135084c9 while preserving the Ford v8 controller and custom path transport. Merge OpenDBC upstream into the Ford branch. Retain the drive summary with upstream USB/loading icons and text alignment APIs.

Validation: 191 main-repository tests and 150 subtests; 203 Ford OpenDBC tests and 9143 subtests (178 skips); focused UI logic smoke; Ruff, generated Sunnylink settings, and diff checks.
2026-09-06 14:03:10 -04:00
Isaac Barham b3bc05acd4 Ford: guard turn release and recover remaining tracking deficit
Prevent same-direction C0/C1 growth when measured turning exceeds current and delayed requests during release, retaining request history across driver feedback resets. Permit bounded C1 correction after opposing bias reaches zero when both requests remain undertracked and measured curvature is no longer catching up.

Keep model allocation, gain, field and slew limits, platform selection, and zero C2/C3 unchanged. Add anonymous recorded-input regressions and diagnostics. Validation: 139 Ford tests plus 150 subtests, 46 Sunnylink tests, Ruff, generated settings check, and recorded-command replays. Physical response and stability remain unverified.
2026-09-06 08:34:10 -04:00
James Vecellio-Grant 6135084c94 modeld_v2: realize frames on npy -> amd (#1993) 2026-09-05 20:17:14 -07:00
Isaac Barham dfcfddb91c Ford: recover opposing heading bias during turn release
Allow release recovery only when fresh measured yaw undertracks both aligned current and delayed requests and PSCM limit is below 2. Unwind the opposing bias toward zero using current yaw error and existing antiwindup; preserve C0, base geometry, gains, rates, and safety guards.

Add mirrored unit checks and a sanitized recorded turn-exit regression. Validate with 142 tests and 97 subtests, full-route frozen-input replay, large-turn retention, CAN packing, diagnostics, and generated Sunnylink schema checks. Physical improvement remains unvalidated.
2026-09-05 18:45:08 -04:00
Isaac Barham 61dac4977b Ford: restore large-turn path demand with bounded heading backoff
Reuse the existing model-pose allocator for aligned large maneuvers while encoding remaining selected curvature as C0/C1 and keeping C2/C3 zero. Permit measured heading backoff during release or PSCM limits without turning model-base changes into stored bias.

Validate with 127 tests and 67 subtests, including recorded large-turn retention, release and reversal, repeated-measurement backoff, CAN packing, logging, and Sunnylink schema checks. Replay checks command behavior; enabled vehicle tracking remains unvalidated.
2026-09-05 13:28:56 -04:00
Isaac Barham 09acf8ec2f Ford: honor C2-free toggle without EPS firmware gate 2026-09-05 12:08:21 -04:00
Isaac Barham 79a4caa1f6 Ford: add bounded yaw feedback to C2-free heading requests
Retain the absolute desired-curvature base and unchanged C0, while adding
measured yaw-error correction to C1 under fresh PSCM status. Preserve command
limits, reset on override or unusable status, and release stored correction
with the base request. Admit reachable partial increments at host slew limits.

Publish PSCM enums with original CAN receipt timestamps through carStateSP.
Add telemetry, status/driver guards, CAN roundtrip tests and recorded fixtures.

Validation: focused suite 116 tests and 38 subtests; Ruff, settings compilation
and diff checks pass. Production replay covers 52,273 route80 cycles; no-status
fallback preserves v4 over 246,961 cycles / 43 segments. Physical stability and
the reported 85-degree plateau remain unvalidated; EPS limits can inhibit the
new correction.
2026-09-05 09:46:32 -04:00
Isaac Barham 0ace0b0510 Ford: align C2-free heading with desired curvature
Derive full absolute C1 heading from the same selected curvature as C0, retaining existing bounds and independent slew. Keep the former filtered model heading as a diagnostic comparison and preserve input validity gates.

Add real route80 command regressions and release/reversal checks. All 97 focused tests pass; 43-segment replay preserves C0 and gates exactly and matches the independent C1 candidate. Physical tracking and stability remain unvalidated for this revision.
2026-09-05 07:56:40 -04:00
James Vecellio-Grant 047ae41c0d modeld_v2: one dev warp and enqueue (#1990) 2026-09-04 21:15:00 -07:00
Nayan 7eb457f6c4 chaos (#1991)
burn it all
2026-09-05 10:50:12 +08:00
Isaac Barham 98662df401 Ford: drive C2-free C0 from planned curvature
Encode the selected bounded curvature as C0 with an 8 m minimum preview while retaining full model-heading C1. Limit each channel independently so C1 transitions cannot delay C0 release, and validate the selected action source timestamp.

Add action, release, source-freshness, CAN and route regressions. Document the slow-turn reference disagreement and the limits of frozen-motion replay; physical centering remains unvalidated.
2026-09-04 21:15:17 -04:00
James Vecellio-Grant 302f3ad892 ci: compile dm warp (#1989) 2026-09-04 16:52:09 -07:00
Isaac Barham 10e354d668 Ford: restore C0 centering and full C1 path demand
Replace the weak nominal acceleration conversion with C2-free spatial path requests. Align retained model geometry using measured CAN yaw before filtering model innovations, and preserve large-turn demand and straight-path centering.

Validate with seven recorded maneuver episodes, full-route command replay, real CAN packing and focused controller/settings tests. Physical closed-loop behavior remains unvalidated.
2026-09-04 16:59:24 -04:00
Isaac Barham 7d558c0650 Ford: replace shared path experiment with bounded virtual angle control
Track the bounded planner reference through C0 and delay-aware PI/rate feedback through C1. Remove the failed Shared Path toggle and add a default-off, Lightning RL38-specific Virtual Angle setting. Reject stale inputs and disable outgoing lateral requests when the path is invalid.

Validation: 85 focused tests plus 22 subtests, native Params, real CAN packing, settings generation and Ruff passed. Frozen route78 replay attenuates the observed command forcing; physical stability and turn authority remain unvalidated.
2026-09-04 16:11:33 -04:00
Isaac Barham daeb966d05 Ford: add C2-free shared path experiment 2026-09-04 15:22:32 -04:00
Isaac Barham 727c26ce8c Ford: allow earlier joint fast-path buildup experimentally
Preserve geometric C0/C1 before nominal plateaus during same-direction buildup. Keep reversal guards, command limits, C2 policy, and the existing default-off Shared Path Controller selection. Require nonzero demand for joint buildup.

Known limitation: nominal short-turn cancellation settles later with queued commands. Retain that regression as an explicit expected failure; this experiment does not establish physical response or resolve unwind. Add entry and zero-demand coverage.

Assisted-by: OpenAI Codex
2026-09-04 11:54:41 -04:00
Isaac Barham 614022defb test(ford): retain queued commands in turn-release regression
Continue the same allocator through a short turn and cancellation so release checks retain command lead as well as nominal coefficient state. Reject earlier geometry buildup that keeps charging after cancellation. No production controller changes.

Assisted-by: OpenAI Codex
2026-09-04 11:51:24 -04:00
Isaac Barham 5e67122e64 Ford: use opendbc LMC2 packing guard
Update opendbc to 72a775d3 for C0/C1/C3 wire-range saturation and nonfinite input rejection. No fallback controller or tuning changes are included.

Assisted-by: OpenAI Codex
2026-09-04 11:18:11 -04:00
Isaac Barham 25d095177e Ford: preserve large shared-path geometry past nominal plateaus
Retain larger model-derived fast fields when nominal allocation is equivalent, with per-field plateau qualification and inward-demand release priority. Keep corrected and geometric fast fields independently selectable without changing the existing contribution map, C2 policy, cadence, or command limits.

Validated with 68 controller/fallback/logging tests, 5 adversarial release tests, and 38300 fixed-input replay updates. Physical turn authority and release remain unverified; the existing experiment stays default off.

Assisted-by: OpenAI Codex
2026-09-04 11:09:13 -04:00
Isaac Barham 3eb7938aad Ford: retain geometric demand in shared path diagnostics 2026-09-04 10:36:17 -04:00
Isaac Barham a525905708 Ford: reuse coefficient calculations in shared allocator
Cache per-field packet conversion and state projections within each allocation instead of recomputing them for every candidate combination. Preserve candidate ordering, scores, limits, and selected commands. Add a deterministic limiter-work regression budget.

Local recorded-input mean controller CPU time falls 56%; 1292 recorded updates and 4000 randomized allocations match the previous outputs exactly. Device timing remains unverified.

Assisted-by: Codex
2026-09-04 09:44:04 -04:00
Isaac Barham e298864a50 Ford: fix controlsd structured logging crashes
Use SwagLogger.event for controller selection and periodic diagnostics. Logger.info forwards arbitrary keywords to Logger._log and crashed all Ford startups, regardless of the experiment toggle. Exercise both actual call sites with INFO enabled and the real logger/formatter.

Assisted-by: Codex
2026-09-04 09:33:28 -04:00
Isaac Barham 8639bdcca4 Ford: add opt-in shared path control experiment
Separate holding demand, bounded pose feedback, and nominal coefficient allocation. Add a default-off Sunnylink selector with startup diagnostics and preserve the existing controller when disabled.

Assisted-by: Codex
2026-09-04 09:19:22 -04:00
Jason Wen 132b31f4cf ci: poll GH API in prepare model jobs (#1987) 2026-09-03 10:10:21 -04:00
James Vecellio-Grant 752c07f9e4 ci: Replace hf oath with token (#1986)
replace oauth with token
2026-09-03 08:22:43 -04:00
Isaac Barham 458a3015cd Ford: add optional PSCM coefficient observer
Assisted-by: Codex
2026-09-02 16:49:22 -04:00
Jason Wen e87dbbaba7 models: sanitize default model name for HF (#1984) 2026-09-02 14:53:31 -04:00
Jason Wen 15efdb392f Sync: commaai/openpilot:mastersunnypilot/sunnypilot:master (#1983)
* ui: remove raygui usage (#38708)

* ui: remove raygui usage

* match previous gui_text_box line spacing

* Revert "match previous gui_text_box line spacing"

This reverts commit ffd2fe31725c6d50bffaebc621c1e170d0926c66.

* Reapply "match previous gui_text_box line spacing"

This reverts commit d41404f09607e225f43868f7747f22dc0bb2cf16.

* log chestnut supply fault (#38711)

* log chestnut INA supply fault

* ci

* bump raylib (#38712)

* cabana: replace custom non-view Qt signals w/ plain observer (#38713)

* cabana: move RoutesDialog out of streams/ (#38716)

* cabana: string helpers in utils return std::string (#38720)

* cabana: use std::string in RoutesDialog API results (#38717)

* cabana: move stream open widgets into streamselector (#38715)

* cabana: remove Qt from livestream (#38722)

* cabana: split SettingsDialog out of settings (#38719)

cabana: split SettingsDialog out of settings.{h,cc}

* cabana: split comma API route fetching out of RoutesDialog (#38721)

* cabana: de-QT streams (#38718)

* ui: fix install update button overflow (#38696)

* cabana: split utils/util into Qt-free util and qtutil (#38723)

* ui: guard branch switcher before internet connected (#38692)

* ui: check for update on target branch switch (#38693)

* ui: sync gpu loading to offroad (#38727)

ui: sync gpu loading state

* add chestnut offroad alerts (#38706)

* system: add chestnut offroad alerts

* system: refine chestnut offroad alerts

* system: refine chestnut power alerts

* system: confirm chestnut power recovery from PCIe

* system: detect missing chestnut power from INA voltage

* common: fix OpenpilotPrefix cleanup on macOS (#38728)

The destructor built its cleanup commands as "rm %s -rf", with the flags
after the operand. GNU rm permutes arguments so this works on device and
in CI, but BSD rm on macOS stops option parsing at the first operand and
treats "-rf" as a second filename:

  $ mkdir -p /tmp/rmtest/sub && rm /tmp/rmtest -rf
  rm: /tmp/rmtest: is a directory
  rm: -rf: No such file or directory
  exit=1

So nothing is removed, and each of the four calls prints two errors plus
"system command failed (256)" from check_system. Every run of a tool that
owns an OpenpilotPrefix (replay, cabana) leaks its params dir, its
comma_home and its /tmp/msgq_ dir; 33 of each had accumulated on my
machine.

Pass the flags first.

* replay: capture downloader's stderr so download progress is reported again (#38734)

* bump panda (new health packet) (#38736)

pandad: support compact health packet

* BMRLNAP (#38681)

* ui: clarify branch switcher error message (#38732)

* ui(mici): name updater signal constants (#38731)

* mici: name updater signal constants

* drop SIGNAL_ prefix

* self contained

---------

Co-authored-by: Shane Smiskol <shane@smiskol.com>

* modem.py: accept hex chars in ICCID (#38735)

E.118 specifies decimal digits, but many real SIMs carry hex characters
in EF_ICCID (e.g. China Mobile's 898600B5... range, some MVNO/IoT SIMs).
AT+QCCID returns them verbatim, and the strict isdigit() check blanked
the ICCID, leaving the modem daemon stuck in INITIALIZING forever and
cellular dead. ModemManager parses ICCID as hex for the same reason.

Verified on a comma four with a China Mobile SIM (EG916Q-GL): previously
stuck retrying 'identity read incomplete', now dials and passes traffic.

* TGC (#38739)

* 23e6a04e-e6e5-462b-a0bb-e4088275ee43/12864 tgc

* here

* monitor chestnut USB in hardwared (#38741)

hardwared: monitor chestnut USB independently

* modeld: wait for stable chestnut (#38742)

modeld: wait for stable chestnut

* Revert "monitor chestnut USB in hardwared (#38741)" (#38744)

This reverts commit 7d5596d5c3.

* amd warp (#38684)

* modeld: fuse warp and policy TinyJit

* bump tg

* fix?

* this simple trick...

* debug 1

* bump tg

* pack all

* wips

* fix

* BIG_INTO_SMALL remove

* slower

* ui: show usb connection (#38745)

* ui: show USB status

* ui: resize USB icon

* ui: classify USB device once

* ui: debounce USB disconnect

* cereal: log big model in drivingModelData (#38747)

* ui: show one GPU status (#38748)

ui: show one GPU status icon

* AGNOS 19.7 (#38750)

---------

Co-authored-by: Trey Moen <50057480+greatgitsby@users.noreply.github.com>
Co-authored-by: Daniel Koepping <elkoled@gmail.com>
Co-authored-by: Robbe Derks <robbe.derks@gmail.com>
Co-authored-by: Harald Schäfer <harald.the.engineer@gmail.com>
Co-authored-by: Shane Smiskol <shane@smiskol.com>
Co-authored-by: XiaoXX <xiaoxx97@outlook.com>
Co-authored-by: YassineYousfi <yyousfi1@binghamton.edu>
2026-09-02 13:57:07 -04:00
Jason Wen f5bb855477 Merge commit '6249f4d5b0e63c05f08bce12ca3afebda9f764a3' into sync-20260902
# Conflicts:
#	openpilot/selfdrive/modeld/SConscript
#	openpilot/selfdrive/modeld/modeld.py
#	openpilot/selfdrive/pandad/pandad.cc
#	openpilot/selfdrive/selfdrived/alerts_offroad.json
#	openpilot/selfdrive/ui/layouts/onboarding.py
#	openpilot/selfdrive/ui/mici/layouts/home.py
#	openpilot/system/hardware/hardwared.py
#	panda
#	tinygrad_repo
2026-09-02 13:47:27 -04:00
Isaac Barham 336ce75f3d Ford: keep gentle driving on C2 only
Remove model-pose residuals and tracking trim from the gentle regime. Blend the model pose into C0/C1 only as maneuver demand rises, while retaining opposing-path C2 unload and the coordinated 100 Hz handoff.

Assisted-by: Codex
2026-09-02 09:53:56 -04:00
Isaac Barham 517c15f9c2 Ford: restore upstream-strength normal C2
Use constrained desired curvature for ordinary C2 while keeping model geometry authoritative in the coordinated C0/C1 residual. This restores normal centering strength without changing large-maneuver or bounded-feedback behavior.

Assisted-by: Codex
2026-09-02 08:32:03 -04:00
Jason Wen 47db84ebfb models: add big model ONNX hash tracking (#1982) 2026-09-02 01:28:43 -04:00
Isaac Barham aa73207ab8 Ford: separate path feedforward from pose feedback
Keep the model's remaining path as feedforward while using the delay-aligned measured pose only as a bounded trim. Allocate common gentle model curvature to C2 and carry changing geometry in C0/C1 without allowing the action head to invent a path.

Assisted-by: Codex
2026-09-01 22:28:35 -04:00
Jason Wen 68be777395 bump tg 2026-09-01 22:14:43 -04:00
github-actions[bot] ab389498a8 [bot] Update Python packages (#1950)
* Update Python packages

* bump tg

* bump

* ci: route build_model runner by target_hardware instead of hardcoding chestnut

* hack, remove before merge

* Revert build-model runner hack and uv.lock update

* why were they hard coded

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Jason Wen <haibin.wen3@gmail.com>
2026-09-01 22:12:54 -04:00
Isaac Barham 184b73d8de Revert "Ford: add optional native path polynomial"
This reverts commit 6a1b697ed3.
2026-09-01 21:33:58 -04:00
Daniel Koepping 6249f4d5b0 AGNOS 19.7 (#38750) 2026-09-01 18:32:59 -07:00
Daniel Koepping 8b88f7dd6e ui: show one GPU status (#38748)
ui: show one GPU status icon
2026-09-01 18:32:39 -07:00
Isaac Barham 6a1b697ed3 Ford: add optional native path polynomial
Assisted-by: Codex
2026-09-01 21:17:52 -04:00
Isaac Barham 7eb7e93deb tools: evaluate native Ford path polynomial
Assisted-by: Codex
2026-09-01 20:59:11 -04:00
Harald Schäfer 79658800ce cereal: log big model in drivingModelData (#38747) 2026-09-01 17:15:13 -07:00
Daniel Koepping 36561258fa ui: show usb connection (#38745)
* ui: show USB status

* ui: resize USB icon

* ui: classify USB device once

* ui: debounce USB disconnect
2026-09-01 15:45:57 -07:00
Isaac Barham 693daf9866 Ford: align path to predicted vehicle pose
Rebase model preview against a gainless 100 ms curvature-trend prediction. Remove direct local-curvature feedback while preserving coordinated C0/C1/C2 authority and geometric C2 unloads.

Assisted-by: Codex
2026-09-01 17:02:28 -04:00
YassineYousfi cb85ac1f0e amd warp (#38684)
* modeld: fuse warp and policy TinyJit

* bump tg

* fix?

* this simple trick...

* debug 1

* bump tg

* pack all

* wips

* fix

* BIG_INTO_SMALL remove

* slower
2026-09-01 13:59:55 -07:00
Isaac Barham afcc2b9455 Ford: track local model curvature
Use the first two meters of model heading for measured-curvature feedback while preserving the existing longer model-pose feedforward. This prevents future geometry from initiating premature correction without weakening turn anticipation.

Assisted-by: Codex
2026-09-01 15:39:14 -04:00
Isaac Barham 3fdab7e8f0 Ford: close path loop on model curvature
Use measured curvature error against the forward model path to add bounded bidirectional C0/C1 correction. Unload stale C2 when it would oppose an unwind or reversal.

Assisted-by: Codex
2026-09-01 15:22:13 -04:00
Daniel Koepping c9f1602040 Revert "monitor chestnut USB in hardwared (#38741)" (#38744)
This reverts commit 7d5596d5c3.
2026-09-01 11:13:30 -07:00
Isaac Barham f488bfc806 Ford: restore responsive path controller
Return to the pre-predicted-pose C2-first controller from a1dcec490 after road testing found both later variants weaker or unstable. Preserve the current sunnypilot master merge and 100 Hz LMC2 transport.

Assisted-by: Codex
2026-09-01 13:26:57 -04:00
Isaac Barham fd62fed669 Merge sunnypilot master into hiimisaac-dev
Preserve the assisted-driving summary while adopting the current Chestnut status UI.

Assisted-by: Codex
2026-09-01 12:55:47 -04:00
Isaac Barham 3a665737c2 Ford: encode path in current vehicle frame
Remove delay-projected measured-curvature feedback that amplified curve hunting. Keep the model polynomial in the current vehicle frame while preserving the coordinated 100 Hz C0/C1/C2 handoff.

Assisted-by: Codex
2026-09-01 12:54:14 -04:00
Daniel Koepping 06af2abe67 modeld: wait for stable chestnut (#38742)
modeld: wait for stable chestnut
2026-09-01 07:20:56 -07:00
Daniel Koepping 7d5596d5c3 monitor chestnut USB in hardwared (#38741)
hardwared: monitor chestnut USB independently
2026-09-01 05:59:02 -07:00
Isaac Barham b6a87b8958 Ford: align path control to predicted pose
Advance the rolling model path by the generic lateral delay, express its remaining seven-meter pose in the predicted vehicle frame, and derive C2 from the same steady geometry. Remove desiredCurvature as a competing Ford path target.

Assisted-by: Codex
2026-09-01 07:50:03 -04:00
YassineYousfi a2e422eee0 TGC (#38739)
* 23e6a04e-e6e5-462b-a0bb-e4088275ee43/12864 tgc

* here
2026-08-31 22:30:28 -07:00
Jason Wen 51987a62d0 ci: route build_model runner by hardware type 2026-09-01 01:16:04 -04:00
Isaac Barham a1dcec490f Ford: preserve pose authority when C1 clips
Move heading authority lost at the DBC angle limit into available C0 endpoint authority while retaining the coordinated output limiter.

Assisted-by: Codex
2026-09-01 01:09:47 -04:00
XiaoXX e10c0fd960 modem.py: accept hex chars in ICCID (#38735)
E.118 specifies decimal digits, but many real SIMs carry hex characters
in EF_ICCID (e.g. China Mobile's 898600B5... range, some MVNO/IoT SIMs).
AT+QCCID returns them verbatim, and the strict isdigit() check blanked
the ICCID, leaving the modem daemon stuck in INITIALIZING forever and
cellular dead. ModemManager parses ICCID as hex for the same reason.

Verified on a comma four with a China Mobile SIM (EG916Q-GL): previously
stuck retrying 'identity read incomplete', now dials and passes traffic.
2026-08-31 21:35:56 -07:00
James Vecellio-Grant 98ed8111f6 modeld_v2: big to small model fallback (#1974) 2026-09-01 00:15:23 -04:00
Isaac Barham 0729ce7c08 Ford: continuously blend model pose with C2
Use the model's forward offset and heading for fast path authority while C2 retains ordinary path following. Coordinate all transmitted coefficients through one bounded handoff and add measured-curvature catch-up without overshoot countersteer.\n\nAssisted-by: Codex
2026-09-01 00:15:07 -04:00
Isaac Barham 916fb1d522 Ford: separate centering and maneuver paths
Use heading/action hysteresis to keep normal driving entirely on C2 and large maneuvers entirely on model C0/C1. Restore a one-second pose horizon with a 7 m floor.

Assisted-by: Codex
2026-08-31 21:58:11 -04:00
Isaac Barham af5e7f5327 Ford: use model pose for large maneuvers
Keep desired curvature in C2 for ordinary driving, then continuously hand off to model offset and heading for large maneuvers. Use one fitted 0.5 second lookahead with a 7 meter floor for both pose fields and keep C3 zero.

Assisted-by: Codex
2026-08-31 20:42:03 -04:00
Isaac Barham bf2e9ca318 Ford: keep slow curvature out of turns
Remove the one-frame C2 persistence and restore the continuous gentle-centering allocation. Real turn demand now clears C2 immediately and remains in the bounded fast path fields.

Assisted-by: Codex
2026-08-31 20:34:42 -04:00
Isaac Barham 41b433c619 Ford: split one model frame into fast path fields
Delay C2 by one 50 ms model frame and place the new-request difference in C0/C1 alongside measured tracking error. Clear the delay on inactive or invalid control.

Assisted-by: Codex
2026-08-31 20:26:46 -04:00
Trey Moen da8ce858ec ui(mici): name updater signal constants (#38731)
* mici: name updater signal constants

* drop SIGNAL_ prefix

* self contained

---------

Co-authored-by: Shane Smiskol <shane@smiskol.com>
2026-08-31 15:55:16 -07:00
Trey Moen 9fa7ef3d17 ui: clarify branch switcher error message (#38732) 2026-08-31 15:46:52 -07:00
Isaac Barham ed56f3ff7c Ford: restore curvature as primary path control
Keep upstream-style desired curvature active in C2 for steady path following. Use C0/C1 only for demand beyond C2 and measured tracking error, preserving fast turn and unwind authority without replacing C2.

Assisted-by: Codex
2026-08-31 17:31:40 -04:00
Isaac Barham 5865ad108c Ford: align Panda safety with 100Hz path control
Update the opendbc pointer for the tested CAN-FD Mode 2 safety cadence fix.

Assisted-by: Codex
2026-08-31 14:50:32 -04:00
Isaac Barham 8ed82eae6f Ford: use direct LMC2 mode transitions
Remove the custom SafeRampOut sequence and follow the proven Mode 2 to Mode 0 behavior.

Assisted-by: Codex
2026-08-31 14:11:02 -04:00
Isaac Barham 88f6f66032 Ford: restore proven LMC2 ramp sequence
Keep active CAN-FD path control at 100 Hz while limiting SafeRampOut to the historically working 20-message sequence.

Assisted-by: Codex
2026-08-31 13:22:02 -04:00
Isaac Barham 33e70080ad Ford: update CAN-FD path control rate
Assisted-by: Codex
2026-08-31 12:57:00 -04:00
Harald Schäfer 4adbb85742 BMRLNAP (#38681) 2026-08-31 09:25:32 -07:00
Isaac Barham b7f0e3fbdc Ford: restore full C2 gentle path following
Assisted-by: Codex
2026-08-31 11:41:29 -04:00
Robbe Derks 70df7f227b bump panda (new health packet) (#38736)
pandad: support compact health packet
2026-08-31 14:01:20 +02:00
royjr de197ba6fa chestnut: alert when big model ready (#1947)
egpu: alert when big model ready

Co-authored-by: Jason Wen <haibin.wen3@gmail.com>
2026-08-30 16:20:16 -04:00
Isaac Barham 7f371b8acd Ford: hold path authority through turns
Assisted-by: Codex
2026-08-30 15:45:21 -04:00
Trey Moen 0e32059484 replay: capture downloader's stderr so download progress is reported again (#38734) 2026-08-30 09:32:36 -07:00
Isaac Barham 24c858e618 Ford: strengthen bounded path tracking feedback
Keep action curvature authoritative while increasing bounded C0/C1 feedback when measured curvature is behind. Keep C2 allocation tied to maneuver demand instead of tracking error.

Assisted-by: Codex <codex@openai.com>
2026-08-30 11:29:09 -04:00
Isaac Barham 405407c252 Ford: drive fast path from desired curvature
Make C0 and C1 a coherent virtual-curvature pair sourced from the constrained action target and measured tracking error. Keep model trend only for supplemental C2 unloading so model geometry cannot inflate fast steering authority across vehicles.

Assisted-by: Codex
2026-08-30 09:37:52 -04:00
Isaac Barham d49b56bff5 ford: drop under-actuating coherent path experiment
Road testing showed the endpoint-constrained C0/C1 pair opposed the requested rotation and delivered less than half the needed authority. Restore the prior same-direction fast-path encoder.

Assisted-by: Codex
2026-08-30 09:12:34 -04:00
Isaac Barham f8d8b8ee56 ui: expose Ford path experiment on comma four
Assisted-by: Codex
2026-08-30 08:54:37 -04:00
Isaac Barham 8774a462ac ford: add coherent path pose experiment
Assisted-by: Codex
2026-08-30 08:54:37 -04:00
Isaac Barham 1b41e9637f ford: balance path pose and curvature unwind
Assisted-by: Codex
2026-08-30 08:54:37 -04:00
Isaac Barham 27a220677a Productionize assisted driving milestones
Assisted-by: OpenAI Codex
2026-08-29 07:52:11 -04:00
Trey Moen 7cf55c3b7a common: fix OpenpilotPrefix cleanup on macOS (#38728)
The destructor built its cleanup commands as "rm %s -rf", with the flags
after the operand. GNU rm permutes arguments so this works on device and
in CI, but BSD rm on macOS stops option parsing at the first operand and
treats "-rf" as a second filename:

  $ mkdir -p /tmp/rmtest/sub && rm /tmp/rmtest -rf
  rm: /tmp/rmtest: is a directory
  rm: -rf: No such file or directory
  exit=1

So nothing is removed, and each of the four calls prints two errors plus
"system command failed (256)" from check_system. Every run of a tool that
owns an OpenpilotPrefix (replay, cabana) leaks its params dir, its
comma_home and its /tmp/msgq_ dir; 33 of each had accumulated on my
machine.

Pass the flags first.
2026-08-28 22:11:52 -07:00
Isaac Barham 26e4889fcb Raise comma four alert volume 2026-08-28 19:38:48 -04:00
Daniel Koepping 682b6a20df add chestnut offroad alerts (#38706)
* system: add chestnut offroad alerts

* system: refine chestnut offroad alerts

* system: refine chestnut power alerts

* system: confirm chestnut power recovery from PCIe

* system: detect missing chestnut power from INA voltage
2026-08-28 15:46:56 -07:00
Daniel Koepping a67cdf9a51 ui: sync gpu loading to offroad (#38727)
ui: sync gpu loading state
2026-08-28 15:08:18 -07:00
Isaac Barham 70fa5d0fca Boost comma four alerts and reset milestones 2026-08-28 16:13:14 -04:00
Trey Moen e571e21d14 ui: check for update on target branch switch (#38693) 2026-08-28 12:07:15 -07:00
Trey Moen 839d3f5004 ui: guard branch switcher before internet connected (#38692) 2026-08-28 12:06:33 -07:00
Trey Moen 5645370f84 cabana: split utils/util into Qt-free util and qtutil (#38723) 2026-08-28 11:37:09 -07:00
Trey Moen 633d17cd12 ui: fix install update button overflow (#38696) 2026-08-28 11:30:13 -07:00
Trey Moen 5419f57b3a cabana: de-QT streams (#38718) 2026-08-28 10:18:10 -07:00
Jason Wen 1dd5a7c91d Sync: commaai/openpilot:mastersunnypilot/sunnypilot:master (#1973) 2026-08-28 12:58:26 -04:00
Trey Moen 46f612224c cabana: split comma API route fetching out of RoutesDialog (#38721) 2026-08-28 09:57:47 -07:00
Trey Moen 6e0f4f4630 cabana: split SettingsDialog out of settings (#38719)
cabana: split SettingsDialog out of settings.{h,cc}
2026-08-28 09:55:28 -07:00
Trey Moen 0f9c753e6e cabana: remove Qt from livestream (#38722) 2026-08-28 09:46:45 -07:00
nayan acb784d207 Merge commit '4a13639cfd122ccb9113a4d6ce225dcbd8e61914' into sync-20260827
# Conflicts:
#	openpilot/selfdrive/modeld/SConscript
#	openpilot/selfdrive/modeld/modeld.py
#	openpilot/selfdrive/ui/mici/layouts/home.py
#	openpilot/selfdrive/ui/ui_state.py
#	tinygrad_repo
2026-08-28 12:39:53 -04:00
Trey Moen 131e473f37 cabana: move stream open widgets into streamselector (#38715) 2026-08-28 09:36:26 -07:00
Isaac Barham 2d700cc0d0 Add alert-style milestone scrim 2026-08-28 11:34:46 -04:00
Trey Moen 30f358eb59 cabana: use std::string in RoutesDialog API results (#38717) 2026-08-28 07:25:34 -07:00
Trey Moen 9b9e3ea604 cabana: string helpers in utils return std::string (#38720) 2026-08-28 07:25:11 -07:00
Isaac Barham cc9ae66b22 Persist assisted driving milestones 2026-08-28 09:41:13 -04:00
Isaac Barham 505270420f Refine milestone celebration typography 2026-08-28 08:40:09 -04:00
Isaac Barham bb1a17d2a0 Prototype assisted driving milestones 2026-08-28 07:34:03 -04:00
Trey Moen 7cc48b5bc9 cabana: move RoutesDialog out of streams/ (#38716) 2026-08-27 22:00:24 -07:00
Trey Moen cbf750de20 cabana: replace custom non-view Qt signals w/ plain observer (#38713) 2026-08-27 18:54:06 -07:00
Isaac Barham 6db807b5a0 ford: narrow lateral path interface
Assisted-by: Codex
2026-08-27 20:06:36 -04:00
Isaac Barham 42e1414bc4 ford: source C2 only from desired curvature
Prevent model-fit curvature jitter from directly modulating the PSCM's slow C2 channel.

Assisted-by: Codex
2026-08-27 19:53:59 -04:00
Isaac Barham 3e020e321f ford: gate curvature rate with maneuver demand
Assisted-by: Codex
2026-08-27 19:44:19 -04:00
Isaac Barham 7e2000e909 ford: make path allocation demand driven
Assisted-by: Codex
2026-08-27 19:08:21 -04:00
Isaac Barham e96055846c ford: distill lateral path controller
Assisted-by: Codex
2026-08-27 16:27:38 -04:00
Isaac Barham d47646b28f Ford: keep LMC2 available through path gaps
Assisted-by: Codex
2026-08-27 15:36:50 -04:00
Isaac Barham e75bc83424 Ford: retain centering through curve exits
Keep a bounded geometric C2 band for lane centering, preserve established rolling arcs during same-direction unwind, and smoothly release old-direction C2 on reversals. Slew-limit the fast C1 command to prevent threshold chatter.

Assisted-by: Codex
2026-08-27 15:13:56 -04:00
Trey Moen 318257fa3b bump raylib (#38712) 2026-08-27 11:38:53 -07:00
Isaac Barham 08e48958b6 Ford: close the loop on path curvature
Use the rolling path for pose and slow geometry while allocating jerk-limited requested curvature and bounded tracking error to the fast heading field. Prevent filtered C2 from reinforcing an unwind or reversal.

Assisted-by: Codex
2026-08-27 14:35:59 -04:00
Daniel Koepping 4cdc16031f log chestnut supply fault (#38711)
* log chestnut INA supply fault

* ci
2026-08-27 11:21:56 -07:00
Trey Moen 31ea1850f7 ui: remove raygui usage (#38708)
* ui: remove raygui usage

* match previous gui_text_box line spacing

* Revert "match previous gui_text_box line spacing"

This reverts commit ffd2fe31725c6d50bffaebc621c1e170d0926c66.

* Reapply "match previous gui_text_box line spacing"

This reverts commit d41404f09607e225f43868f7747f22dc0bb2cf16.
2026-08-27 10:54:52 -07:00
Isaac Barham 25d0d0f1ff Ford: embed model path in rolling reference
Assisted-by: Codex
2026-08-27 13:08:15 -04:00
Nayan 4075befc5e osm: support map deletion via sunnylink (#1971)
delete delete
2026-08-27 11:23:26 -04:00
Jason Wen 9f43d2477d [MICI] ui: move and restyle the sunnylink pill in settings (#1972) 2026-08-27 03:52:57 -04:00
Nayan 2d6cc4c065 models: Model Selector upgrades (#1953)
* uh, i did not commit anything all this time

* slideee to the left, cha cha

* lint lint

* ui: unify model source predicate and per-source bundle lookup in model_info

* [TIZI/TICI] ui: disable the other-model row onroad like the active row

* [TIZI/TICI] ui: drop docstring that restates the function name

* [TIZI/TICI] ui: keep Favorites as the first model folder in the picker

* ui: record why model names read the params slots and not modelManagerSP

* ui: show the default model's name on the picker Default entries

* models: bind a download to its ref so cancel and reselect work everywhere

* models: resume partial chunked downloads and verify silently

* models: publish a verifying status so cached checks read as verification, not a stuck download

* [TIZI/TICI] ui: move download status onto each model's own row

* [TIZI/TICI] ui: show the row status description while it has text

* [TIZI/TICI] ui: restore the Model Status bar row

* models: a cancel interrupts verification immediately and keeps on-disk chunks

* models: a selection made mid-download queues instead of cancelling the transfer

* [TIZI/TICI] ui: Model Status shows both slots idle and the queued pick while busy

* [TIZI/TICI] ui: label the Model Status slots small and big and scroll long names

* models: start a queued download in the same tick and label empty slots (Default)

* ui: scroll Model Status names at the corrected speed

* [TIZI/TICI] ui: Model Status shows the big model failing over to small

* [TIZI/TICI] ui: stable model rows and a runner-matched failover note on Model Status

* [TIZI/TICI] ui: model rows show full names and the failover note reopens with the page

* ui: name the actually driving model runner-matched and bring mici to state parity

* fix ugly

---------

Co-authored-by: Jason Wen <haibin.wen3@gmail.com>
Co-authored-by: James Vecellio-Grant <159560811+Discountchubbs@users.noreply.github.com>
2026-08-27 02:03:53 -04:00
Daniel Koepping 4a13639cfd reduce chestnut states (#38705)
ui: unify chestnut status presentation
2026-08-26 19:12:49 -07:00
YassineYousfi fa75fdd852 chestnut stats: overlap with gpu work (#38704)
* chestnut stats: overlap with gpu work

* ci

---------

Co-authored-by: elkoled <elkoled@gmail.com>
2026-08-26 17:57:55 -07:00
Daniel Koepping 5cfdb2f4da rename usbgpu to chestnut (#38703)
chestnut: rename eGPU interfaces
2026-08-26 15:42:59 -07:00
Daniel Koepping 63548ce10d bump tinygrad (#38702) 2026-08-26 15:20:04 -07:00
Daniel Koepping 980fb79c1a update orange GPU icon (#38701)
mici: update failed eGPU icon
2026-08-26 12:21:14 -07:00
Harald Schäfer d40df6f829 modeld: fall back on invalid big model outputs (#38700) 2026-08-26 12:06:53 -07:00
Nayan da28afca91 models: dual-slot backend (qcom/usbgpu) with ref-based downloads (#1966)
* models: dual-slot backend (qcom/usbgpu) with ref-based downloads

* models: restore get_active_source and the usbgpu-to-qcom fallback

* models: fix per-slot validation and cap mismatched-source refetches

* ui/models: select models by ref and seed the usbgpu slot on migration

* models: drop defensive attribute guards on capnp bundles

* models: remove vestigial fetcher state and dead fallbacks

* models: resolve the active bundle from the active source slot only

* models: pass the usbgpu kwarg through the modeld test stubs

* models: resolve the displayed model from the active slot in ui_state

* models: correct the validation memo type hint

* models: drop docstrings that restate the function name

---------

Co-authored-by: Jason Wen <haibin.wen3@gmail.com>
2026-08-26 02:34:02 -04:00
Jason Wen 15f201caed ui: use full big model failure detection for sidebar and home eGPU icons (#1969) 2026-08-25 21:07:44 -04:00
Jason Wen 1d4558c067 [TIZI/TICI] sidebar: show eGPU icon when chestnut is present (#1968)
* [tizi/tici] sidebar: show eGPU icon when chestnut is present

* matchy match

* fix
2026-08-25 20:47:52 -04:00
Jason Wen 78a766eb61 ui: fix scrolling label speed at non-60fps refresh rates (#1967)
* ui: fix scrolling label speed at non-60fps refresh rates

* send it

* nope

* more
2026-08-25 20:38:02 -04:00
Jason Wen b742b96c44 [MICI] ui: four-state eGPU icon for non-default big models (#1945)
* ui: four-state eGPU icon for non-default big models

* oops

* try this out

* align
2026-08-25 12:04:39 -04:00
Jason Wen 25c25047b8 models: persist model selection per catalog across chestnut state changes (#1960) 2026-08-25 01:12:12 -04:00
Jason Wen cefe5737b9 models: fix current model not updating on chestnut status (#1959)
* models: preserve user model selection across reboots and power cycles

* no

* again

* idk

* over
2026-08-25 00:41:31 -04:00
Jason Wen 760c19d3f9 ui/models: handle missing files during cache size calculation (#1958) 2026-08-24 23:31:52 -04:00
James Vecellio-Grant 45814e3313 modeld_v2: spatial features (#1934)
* modeld_v2: spatial features

* Update fetcher.py

* dont reshape non 4 dim arrays

* realize for non compiled

* Update compile_modeld.py

* god dammit it was realize()

* it was fucking frozen tinygrad. just need to recompile

* bump

* ci: add is_big flag to metadata.json to support backward compat

* Update model_generator.py

* Update sunnypilot-build-model.yaml

* Update helpers.py

* Revert "Update helpers.py"

This reverts commit 3a955ca11a.

* Reapply "Update helpers.py"

This reverts commit ca9c6e1933.

* models: use less strict chestnut detection state

---------

Co-authored-by: Jason Wen <haibin.wen3@gmail.com>
2026-08-24 22:52:29 -04:00
Jason Wen 2ba91d2be5 ci: add tinygrad ref check to prepare_chestnut and even faster prebuilt stages (#1957)
* ci: faster prebuilt stages

* tg check chestnut

* zoomer!
2026-08-24 22:42:45 -04:00
Jason Wen 19f83b274f ci: identical environment for publish_chestnut prebuilt 2026-08-24 22:06:20 -04:00
Jason Wen d14d0b1dd0 ci: parallelize models chunk downloads and split branch publishing (#1955)
* ci: parallelize model chunk downloads and better publish

* ci: download all model chunks in parallel with xargs -P8

* split split

* ew

* must require
2026-08-24 21:48:53 -04:00
Jason Wen 6cc5f3aad8 ci: fix DM model build, separate HF defaults paths, nuke build races (#1956)
* ci: fix DM model build, separate HF defaults paths, nuke build races

* more split!

* name

* ci: download driving and DM model chunks into chestnut prebuilt output
2026-08-24 20:02:48 -04:00
Jason Wen 8e16c9babb ci: offload small model compilation (#1952)
* ci: compile default big model with stock modeld

* Revert "Revert big RL model (#38627)"

This reverts commit 516ec1e682.

* ci: compile default small model with stock modeld compiler

* ci: offload small model compilation

* Reapply "Revert big RL model (#38627)"

This reverts commit d06cfabb62.

* Reapply "Revert big RL model (#38627)"

This reverts commit d06cfabb62.
2026-08-24 16:24:19 -04:00
Jason Wen 2bcfed5c71 ci: compile default models with stock modeld (#1954)
* ci: compile default big model with stock modeld

* Revert "Revert big RL model (#38627)"

This reverts commit 516ec1e682.

* ci: compile default small model with stock modeld compiler

* Reapply "Revert big RL model (#38627)"

This reverts commit d06cfabb62.
2026-08-24 15:37:46 -04:00
Jason Wen 66cf334067 ci: unify default model build into single workflow (#1951)
* ci: unify default model build into single workflow

* ci: consolidate upload jobs and add tinygrad ref validation
2026-08-24 12:35:37 -04:00
Jason Wen 94ed0608e6 models: use less strict chestnut detection state (#1948) 2026-08-24 01:40:31 -04:00
Jason Wen 0fbca979df models: show big model list when Chestnut present (#1943) 2026-08-23 20:19:57 -04:00
Jason Wen dcddb2a0bd models: revert icon override from this branch scope 2026-08-23 19:53:46 -04:00
Jason Wen 699eaf7957 include them! 2026-08-23 19:16:15 -04:00
Jason Wen c246e6318a Merge branch 'master' into models-good-detect 2026-08-23 16:56:44 -04:00
Jason Wen 718db8c62e Sync: commaai/openpilot:mastersunnypilot/sunnypilot:master (#1944) 2026-08-23 16:56:00 -04:00
Jason Wen c2214d4c32 Merge commit '084747c75d2cbd23af65ab7a9e770bbd7b98bac9' into sync-20260823
# Conflicts:
#	openpilot/common/params_keys.h
2026-08-23 15:36:54 -04:00
Jason Wen 0de7fbf33d new 2026-08-23 15:06:53 -04:00
Jason Wen 211f990f6b models: fix sunnylink default model display and false big model re-downloading (#1941)
* big needs small

* no download

* actually

* send it
2026-08-23 04:04:46 -04:00
Jason Wen 97468e4fa4 [TIZI/TICI] ui: remove calibration reset dialog on model change (#1942) 2026-08-23 03:48:52 -04:00
Jason Wen 6c6fba9a14 ci: fix flaky LLK test (#1940) 2026-08-23 02:57:02 -04:00
Jason Wen 34621cf816 ci: refactor big model chunk handling (#1939) 2026-08-23 02:48:10 -04:00
Jason Wen 086530b7c6 [TIZI/TICI] ui: fix path width during gas and steering override (#1938) 2026-08-22 21:47:38 -04:00
Shane Smiskol 084747c75d Fix button label widths (#38680)
* Revert "ui: fix text and icon overlap on button (#38628)"

This reverts commit d9c4120f89.

* simple

* can do this

* fix eliding

* Revert "fix eliding"

This reverts commit b271a350182ad87f9942d7363383ee8ec72d0e36.

* clean up

* clean up
2026-08-21 15:41:12 -07:00
stef 555f48c5d2 params: remove livestream param on ignition (#38679)
* remove livestream param on ignition

* simplify process config
2026-08-19 14:21:59 -07:00
stef dcf9d25bf3 webrtcd: more descriptive errors (#38677)
more descriptive errors
2026-08-19 14:02:03 -07:00
stef a8d1a280c6 webrtcd/athenad: we don't have to fail on no car params (#38678)
we don't have to fail on no car params
2026-08-19 13:49:19 -07:00
stef 5b36799eec webrtc: fix message handler race (#38675)
open message handler early
2026-08-18 21:56:33 -07:00
stef 20fdc3d824 webrtcd: cloud logging (#38674)
* logging

* remove test

* get rid of redudant try except

* fix logger context
2026-08-18 21:13:57 -07:00
Shane Smiskol 7bd6cad821 Re-open agnos updater UI if crash (#38672)
loop if crash
2026-08-18 19:06:48 -07:00
276 changed files with 18092 additions and 3194 deletions
+1
View File
@@ -9,6 +9,7 @@
*.ttf filter=lfs diff=lfs merge=lfs -text
*.otf filter=lfs diff=lfs merge=lfs -text
*.wav filter=lfs diff=lfs merge=lfs -text
openpilot/selfdrive/assets/sounds/milestone.wav -filter -diff -merge -text
openpilot/selfdrive/car/tests/test_models_segs.txt filter=lfs diff=lfs merge=lfs -text
openpilot/common/hardware/comma/updater filter=lfs diff=lfs merge=lfs -text
-11
View File
@@ -1,11 +0,0 @@
* @sunnypilot/dev-internal
/.github/ @devtekve @sunnyhaibin
/release/ci/ @devtekve @sunnyhaibin
/tinygrad_repo @devtekve @Discountchubbs
/tinygrad/ @devtekve @Discountchubbs
/selfdrive/controls/lib/longitudinal_planner.py @devtekve @Discountchubbs
/selfdrive/controls/lib/longitudinal_mpc_lib/long_mpc.py @devtekve @Discountchubbs
/selfdrive/modeld/ @devtekve @Discountchubbs
/sunnypilot/model* @devtekve @Discountchubbs
/sunnypilot/sunnylink/ @devtekve
/system/athena/ @devtekve
@@ -8,13 +8,13 @@ on:
required: true
type: string
target_hardware:
description: 'Hardware target to compile for (qcom or usbgpu)'
description: 'Hardware target to compile for (qcom or chestnut)'
required: true
type: choice
default: 'qcom'
options:
- qcom
- usbgpu
- chestnut
hf_repo:
description: 'Hugging Face dataset repository'
required: false
@@ -59,7 +59,7 @@ jobs:
id: get-json
run: |
cd docs/docs
PREFIX="driving_models_${{ inputs.target_hardware == 'usbgpu' && 'usbgpu_' || '' }}v"
PREFIX="driving_models_${{ inputs.target_hardware == 'chestnut' && 'chestnut_' || '' }}v"
latest=$(ls ${PREFIX}*.json | sed -E "s/${PREFIX}([0-9]+)\.json/\1/" | sort -n | tail -1)
next=$((latest+1))
json_file="${PREFIX}${next}.json"
@@ -78,6 +78,7 @@ jobs:
- name: Get next recompiled dir number
id: create-recompiled-dir
env:
HF_TOKEN: ${{ secrets.HF_TOKEN }}
HF_REPO: ${{ github.event.inputs.hf_repo }}
run: |
pip install huggingface_hub
@@ -1,83 +0,0 @@
name: Build default big model
on:
workflow_dispatch:
env:
HF_REPO: sunnypilot/sunnypilot_models_v1
HF_DEFAULTS_PATH: models/defaults/big
jobs:
resolve_name:
runs-on: ubuntu-24.04
outputs:
model_name: ${{ steps.name.outputs.model_name }}
onnx_ref: ${{ steps.name.outputs.onnx_ref }}
steps:
- uses: actions/checkout@v4
- id: name
run: |
NAME=$(PYTHONPATH=${{ github.workspace }} python3 -c "from openpilot.sunnypilot.models.model_name import DEFAULT_BIG_MODEL; print(DEFAULT_BIG_MODEL)")
ONNX_REF=$(git log -1 --format='%H' -- openpilot/selfdrive/modeld/models/big_driving_supercombo.onnx)
echo "model_name=${NAME}" >> $GITHUB_OUTPUT
echo "onnx_ref=$ONNX_REF" >> $GITHUB_OUTPUT
build_model:
needs: resolve_name
uses: ./.github/workflows/sunnypilot-build-model.yaml
with:
upstream_branch: ${{ needs.resolve_name.outputs.onnx_ref }}
custom_name: ${{ needs.resolve_name.outputs.model_name }}
target_hardware: usbgpu
secrets: inherit
upload_defaults:
needs: [ resolve_name, build_model ]
runs-on: ubuntu-24.04
permissions:
id-token: write
contents: write
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
- run: git lfs pull -I "openpilot/selfdrive/modeld/models/big_driving_supercombo.onnx"
- name: Install huggingface_hub
run: pip install --upgrade "huggingface_hub>=0.22.0"
- name: Download artifact name
uses: actions/download-artifact@v4
with:
name: artifact-name-${{ needs.resolve_name.outputs.model_name }}
path: artifact_name
- name: Read artifact name
id: artifact
run: |
ARTIFACT_NAME=$(cat artifact_name/artifact_name.txt)
echo "artifact_name=$ARTIFACT_NAME" >> $GITHUB_OUTPUT
- name: Download model artifact
uses: actions/download-artifact@v4
with:
name: ${{ steps.artifact.outputs.artifact_name }}
path: output
- name: Upload to HF and update default_models.json
env:
HF_OIDC_RESOURCE: datasets/${{ env.HF_REPO }}
ARTIFACT_NAME: ${{ steps.artifact.outputs.artifact_name }}
run: |
rm -f output/artifact_name.txt
export PYTHONPATH=$(pwd)
python3 release/ci/upload_default_model.py \
--hf-repo "${{ env.HF_REPO }}" \
--hf-defaults-path "${{ env.HF_DEFAULTS_PATH }}" \
--artifact-name "$ARTIFACT_NAME" \
--model-dir output \
--onnx-path "openpilot/selfdrive/modeld/models/big_driving_supercombo.onnx" \
--onnx-ref "${{ needs.resolve_name.outputs.onnx_ref }}" \
--model-name "${{ needs.resolve_name.outputs.model_name }}" \
--tinygrad-ref "$(python3 openpilot/sunnypilot/models/tinygrad_ref.py)" \
--run-number "${{ github.run_number }}"
+522
View File
@@ -0,0 +1,522 @@
name: Build default models
on:
workflow_dispatch:
inputs:
target:
description: 'Model target to build'
required: true
type: choice
options:
- small
- big
- dm
workflow_call:
inputs:
target:
description: 'Model target to build (small, big, or dm)'
required: true
type: string
concurrency:
group: build-default-models-${{ inputs.target }}
cancel-in-progress: false
env:
HF_REPO: sunnypilot/sunnypilot_models_v1
jobs:
resolve:
runs-on: ubuntu-24.04
outputs:
model_name: ${{ steps.resolve.outputs.model_name }}
safe_model_name: ${{ steps.resolve.outputs.safe_model_name }}
onnx_ref: ${{ steps.resolve.outputs.onnx_ref }}
onnx_path: ${{ steps.resolve.outputs.onnx_path }}
hf_defaults_path: ${{ steps.resolve.outputs.hf_defaults_path }}
tinygrad_ref: ${{ steps.resolve.outputs.tinygrad_ref }}
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
- id: resolve
run: |
export PYTHONPATH=${{ github.workspace }}
if [ "${{ inputs.target }}" = "big" ]; then
NAME=$(python3 -c "from openpilot.sunnypilot.models.model_name import DEFAULT_BIG_MODEL; print(DEFAULT_BIG_MODEL)")
ONNX_PATH="openpilot/selfdrive/modeld/models/big_driving_supercombo.onnx"
HF_DEFAULTS_PATH="models/defaults/big"
elif [ "${{ inputs.target }}" = "dm" ]; then
ONNX_PATH="openpilot/selfdrive/modeld/models/dmonitoring_model.onnx"
HF_DEFAULTS_PATH="models/defaults/dm"
NAME="dmonitoring_model ($(git log -1 --format=%cd --date=format:'%B %d, %Y' -- "$ONNX_PATH"))"
else
NAME=$(python3 -c "from openpilot.sunnypilot.models.model_name import DEFAULT_MODEL; print(DEFAULT_MODEL)")
ONNX_PATH="openpilot/selfdrive/modeld/models/driving_supercombo.onnx"
HF_DEFAULTS_PATH="models/defaults/small"
fi
ONNX_REF=$(git log -1 --format='%H' -- "$ONNX_PATH")
TINYGRAD_REF=$(python3 openpilot/sunnypilot/models/tinygrad_ref.py)
if [ -z "$TINYGRAD_REF" ]; then
echo "::error::Failed to resolve tinygrad ref"
exit 1
fi
SAFE_NAME="${NAME// /-}"
echo "model_name=${NAME}" >> $GITHUB_OUTPUT
echo "safe_model_name=${SAFE_NAME}" >> $GITHUB_OUTPUT
echo "onnx_ref=${ONNX_REF}" >> $GITHUB_OUTPUT
echo "onnx_path=${ONNX_PATH}" >> $GITHUB_OUTPUT
echo "hf_defaults_path=${HF_DEFAULTS_PATH}" >> $GITHUB_OUTPUT
echo "tinygrad_ref=${TINYGRAD_REF}" >> $GITHUB_OUTPUT
build_small_model:
needs: resolve
if: ${{ inputs.target == 'small' }}
runs-on: [self-hosted, tici]
env:
SMALL_ONNX: openpilot/selfdrive/modeld/models/driving_supercombo.onnx
SMALL_PKL: openpilot/selfdrive/modeld/models/driving_tinygrad.pkl
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
- name: Pull ONNX via LFS
run: git lfs pull -I "${{ env.SMALL_ONNX }}"
- name: Set environment variables
run: |
source /etc/profile
export UV_PROJECT_ENVIRONMENT=${HOME}/venv
export UV_PYTHON_PREFERENCE=managed
export UV_PYTHON_INSTALL_DIR=${HOME}/uv/python
export VIRTUAL_ENV=$UV_PROJECT_ENVIRONMENT
uv sync --frozen
printenv >> $GITHUB_ENV
- name: Disable powersave
run: |
source ${UV_PROJECT_ENVIRONMENT}/bin/activate
PYTHONPATH=$PYTHONPATH:${{ github.workspace }}/ ${{ github.workspace }}/scripts/manage-powersave.py --disable
- name: Compile small model with stock compiler
run: |
source ${UV_PROJECT_ENVIRONMENT}/bin/activate
export PYTHONPATH="${PYTHONPATH}:${{ github.workspace }}/tinygrad_repo:${{ github.workspace }}"
MODEL_SIZE=$(python3 -c "from openpilot.common.transformations.model import MEDMODEL_INPUT_SIZE as s; print(f'{s[0]}x{s[1]}')")
CAMERA_RES=$(python3 -c "from openpilot.common.transformations.camera import _ar_ox_fisheye as a, _os_fisheye as o; print(f'{a.width}x{a.height} {o.width}x{o.height}')")
FRAME_SKIP=$(python3 -c "from openpilot.selfdrive.modeld.constants import ModelConstants as MC; print(MC.MODEL_RUN_FREQ // MC.MODEL_CONTEXT_FREQ)")
TG_FLAGS="DEV=QCOM IMAGE=1 FLOAT16=1 NOLOCALS=1 JIT_BATCH_SIZE=0 OPENPILOT_HACKS=1"
env ${TG_FLAGS} python3 \
${{ github.workspace }}/openpilot/selfdrive/modeld/compile_modeld.py \
--onnx ${{ github.workspace }}/${{ env.SMALL_ONNX }} \
--model-size $MODEL_SIZE \
--camera-resolutions $CAMERA_RES \
--frame-skip $FRAME_SKIP \
--output ${{ github.workspace }}/${{ env.SMALL_PKL }}
- name: Chunk small pkl
run: |
source ${UV_PROJECT_ENVIRONMENT}/bin/activate
export PYTHONPATH=${{ github.workspace }}
python3 -c "
from openpilot.common.file_chunker import chunk_file, get_chunk_targets
import os
pkl = '${{ github.workspace }}/${{ env.SMALL_PKL }}'
size = os.path.getsize(pkl)
targets = get_chunk_targets(pkl, size)
chunk_file(pkl, targets)
print(f'Chunked into {len(targets)} files')
"
- name: Prepare output
env:
MODEL_NAME: ${{ needs.resolve.outputs.safe_model_name }}
run: |
source ${UV_PROJECT_ENVIRONMENT}/bin/activate
export PYTHONPATH=${{ github.workspace }}
MODELS_DIR="${{ github.workspace }}/openpilot/selfdrive/modeld/models"
OUTPUT_DIR="${{ github.workspace }}/small_output"
PKL_BASE="driving_tinygrad.pkl"
mkdir -p "$OUTPUT_DIR"
cp "$MODELS_DIR/${PKL_BASE}".chunk* "$OUTPUT_DIR/"
cp "$MODELS_DIR/${PKL_BASE}.chunkmanifest" "$OUTPUT_DIR/"
python3 "${{ github.workspace }}/release/ci/model_generator.py" \
--model-dir "$MODELS_DIR" \
--output-dir "$OUTPUT_DIR" \
--custom-name "$MODEL_NAME" \
--upstream-branch "${{ needs.resolve.outputs.onnx_ref }}"
echo "model-${MODEL_NAME}-${{ github.run_number }}" > "$OUTPUT_DIR/artifact_name.txt"
- name: Upload small model artifact
uses: actions/upload-artifact@v4
with:
name: model-${{ needs.resolve.outputs.safe_model_name }}-${{ github.run_number }}
path: ${{ github.workspace }}/small_output/
- name: Upload artifact name file
uses: actions/upload-artifact@v4
with:
name: artifact-name-${{ needs.resolve.outputs.safe_model_name }}
path: ${{ github.workspace }}/small_output/artifact_name.txt
- name: Re-enable powersave
if: always()
run: |
source ${UV_PROJECT_ENVIRONMENT}/bin/activate
PYTHONPATH=$PYTHONPATH:${{ github.workspace }}/ ${{ github.workspace }}/scripts/manage-powersave.py --enable
build_big_model:
needs: resolve
if: ${{ inputs.target == 'big' }}
runs-on: [self-hosted, chestnut]
env:
BIG_ONNX: openpilot/selfdrive/modeld/models/big_driving_supercombo.onnx
BIG_PKL: openpilot/selfdrive/modeld/models/big_driving_tinygrad.pkl
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
- name: Pull big ONNX via LFS
run: git lfs pull -I "${{ env.BIG_ONNX }}"
- name: Set environment variables
run: |
source /etc/profile
export UV_PROJECT_ENVIRONMENT=${HOME}/venv
export UV_PYTHON_PREFERENCE=managed
export UV_PYTHON_INSTALL_DIR=${HOME}/uv/python
export VIRTUAL_ENV=$UV_PROJECT_ENVIRONMENT
uv sync --frozen
printenv >> $GITHUB_ENV
- name: Disable powersave
run: |
source ${UV_PROJECT_ENVIRONMENT}/bin/activate
PYTHONPATH=$PYTHONPATH:${{ github.workspace }}/ ${{ github.workspace }}/scripts/manage-powersave.py --disable
- name: Wait for chestnut PCIe link
run: |
source ${UV_PROJECT_ENVIRONMENT}/bin/activate
export PYTHONPATH="${PYTHONPATH}:${{ github.workspace }}/tinygrad_repo:${{ github.workspace }}"
python3 -c "
import time
from openpilot.system.hardware.chestnut.flash import link_up
for i in range(10):
if link_up():
print(f'PCIe link up after {i+1} attempt(s)')
break
time.sleep(1)
else:
raise RuntimeError('Chestnut PCIe link not ready after 10 attempts')
"
- name: Compile big model with stock compiler
run: |
source ${UV_PROJECT_ENVIRONMENT}/bin/activate
export PYTHONPATH="${PYTHONPATH}:${{ github.workspace }}/tinygrad_repo:${{ github.workspace }}"
MODEL_SIZE=$(python3 -c "from openpilot.common.transformations.model import MEDMODEL_INPUT_SIZE as s; print(f'{s[0]}x{s[1]}')")
CAMERA_RES=$(python3 -c "from openpilot.common.transformations.camera import _ar_ox_fisheye as a, _os_fisheye as o; print(f'{a.width}x{a.height} {o.width}x{o.height}')")
FRAME_SKIP=$(python3 -c "from openpilot.selfdrive.modeld.constants import ModelConstants as MC; print(MC.MODEL_RUN_FREQ // MC.MODEL_CONTEXT_FREQ)")
TG_FLAGS="DEBUG=2 DEV=USB+AMD:LLVM WARP_DEV=QCOM FLOAT16=1 JIT_BATCH_SIZE=0 GMMU=0 TC_OPT=2"
env ${TG_FLAGS} python3 \
${{ github.workspace }}/openpilot/selfdrive/modeld/compile_modeld.py \
--onnx ${{ github.workspace }}/${{ env.BIG_ONNX }} \
--model-size $MODEL_SIZE \
--camera-resolutions $CAMERA_RES \
--frame-skip $FRAME_SKIP \
--output ${{ github.workspace }}/${{ env.BIG_PKL }}
- name: Chunk big pkl
run: |
source ${UV_PROJECT_ENVIRONMENT}/bin/activate
export PYTHONPATH=${{ github.workspace }}
python3 -c "
from openpilot.common.file_chunker import chunk_file, get_chunk_targets
import os
pkl = '${{ github.workspace }}/${{ env.BIG_PKL }}'
size = os.path.getsize(pkl)
targets = get_chunk_targets(pkl, size)
chunk_file(pkl, targets)
print(f'Chunked into {len(targets)} files')
"
- name: Prepare output
env:
MODEL_NAME: ${{ needs.resolve.outputs.safe_model_name }}
run: |
source ${UV_PROJECT_ENVIRONMENT}/bin/activate
export PYTHONPATH=${{ github.workspace }}
MODELS_DIR="${{ github.workspace }}/openpilot/selfdrive/modeld/models"
OUTPUT_DIR="${{ github.workspace }}/big_output"
PKL_BASE="big_driving_tinygrad.pkl"
mkdir -p "$OUTPUT_DIR"
cp "$MODELS_DIR/${PKL_BASE}".chunk* "$OUTPUT_DIR/"
cp "$MODELS_DIR/${PKL_BASE}.chunkmanifest" "$OUTPUT_DIR/"
python3 "${{ github.workspace }}/release/ci/model_generator.py" \
--model-dir "$MODELS_DIR" \
--output-dir "$OUTPUT_DIR" \
--custom-name "$MODEL_NAME" \
--upstream-branch "${{ needs.resolve.outputs.onnx_ref }}"
echo "model-${MODEL_NAME}-${{ github.run_number }}" > "$OUTPUT_DIR/artifact_name.txt"
- name: Upload big model artifact
uses: actions/upload-artifact@v4
with:
name: model-${{ needs.resolve.outputs.safe_model_name }}-${{ github.run_number }}
path: ${{ github.workspace }}/big_output/
- name: Upload artifact name file
uses: actions/upload-artifact@v4
with:
name: artifact-name-${{ needs.resolve.outputs.safe_model_name }}
path: ${{ github.workspace }}/big_output/artifact_name.txt
- name: Re-enable powersave
if: always()
run: |
source ${UV_PROJECT_ENVIRONMENT}/bin/activate
PYTHONPATH=$PYTHONPATH:${{ github.workspace }}/ ${{ github.workspace }}/scripts/manage-powersave.py --enable
upload_defaults:
needs: [ resolve, build_small_model, build_big_model, build_dm_model ]
if: |
${{
!cancelled() &&
(inputs.target == 'big' && needs.build_big_model.result == 'success' ||
inputs.target == 'small' && needs.build_small_model.result == 'success' ||
inputs.target == 'dm' && needs.build_dm_model.result == 'success')
}}
runs-on: ubuntu-24.04
permissions:
id-token: write
contents: write
steps:
- uses: actions/checkout@v4
- name: Pull ONNX via LFS
run: git lfs pull -I "${{ needs.resolve.outputs.onnx_path }}"
- name: Install huggingface_hub
run: pip install --upgrade "huggingface_hub>=0.22.0"
- name: Download artifact name
if: ${{ inputs.target == 'small' || inputs.target == 'big' }}
uses: actions/download-artifact@v4
with:
name: artifact-name-${{ needs.resolve.outputs.safe_model_name }}
path: artifact_name
- name: Read artifact name
if: ${{ inputs.target == 'small' || inputs.target == 'big' }}
id: artifact
run: |
ARTIFACT_NAME=$(cat artifact_name/artifact_name.txt)
echo "artifact_name=$ARTIFACT_NAME" >> $GITHUB_OUTPUT
- name: Download model artifact
if: ${{ inputs.target == 'small' || inputs.target == 'big' }}
uses: actions/download-artifact@v4
with:
name: ${{ steps.artifact.outputs.artifact_name }}
path: output
- name: Upload model to HF
if: ${{ inputs.target == 'small' || inputs.target == 'big' }}
env:
HF_TOKEN: ${{ secrets.HF_TOKEN }}
ARTIFACT_NAME: ${{ steps.artifact.outputs.artifact_name }}
run: |
rm -f output/artifact_name.txt
export PYTHONPATH=$(pwd)
python3 release/ci/upload_default_model.py \
--hf-repo "${{ env.HF_REPO }}" \
--hf-defaults-path "${{ needs.resolve.outputs.hf_defaults_path }}" \
--artifact-name "$ARTIFACT_NAME" \
--model-dir output \
--onnx-path "${{ needs.resolve.outputs.onnx_path }}" \
--onnx-ref "${{ needs.resolve.outputs.onnx_ref }}" \
--model-name "${{ needs.resolve.outputs.model_name }}" \
--tinygrad-ref "${{ needs.resolve.outputs.tinygrad_ref }}" \
--run-number "${{ github.run_number }}"
- name: Download DM artifact
if: ${{ inputs.target == 'dm' }}
uses: actions/download-artifact@v4
with:
name: dm-model-${{ github.run_number }}
path: dm_output
- name: Generate DM metadata and upload to HF
if: ${{ inputs.target == 'dm' }}
env:
HF_TOKEN: ${{ secrets.HF_TOKEN }}
run: |
export PYTHONPATH=$(pwd)
python3 -c "
import json, hashlib
from pathlib import Path
from datetime import datetime, UTC
dm_dir = Path('dm_output')
manifest = list(dm_dir.glob('*.chunkmanifest'))
assert manifest, 'No chunkmanifest found'
pkl_name = manifest[0].name.removesuffix('.chunkmanifest')
num_chunks = int(manifest[0].read_text().strip())
chunks = []
for i in range(num_chunks):
chunk = dm_dir / f'{pkl_name}.chunk{i+1:02d}of{num_chunks:02d}'
chunks.append({
'file_name': chunk.name,
'sha256': hashlib.sha256(chunk.read_bytes()).hexdigest()
})
digest = hashlib.sha256()
for c in chunks:
with open(dm_dir / c['file_name'], 'rb') as f:
while block := f.read(1024*1024):
digest.update(block)
metadata = {
'bundles': [{
'short_name': 'DMMODEL',
'display_name': '${{ needs.resolve.outputs.model_name }}',
'ref': '${{ needs.resolve.outputs.onnx_ref }}',
'runner': 'tinygrad',
'build_time': datetime.now(UTC).strftime('%Y-%m-%dT%H:%M:%SZ'),
'models': [{
'type': 'chunked',
'artifact': {
'file_name': pkl_name,
'download_uri': {'url': '', 'sha256': digest.hexdigest()},
'chunks': chunks
}
}]
}]
}
with open(dm_dir / 'metadata.json', 'w') as f:
json.dump(metadata, f, indent=2)
print('Generated DM metadata.json')
"
python3 release/ci/upload_default_model.py \
--hf-repo "${{ env.HF_REPO }}" \
--hf-defaults-path "${{ needs.resolve.outputs.hf_defaults_path }}" \
--artifact-name "dm-model-${{ github.run_number }}" \
--model-dir dm_output \
--onnx-path "${{ needs.resolve.outputs.onnx_path }}" \
--onnx-ref "${{ needs.resolve.outputs.onnx_ref }}" \
--model-name "${{ needs.resolve.outputs.model_name }}" \
--tinygrad-ref "${{ needs.resolve.outputs.tinygrad_ref }}" \
--run-number "${{ github.run_number }}"
build_dm_model:
needs: resolve
if: ${{ inputs.target == 'dm' }}
runs-on: [self-hosted, tici]
env:
DM_ONNX: openpilot/selfdrive/modeld/models/dmonitoring_model.onnx
DM_PKL: openpilot/selfdrive/modeld/models/dmonitoring_model_tinygrad.pkl
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
- name: Pull DM ONNX via LFS
run: git lfs pull -I "${{ env.DM_ONNX }}"
- name: Set environment variables
run: |
source /etc/profile
export UV_PROJECT_ENVIRONMENT=${HOME}/venv
export UV_PYTHON_PREFERENCE=managed
export UV_PYTHON_INSTALL_DIR=${HOME}/uv/python
export VIRTUAL_ENV=$UV_PROJECT_ENVIRONMENT
uv sync --frozen
printenv >> $GITHUB_ENV
- name: Disable powersave
run: |
source ${UV_PROJECT_ENVIRONMENT}/bin/activate
PYTHONPATH=$PYTHONPATH:${{ github.workspace }}/ ${{ github.workspace }}/scripts/manage-powersave.py --disable
- name: Compile DM model
run: |
source ${UV_PROJECT_ENVIRONMENT}/bin/activate
export PYTHONPATH="${PYTHONPATH}:${{ github.workspace }}/tinygrad_repo:${{ github.workspace }}"
TG_FLAGS="DEV=QCOM IMAGE=1 FLOAT16=1 NOLOCALS=1 JIT_BATCH_SIZE=0 OPENPILOT_HACKS=1"
taskset -c 7 env ${TG_FLAGS} python3 \
${{ github.workspace }}/tinygrad_repo/examples/openpilot/compile3.py \
${{ github.workspace }}/${{ env.DM_ONNX }} \
${{ github.workspace }}/${{ env.DM_PKL }}
- name: Chunk DM pkl
run: |
source ${UV_PROJECT_ENVIRONMENT}/bin/activate
export PYTHONPATH=${{ github.workspace }}
python3 -c "
from openpilot.common.file_chunker import chunk_file, get_chunk_targets
import os
pkl = '${{ github.workspace }}/${{ env.DM_PKL }}'
size = os.path.getsize(pkl)
targets = get_chunk_targets(pkl, size)
chunk_file(pkl, targets)
print(f'Chunked {pkl} into {len(targets)} chunks')
"
- name: Compile DM warp
run: |
source ${UV_PROJECT_ENVIRONMENT}/bin/activate
export PYTHONPATH="${PYTHONPATH}:${{ github.workspace }}/tinygrad_repo:${{ github.workspace }}"
TG_FLAGS="DEV=QCOM IMAGE=1 FLOAT16=1 NOLOCALS=1 JIT_BATCH_SIZE=0 OPENPILOT_HACKS=1"
MODEL_DIR="${{ github.workspace }}/openpilot/selfdrive/modeld"
DM_SIZE=$(python3 -c "from openpilot.common.transformations.model import DM_INPUT_SIZE as s; print(f'{s[0]}x{s[1]}')")
for res in $(python3 -c "from openpilot.common.transformations.camera import _ar_ox_fisheye as a, _os_fisheye as o; print(f'{a.width}x{a.height} {o.width}x{o.height}')"); do
WARP_PKL="${MODEL_DIR}/models/dm_warp_${res}_tinygrad.pkl"
taskset -c 7 env ${TG_FLAGS} python3 ${MODEL_DIR}/compile_dm_warp.py \
--camera-resolution ${res} \
--warp-to ${DM_SIZE} \
--output ${WARP_PKL}
done
- name: Prepare DM output
run: |
mkdir -p dm_output
cp ${{ github.workspace }}/${{ env.DM_PKL }}.chunk* dm_output/
cp ${{ github.workspace }}/${{ env.DM_PKL }}.chunkmanifest dm_output/
cp ${{ github.workspace }}/openpilot/selfdrive/modeld/models/dm_warp_* dm_output/
- name: Upload DM artifact
uses: actions/upload-artifact@v4
with:
name: dm-model-${{ github.run_number }}
path: dm_output/
- name: Re-enable powersave
if: always()
run: |
source ${UV_PROJECT_ENVIRONMENT}/bin/activate
PYTHONPATH=$PYTHONPATH:${{ github.workspace }}/ ${{ github.workspace }}/scripts/manage-powersave.py --enable
@@ -30,7 +30,7 @@ on:
type: boolean
default: true
target_hardware:
description: 'Hardware target to compile for (qcom or usbgpu)'
description: 'Hardware target to compile for (qcom or chestnut)'
required: false
type: string
default: 'qcom'
@@ -101,7 +101,7 @@ on:
default: 'qcom'
options:
- qcom
- usbgpu
- chestnut
hf_repo:
description: 'Hugging Face dataset repository'
required: false
@@ -109,7 +109,7 @@ on:
default: 'sunnypilot/sunnypilot_models_v1'
env:
RECOMPILED_DIR: recompiled${{ inputs.recompiled_dir }}
JSON_FILE: docs/docs/driving_models_${{ inputs.target_hardware == 'usbgpu' && 'usbgpu_v' || 'v' }}${{ inputs.json_version }}.json
JSON_FILE: docs/docs/driving_models_${{ inputs.target_hardware == 'chestnut' && 'chestnut_v' || 'v' }}${{ inputs.json_version }}.json
jobs:
build_model:
@@ -146,7 +146,7 @@ jobs:
- name: Validate hf_repo and JSON version
env:
HF_OIDC_RESOURCE: datasets/${{ inputs.hf_repo }}
HF_TOKEN: ${{ secrets.HF_TOKEN }}
run: |
if [ ! -f "$JSON_FILE" ]; then
echo "JSON file $JSON_FILE does not exist!"
@@ -155,13 +155,8 @@ jobs:
python3 -c "
import sys
from huggingface_hub import HfApi
try:
api = HfApi()
api.repo_info(repo_id=sys.argv[1], repo_type='dataset')
print(f'Success: Repo {sys.argv[1]} exists.')
except Exception as e:
print('HF validation failed:', e)
sys.exit(1)
HfApi().repo_info(repo_id=sys.argv[1], repo_type='dataset')
print(f'Success: Repo {sys.argv[1]} exists.')
" "${{ inputs.hf_repo }}"
- name: Download artifact name file
@@ -192,7 +187,7 @@ jobs:
- name: Upload to Hugging Face
env:
HF_OIDC_RESOURCE: datasets/${{ inputs.hf_repo }}
HF_TOKEN: ${{ secrets.HF_TOKEN }}
ARTIFACT_NAME: ${{ steps.read-artifact-name.outputs.artifact_name }}
run: |
hf upload ${{ inputs.hf_repo }} \
@@ -0,0 +1,73 @@
name: Download HF model chunks
description: Resolve and download model chunks from HuggingFace in parallel
inputs:
hf_repo:
description: HuggingFace dataset repo
required: true
models:
description: 'JSON array of {hf_path, onnx_hash, canonical} objects'
required: true
dest_dir:
description: Destination directory for downloaded chunks
required: true
runs:
using: composite
steps:
- name: Download model chunks
shell: bash
env:
HF_REPO: ${{ inputs.hf_repo }}
MODELS_JSON: ${{ inputs.models }}
DEST_DIR: ${{ inputs.dest_dir }}
run: |
set -eo pipefail
DOWNLOAD_LIST=$(mktemp)
resolve_chunks() {
local HF_PATH="$1" ONNX_HASH="$2" CANONICAL="$3" DEST_DIR="$4"
local JSON_URL="https://huggingface.co/datasets/${HF_REPO}/resolve/main/${HF_PATH}/default_models.json"
local DEFAULTS BUNDLE ARTIFACT BASE_URL NUM_CHUNKS
DEFAULTS=$(curl -fsSL "$JSON_URL")
BUNDLE=$(echo "$DEFAULTS" | jq --arg hash "$ONNX_HASH" '.bundles[] | select(.onnx_sha256 == $hash)')
ARTIFACT=$(echo "$BUNDLE" | jq -r '.models[0].artifact')
BASE_URL=$(echo "$ARTIFACT" | jq -r '.download_uri.url' | sed 's|/[^/]*$||')
NUM_CHUNKS=$(echo "$ARTIFACT" | jq -r '.chunks | length')
mkdir -p "$DEST_DIR"
while IFS= read -r CHUNK_NAME; do
CHUNK_IDX=$(echo "$CHUNK_NAME" | grep -oP 'chunk\K[0-9]+of[0-9]+' || true)
if [ -z "$CHUNK_IDX" ]; then
echo "::error::Failed to parse chunk index from: $CHUNK_NAME"
return 1
fi
ENCODED_URL=$(python3 -c "import urllib.parse; print(urllib.parse.quote('${BASE_URL}/${CHUNK_NAME}', safe=':/'))")
printf '%s\t%s\n' "$ENCODED_URL" "${DEST_DIR}/${CANONICAL}.chunk${CHUNK_IDX}" >> "$DOWNLOAD_LIST"
done < <(echo "$ARTIFACT" | jq -r '.chunks[].file_name')
echo "$NUM_CHUNKS" > "${DEST_DIR}/${CANONICAL}.chunkmanifest"
if [ "$CANONICAL" = "dmonitoring_model_tinygrad.pkl" ]; then
for warp in dm_warp_1928x1208_tinygrad.pkl dm_warp_1344x760_tinygrad.pkl; do
ENCODED_URL=$(python3 -c "import urllib.parse; print(urllib.parse.quote('${BASE_URL}/${warp}', safe=':/'))")
printf '%s\t%s\n' "$ENCODED_URL" "${DEST_DIR}/${warp}" >> "$DOWNLOAD_LIST"
done
fi
}
echo "$MODELS_JSON" | jq -c '.[]' | while IFS= read -r model; do
HF_PATH=$(echo "$model" | jq -r '.hf_path')
ONNX_HASH=$(echo "$model" | jq -r '.onnx_hash')
CANONICAL=$(echo "$model" | jq -r '.canonical')
resolve_chunks "$HF_PATH" "$ONNX_HASH" "$CANONICAL" "$DEST_DIR"
done
TOTAL=$(wc -l < "$DOWNLOAD_LIST")
echo "Downloading $TOTAL chunks with 8 parallel connections..."
xargs -P8 -d'\n' -I{} bash -c '
URL="${1%% *}"
DEST="${1#* }"
echo "Downloading $(basename "$DEST")"
curl -fsSL --retry 3 --retry-delay 5 -o "$DEST" "$URL"
' _ {} < "$DOWNLOAD_LIST"
rm -f "$DOWNLOAD_LIST"
@@ -31,7 +31,7 @@ on:
type: string
default: ''
target_hardware:
description: 'Hardware target to compile for (qcom or usbgpu)'
description: 'Hardware target to compile for (qcom or chestnut)'
required: false
type: string
default: 'qcom'
@@ -57,7 +57,7 @@ on:
type: choice
options:
- qcom
- usbgpu
- chestnut
default: 'qcom'
@@ -102,7 +102,7 @@ jobs:
cat $GITHUB_OUTPUT
- run: |
cd ${{ github.workspace }}/openpilot/openpilot
if [ "${{ inputs.target_hardware }}" != "usbgpu" ]; then
if [ "${{ inputs.target_hardware }}" != "chestnut" ]; then
git lfs pull -X "**/selfdrive/modeld/models/big_*.onnx,**/selfdrive/modeld/models/dmonitoring_*.onnx"
rm -f selfdrive/modeld/models/big_*.onnx selfdrive/modeld/models/dmonitoring_*.onnx
else
@@ -121,7 +121,7 @@ jobs:
if-no-files-found: error
build_model:
runs-on: [self-hosted, usbgpu]
runs-on: [self-hosted, "${{ inputs.target_hardware == 'chestnut' && 'chestnut' || 'tici' }}"]
needs: get_model
env:
MODEL_NAME: ${{ inputs.custom_name || inputs.upstream_branch }} (${{ needs.get_model.outputs.model_date }})
@@ -185,10 +185,10 @@ jobs:
CAMERA_RES=$(python3 -c "from openpilot.common.transformations.camera import _ar_ox_fisheye as a, _os_fisheye as o; print(f'{a.width}x{a.height} {o.width}x{o.height}')")
TG_FLAGS_QCOM="DEV=QCOM IMAGE=1 FLOAT16=1 NOLOCALS=1 JIT_BATCH_SIZE=0 OPENPILOT_HACKS=1"
if [ "${{ inputs.target_hardware }}" == "usbgpu" ]; then
echo "USBGPU build"
export USBGPU=1
TG_FLAGS="DEBUG=2 DEV=USB+AMD:LLVM WARP_DEV=QCOM FLOAT16=1 JIT_BATCH_SIZE=0 GMMU=0 TC_OPT=2"
if [ "${{ inputs.target_hardware }}" == "chestnut" ]; then
echo "CHESTNUT build"
export CHESTNUT=1
TG_FLAGS="DEBUG=1 DEV=USB+AMD:LLVM FLOAT16=1 JIT_BATCH_SIZE=0 GMMU=0 TC_OPT=2 TC_OCCUPANCY_OPT=1"
OUTPUT_PKL="${{ env.MODELS_DIR }}/big_driving_tinygrad.pkl"
else
echo "QCOM build"
+303 -99
View File
@@ -39,6 +39,8 @@ jobs:
include_big_model: ${{ steps.strategy.outputs.include_big_model }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Extract deploy strategy
id: strategy
run: |
@@ -96,6 +98,8 @@ jobs:
}}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Wait for Tests
uses: ./.github/workflows/wait-for-action # Path to where you place the action
with:
@@ -119,6 +123,7 @@ jobs:
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 1
submodules: recursive
ref: ${{ env.SOURCE_BRANCH }}
repository: ${{ github.event.pull_request.head.repo.fork && github.event.pull_request.head.repo.full_name || github.repository }}
@@ -165,7 +170,7 @@ jobs:
scons -j1 cache_dir="$SCONS_CACHE" --minimal \
openpilot/selfdrive/locationd openpilot/sunnypilot/selfdrive/locationd
echo "Building rest of sunnypilot"
/usr/bin/time -v scons -j$(nproc) cache_dir="$SCONS_CACHE" --minimal
SKIP_TINYGRAD_COMPILE=1 /usr/bin/time -v scons -j$(nproc) cache_dir="$SCONS_CACHE" --minimal
touch ${BUILD_DIR}/prebuilt
if [[ "${{ runner.debug }}" == "1" ]]; then
ls -la ${BUILD_DIR}
@@ -211,91 +216,245 @@ jobs:
needs: [ prepare_strategy ]
runs-on: ubuntu-24.04
if: ${{ needs.prepare_strategy.outputs.include_big_model == 'true' }}
concurrency:
group: prepare-chestnut
cancel-in-progress: false
outputs:
onnx_sha256: ${{ steps.resolve.outputs.onnx_sha256 }}
env:
GH_REPO: ${{ github.repository }}
HF_REPO: sunnypilot/sunnypilot_models_v1
HF_DEFAULTS_PATH: models/defaults/big
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.head_ref || github.ref_name }}
- run: git lfs pull -I "openpilot/selfdrive/modeld/models/big_driving_supercombo.onnx"
- name: Check HF defaults and build if needed
- name: Resolve ONNX hash and tinygrad ref via API
id: resolve
run: |
ACTUAL_ONNX_HASH=$(sha256sum "openpilot/selfdrive/modeld/models/big_driving_supercombo.onnx" | cut -d' ' -f1)
echo "Repo ONNX hash: $ACTUAL_ONNX_HASH"
REF="${{ github.head_ref || github.ref_name }}"
BLOB_SHA=$(gh api "repos/${GH_REPO}/contents/openpilot/selfdrive/modeld/models/big_driving_supercombo.onnx?ref=${REF}" --jq '.sha')
ONNX_HASH=$(gh api "repos/${GH_REPO}/git/blobs/${BLOB_SHA}" --jq '.content' | base64 -d | grep '^oid sha256:' | cut -d: -f2)
echo "ONNX hash: $ONNX_HASH"
[ -n "$ONNX_HASH" ] || { echo "::error::Failed to extract ONNX hash"; exit 1; }
echo "onnx_sha256=$ONNX_HASH" >> $GITHUB_OUTPUT
TINYGRAD_REF=$(gh api "repos/${GH_REPO}/contents/tinygrad_repo?ref=${REF}" --jq '.sha')
echo "tinygrad ref: $TINYGRAD_REF"
JSON_URL="https://huggingface.co/datasets/${HF_REPO}/resolve/main/${HF_DEFAULTS_PATH}/default_models.json"
check_hash() {
DEFAULTS=$(curl -fsSL "$JSON_URL" 2>/dev/null) || return 1
BUNDLE=$(echo "$DEFAULTS" | jq --arg hash "$ACTUAL_ONNX_HASH" '.bundles[] | select(.onnx_sha256 == $hash)' 2>/dev/null)
check_defaults() {
DEFAULTS=$(curl -fsSL "${JSON_URL}?t=$(date +%s)" 2>/dev/null) || return 1
TINYGRAD_MATCH=$(echo "$DEFAULTS" | jq -r --arg ref "$TINYGRAD_REF" '.tinygrad_ref == $ref' 2>/dev/null)
[ "$TINYGRAD_MATCH" = "true" ] || return 1
BUNDLE=$(echo "$DEFAULTS" | jq --arg hash "$ONNX_HASH" '.bundles[] | select(.onnx_sha256 == $hash)' 2>/dev/null)
[ -n "$BUNDLE" ] && [ "$BUNDLE" != "null" ]
}
if check_hash; then
echo "HF defaults match repo ONNX"
else
echo "No matching model on HF — triggering build"
gh workflow run build-default-big-model.yaml --ref "${{ github.head_ref || github.ref_name }}"
echo "Waiting for build to start..."
sleep 120
RUN_ID=$(gh run list --workflow=build-default-big-model.yaml --branch="${{ github.head_ref || github.ref_name }}" --limit=1 --json databaseId --jq '.[0].databaseId')
if [ -z "$RUN_ID" ] || [ "$RUN_ID" = "null" ]; then
echo "::error::Failed to find build-default-big-model run"
exit 1
fi
echo "Waiting for run $RUN_ID..."
gh run watch "$RUN_ID"
CONCLUSION=$(gh run view "$RUN_ID" --json conclusion --jq '.conclusion')
if [ "$CONCLUSION" != "success" ]; then
echo "::error::build-default-big-model failed: $CONCLUSION"
exit 1
fi
if ! check_hash; then
echo "::error::HF defaults still don't match after build"
exit 1
fi
if check_defaults; then
echo "HF defaults match repo ONNX hash and tinygrad ref"
exit 0
fi
echo "No matching model on HF — dispatching build"
gh workflow run build-default-models.yaml --ref "$REF" -f target=big
sleep 10
BUILD_RUN_ID=$(gh run list --workflow build-default-models.yaml --branch "$REF" --limit 1 --json databaseId --jq '.[0].databaseId')
echo "Dispatched build run: $BUILD_RUN_ID"
echo "Waiting for build run to complete..."
for i in $(seq 1 90); do
sleep 30
STATUS=$(gh api "repos/${GH_REPO}/actions/runs/${BUILD_RUN_ID}" --jq '.status')
CONCLUSION=$(gh api "repos/${GH_REPO}/actions/runs/${BUILD_RUN_ID}" --jq '.conclusion')
echo "Poll $i/90: status=$STATUS conclusion=$CONCLUSION"
if [ "$STATUS" = "completed" ]; then
if [ "$CONCLUSION" = "success" ]; then
echo "Build run succeeded, verifying HF..."
sleep 10
if check_defaults; then
echo "Big model verified on HF"
exit 0
fi
echo "::error::Build succeeded but model not found on HF"
exit 1
else
echo "::error::Build run failed with conclusion=$CONCLUSION"
exit 1
fi
fi
done
echo "::error::Build run did not complete within 45 minutes"
exit 1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Download big model chunks
- name: Cancel run on failure
if: failure()
run: gh run cancel ${{ github.run_id }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
prepare_small_model:
needs: [ prepare_strategy ]
runs-on: ubuntu-24.04
concurrency:
group: prepare-small-model
cancel-in-progress: false
outputs:
driving_onnx_sha256: ${{ steps.resolve.outputs.driving_onnx_sha256 }}
env:
GH_REPO: ${{ github.repository }}
HF_REPO: sunnypilot/sunnypilot_models_v1
HF_DEFAULTS_PATH: models/defaults/small
steps:
- name: Resolve ONNX hash and tinygrad ref via API
id: resolve
run: |
ACTUAL_ONNX_HASH=$(sha256sum "openpilot/selfdrive/modeld/models/big_driving_supercombo.onnx" | cut -d' ' -f1)
REF="${{ github.head_ref || github.ref_name }}"
BLOB_SHA=$(gh api "repos/${GH_REPO}/contents/openpilot/selfdrive/modeld/models/driving_supercombo.onnx?ref=${REF}" --jq '.sha')
DRIVING_HASH=$(gh api "repos/${GH_REPO}/git/blobs/${BLOB_SHA}" --jq '.content' | base64 -d | grep '^oid sha256:' | cut -d: -f2)
echo "Driving ONNX hash: $DRIVING_HASH"
[ -n "$DRIVING_HASH" ] || { echo "::error::Failed to extract driving ONNX hash"; exit 1; }
echo "driving_onnx_sha256=$DRIVING_HASH" >> $GITHUB_OUTPUT
TINYGRAD_REF=$(gh api "repos/${GH_REPO}/contents/tinygrad_repo?ref=${REF}" --jq '.sha')
echo "tinygrad ref: $TINYGRAD_REF"
JSON_URL="https://huggingface.co/datasets/${HF_REPO}/resolve/main/${HF_DEFAULTS_PATH}/default_models.json"
DEFAULTS=$(curl -fsSL "$JSON_URL")
BUNDLE=$(echo "$DEFAULTS" | jq --arg hash "$ACTUAL_ONNX_HASH" '.bundles[] | select(.onnx_sha256 == $hash)')
mkdir -p big_model_chunks
ARTIFACT=$(echo "$BUNDLE" | jq -r '.models[0].artifact')
BASE_URL=$(echo "$ARTIFACT" | jq -r '.download_uri.url' | sed 's|/[^/]*$||')
NUM_CHUNKS=$(echo "$ARTIFACT" | jq -r '.chunks | length')
check_defaults() {
DEFAULTS=$(curl -fsSL "${JSON_URL}?t=$(date +%s)" 2>/dev/null) || return 1
TINYGRAD_MATCH=$(echo "$DEFAULTS" | jq -r --arg ref "$TINYGRAD_REF" '.tinygrad_ref == $ref' 2>/dev/null)
[ "$TINYGRAD_MATCH" = "true" ] || return 1
DRIVING=$(echo "$DEFAULTS" | jq --arg hash "$DRIVING_HASH" '.bundles[] | select(.onnx_sha256 == $hash)' 2>/dev/null)
[ -n "$DRIVING" ] && [ "$DRIVING" != "null" ] || return 1
}
CANONICAL="big_driving_tinygrad.pkl"
echo "$ARTIFACT" | jq -r '.chunks[].file_name' | while read CHUNK_NAME; do
CHUNK_IDX=$(echo "$CHUNK_NAME" | grep -oP 'chunk\K[0-9]+of[0-9]+')
CANONICAL_CHUNK="${CANONICAL}.chunk${CHUNK_IDX}"
ENCODED_URL=$(python3 -c "import urllib.parse; print(urllib.parse.quote('${BASE_URL}/${CHUNK_NAME}', safe=':/'))")
echo "Downloading $CHUNK_NAME -> $CANONICAL_CHUNK"
curl -fsSL -o "big_model_chunks/${CANONICAL_CHUNK}" "$ENCODED_URL"
if check_defaults; then
echo "HF defaults match repo ONNX hash and tinygrad ref"
exit 0
fi
echo "No matching model on HF — dispatching build"
gh workflow run build-default-models.yaml --ref "$REF" -f target=small
sleep 10
BUILD_RUN_ID=$(gh run list --workflow build-default-models.yaml --branch "$REF" --limit 1 --json databaseId --jq '.[0].databaseId')
echo "Dispatched build run: $BUILD_RUN_ID"
echo "Waiting for build run to complete..."
for i in $(seq 1 60); do
sleep 30
STATUS=$(gh api "repos/${GH_REPO}/actions/runs/${BUILD_RUN_ID}" --jq '.status')
CONCLUSION=$(gh api "repos/${GH_REPO}/actions/runs/${BUILD_RUN_ID}" --jq '.conclusion')
echo "Poll $i/60: status=$STATUS conclusion=$CONCLUSION"
if [ "$STATUS" = "completed" ]; then
if [ "$CONCLUSION" = "success" ]; then
echo "Build run succeeded, verifying HF..."
sleep 10
if check_defaults; then
echo "Small model verified on HF"
exit 0
fi
echo "::error::Build succeeded but model not found on HF"
exit 1
else
echo "::error::Build run failed with conclusion=$CONCLUSION"
exit 1
fi
fi
done
echo "$NUM_CHUNKS" > "big_model_chunks/${CANONICAL}.chunkmanifest"
echo "::error::Small model build did not complete within 30 minutes"
exit 1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Upload big model chunks
uses: actions/upload-artifact@v4
with:
name: big-model-chunks
path: big_model_chunks/
compression-level: 0
- name: Cancel run on failure
if: failure()
run: gh run cancel ${{ github.run_id }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
prepare_dm_model:
needs: [ prepare_strategy ]
runs-on: ubuntu-24.04
concurrency:
group: prepare-dm-model
cancel-in-progress: false
outputs:
dm_onnx_sha256: ${{ steps.resolve.outputs.dm_onnx_sha256 }}
env:
GH_REPO: ${{ github.repository }}
HF_REPO: sunnypilot/sunnypilot_models_v1
HF_DEFAULTS_PATH: models/defaults/dm
steps:
- name: Resolve ONNX hash and tinygrad ref via API
id: resolve
run: |
REF="${{ github.head_ref || github.ref_name }}"
BLOB_SHA=$(gh api "repos/${GH_REPO}/contents/openpilot/selfdrive/modeld/models/dmonitoring_model.onnx?ref=${REF}" --jq '.sha')
DM_HASH=$(gh api "repos/${GH_REPO}/git/blobs/${BLOB_SHA}" --jq '.content' | base64 -d | grep '^oid sha256:' | cut -d: -f2)
echo "DM ONNX hash: $DM_HASH"
[ -n "$DM_HASH" ] || { echo "::error::Failed to extract DM ONNX hash"; exit 1; }
echo "dm_onnx_sha256=$DM_HASH" >> $GITHUB_OUTPUT
TINYGRAD_REF=$(gh api "repos/${GH_REPO}/contents/tinygrad_repo?ref=${REF}" --jq '.sha')
echo "tinygrad ref: $TINYGRAD_REF"
JSON_URL="https://huggingface.co/datasets/${HF_REPO}/resolve/main/${HF_DEFAULTS_PATH}/default_models.json"
check_defaults() {
DEFAULTS=$(curl -fsSL "${JSON_URL}?t=$(date +%s)" 2>/dev/null) || return 1
TINYGRAD_MATCH=$(echo "$DEFAULTS" | jq -r --arg ref "$TINYGRAD_REF" '.tinygrad_ref == $ref' 2>/dev/null)
[ "$TINYGRAD_MATCH" = "true" ] || return 1
DM=$(echo "$DEFAULTS" | jq --arg hash "$DM_HASH" '.bundles[] | select(.onnx_sha256 == $hash)' 2>/dev/null)
[ -n "$DM" ] && [ "$DM" != "null" ] || return 1
}
if check_defaults; then
echo "HF defaults match DM ONNX hash and tinygrad ref"
exit 0
fi
echo "No matching DM model on HF — dispatching build"
gh workflow run build-default-models.yaml --ref "$REF" -f target=dm
sleep 10
BUILD_RUN_ID=$(gh run list --workflow build-default-models.yaml --branch "$REF" --limit 1 --json databaseId --jq '.[0].databaseId')
echo "Dispatched build run: $BUILD_RUN_ID"
echo "Waiting for build run to complete..."
for i in $(seq 1 60); do
sleep 30
STATUS=$(gh api "repos/${GH_REPO}/actions/runs/${BUILD_RUN_ID}" --jq '.status')
CONCLUSION=$(gh api "repos/${GH_REPO}/actions/runs/${BUILD_RUN_ID}" --jq '.conclusion')
echo "Poll $i/60: status=$STATUS conclusion=$CONCLUSION"
if [ "$STATUS" = "completed" ]; then
if [ "$CONCLUSION" = "success" ]; then
echo "Build run succeeded, verifying HF..."
sleep 10
if check_defaults; then
echo "DM model verified on HF"
exit 0
fi
echo "::error::Build succeeded but DM model not found on HF"
exit 1
else
echo "::error::Build run failed with conclusion=$CONCLUSION"
exit 1
fi
fi
done
echo "::error::DM model build did not complete within 30 minutes"
exit 1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Cancel run on failure
if: failure()
@@ -305,23 +464,24 @@ jobs:
publish:
concurrency:
# We do a bit of a hack here to avoid canceling the publishing job if a new commit comes in while we're publishing by adding the sha to the group name.
# This means that if multiple commits come in while we're publishing, they will be queued up and publish one after the other.
# Otherwise, if a job is waiting to be published due to environment wait time, it would be canceled by a new commit and restart the wait time.
group: ${{ needs.prepare_strategy.outputs.publish_concurrency_group }}
cancel-in-progress: ${{ needs.prepare_strategy.outputs.cancel_publish_in_progress == 'true' }}
if: ${{
always() && !cancelled() &&
needs.build.result == 'success' &&
needs.prepare_strategy.result == 'success' &&
needs.prepare_small_model.result == 'success' &&
needs.prepare_dm_model.result == 'success' &&
(!contains(github.event_name, 'pull_request') || (github.event.action == 'labeled' && github.event.label.name == 'prebuilt')) &&
(needs.prepare_strategy.outputs.include_big_model != 'true' || needs.prepare_chestnut.result == 'success')
}}
needs: [ build, prepare_strategy, prepare_chestnut ]
needs: [ build, prepare_strategy, prepare_chestnut, prepare_small_model, prepare_dm_model ]
runs-on: ubuntu-24.04
environment: ${{ needs.prepare_strategy.outputs.environment }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Download prebuilt artifact
uses: actions/download-artifact@v4
@@ -333,23 +493,16 @@ jobs:
mkdir -p ${{ env.OUTPUT_DIR }}
tar xzf prebuilt.tar.gz -C ${{ env.OUTPUT_DIR }}
- name: Prepare chestnut output
if: ${{ needs.prepare_chestnut.result == 'success' }}
run: |
mkdir -p "${{ github.workspace }}/chestnut_output"
tar xzf prebuilt.tar.gz -C "${{ github.workspace }}/chestnut_output"
- name: Download big model chunks
if: ${{ needs.prepare_chestnut.result == 'success' }}
uses: actions/download-artifact@v4
- name: Download model chunks from HF
uses: ./.github/workflows/download-hf-model-chunks
with:
name: big-model-chunks
path: big_model_chunks
- name: Inject big model into chestnut
if: ${{ needs.prepare_chestnut.result == 'success' }}
run: |
cp big_model_chunks/* "${{ github.workspace }}/chestnut_output/openpilot/selfdrive/modeld/models/"
hf_repo: sunnypilot/sunnypilot_models_v1
dest_dir: ${{ env.OUTPUT_DIR }}/openpilot/selfdrive/modeld/models
models: |
[
{"hf_path": "models/defaults/small", "onnx_hash": "${{ needs.prepare_small_model.outputs.driving_onnx_sha256 }}", "canonical": "driving_tinygrad.pkl"},
{"hf_path": "models/defaults/dm", "onnx_hash": "${{ needs.prepare_dm_model.outputs.dm_onnx_sha256 }}", "canonical": "dmonitoring_model_tinygrad.pkl"}
]
- name: Configure Git
run: |
@@ -371,22 +524,6 @@ jobs:
"https://x-access-token:${{github.token}}@github.com/sunnypilot/sunnypilot.git" \
"${{ needs.prepare_strategy.outputs.extra_version_identifier }}"
- name: Publish chestnut branch
if: ${{ needs.prepare_chestnut.result == 'success' }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
CHESTNUT_BRANCH="${{ needs.prepare_strategy.outputs.new_branch }}-chestnut"
CHESTNUT_DIR="${{ github.workspace }}/chestnut_output"
${{ env.CI_DIR }}/publish.sh \
"${{ github.workspace }}" \
"$CHESTNUT_DIR" \
"$CHESTNUT_BRANCH" \
"${{ needs.prepare_strategy.outputs.version }}" \
"https://x-access-token:${{github.token}}@github.com/sunnypilot/sunnypilot.git" \
"${{ needs.prepare_strategy.outputs.extra_version_identifier }}"
- name: Tag ${{ needs.prepare_strategy.outputs.environment }}
if: ${{ needs.prepare_strategy.outputs.is_stable_branch == 'true' && (github.event_name != 'push' || !startsWith(github.ref, 'refs/tags/')) }}
run: |
@@ -394,12 +531,77 @@ jobs:
git tag -f -a ${TAG} -m "${{ needs.prepare_strategy.outputs.environment }} @ ${{ needs.prepare_strategy.outputs.version }} of build ${{ needs.prepare_strategy.outputs.build }}."
git push -f origin ${TAG}
publish_chestnut:
concurrency:
group: ${{ needs.prepare_strategy.outputs.publish_concurrency_group }}-chestnut
cancel-in-progress: ${{ needs.prepare_strategy.outputs.cancel_publish_in_progress == 'true' }}
if: ${{
always() && !cancelled() &&
needs.build.result == 'success' &&
needs.prepare_strategy.result == 'success' &&
needs.prepare_small_model.result == 'success' &&
needs.prepare_dm_model.result == 'success' &&
needs.prepare_chestnut.result == 'success' &&
(!contains(github.event_name, 'pull_request') || (github.event.action == 'labeled' && github.event.label.name == 'prebuilt'))
}}
needs: [ build, prepare_strategy, prepare_chestnut, prepare_small_model, prepare_dm_model ]
runs-on: ubuntu-24.04
environment: ${{ needs.prepare_strategy.outputs.environment }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Download prebuilt artifact
uses: actions/download-artifact@v4
with:
name: prebuilt
- name: Untar prebuilt
run: |
mkdir -p ${{ env.OUTPUT_DIR }}
tar xzf prebuilt.tar.gz -C ${{ env.OUTPUT_DIR }}
- name: Download model chunks from HF
uses: ./.github/workflows/download-hf-model-chunks
with:
hf_repo: sunnypilot/sunnypilot_models_v1
dest_dir: ${{ env.OUTPUT_DIR }}/openpilot/selfdrive/modeld/models
models: |
[
{"hf_path": "models/defaults/small", "onnx_hash": "${{ needs.prepare_small_model.outputs.driving_onnx_sha256 }}", "canonical": "driving_tinygrad.pkl"},
{"hf_path": "models/defaults/dm", "onnx_hash": "${{ needs.prepare_dm_model.outputs.dm_onnx_sha256 }}", "canonical": "dmonitoring_model_tinygrad.pkl"},
{"hf_path": "models/defaults/big", "onnx_hash": "${{ needs.prepare_chestnut.outputs.onnx_sha256 }}", "canonical": "big_driving_tinygrad.pkl"}
]
- name: Configure Git
run: |
git config --global user.email "github-actions[bot]@users.noreply.github.com"
git config --global user.name "github-actions[bot]"
- name: Publish chestnut branch
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
CHESTNUT_BRANCH="${{ needs.prepare_strategy.outputs.new_branch }}-chestnut"
${{ env.CI_DIR }}/publish.sh \
"${{ github.workspace }}" \
"${{ env.OUTPUT_DIR }}" \
"$CHESTNUT_BRANCH" \
"${{ needs.prepare_strategy.outputs.version }}" \
"https://x-access-token:${{github.token}}@github.com/sunnypilot/sunnypilot.git" \
"${{ needs.prepare_strategy.outputs.extra_version_identifier }}"
notify:
needs:
- prepare_strategy
- build
- publish
- publish_chestnut
- prepare_chestnut
- prepare_small_model
- prepare_dm_model
runs-on: ubuntu-24.04
if: ${{ (always() && !cancelled() && !failure())
&& needs.publish.result == 'success'
@@ -407,6 +609,8 @@ jobs:
&& (fromJSON(vars.DEV_FEEDBACK_NOTIFICATION_BRANCHES_V2)[github.head_ref || github.ref_name] != null) }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Prepare notification message
id: message
+60
View File
@@ -0,0 +1,60 @@
# Selected-action 20 Hz cadence experiment
The selected-action controller now sends LateralMotionControl2 every fifth
100 Hz update (20 Hz, nominally 50 ms). Each send uses the latest published
C0/C1. The v6 controller module is byte-identical to `c70a9ee84`: its geometry,
150 ms forecast, caps, two states, and 4 m/s / 0.5 rad/s slew are unchanged.
C2 and C3 remain zero. This tests transport cadence, not a new strength gain.
The existing default-off `FordModelActionController` Sunnylink setting is
snapshotted by `card` into `CarParamsSP.flags` on the supported CAN FD Lightning.
Both controller selection and send cadence use that snapshot. An onroad setting
write cannot switch either one. Complete an offroad-to-onroad cycle after updating.
The diagnostic hypothesis remains `model-action-measured-pose-v6`; the build
commit, CarParamsSP flag, and measured send cadence distinguish this experiment.
With the toggle off, existing CAN FD controllers retain 100 Hz transmission.
Legacy CAN stays at 20 Hz. Panda safety is byte-identical to `c21a9013`.
The sender retains Panda's existing per-message C2 slew bound even on the
20 Hz path; this controller does not use C2. No safety limit is relaxed.
Counters advance once per transmitted request, including wrap from 15 to 0.
Invalid paths zero the next scheduled request; disengagement sends mode 0 on
that request, without a new ramp-out sequence. Relative to a 100 Hz sender,
a change can wait up to four more control ticks (nominally 40 ms).
## Evidence and limits
On route `84865544361f55cb_000000a5--d0f935d323`, the camera's observed inactive
LMC2 stream ran at 19.993 Hz (7,952 messages, median 50.051 ms). Our sender ran
at 99.321 Hz (38,496 messages, median 9.938 ms). The camera data does not establish
the factory's active-mode cadence. All supplied v1v6 drives already used
100 Hz; cadence has not been established as the cause of weak tracking.
The [transport replay record](ford_model_action_cadence_validation.json) covers
38,496 recorded send cycles at each of five possible scheduling phases:
192,480 sender updates and 38,496 transmitted requests in total. Every emitted
C0/C1 exactly matched the corresponding recorded request; C2/C3 stayed zero;
mode, counter and checksum checks passed. Every request passed the unchanged,
compiled Panda TX hook with controls eligibility set from the recorded mode.
This tests TX bounds, not a full Panda RX watchdog, vehicle response, or device boot.
Targeted tests additionally cover exact send intervals, latest-sample delivery,
counter wrap, every disengagement/invalid-input phase, unchanged fallback cadence,
100 Hz core slew, and a shared selection snapshot surviving serialization and
subsequent stored-toggle changes. The broader offline run passed 689 tests and
9,146 subtests; 178 inherited safety cases were skipped as inapplicable.
Ruff and typechecking of the touched production modules passed.
No physical tracking improvement is claimed. The next drive must establish
whether the lower cadence helps ordinary bends and turn exits, while checking
for added turn-in delay. Offline replay cannot predict that closed-loop response.
## Reproduce
Initialize the pinned submodule and use the project's built Python/native environment:
```sh
export PYTHONDONTWRITEBYTECODE=1
export PYTHONPATH=.:opendbc_repo
python -m pytest -q openpilot/selfdrive/controls/tests/test_ford*.py tools/ford_pscm_lab opendbc_repo/opendbc/car/ford/tests/test_ford.py openpilot/sunnypilot/sunnylink/tests openpilot/sunnypilot/mads/tests openpilot/selfdrive/car/tests/test_ford_pscm_status.py openpilot/common/tests/test_params.py opendbc_repo/opendbc/safety/tests/test_ford.py
```
@@ -0,0 +1,45 @@
{
"scope": "Frozen a5 publications through actual 20Hz CarController and unchanged compiled Panda TX hook, all five scheduling phases. No physical response simulation.",
"route": "84865544361f55cb_000000a5--d0f935d323",
"baseline_root": "c70a9ee84bbf6db9a687d63ef674ed4db9c13e3a",
"baseline_opendbc": "c21a9013700734dd20b09e05aa68329ad8cc20f9",
"input_sha256": "815d1e248ff5c3e5e5cfc11dfbd0690d0d13ddafe1436890e239975a72dae9b8",
"phases": [
{
"phase": 0,
"input_cycles": 38496,
"sent_and_accepted": 7700
},
{
"phase": 1,
"input_cycles": 38496,
"sent_and_accepted": 7699
},
{
"phase": 2,
"input_cycles": 38496,
"sent_and_accepted": 7699
},
{
"phase": 3,
"input_cycles": 38496,
"sent_and_accepted": 7699
},
{
"phase": 4,
"input_cycles": 38496,
"sent_and_accepted": 7699
}
],
"core_byte_identical": true,
"ford_safety_byte_identical": true,
"source_sha256": {
"openpilot/selfdrive/controls/lib/ford_model_action.py": "326539389b04034932db41ca2c67794779403b2c0e97f408a81a9f5734114899",
"openpilot/selfdrive/controls/controlsd.py": "002d57a0b5b6e4e3a04789bee20b9b175a71d6893dd06074cebca2738f3986da",
"openpilot/sunnypilot/mads/helpers.py": "24970993d37242fe8a0457bac118e2c265c4c32427dc930c0001de12e5f204dd",
"opendbc_repo/opendbc/car/ford/carcontroller.py": "591b0d8455d256f7504cbd2a6a11fe1a54ba6ad3e12ac16a6952dbd92274f303",
"opendbc_repo/opendbc/car/ford/values.py": "edadaacc13581642917d1fba473ef3efc90d7145dbe801915f860747a4294046",
"opendbc_repo/opendbc/safety/modes/ford.h": "1d9d996292d6697ab4f02d55fae348d6aca1df94a07f7bdae48b68971b91afe7",
".cache/ford_cadence/replay.py": "7227f0b4f130be0e5b28f517416ffbf281f0b33085b8391f66814aa531c3e514"
}
}
+153
View File
@@ -0,0 +1,153 @@
# Offline Ford selected-action candidate
This document and `ford_model_action_validation.json` record the offline
stage committed as `7ca3c6e3b`. The candidate is now available behind a
separate default-off Sunnylink toggle; see
[drive-test setup and validation](ford_model_action_drive_test.md).
The counts, source hashes and selector status below describe that earlier
stage, not the subsequent wiring change.
The decision is `C0 = current model y(7 m)`,
`C1 = max(7 m, speed × 1 s) × selected upstream-limited desiredCurvature`,
with C2=C3=0. The 7 m station and one-second scale are engineering choices,
not identified PSCM gains. `calibration_approved=false`.
`openpilot/selfdrive/controls/lib/ford_model_action.py` contains the core
and a separate adapter compatible with the existing controlsd call.
At that stage, the production selector, v8 implementation, settings, opendbc
submodule and Panda safety remained unchanged. Tests injected the adapter
offline; there was no production setting. No hardware or CAN transmission
occurs in the lab tools.
## Construction and integration
Only the unquantized C0 and C1 slew positions persist in the core.
Each field is clipped independently (±5.11 m / ±0.5 rad), slewed independently
(4 m/s / 0.5 rad/s), then packed using the existing Float32/sign-negation
rounding contract (0.01 m / 0.0005 rad). Heading overflow is not transferred
to C0. No yaw integral, blend, additional curvature contribution, reference
filter, turn modes, or 10 m C1 cap is introduced.
The selected standalone implementation from worktree 3548 is the provenance
for this law. Its two-state packer has been moved into the library core so
the controller does not depend on experimental lab code. Invalid numeric
types, overflowing arc geometry and malformed paths reset the core instead
of throwing or retaining a command.
Arc stations use cumulative model x/y distance, not forward x. As in the
reviewed standalone core, a path ending before 7 m holds its available
endpoint instead of extrapolating. This matters: route95 contains 44 active
cycles with 5.456.94 m of path at 2.783.46 m/s. A tested strict 7 m
coverage gate would have introduced disengagements and was removed. There
is no speed-dependent C0 horizon beyond this existing endpoint behavior.
The adapter retains the existing input age allowance (5 to +150 ms),
speed domain (0.355 m/s), yaw sanity bound (±3 rad/s), selected curvature
sanity bound (±1/m), and control interval (2100 ms). It rejects backward
model/measurement timestamps and invalid services. Repeated timestamps may
continue slew, but geometry is validated again on each tick. Disengagement,
invalid inputs and timing faults clear all command and adapter timing state.
The first valid tick after reset uses 10 ms, as v8 does.
controlsd still owns reference selection, upstream curvature limiting,
service health and engagement. Tests execute its actual source-selection
and limiter code, its Ford call, Float32 publication in ControlsExt, conversion
to CarControlSP, and the pinned Ford CarController's in-memory CAN builder.
Both model-action and maneuver-planner selection are covered, including
disabling latActive after invalid output. Only the test chooses the adapter.
Yaw is not an input to the control law. The adapter checks it solely for the
inherited invalid-input policy. Driver override and optional PSCM status
do not modify the candidate base; existing engagement and downstream driver
arbitration remain responsible for authorization, as with v8's base request.
## Offline evidence
The checked-in `ford_model_action_validation.json` records the completed
checks and source hashes. Full arrays and detailed reports are generated
locally under `.cache/ford_model_action/`; original route files are read-only.
Completed validation: **264 Ford tests and 150 subtests pass**, including
120 new core/adapter/replay-validator cases. The candidate module has 100%
statement and branch coverage (78 statements, 24 branches). Ruff and Ty pass.
The 200,000-cycle numerical stress test also checks 200,000 mirrored core
updates and 18,138 field-boundary cases. Across route and stress runs,
485,238 Float32/CAN round trips pass. Eight deliberately injected faults
(heading gain/cap, erased C0, wrong C0 slew, retained invalid state, stale
model acceptance, model clock rollback and reversed C0 sign) are all caught
by the tests. Mutation runs replace code only inside isolated Python
processes; production source files are never modified by those probes.
Independent Standards and Spec reviews reported zero findings. The full
suite's Params setting test uses an existing local native library from
worktree 3548 after checking relevant source files are byte-identical;
its hash and provenance are in the manifest. That library is an ignored
test dependency, not part of this change. This is the full relevant Ford
suite, not the hardware-dependent test suite for every openpilot subsystem.
The replay has two separate passes:
* Core compatibility uses the archived eligibility mask and requires exact
equality with the independently implemented `action_heading` commands.
* Adapter reconstruction derives eligibility from recorded service streams
independently of the archived output mask. It retains original timestamps,
gaps and consumed model frames. Controls publication time proxies the
unlogged computation clock, and complete SubMaster health is unavailable.
All 54,738 route95 and 78,812 route90 core cycles match exactly, including
37,614 and 73,055 active cycles. The adapter preserves those active counts.
Its 59 / 19 changed commands arise solely from the fresh 10 ms engagement
tick instead of the archived harness's preceding publication interval;
the replay checks that attribution on every cycle. Maximum differences are
0.01 m / 0.001 rad (95) and 0.02 m / 0.002 rad (90).
Every core and adapter replay output is round-tripped through Float32 and
the real CAN packer/parser, including zero C2/C3, signs, mode and counter.
Continuous field slew and quantization allowance are checked separately
from immediate invalid-command resets. The original driver-clean cohorts,
speed strata and command RMS are reproduced without redoing the encoder search.
The numerical stress harness uses analytic rotated paths, scalar slew
arithmetic, mirrored requests, irregular intervals and invalid-input resets.
It also sweeps every representable host field value and the Float32 values
immediately below, at and above every half-quantum boundary. Direct CAN
packing of the continuous state must agree with the host's quantized output.
The unit tests cover releases, reversals, clipping, service freshness,
clock resets, malformed inputs, endpoint fallback and actual integration.
## Limits of the result
On turns at ≥15 m/s, candidate C0 RMS is 79%/81% below v8 on routes95/90,
while C1 is 33%/41% higher. Those are command changes, not evidence of
equivalent steering authority. The PSCM's independent C0/C1 response remains
unknown. Replay cannot establish physical model following, strong turns,
centering, overshoot, oscillation or closed-loop stability.
The release probe is intentionally explicit: a model bend can increase
while selected curvature decreases. At 20 m/s, one synthetic probe changes
C0/C1 from 0.24 m / 0.10 rad to 0.49 m / 0.08 rad. Zero selected curvature
sets the C1 target to zero but does not erase a nonzero current model C0.
Removing a yaw-integral tail does not prove that physical overshoot is solved.
No additional release policy or unsupported plant model is added to hide
that uncertainty.
## Reproduce
From this worktree, use the logged construction dependency explicitly:
```sh
export PYTHONDONTWRITEBYTECODE=1
export PYTHONPATH=.:/Users/ibpersonal/.codex/worktrees/b926/sunnypilot/opendbc_repo
PY=/Users/ibpersonal/dev/sunnypilot/.venv/bin/python
EVIDENCE=/Users/ibpersonal/.codex/worktrees/3548/sunnypilot/analysis/controller_search_20260904
$PY -m pytest -q -p no:cacheprovider openpilot/selfdrive/controls/tests/test_ford_*.py tools/ford_pscm_lab openpilot/selfdrive/car/tests/test_ford_pscm_status.py
$PY -m tools.ford_pscm_lab.model_action_replay "$EVIDENCE/route95" --output .cache/ford_model_action/route95
$PY -m tools.ford_pscm_lab.model_action_replay "$EVIDENCE/route90" --output .cache/ford_model_action/route90
$PY -m tools.ford_pscm_lab.stress_model_action --cycles 200000 --seed 20260907 --output .cache/ford_model_action/stress.json
```
The route replay refuses an opendbc revision other than
`72a775d35e54c21ff5c5798acef22016eedcc0a7`. Stress defaults to this pin and
also accepts an explicitly required commit with `--opendbc-revision` for
deployment checks. A mismatch still fails. This historical pin reproduces
logged construction; it does not change the merge's submodule pointer.
+88
View File
@@ -0,0 +1,88 @@
# Experimental Ford offset damping, v2
This document and its validation counts describe the archived v2 source. The
[current v4 experiment](ford_model_action_full_prediction.md) uses full path prediction.
Segment 10 of the supplied route9b recording shows measured turning persisting
as requested right curvature falls. At about 643.0 s, before strong driver
intervention, device-gyro curvature is approximately 0.01786/m against a
0.01172/m request. Around 643.9 s, heading demand has reversed slightly but
C0 still requests approximately +0.12 m into the turn. Strong column input
starts around 643.852 s; later motion cannot establish autonomous recovery.
Earlier light driver input also exists.
The outgoing CAN commands match preceding publications. All 70,937 decoded
frames have zero C2/C3 and valid checksums. Focused exit diagnostics have fresh
model/carState inputs and targets within normal quantization of the outputs.
This supports trying less residual C0; it does not identify PSCM dynamics or
prove C0 alone caused the physical oversteer.
## Change
C0 starts from the clipped current model offset at 7 m. When C0 and measured
host yaw point in the same direction, compute:
```
requested_yaw = max(0, sign(C0) * speed * desiredCurvature)
excess = max(0, sign(C0) * yaw - requested_yaw - 0.02 rad/s)
reduction = 7 m * 0.2 s * excess
target = sign(C0) * max(0, abs(C0) - reduction)
```
Opposing centering demand is unchanged. The correction cannot increase the
target's magnitude or reverse its sign. Opposite-direction planned curvature
cannot amplify a small yaw bias into a correction. Existing 4 m/s C0 slew still
applies; this target bound is not a claim that every stateful output is smaller than a
separate v1 controller after arbitrary direction reversals. C1 construction,
clipping and slew are unchanged; C2=C3=0. Only C0/C1 slew states persist.
There is no integral, model-history filter, turn state machine or fitted plant.
Host yaw is `-carState.yawRate`, as in the existing Ford call path. The
0.02 rad/s deadband exceeds the approximately 0.008 rad/s offset measured
against the device gyro on quiet straights. The 0.2 s scale is an initial
engineering choice, not an identified delay or gain. Both remain physically
unvalidated. Large biased or noisy yaw within the existing sanity gate can
still attenuate useful centering; fixed-input replay cannot establish stability.
## Offline evidence
The complete 12-rlog route is replayed at original controls publication times,
with exact consumed model geometry, causal carState, and carControl matched
within 5 ms. These times proxy computation; full SubMaster health is unavailable.
V1 reconstruction is within one field quantum of all 64,701 paired active
publications. V2 has identical eligibility and exactly identical C1.
On 381.78 seconds of driver-clean low requests above 8 m/s, only 3 of 37,937 cycles
change C0, each by one 0.01 m quantum. Across the 642.7643.852 s exit window,
C0 changes on all 115 cycles, averaging 0.080 m reduction. Entry/peak C0
maximum stays 2.80 m; some entry-window samples decrease by up to 0.05 m.
These are command comparisons on recorded inputs, not predicted tracking.
Low request means requested lateral acceleration below 0.15 m/s²; it is a
proxy for straight driving and does not establish a physically straight path.
The original routes90/95 also run through v2. Their zero-yaw baseline pass
checks archived v1 compatibility; the measured-yaw adapter pass checks current
construction, eligibility, field limits and packing. It is not an exact match
to v1 or v8. Randomized testing checks the damping against an independent
piecewise oracle, mirror symmetry, resets and slew, with real Float32/CAN
round trips. See `ford_model_action_damping_validation.json` for counts and hashes.
Final validation passes 325 tests and 26 subtests, with 100% controller
statement/branch coverage, 204,946 original route cycles and 628,030 CAN round
trips. The module is 166 total lines, including 107 code lines excluding
comments, blanks and docstrings. Standards and Spec reviews have no remaining findings.
## Reproduce
Use the dependency setup and suite command in the [drive-test guide](ford_model_action_drive_test.md).
The new route replay requires the deployment opendbc pin recorded there:
```sh
python -m tools.ford_pscm_lab.damping_replay /path/to/complete/rlogs --baseline v1 --candidate v2 --window segment10_entry_peak 637 640 --window segment10_exit_before_strong_input 642.7 643.852 --output /path/to/separate/results
python -m tools.ford_pscm_lab.stress_model_action --cycles 200000 --seed 20260907 --opendbc-revision c21a9013700734dd20b09e05aa68329ad8cc20f9 --output /path/to/stress.json
```
At the v2 revision, the same default-off Sunnylink toggle selects v2; no additional setting is
introduced. Updating an installation with the toggle already enabled selects
v2 at the next controlsd startup. `calibration_approved=false` remains explicit.
No physical fix, hardware build or device boot is established by these checks.
@@ -0,0 +1,164 @@
{
"date": "2026-09-07",
"baseline_commit": "5fc16abc7662020706e29f57d31a6d5e2bc1293a",
"deployment_target": {
"repository": "sunnypilot/sunnypilot",
"branch": "hiimisaac-dev"
},
"hypothesis": "model-action-c0-c1-yaw-damping-v2",
"scope": "Experimental bounded offset damping; fixed-input offline evidence only.",
"calibration_approved": false,
"hardware_build_and_device_boot": "not performed",
"controller_size": {
"total_lines": 166,
"code_lines_excluding_blanks_comments_docstrings": 107,
"core_persistent_values": 2,
"adapter_timestamps": 3
},
"checks": {
"combined_ford_params_sunnylink_suite": "325 passed, 26 subtests passed; no skips",
"suite_log_sha256": "69cb8ab40e93e00a9ff7b6ea1933e4c3554336746860efb53dad3b29f94fc9d3",
"coverage": {
"statements": 98,
"branches": 28,
"percent": 100.0
},
"ruff": "pass",
"ty_controller_and_lab": "pass",
"settings_compiler_check": "pass",
"standards_review_remaining_findings": 0,
"spec_review_remaining_findings": 0,
"review_resolutions": [
"Prevent opposite-direction planned curvature amplifying small yaw bias; eight new cases failed before the fix and passed after it.",
"Relabel requested-acceleration cohort as low request; it does not establish physically straight driving."
],
"mutation_probe": "Disabling damping fails all four mirrored recorded-exit cases."
},
"stress": {
"random_cycles": 200000,
"mirrored_core_updates": 200000,
"field_boundary_cases": 18138,
"float32_can_round_trips": 218138,
"bounded_excess_yaw_damping_checked": true,
"opendbc_import_head": "c21a9013700734dd20b09e05aa68329ad8cc20f9"
},
"routes": {
"route90": {
"cycles": 78812,
"core_exact_archived_match": true,
"adapter_active_cycles": 73055,
"adapter_matches_current_core_with_yaw_and_fresh_engagement_dt": true,
"adapter_validity_differs_from_archive_cycles": 0,
"adapter_command_differs_from_archive_cycles": 2280,
"adapter_max_absolute_command_difference_c0_c1": [
0.1900000000000004,
0.0020000000000000018
],
"float32_can_round_trips": 157624,
"opendbc_import_head": "72a775d35e54c21ff5c5798acef22016eedcc0a7"
},
"route95": {
"cycles": 54738,
"core_exact_archived_match": true,
"adapter_active_cycles": 37614,
"adapter_matches_current_core_with_yaw_and_fresh_engagement_dt": true,
"adapter_validity_differs_from_archive_cycles": 0,
"adapter_command_differs_from_archive_cycles": 2051,
"adapter_max_absolute_command_difference_c0_c1": [
0.22999999999999998,
0.0010000000000000009
],
"float32_can_round_trips": 109476,
"opendbc_import_head": "72a775d35e54c21ff5c5798acef22016eedcc0a7"
},
"route9b": {
"cycles": 71396,
"eligible_cycles": 64701,
"same_validity": true,
"c1_exactly_unchanged": true,
"field_slew_zero_c2_c3_pass": true,
"float32_can_round_trips": 142792,
"v1_reconstruction_vs_recorded": {
"paired_cycles": 64701,
"within_one_quantum_cycles": 64701,
"maximum_absolute_error_c0_c1": [
0.010000114440917862,
0.0005000143051147043
]
},
"timing": "Publication-time proxy, causal carState, exact consumed model; full SubMaster health unavailable.",
"baseline": "Current adapter with zero yaw retains v1 targets and actual-yaw sanity gate.",
"opendbc_head": "c21a9013700734dd20b09e05aa68329ad8cc20f9",
"cohorts": {
"driver_clean_low_request_above_8mps": {
"cycles": 37937,
"seconds": 381.77672216900055,
"changed_c0_cycles": 3,
"mean_absolute_c0_change_m": 8.419774473786704e-07,
"max_absolute_c0_change_m": 0.009999999999999787,
"v1_peak_absolute_c0_m": 0.08999999999999986,
"v2_peak_absolute_c0_m": 0.08999999999999986
},
"segment10_entry_peak": {
"cycles": 298,
"seconds": 2.9932484459999387,
"changed_c0_cycles": 100,
"mean_absolute_c0_change_m": 0.007564164795694466,
"max_absolute_c0_change_m": 0.04999999999999982,
"v1_peak_absolute_c0_m": 2.8000000000000003,
"v2_peak_absolute_c0_m": 2.8000000000000003
},
"segment10_exit_before_strong_input": {
"cycles": 115,
"seconds": 1.156770048999988,
"changed_c0_cycles": 115,
"mean_absolute_c0_change_m": 0.0799313220721162,
"max_absolute_c0_change_m": 0.1200000000000001,
"v1_peak_absolute_c0_m": 0.75,
"v2_peak_absolute_c0_m": 0.7000000000000002
}
},
"exit_c0_strictly_lower_on_all_115_cycles": true,
"rlog_sha256_by_segment": {
"0": "22746f7119109b73ed7f2c26ce8c99f87136e9124fb7fc14c9554409a28a7c3f",
"1": "4c2e1d7083c31a2b37d0f8dd3be4d330898511b7e02c26f7d40ca9bc2779397d",
"2": "62f3e049e220cd3681fadf386f2969537bd571998ae2f6ba2d08479428b5a28f",
"3": "83bf0131b2d36b2ba7e5ba050bbc13c0a3350feb5c9b89dc9c87d3a37abebfb3",
"4": "430985a80dd6e10f7abeb89457a17022e6bb6978617f415c905f584b1647603e",
"5": "8c0c5ae6323ec33b3e14f84ca834f70cb56f6b29f471a350f1e3efc06b6ba553",
"6": "db53dfa8156b9d66792c3eff0b2ce5d31b71ad41cc580dec85f528845593c184",
"7": "91b0b3be10cb7d7d7f7dd2024d8f9ee99d1e9fd2204203a3a9a2f2f1c6e3fa03",
"8": "687dbbfc49837efbfe8fa6bc091e40f7fad2908832234d7884f4616d1bc9ccff",
"9": "a88ec4d25b04cdbf5844686fc77f6b28dca920c9b164e37ebf69844a3ae398fc",
"10": "fe6b29580a6c94e1c236d13e18db4cd9f31cc1b25d52e1e6e19a5021125c9932",
"11": "150d31b1944d7a1b8c562f3aee20b66cefa6c4e8d02660ec889907d835142f45"
}
}
},
"total_original_route_cycles": 204946,
"total_float32_can_round_trips": 628030,
"source_sha256": {
"docs/ford_model_action_damping.md": "1aca8ca8e78d953beeda5b0c9803161a1d7c58556b1966040c71f809c3960bf8",
"docs/ford_model_action_drive_test.md": "3ce8bf6cb511a4461fa7abf194f47020af7ef5a6c90fcae7ed09571d1f750846",
"openpilot/selfdrive/controls/lib/ford_model_action.py": "59d66297a017557f3d4f28b115be3f6220b800566c11935e2284b3814783fb7e",
"openpilot/selfdrive/controls/tests/test_ford_controlsd_logging.py": "318742bd707ae526d0f5181bf7f081660c2c55de4bdf28518ccdf50d60e88080",
"openpilot/selfdrive/controls/tests/test_ford_model_action_adapter.py": "de6f8524347f7c4a339941bc8565ccaa131cb93aa0418c75006ce08ab7edeb99",
"openpilot/selfdrive/controls/tests/test_ford_model_action_damping.py": "76466997ab4fef435f44339a6cb2d06303d5f0ab8717d487f27655297bd429d1",
"openpilot/sunnypilot/sunnylink/settings_ui.json": "dbb78c98f57eef532f0dff0cb0b38396442882876e6d115f0d3c36159f64baf5",
"openpilot/sunnypilot/sunnylink/settings_ui_src/pages/vehicle.yaml": "b92b55e23467e74227988fb39fff13a4ebba3c960fb90e34a6b341411699fc95",
"tools/ford_pscm_lab/damping_replay.py": "2f52fef12116ce87c0a1f465cf13d6af5e9fd76ce05d4318d72e34b70bc6a11f",
"tools/ford_pscm_lab/model_action_replay.py": "05a658dfcaf81693bf0d92184c0edff0172802f351a61a9e866a7967e74ae46d",
"tools/ford_pscm_lab/stress_model_action.py": "3e308733f4af0101ad0c414fbd724f6a99f56269ed8e791f0e15526d8bfd8f17"
},
"artifact_sha256": {
"route9b/report.json": "1b9317850c1724f433269c6a58349d0a0ee4eb6c9a03d6ec5858fa17713796c5",
"route9b/commands.npz": "d9f553a5384c84416a27c52b5dda0e751dda25edc5d6620fc311977f34a7a946",
"route90/report.json": "e37f71dc032e375b1c9b0beb4d6e0c915257bf72e59b0785ac0ca49c2472d2c2",
"route95/report.json": "d42d5a080fef8a1f0b3c7ae2cabcad20c88ce01d91be07ab33770ff5948c06b6",
"stress.json": "06e69a23340e3f5ed174e8e0b2e5791b320686dce7df963233d50a9982dca17b",
"coverage.json": "8f7915b9bd884abedfdbc2e0c18ef4474737e27225a714414384242535cc396f",
"mutation.txt": "67a76549fd7bb71e7092a155d4d0c3459be7b04dc2ca36eef9b54fbb574ad83d",
"bias_regression_red.txt": "8903c5a967e8c376050db85f7cf5f73abf71ee972f6025b25eb874479f94c62c",
"segment10_damping.png": "7d60cf9aabdcca9000fcf49bd14bd6130418aa6ab1b57af2678b171498ee9505"
}
}
+82
View File
@@ -0,0 +1,82 @@
# Ford model-point drive-test branch
The default-off **Selected-Action Path Tracking (Experimental)** Sunnylink toggle
now selects the C1 early-release candidate on the **Ford CAN FD F-150 Lightning**.
The stored key remains `FordModelActionController`; an already enabled setting
selects this revision after updating and completing an offroad-to-onroad cycle.
The controller reads **model lateral position and model heading at the same
point**. Start with the model's predicted distance at one second, enforce the
existing seven-metre minimum, and hold the available endpoint when necessary.
C0 is that point's lateral position in metres. C1 starts from its unwrapped
heading and is bounded toward zero using the model's terminal spatial curvature
at the same point. This asks for earlier release when the path straightens
ahead. C2 and C3 stay zero. See [the exact rule and its authority tradeoff](ford_model_release.md).
This point choice is an engineering guess, not an identified Ford reference or
PSCM calibration. `calibration_approved=false`: offline tests do not establish
physical tracking, turn-exit behavior, or stability across different PSCMs.
The [v7 model-point decision and validation](ford_model_points.md) is historical.
## Select and restore
1. Install branch `hiimisaac-dev` from `sunnypilot/sunnypilot` and allow the build
to finish.
2. While offroad, open Sunnylink device settings → Vehicle → Ford. Keep or enable
**Selected-Action Path Tracking (Experimental)**.
3. Complete a real offroad-to-onroad cycle. `card` snapshots the toggle into
`CarParamsSP`; the sender and `controlsd` share that selection. Changing a
stored toggle or disengaging alone cannot swap an active controller.
The startup event `Ford path controller selected` reports
`FordModelActionController`. Periodic `Ford C2-free path tracking` events report
`hypothesis=model-pose-terminal-c1-v1`, `pose_source=model`, `preview_time_s=1.0`
and `minimum_station_m=7.0`, plus input ages, slew state and the command tuple.
Active diagnostics also report `c1_release=terminal_spatial_curvature`.
Selected desired curvature is still logged, but does not construct C0/C1.
Turning the toggle off and completing another offroad-to-onroad cycle restores
**PSCM Coefficient Observer** if selected, otherwise the original Ford path
controller. Other vehicles keep their previous selection. The retired v8 toggle
cannot select this candidate.
## Wiring and limits
Both model fields must have matching, finite, strictly increasing time arrays
starting at zero. Malformed geometry, stale required services, invalid timing,
or disengagement resets both actuator states. Freshness still requires model,
car-state and reference publications no older than 150 ms, with at most 5 ms
future skew. The valid control timestep remains 2100 ms.
Only the two unquantized C0/C1 slew positions persist in the core. Field caps are
±5.11 m / ±0.5 rad and slew rates are 4 m/s / 0.5 rad/s. Calculation stays at
100 Hz; the existing [cadence experiment](ford_model_action_cadence.md) sends this
candidate at 20 Hz. Float32 publication, host-to-wire negation, packing and
Panda safety are unchanged. The opendbc pin remains
`87ca78e6e641eefb2d654f260a6ab08df3058bd5`.
Normal operation uses the model point with the one-sided C1 release bound. The upstream scalar curvature
and its clipping still exist for logging/other controllers, but no longer bound
this candidate's heading target. Its C0/C1 field caps and slew still apply;
passing Panda TX checks does not establish an actual vehicle acceleration bound.
Lateral maneuver test mode supplies only a scalar curvature, not a model pose.
It explicitly invalidates/disengages this candidate as `unsupported_reference`.
Optional measured motion is no longer a command input.
## Reproduce offline checks
Initialize the pinned submodule and build the project's native Python dependencies:
```sh
git submodule update --init opendbc_repo
export PYTHONDONTWRITEBYTECODE=1
export PYTHONPATH=.:opendbc_repo
python -m pytest -q openpilot/selfdrive/controls/tests/test_ford*.py tools/ford_pscm_lab opendbc_repo/opendbc/car/ford/tests/test_ford.py openpilot/sunnypilot/sunnylink/tests openpilot/sunnypilot/mads/tests openpilot/selfdrive/car/tests/test_ford_pscm_status.py openpilot/common/tests/test_params.py opendbc_repo/opendbc/safety/tests/test_ford.py
python -m tools.ford_pscm_lab.stress_model_action --cycles 200000 --seed 20260908 --opendbc-revision 87ca78e6e641eefb2d654f260a6ab08df3058bd5 --output .cache/ford_model_points/stress.json
```
Historical v1v7 validation files retain their original source hashes and apply
to those revisions. In particular, `ford_model_action_measured_pose_validation.json`
describes v6, not the current model-point mapping. The hardware build and device
boot are not performed by these offline checks. Pushing a branch does not update
a device or change its stored settings.
@@ -0,0 +1,145 @@
{
"date": "2026-09-07",
"baseline_commit": "7ca3c6e3b3e659c6f446039501c5826bbd14092e",
"branch": "codex/ford-model-action-drive-test",
"scope": "Default-off Sunnylink selection and v8 retirement; offline validation only. No device installation or physical performance validation.",
"calibration_approved": false,
"production_selector_changed": true,
"toggle": "FordModelActionController",
"default_enabled": false,
"v8_removed": true,
"panda_safety_changed": false,
"opendbc_submodule_changed": false,
"deployment_opendbc_head": "c21a9013700734dd20b09e05aa68329ad8cc20f9",
"controller_size": {
"total_lines": 145,
"code_lines_excluding_blanks_comments_docstrings": 95,
"core_persistent_values": 2,
"adapter_timestamps": 3,
"removed_v8_module_lines": 469
},
"tests": {
"combined_ford_params_sunnylink_suite": "284 passed, 26 subtests passed in 2.63s",
"suite_log_sha256": "2e223a507f0630481cf6f83b9f8893d226f3f4273a79a09fc35905aa875b1d2c",
"coverage": {
"covered_lines": 87,
"num_statements": 87,
"percent_covered": 100.0,
"percent_covered_display": "100",
"missing_lines": 0,
"excluded_lines": 0,
"percent_statements_covered": 100.0,
"percent_statements_covered_display": "100",
"num_branches": 26,
"num_partial_branches": 0,
"covered_branches": 26,
"missing_branches": 0,
"percent_branches_covered": 100.0,
"percent_branches_covered_display": "100"
},
"ruff": "pass",
"ty_controller_and_lab": "pass",
"settings_compiler_check": "pass",
"standards_review_remaining_findings": 0,
"spec_review_remaining_findings": 0,
"resolved_review_finding": "Updated YAML authoring source and regenerated settings JSON before final compiler/schema suite."
},
"routes": {
"route95": {
"cycles": 54738,
"core_active_cycles": 37614,
"core_exact_archived_match": true,
"cohorts_reproduced": true,
"adapter_active_cycles": 37614,
"adapter_exact_match_with_fresh_engagement_dt": true,
"adapter_validity_differs_from_archive_cycles": 0,
"adapter_command_differs_from_archive_cycles": 59,
"adapter_max_absolute_command_difference_c0_c1": [
0.010000000000000675,
0.0010000000000000009
],
"field_slew_zero_c2_c3_pass": true,
"float32_can_round_trips": 109476,
"opendbc_import_head": "72a775d35e54c21ff5c5798acef22016eedcc0a7",
"report_sha256": "72fab710dc81c8c7d9b75d371b97fec32402d3b01fa05517dfa814d8daff3134"
},
"route90": {
"cycles": 78812,
"core_active_cycles": 73055,
"core_exact_archived_match": true,
"cohorts_reproduced": true,
"adapter_active_cycles": 73055,
"adapter_exact_match_with_fresh_engagement_dt": true,
"adapter_validity_differs_from_archive_cycles": 0,
"adapter_command_differs_from_archive_cycles": 19,
"adapter_max_absolute_command_difference_c0_c1": [
0.020000000000000462,
0.0020000000000000018
],
"field_slew_zero_c2_c3_pass": true,
"float32_can_round_trips": 157624,
"opendbc_import_head": "72a775d35e54c21ff5c5798acef22016eedcc0a7",
"report_sha256": "cc2224bae597a341697a7681560a077cb209d77e4d060c0850997691c57d32fb"
}
},
"stress": {
"seed": 20260907,
"random_cycles": 200000,
"mirrored_core_updates": 200000,
"invalid_or_inactive_resets": 3537,
"field_boundary_cases": 18138,
"float32_can_round_trips": 218138,
"analytic_targets_scalar_slew_and_mirror_checks_pass": true,
"direct_raw_float32_packing_matches_host_output": true,
"max_continuous_step_c0_c1": [
0.40000000000000147,
0.05000000000000002
],
"calibration_approved": false,
"scope": "Numerical construction only; no PSCM response or closed-loop performance claims.",
"opendbc_import_head": "c21a9013700734dd20b09e05aa68329ad8cc20f9"
},
"total_float32_can_round_trips": 485238,
"native_params": {
"source": "Rebuilt locally from this branch with clang++ and generated Capnp headers; ignored test dependency, not committed binary.",
"library_sha256": "270bf43241cf7c02cc432cf78ec9411a62d7653ca445695efe785ae82241aa09",
"sources_sha256": {
"openpilot/common/params_c.cc": "57e3bcc7eba939bc91aadafb4ed1248b8123a8fe5c48fd8530298d966ea4db63",
"openpilot/common/params.cc": "a5adacb1d47cb3bf6e0d87d44ce158b41982d7eaf2e8114e32c48d3a6631304c",
"openpilot/common/params.h": "ed03d137e126ecd6f1608016020af18c0339fb987e27d0a2aa6830bba396970c",
"openpilot/common/params_keys.h": "39d36465f66405843b926ba18473fb6aee81c0f1c7bea87246aa08ffe3f67c58",
"openpilot/common/util.cc": "4479ecf72465e8f453d8af78447f7715f02d9397c58a49048f2bbc87a96d6b8a",
"openpilot/common/swaglog.cc": "9c2f88a2f1c3c4253b73defb264cc367a13ade23e02928e1d469b5c5833df176"
}
},
"test_dependency_notes": {
"python": "/Users/ibpersonal/dev/sunnypilot/.venv/bin/python",
"pyyaml": "6.0.3 from local uv cache",
"jsonschema": "Local cached package appended after venv to run schema validator without skips",
"hardware_build_and_device_boot": "not performed"
},
"source_sha256": {
"docs/ford_model_action_candidate.md": "c968132348d20891a9396f6e69db1315d570505796e19e09ffbb2da748e6e687",
"docs/ford_virtual_angle_experiment.md": "da6322f3c3d2d81463e44c50cc6cad1a962f97008ff9425c314da333ebe47a87",
"openpilot/common/params_c.cc": "57e3bcc7eba939bc91aadafb4ed1248b8123a8fe5c48fd8530298d966ea4db63",
"openpilot/common/params_keys.h": "39d36465f66405843b926ba18473fb6aee81c0f1c7bea87246aa08ffe3f67c58",
"openpilot/common/tests/test_params.py": "557a1f616af5fd9f5e623fbee0fd6f44cb059a30c290c68ce2b57e9bcceed081",
"openpilot/selfdrive/controls/controlsd.py": "102b383e5beff43b8dd7c219178bef62e8a4b54443ebe682694606862bcd4e7f",
"openpilot/selfdrive/controls/lib/ford_model_action.py": "8f3bc5d68e0051776f614a2ccffae84a88f7898dc95bdc12c23dcfe10dfe676a",
"openpilot/selfdrive/controls/tests/test_ford_controlsd_logging.py": "1c9448d88d8021e5d34a5dccd14a17c6c1bc64b5531342bfc6d15574d9d3e710",
"openpilot/selfdrive/controls/tests/test_ford_model_action_adapter.py": "2c5b14f814e84e59d749f61a43ef1dcfe06253f6e185443fa123c37525ac8466",
"openpilot/sunnypilot/sunnylink/settings_ui.json": "9974df3ac4cc58ae78d47848cd18ef4aca1bcbb00edb257f28b4220d92890528",
"openpilot/sunnypilot/sunnylink/settings_ui_src/pages/vehicle.yaml": "7a3fa18e562d5a03a5b85c72ee3f3ebeb836c497285dcd9aaad24f8fb4dd6942",
"openpilot/sunnypilot/sunnylink/tests/test_settings_schema.py": "3db566612381fd87d3655a3ccff470be7da998c4ea7f6365f53e58cdb9c0ffb7",
"tools/ford_pscm_lab/model_action_replay.py": "827a6dc488d554bdf6e87438c6a2a985b3195bf6d01ab09002bfd6049d22a868",
"tools/ford_pscm_lab/stress_model_action.py": "2d5c72cc4b8ae214f2f5a19a050fe138f5c2ab0294d92d24e2481af5f8185613",
"tools/ford_pscm_lab/test_model_action_replay.py": "ebf6bcd9260745100311521f8e11e85b7aebdd5561ab0876bfc2e802429d6896",
"openpilot/selfdrive/controls/tests/test_ford_model_action_selection.py": "f826c6328f0abac2a61f1a0a6f8d119fdbab858e363cb466d84ba9a7783059cd",
"docs/ford_model_action_drive_test.md": "d825b177cd099efd797fe89b7041695d6d41e4b9e8bba6bcaeb32aece164262b"
},
"deployment_target": {
"repository": "sunnypilot/sunnypilot",
"branch": "hiimisaac-dev",
"validated_code_commit": "ea1ed70c718d32539ef6b9a89b89c0e297c92e06"
}
}
+86
View File
@@ -0,0 +1,86 @@
# Experimental Ford full path prediction, v4
This document archives v4. The [current v5 controller](ford_model_action_no_yaw_damping.md)
retains this prediction and removes yaw damping.
V4 removes the extra 15 cm / 25% limit on the geometric prediction introduced
in [v3](ford_model_action_prediction.md). Those numbers were hand-chosen tuning
bounds, not identified Ford response limits. The current user request is to
remove that restriction; the existing default-off Sunnylink toggle remains.
The controller now uses the full predicted offset from the same model path,
assuming 150 ms of motion along the selected, upstream-limited curvature. The
150 ms horizon remains an engineering assumption. Available model geometry
still limits the prediction distance, with endpoint hold for short paths and
fallback to the valid base offset if prediction arithmetic is nonfinite.
The existing total command limits (C0 ±5.11 m, C1 ±0.5 rad), independent slew
rates (4 m/s, 0.5 rad/s), quantization, yaw damping, input/service gates and
zero C2/C3 remain unchanged. Only two control states persist. No integrator,
model history, extra toggle or PSCM feedback loop is added.
Removing the adjustment cap also permits the predicted C0 to oppose the
original offset or become nonzero from a zero original offset. For example,
a straight path with a nonzero selected turn request can have an opposing
future-frame offset. Tests cover that behavior, mirrored turn releases,
return to zero, and unchanged slew; sign preservation of the original C0 is
no longer claimed. The yaw damper still cannot reverse its input target.
## Evidence and interpretation
The PSCM reports a generic `LimitReached` state. It does not tell us whether
an incoming target is geometrically correct or well timed. Its internal
limits cannot establish the tracking performance of this predictor. Removal
is an experiment supported by command comparisons, not by an assumption that
the PSCM will correct an excessive or mistimed request.
Compared with capped v3 on identical recorded inputs:
| Interval | Effect of removing the extra cap |
| --- | --- |
| Latest tight-left entry, 173175.4 s | Mean C0 magnitude +0.032 m, maximum change 0.07 m |
| Earlier right entry, 637640 s | Mean magnitude +0.028 m, maximum change 0.10 m |
| Earlier right exit, 642.7643.852 s | All 115 commands unchanged |
| Driver-clean low requests above 8 m/s, routes9b/9e | Mean absolute change 0.0018 / 0.0014 m; maximum 0.02 m |
| All eligible samples on either recent route | Maximum absolute command change 0.13 m |
C1 and command eligibility are exactly identical to v3 on both recent routes.
Some command signs change near zero: this is an intended consequence of using
the full transform, not proof those corrections improve driving. Entry windows
include driver input, reported in the validation record. The magnitude changes
above describe controller C0, not measured lateral vehicle displacement.
The earlier v1 archive is also reproduced exactly on routes90/95, separately
from the current controller pass. All replay uses original timestamps;
publication times proxy computation, exact consumed model frames and causal
carState are retained, and complete SubMaster health is unavailable. The
recorded model and vehicle motion remain fixed. There is no measured physical
improvement, stability result or new desired-versus-actual steering trajectory.
Final validation passes 374 tests and 26 subtests with 100% controller statement
and branch coverage, 299,604 original route cycles and 817,346 Float32/CAN
round trips. The controller is 190 total lines / 122 code lines excluding
blanks, comments and docstrings; two control states persist.
See `ford_model_action_full_prediction_validation.json` for final test counts,
coverage, dependency pins, source hashes and route/packing results. The initial
uncapped variant was evaluated in a separate lab file before editing production;
final route checks execute the production v4 source.
## Reproduce and select
Use the dependencies and combined suite command in the
[drive-test guide](ford_model_action_drive_test.md). For the recent routes:
```sh
python -m tools.ford_pscm_lab.damping_replay /path/to/route9e/rlogs --baseline v3 --candidate current --window left_entry 173 175.4 --window left_peak 175.4 178.3 --window left_exit 178.3 180.5 --window reversal 728 734 --output /path/to/separate/route9e-results
python -m tools.ford_pscm_lab.damping_replay /path/to/route9b/rlogs --baseline v3 --candidate current --window right_entry 637 640 --window right_exit 642.7 643.852 --output /path/to/separate/route9b-results
python -m tools.ford_pscm_lab.stress_model_action --cycles 200000 --seed 20260907 --opendbc-revision c21a9013700734dd20b09e05aa68329ad8cc20f9 --output /path/to/stress.json
```
The same **Selected-Action Path Tracking (Experimental)** toggle selects v4
on the CAN FD F-150 Lightning. An installation with the toggle already enabled
selects v4 after updating and restarting controlsd. Diagnostics identify
`model-action-c0-c1-prediction-v4`; `calibration_approved=false` remains explicit.
Deployment branch: `sunnypilot/sunnypilot`, `hiimisaac-dev`. This work does not
install software on the device or change its settings.
@@ -0,0 +1,799 @@
{
"date": "2026-09-07",
"baseline_commit": "01f8d51c82b3e863f1012d383b5994813ef01b81",
"hypothesis": "model-action-c0-c1-prediction-v4",
"scope": "Removal of only the extra prediction adjustment cap; no physical tracking or stability claim.",
"deployment_target": {
"repository": "sunnypilot/sunnypilot",
"branch": "hiimisaac-dev"
},
"calibration_approved": false,
"hardware_build_and_device_boot": "not performed",
"controller_size": {
"total_lines": 190,
"code_lines_excluding_blanks_comments_docstrings": 122,
"core_persistent_values": 2,
"adapter_timestamps": 3
},
"checks": {
"combined_ford_params_sunnylink_suite": "374 passed, 26 subtests passed; no skips",
"coverage": {
"covered_lines": 113,
"num_statements": 113,
"percent_covered": 100.0,
"percent_covered_display": "100",
"missing_lines": 0,
"excluded_lines": 0,
"percent_statements_covered": 100.0,
"percent_statements_covered_display": "100",
"num_branches": 32,
"num_partial_branches": 0,
"covered_branches": 32,
"missing_branches": 0,
"percent_branches_covered": 100.0,
"percent_branches_covered_display": "100"
},
"ruff": "pass",
"ty_controller_and_lab": "pass",
"settings_compiler_check": "pass",
"cap_removal_red_probe": "11 tests fail with the v3 cap present; all 40 prediction tests pass after removal.",
"standards_review_remaining_findings": 0,
"spec_review_remaining_findings": 0,
"independent_review_verification": "Each reviewer passed 172 focused tests and verified source/artifact hashes and route/packing totals."
},
"stress": {
"seed": 20260907,
"random_cycles": 200000,
"mirrored_core_updates": 200000,
"invalid_or_inactive_resets": 3537,
"field_boundary_cases": 18138,
"float32_can_round_trips": 218138,
"analytic_targets_scalar_slew_and_mirror_checks_pass": true,
"bounded_excess_yaw_damping_checked": true,
"full_geometric_prediction_checked": true,
"direct_raw_float32_packing_matches_host_output": true,
"max_continuous_step_c0_c1": [
0.400000000000329,
0.05000000000000002
],
"calibration_approved": false,
"scope": "Numerical construction only; no PSCM response or closed-loop performance claims.",
"opendbc_import_head": "c21a9013700734dd20b09e05aa68329ad8cc20f9",
"source_sha256": {
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/stress_model_action.py": "66adef6cba120a1a3d8e9730ea987ef49f8fe283023362dbb99608dee0e3229f",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/openpilot/selfdrive/controls/lib/ford_model_action.py": "7a226cf3cdf6dc8c3b15829078a7b93e486ba4722c4a1d93ceb55dd2e6c77b21",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/model_action_replay.py": "7cddac7ce9e88cc32bc7afbec7f9df79b66bb4dfa2fa5f9b36654891fae23a19"
}
},
"routes": {
"route90": {
"scope": "Command construction and adapter reconstruction only; no counterfactual closed-loop score.",
"calibration_approved": false,
"executes_live_selector": false,
"cycles": 78812,
"core_active_cycles": 73055,
"core_exact_archived_match": true,
"cohorts_reproduced": true,
"adapter_active_cycles": 73055,
"adapter_status_counts": {
"inactive": 5757,
"active": 73055
},
"adapter_matches_current_core_with_yaw_and_fresh_engagement_dt": true,
"core_active_path_shorter_than_7m_cycles": 0,
"adapter_validity_differs_from_archive_cycles": 0,
"adapter_command_differs_from_archive_cycles": 42170,
"adapter_max_absolute_command_difference_c0_c1": [
0.33999999999999986,
0.0020000000000000018
],
"field_slew_zero_c2_c3_pass": true,
"float32_can_round_trips": 157624,
"timing": "Original controls publication timestamps proxy computation time; repeated frames and gaps retained. No identified delay.",
"eligibility": "Adapter checks recorded services independently; full SubMaster health is unavailable. Core uses archived validity.",
"reference": "Recorded controlsState.desiredCurvature, already selected/limited. These two routes have no maneuver publications.",
"host_yaw": "Extract cs.yaw equals -carState.yawRate; current adapter uses it for bounded damping.",
"archived_core_revision": "5fc16abc7662020706e29f57d31a6d5e2bc1293a",
"archived_core_source_sha256": "8f3bc5d68e0051776f614a2ccffae84a88f7898dc95bdc12c23dcfe10dfe676a",
"cohorts": {
"small_request": {
"seconds": 475.24161910000026,
"core_c0_c1_rms": [
0.06125184365338734,
0.0057692154738982855
],
"recorded_v8_c0_c1_rms": [
0.057970374224682646,
0.007060795713496332
],
"adapter_eligible_seconds": 475.24161910000026,
"adapter_c0_c1_rms": [
0.0666350702799088,
0.0057692154738982855
]
},
"turn": {
"seconds": 47.85871389900012,
"core_c0_c1_rms": [
0.18725251016049418,
0.046449510236699354
],
"recorded_v8_c0_c1_rms": [
0.4773534845264775,
0.04315935101392594
],
"adapter_eligible_seconds": 47.85871389900012,
"adapter_c0_c1_rms": [
0.21255148222786066,
0.046449510236699354
]
},
"small_speed_2_8": {
"seconds": 38.696717235999785,
"core_c0_c1_rms": [
0.209264275431127,
0.014019080484620104
],
"recorded_v8_c0_c1_rms": [
0.1593558816527209,
0.01823849401244768
],
"adapter_eligible_seconds": 38.696717235999785,
"adapter_c0_c1_rms": [
0.22422047076469706,
0.014019080484620104
]
},
"turn_speed_2_8": {
"seconds": 0.30159887100000304,
"core_c0_c1_rms": [
2.090789090165622,
0.21878942570759013
],
"recorded_v8_c0_c1_rms": [
2.155728831937131,
0.38375302254248855
],
"adapter_eligible_seconds": 0.30159887100000304,
"adapter_c0_c1_rms": [
2.2977717861671105,
0.21878942570759013
]
},
"small_speed_8_15": {
"seconds": 79.2599167590001,
"core_c0_c1_rms": [
0.02250905681378238,
0.005843904113223639
],
"recorded_v8_c0_c1_rms": [
0.03477379324171293,
0.006545440153133947
],
"adapter_eligible_seconds": 79.2599167590001,
"adapter_c0_c1_rms": [
0.027461818062006878,
0.005843904113223639
]
},
"turn_speed_8_15": {
"seconds": 0.022264622000001566,
"core_c0_c1_rms": [
0.20999999999999996,
0.04349999999999998
],
"recorded_v8_c0_c1_rms": [
0.25,
0.04050000011920929
],
"adapter_eligible_seconds": 0.022264622000001566,
"adapter_c0_c1_rms": [
0.20999999999999996,
0.04349999999999998
]
},
"small_speed_15_55": {
"seconds": 357.2849851050004,
"core_c0_c1_rms": [
0.011622097035118838,
0.003925571829874466
],
"recorded_v8_c0_c1_rms": [
0.03809717934501699,
0.0045587590980714285
],
"adapter_eligible_seconds": 357.2849851050004,
"adapter_c0_c1_rms": [
0.017138026451004092,
0.003925571829874466
]
},
"turn_speed_15_55": {
"seconds": 47.53485040600012,
"core_c0_c1_rms": [
0.08686835454709245,
0.043216347944464766
],
"recorded_v8_c0_c1_rms": [
0.4471065308273131,
0.030663943784303503
],
"adapter_eligible_seconds": 47.53485040600012,
"adapter_c0_c1_rms": [
0.10939067791228956,
0.043216347944464766
]
},
"pose_quiet": {
"seconds": 427.99642020700026,
"core_c0_c1_rms": [
0.01261133542366393,
0.00422389167351435
],
"recorded_v8_c0_c1_rms": [
0.03724956081568198,
0.0046880581807907966
],
"adapter_eligible_seconds": 427.99642020700026,
"adapter_c0_c1_rms": [
0.017590935519277658,
0.00422389167351435
]
}
},
"workspace_head": "01f8d51c82b3e863f1012d383b5994813ef01b81",
"opendbc_import_head": "72a775d35e54c21ff5c5798acef22016eedcc0a7",
"opendbc_import_path": "/Users/ibpersonal/.codex/worktrees/b926/sunnypilot/opendbc_repo",
"source_sha256": {
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/model_action_replay.py": "7cddac7ce9e88cc32bc7afbec7f9df79b66bb4dfa2fa5f9b36654891fae23a19",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/openpilot/selfdrive/controls/lib/ford_model_action.py": "7a226cf3cdf6dc8c3b15829078a7b93e486ba4722c4a1d93ceb55dd2e6c77b21",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/openpilot/selfdrive/controls/lib/ford_path.py": "383538fc7cdae3bc28dffb71fe12ac5f3f9866ffbe6adfb7457f3593e9fc903a",
"/Users/ibpersonal/.codex/worktrees/3548/sunnypilot/analysis/controller_search_20260904/route90/route.npz": "51e8c26eedde253e171af47d704c1967ba45ae6825d883393bec1fb9e00251c1",
"/Users/ibpersonal/.codex/worktrees/3548/sunnypilot/analysis/controller_search_20260904/route90/metadata.json": "742afec55ed629155b22d387f376f878f7b2765221fe94f515a829052bdd916f",
"/Users/ibpersonal/.codex/worktrees/3548/sunnypilot/analysis/controller_search_20260904/route90/encoder_comparison.npz": "7a625d3ed5cbd8013d1028aa3bc421740551dcae5c3d60981208bd047af9794c",
"/Users/ibpersonal/.codex/worktrees/3548/sunnypilot/analysis/controller_search_20260904/route90/encoder_comparison.json": "64e1cc4be84394ac7ec408d383b99b48ada9b3fd129cdef2fd846e5fac620d66",
"/Users/ibpersonal/.codex/worktrees/3548/sunnypilot/analysis/controller_search_20260904/route90/pose_candidate/pose_replay.npz": "4457ccc0868354749da5b72c1dea0faf750f783dfdc87038101288fdcba1e707"
}
},
"route95": {
"scope": "Command construction and adapter reconstruction only; no counterfactual closed-loop score.",
"calibration_approved": false,
"executes_live_selector": false,
"cycles": 54738,
"core_active_cycles": 37614,
"core_exact_archived_match": true,
"cohorts_reproduced": true,
"adapter_active_cycles": 37614,
"adapter_status_counts": {
"inactive": 17124,
"active": 37614
},
"adapter_matches_current_core_with_yaw_and_fresh_engagement_dt": true,
"core_active_path_shorter_than_7m_cycles": 44,
"adapter_validity_differs_from_archive_cycles": 0,
"adapter_command_differs_from_archive_cycles": 25912,
"adapter_max_absolute_command_difference_c0_c1": [
0.2999999999999998,
0.0010000000000000009
],
"field_slew_zero_c2_c3_pass": true,
"float32_can_round_trips": 109476,
"timing": "Original controls publication timestamps proxy computation time; repeated frames and gaps retained. No identified delay.",
"eligibility": "Adapter checks recorded services independently; full SubMaster health is unavailable. Core uses archived validity.",
"reference": "Recorded controlsState.desiredCurvature, already selected/limited. These two routes have no maneuver publications.",
"host_yaw": "Extract cs.yaw equals -carState.yawRate; current adapter uses it for bounded damping.",
"archived_core_revision": "5fc16abc7662020706e29f57d31a6d5e2bc1293a",
"archived_core_source_sha256": "8f3bc5d68e0051776f614a2ccffae84a88f7898dc95bdc12c23dcfe10dfe676a",
"cohorts": {
"small_request": {
"seconds": 160.93427628100014,
"core_c0_c1_rms": [
0.027283284164810895,
0.006054573235641941
],
"recorded_v8_c0_c1_rms": [
0.04189890174428264,
0.0075342351227490395
],
"adapter_eligible_seconds": 160.93427628100014,
"adapter_c0_c1_rms": [
0.03232621215993653,
0.006054573235641941
]
},
"turn": {
"seconds": 58.39837079799993,
"core_c0_c1_rms": [
0.5425346915921071,
0.09102985199238507
],
"recorded_v8_c0_c1_rms": [
0.63700479987207,
0.09691236120507696
],
"adapter_eligible_seconds": 58.39837079799993,
"adapter_c0_c1_rms": [
0.5674989495695087,
0.09102985199238507
]
},
"small_speed_2_8": {
"seconds": 8.083969085000064,
"core_c0_c1_rms": [
0.09941192804175551,
0.016572084086884566
],
"recorded_v8_c0_c1_rms": [
0.07597383189748476,
0.016403469943482765
],
"adapter_eligible_seconds": 8.083969085000064,
"adapter_c0_c1_rms": [
0.11170387210996009,
0.016572084086884566
]
},
"turn_speed_2_8": {
"seconds": 2.9056519089999995,
"core_c0_c1_rms": [
2.286598281178436,
0.3420570411209625
],
"recorded_v8_c0_c1_rms": [
2.4371443775668484,
0.37360610108051345
],
"adapter_eligible_seconds": 2.9056519089999995,
"adapter_c0_c1_rms": [
2.3669481787447944,
0.3420570411209625
]
},
"small_speed_8_15": {
"seconds": 32.021085868,
"core_c0_c1_rms": [
0.028387860150467858,
0.0067602044219821
],
"recorded_v8_c0_c1_rms": [
0.036941169841952046,
0.007349671742584858
],
"adapter_eligible_seconds": 32.021085868,
"adapter_c0_c1_rms": [
0.033317964465740625,
0.0067602044219821
]
},
"turn_speed_8_15": {
"seconds": 22.160645306000006,
"core_c0_c1_rms": [
0.28668316323581683,
0.0671915746138579
],
"recorded_v8_c0_c1_rms": [
0.3391022553367454,
0.07297678510653233
],
"adapter_eligible_seconds": 22.160645306000006,
"adapter_c0_c1_rms": [
0.32040564199313326,
0.0671915746138579
]
},
"small_speed_15_55": {
"seconds": 120.82922132800007,
"core_c0_c1_rms": [
0.010802222799973712,
0.004282512931613862
],
"recorded_v8_c0_c1_rms": [
0.03987958989535526,
0.0065793876512313535
],
"adapter_eligible_seconds": 120.82922132800007,
"adapter_c0_c1_rms": [
0.016212091011218406,
0.004282512931613862
]
},
"turn_speed_15_55": {
"seconds": 33.332073582999925,
"core_c0_c1_rms": [
0.07257996778378971,
0.03628926246224237
],
"recorded_v8_c0_c1_rms": [
0.3416081588451539,
0.027322786376822172
],
"adapter_eligible_seconds": 33.332073582999925,
"adapter_c0_c1_rms": [
0.08724830487921002,
0.03628926246224237
]
},
"pose_quiet": {
"seconds": 145.24136911700015,
"core_c0_c1_rms": [
0.012566204405760934,
0.004614194153565963
],
"recorded_v8_c0_c1_rms": [
0.03855743841849719,
0.006621274926345545
],
"adapter_eligible_seconds": 145.24136911700015,
"adapter_c0_c1_rms": [
0.01710948709543248,
0.004614194153565963
]
}
},
"workspace_head": "01f8d51c82b3e863f1012d383b5994813ef01b81",
"opendbc_import_head": "72a775d35e54c21ff5c5798acef22016eedcc0a7",
"opendbc_import_path": "/Users/ibpersonal/.codex/worktrees/b926/sunnypilot/opendbc_repo",
"source_sha256": {
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/model_action_replay.py": "7cddac7ce9e88cc32bc7afbec7f9df79b66bb4dfa2fa5f9b36654891fae23a19",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/openpilot/selfdrive/controls/lib/ford_model_action.py": "7a226cf3cdf6dc8c3b15829078a7b93e486ba4722c4a1d93ceb55dd2e6c77b21",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/openpilot/selfdrive/controls/lib/ford_path.py": "383538fc7cdae3bc28dffb71fe12ac5f3f9866ffbe6adfb7457f3593e9fc903a",
"/Users/ibpersonal/.codex/worktrees/3548/sunnypilot/analysis/controller_search_20260904/route95/route.npz": "6e5438867ea618e53ca395b60ff4b9b146256f1bf6f07dc8e92d663286074154",
"/Users/ibpersonal/.codex/worktrees/3548/sunnypilot/analysis/controller_search_20260904/route95/metadata.json": "f7c95a102e0e084bbcc9938399219080c1d3b26274b4ea3c3a9514ff7dc8ea8d",
"/Users/ibpersonal/.codex/worktrees/3548/sunnypilot/analysis/controller_search_20260904/route95/encoder_comparison.npz": "23bd05c4b6400299844acaba1d97051c96c23c682bf17b46e89e7f2cca5fce38",
"/Users/ibpersonal/.codex/worktrees/3548/sunnypilot/analysis/controller_search_20260904/route95/encoder_comparison.json": "1ab24a796d14817b8672ea6dfd39711d65cb06744eaaef7dcc02a840c392c4d3",
"/Users/ibpersonal/.codex/worktrees/3548/sunnypilot/analysis/controller_search_20260904/route95/pose_candidate/pose_replay.npz": "9cfbb3c6f0b1fdb7e3d38e6b64b8c94cffbcc9fabe41f6344d84a9b657b6af9b"
}
},
"route9b": {
"scope": "Fixed-input command replay only; no physical improvement or stability claim.",
"calibration_approved": false,
"cycles": 82386,
"eligible_cycles": 70703,
"status_counts": {
"inactive": 11683,
"active": 70703
},
"c1_exactly_unchanged": true,
"same_validity": true,
"field_slew_zero_c2_c3_pass": true,
"float32_can_round_trips": 164772,
"cohorts": {
"eligible": {
"cycles": 70703,
"seconds": 711.571703289,
"changed_c0_cycles": 10475,
"mean_absolute_c0_change_m": 0.00219777369954635,
"max_absolute_c0_change_m": 0.13000000000000078,
"increased_absolute_c0_cycles": 9822,
"decreased_absolute_c0_cycles": 633,
"driver_input_percent": 3.0297623863274845,
"baseline_peak_absolute_c0_m": 3.96,
"candidate_peak_absolute_c0_m": 3.99
},
"driver_clean": {
"cycles": 63267,
"seconds": 636.7469998759999,
"changed_c0_cycles": 8901,
"mean_absolute_c0_change_m": 0.0014713899370902674,
"max_absolute_c0_change_m": 0.05999999999999961,
"increased_absolute_c0_cycles": 8314,
"decreased_absolute_c0_cycles": 568,
"driver_input_percent": 0.0,
"baseline_peak_absolute_c0_m": 2.24,
"candidate_peak_absolute_c0_m": 2.24
},
"driver_clean_low_request_above_8mps": {
"cycles": 40656,
"seconds": 409.13067679800054,
"changed_c0_cycles": 7132,
"mean_absolute_c0_change_m": 0.0017587822497730527,
"max_absolute_c0_change_m": 0.020000000000000462,
"increased_absolute_c0_cycles": 6888,
"decreased_absolute_c0_cycles": 244,
"driver_input_percent": 0.0,
"baseline_peak_absolute_c0_m": 0.11000000000000032,
"candidate_peak_absolute_c0_m": 0.11000000000000032
},
"turn": {
"cycles": 5459,
"seconds": 54.939139939000256,
"changed_c0_cycles": 119,
"mean_absolute_c0_change_m": 0.00021719119817404867,
"max_absolute_c0_change_m": 0.010000000000000675,
"increased_absolute_c0_cycles": 119,
"decreased_absolute_c0_cycles": 0,
"driver_input_percent": 0.0,
"baseline_peak_absolute_c0_m": 2.24,
"candidate_peak_absolute_c0_m": 2.24
},
"right_entry": {
"cycles": 298,
"seconds": 2.9932484459999387,
"changed_c0_cycles": 216,
"mean_absolute_c0_change_m": 0.028050765542768245,
"max_absolute_c0_change_m": 0.10000000000000009,
"increased_absolute_c0_cycles": 216,
"decreased_absolute_c0_cycles": 0,
"driver_input_percent": 20.395198628296086,
"baseline_peak_absolute_c0_m": 2.95,
"candidate_peak_absolute_c0_m": 3.0300000000000002
},
"right_exit": {
"cycles": 115,
"seconds": 1.156770048999988,
"changed_c0_cycles": 0,
"mean_absolute_c0_change_m": 0.0,
"max_absolute_c0_change_m": 0.0,
"increased_absolute_c0_cycles": 0,
"decreased_absolute_c0_cycles": 0,
"driver_input_percent": 0.0,
"baseline_peak_absolute_c0_m": 0.7199999999999998,
"candidate_peak_absolute_c0_m": 0.7199999999999998
}
},
"baseline_commands_vs_recorded_publications": {
"paired_cycles": 70703,
"within_one_quantum_cycles": 65890,
"maximum_absolute_error_c0_c1": [
0.25000004768371564,
0.0005000143051147043
]
},
"timing": "Publication-time proxy, causal carState, exact consumed model; full SubMaster health unavailable.",
"baseline_version": "v3",
"baseline_revision": "01f8d51c82b3e863f1012d383b5994813ef01b81",
"candidate_version": "current",
"candidate_revision": "working_tree",
"focus_windows": [
[
"right_entry",
"637",
"640"
],
[
"right_exit",
"642.7",
"643.852"
]
],
"baseline_source_sha256": "5b9f7946f29b5fcd94ca93e53be14a12b18ae74d7ca27380e92b90c8c599dce0",
"candidate_source_sha256": "7a226cf3cdf6dc8c3b15829078a7b93e486ba4722c4a1d93ceb55dd2e6c77b21",
"source_rlog_sha256": {
"84865544361f55cb_0000009b--e4616dddaa--0--rlog.zst": "22746f7119109b73ed7f2c26ce8c99f87136e9124fb7fc14c9554409a28a7c3f",
"84865544361f55cb_0000009b--e4616dddaa--1--rlog.zst": "4c2e1d7083c31a2b37d0f8dd3be4d330898511b7e02c26f7d40ca9bc2779397d",
"84865544361f55cb_0000009b--e4616dddaa--2--rlog.zst": "62f3e049e220cd3681fadf386f2969537bd571998ae2f6ba2d08479428b5a28f",
"84865544361f55cb_0000009b--e4616dddaa--3--rlog.zst": "83bf0131b2d36b2ba7e5ba050bbc13c0a3350feb5c9b89dc9c87d3a37abebfb3",
"84865544361f55cb_0000009b--e4616dddaa--4--rlog.zst": "430985a80dd6e10f7abeb89457a17022e6bb6978617f415c905f584b1647603e",
"84865544361f55cb_0000009b--e4616dddaa--5--rlog.zst": "8c0c5ae6323ec33b3e14f84ca834f70cb56f6b29f471a350f1e3efc06b6ba553",
"84865544361f55cb_0000009b--e4616dddaa--6--rlog.zst": "db53dfa8156b9d66792c3eff0b2ce5d31b71ad41cc580dec85f528845593c184",
"84865544361f55cb_0000009b--e4616dddaa--7--rlog.zst": "91b0b3be10cb7d7d7f7dd2024d8f9ee99d1e9fd2204203a3a9a2f2f1c6e3fa03",
"84865544361f55cb_0000009b--e4616dddaa--8--rlog.zst": "687dbbfc49837efbfe8fa6bc091e40f7fad2908832234d7884f4616d1bc9ccff",
"84865544361f55cb_0000009b--e4616dddaa--9--rlog.zst": "a88ec4d25b04cdbf5844686fc77f6b28dca920c9b164e37ebf69844a3ae398fc",
"84865544361f55cb_0000009b--e4616dddaa--10--rlog.zst": "fe6b29580a6c94e1c236d13e18db4cd9f31cc1b25d52e1e6e19a5021125c9932",
"84865544361f55cb_0000009b--e4616dddaa--11--rlog.zst": "150d31b1944d7a1b8c562f3aee20b66cefa6c4e8d02660ec889907d835142f45",
"84865544361f55cb_0000009b--e4616dddaa--12--rlog.zst": "e105e5f703a70f36f1989c66fb46c35a65ff60a265b4332e10fa2e6875a2ced4",
"84865544361f55cb_0000009b--e4616dddaa--13--rlog.zst": "69304cf0c81401374f047c5022ca47d257ff9e59e30ec171325a2ee1e7ed13d4"
},
"opendbc_head": "c21a9013700734dd20b09e05aa68329ad8cc20f9",
"source_sha256": {
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/damping_replay.py": "8c4f1f987b65e2be146c473a6e529168f08b1f7ce3c9514e22a9874450fa430e",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/openpilot/selfdrive/controls/lib/ford_model_action.py": "7a226cf3cdf6dc8c3b15829078a7b93e486ba4722c4a1d93ceb55dd2e6c77b21"
}
},
"route9e": {
"scope": "Fixed-input command replay only; no physical improvement or stability claim.",
"calibration_approved": false,
"cycles": 83668,
"eligible_cycles": 74669,
"status_counts": {
"inactive": 8999,
"active": 74669
},
"c1_exactly_unchanged": true,
"same_validity": true,
"field_slew_zero_c2_c3_pass": true,
"float32_can_round_trips": 167336,
"cohorts": {
"eligible": {
"cycles": 74669,
"seconds": 751.5479497329998,
"changed_c0_cycles": 8108,
"mean_absolute_c0_change_m": 0.00147612892002983,
"max_absolute_c0_change_m": 0.13000000000000034,
"increased_absolute_c0_cycles": 7665,
"decreased_absolute_c0_cycles": 423,
"driver_input_percent": 2.7324748055386094,
"baseline_peak_absolute_c0_m": 4.38,
"candidate_peak_absolute_c0_m": 4.38
},
"driver_clean": {
"cycles": 66321,
"seconds": 667.5291971840106,
"changed_c0_cycles": 7257,
"mean_absolute_c0_change_m": 0.0012096637080993352,
"max_absolute_c0_change_m": 0.08000000000000007,
"increased_absolute_c0_cycles": 6883,
"decreased_absolute_c0_cycles": 374,
"driver_input_percent": 0.0,
"baseline_peak_absolute_c0_m": 2.4699999999999998,
"candidate_peak_absolute_c0_m": 2.4699999999999998
},
"driver_clean_low_request_above_8mps": {
"cycles": 39635,
"seconds": 398.9613925449994,
"changed_c0_cycles": 5427,
"mean_absolute_c0_change_m": 0.0013748276784907216,
"max_absolute_c0_change_m": 0.020000000000000462,
"increased_absolute_c0_cycles": 5341,
"decreased_absolute_c0_cycles": 86,
"driver_input_percent": 0.0,
"baseline_peak_absolute_c0_m": 0.07000000000000028,
"candidate_peak_absolute_c0_m": 0.07000000000000028
},
"turn": {
"cycles": 6219,
"seconds": 62.59491144700405,
"changed_c0_cycles": 244,
"mean_absolute_c0_change_m": 0.000391833417652352,
"max_absolute_c0_change_m": 0.010000000000000675,
"increased_absolute_c0_cycles": 244,
"decreased_absolute_c0_cycles": 0,
"driver_input_percent": 0.0,
"baseline_peak_absolute_c0_m": 2.4699999999999998,
"candidate_peak_absolute_c0_m": 2.4699999999999998
},
"left_entry": {
"cycles": 238,
"seconds": 2.4048367620016506,
"changed_c0_cycles": 204,
"mean_absolute_c0_change_m": 0.03167584138917884,
"max_absolute_c0_change_m": 0.07000000000000028,
"increased_absolute_c0_cycles": 204,
"decreased_absolute_c0_cycles": 0,
"driver_input_percent": 2.403816920672437,
"baseline_peak_absolute_c0_m": 2.6100000000000003,
"candidate_peak_absolute_c0_m": 2.6399999999999997
},
"left_peak": {
"cycles": 288,
"seconds": 2.891819394000777,
"changed_c0_cycles": 6,
"mean_absolute_c0_change_m": 0.00022373297976799967,
"max_absolute_c0_change_m": 0.019999999999999574,
"increased_absolute_c0_cycles": 6,
"decreased_absolute_c0_cycles": 0,
"driver_input_percent": 6.41034524444729,
"baseline_peak_absolute_c0_m": 2.9400000000000004,
"candidate_peak_absolute_c0_m": 2.95
},
"left_exit": {
"cycles": 219,
"seconds": 2.203320298998733,
"changed_c0_cycles": 11,
"mean_absolute_c0_change_m": 0.0008062973780191794,
"max_absolute_c0_change_m": 0.019999999999999574,
"increased_absolute_c0_cycles": 11,
"decreased_absolute_c0_cycles": 0,
"driver_input_percent": 9.936619705158035,
"baseline_peak_absolute_c0_m": 2.51,
"candidate_peak_absolute_c0_m": 2.51
},
"reversal": {
"cycles": 554,
"seconds": 5.570295188001182,
"changed_c0_cycles": 54,
"mean_absolute_c0_change_m": 0.002577813237060219,
"max_absolute_c0_change_m": 0.04999999999999982,
"increased_absolute_c0_cycles": 29,
"decreased_absolute_c0_cycles": 25,
"driver_input_percent": 0.0,
"baseline_peak_absolute_c0_m": 0.21999999999999975,
"candidate_peak_absolute_c0_m": 0.21999999999999975
}
},
"baseline_commands_vs_recorded_publications": {
"paired_cycles": 74669,
"within_one_quantum_cycles": 69690,
"maximum_absolute_error_c0_c1": [
0.369999988079071,
0.0010000016689301061
]
},
"timing": "Publication-time proxy, causal carState, exact consumed model; full SubMaster health unavailable.",
"baseline_version": "v3",
"baseline_revision": "01f8d51c82b3e863f1012d383b5994813ef01b81",
"candidate_version": "current",
"candidate_revision": "working_tree",
"focus_windows": [
[
"left_entry",
"173",
"175.4"
],
[
"left_peak",
"175.4",
"178.3"
],
[
"left_exit",
"178.3",
"180.5"
],
[
"reversal",
"728",
"734"
]
],
"baseline_source_sha256": "5b9f7946f29b5fcd94ca93e53be14a12b18ae74d7ca27380e92b90c8c599dce0",
"candidate_source_sha256": "7a226cf3cdf6dc8c3b15829078a7b93e486ba4722c4a1d93ceb55dd2e6c77b21",
"source_rlog_sha256": {
"84865544361f55cb_0000009e--592f7dc149--0--rlog.zst": "de63532ae6aedf5dc7fd3ac8e47a2d96a8f065614f4aca79ae2f120ea00390c7",
"84865544361f55cb_0000009e--592f7dc149--1--rlog.zst": "67b58197158b3e8f0581643f6657d2a85c47b0bb75dd3e00306d9ffae790008b",
"84865544361f55cb_0000009e--592f7dc149--2--rlog.zst": "a4840f338f51b5f1864c79ce3a4f2b11dc13d58d52b8459961c9da57237f2cc2",
"84865544361f55cb_0000009e--592f7dc149--3--rlog.zst": "ff3ea8d948006ab19c4dbfeeff59a509de3f193da47b91c88faa27ecd0b3f1fc",
"84865544361f55cb_0000009e--592f7dc149--4--rlog.zst": "5ff1996c2336299128a11e657c32bc21921716b7e3bd401de69a2d49484aa223",
"84865544361f55cb_0000009e--592f7dc149--5--rlog.zst": "238763457e89896933afaf9a5df325469ee8c02ddc53550df252531bc94fc540",
"84865544361f55cb_0000009e--592f7dc149--6--rlog.zst": "2ee86de80cfd762be10cd2dfb2895ddbee6b813706e9c7261460203e09b9bc4d",
"84865544361f55cb_0000009e--592f7dc149--7--rlog.zst": "017801f080861c63d799f87aebe30ab57cdf82f078ac882e6187d3870c403538",
"84865544361f55cb_0000009e--592f7dc149--8--rlog.zst": "3729016bd1f00bb1077613b63fe25b21ba7112822b994e0e0aa2b4cd93bdb940",
"84865544361f55cb_0000009e--592f7dc149--9--rlog.zst": "c40b3c1f6eb9252b85f176fee32cae16c23eaad3db830f6bbf37a730034a5ccf",
"84865544361f55cb_0000009e--592f7dc149--10--rlog.zst": "e0ce8f231798073e5bbc34551fbd9493f169a06a068adc1957ef8cd914b71fbb",
"84865544361f55cb_0000009e--592f7dc149--11--rlog.zst": "90863353f7c862a05618b4d0761dda3fcedd3cf6b4234c21c6dcc2eee3ec20b2",
"84865544361f55cb_0000009e--592f7dc149--12--rlog.zst": "e064f8abde9615b2daf00f469ef36b438cd4b3a1637069467c84de91bee3bdf1",
"84865544361f55cb_0000009e--592f7dc149--13--rlog.zst": "790fc438ce2675e0690fdce3b07bf54fdf6d06b17bbe7f5ff447adfa71189754",
"84865544361f55cb_0000009e--592f7dc149--14--rlog.zst": "b98f5a5be660013b1fe11250fdc24ee4fe1d9dd1301b787ddfdff1f2bcd3e27f"
},
"opendbc_head": "c21a9013700734dd20b09e05aa68329ad8cc20f9",
"source_sha256": {
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/damping_replay.py": "8c4f1f987b65e2be146c473a6e529168f08b1f7ce3c9514e22a9874450fa430e",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/openpilot/selfdrive/controls/lib/ford_model_action.py": "7a226cf3cdf6dc8c3b15829078a7b93e486ba4722c4a1d93ceb55dd2e6c77b21"
}
}
},
"ablation_comparison": {
"route9b": {
"lab_variant_commands_exactly_match_production": true,
"opposed_nonzero_c0_commands": 24,
"max_absolute_difference_on_opposed_commands_m": 0.05999999999999961
},
"route9e": {
"lab_variant_commands_exactly_match_production": true,
"opposed_nonzero_c0_commands": 30,
"max_absolute_difference_on_opposed_commands_m": 0.02999999999999936
}
},
"total_original_route_cycles": 299604,
"total_float32_can_round_trips": 817346,
"source_sha256": {
"openpilot/selfdrive/controls/lib/ford_model_action.py": "7a226cf3cdf6dc8c3b15829078a7b93e486ba4722c4a1d93ceb55dd2e6c77b21",
"openpilot/selfdrive/controls/tests/test_ford_model_action.py": "30cfffb86fcd3830320e9b2ec1f65cdfd219684a55d0bc0bb13f77966cc18b53",
"openpilot/selfdrive/controls/tests/test_ford_model_action_adapter.py": "8ab5dd52958f13a2426e07f8713b7d8d56c52b8a05d173ffba87ffccc0087796",
"openpilot/selfdrive/controls/tests/test_ford_model_action_damping.py": "a7062d2c3c46b3abcd4c3facd97970644ce609fb55285daa0359681950948250",
"openpilot/selfdrive/controls/tests/test_ford_model_action_prediction.py": "eb50e1d6acdb5e7332fadc3dcfcfa1b3809545e607c47c54b0ba3de80b2df208",
"openpilot/selfdrive/controls/tests/test_ford_model_action_selection.py": "f826c6328f0abac2a61f1a0a6f8d119fdbab858e363cb466d84ba9a7783059cd",
"openpilot/selfdrive/controls/tests/test_ford_controlsd_logging.py": "175a1969ac83b14b88798bbde37371f740cd05cf3f8f6e5d21bbf9afe857bb57",
"tools/ford_pscm_lab/__init__.py": "db4b8b7d2e317ed34ca0ec220bf9d53e7b80a23e766f4e1cb2c8224dada45f2e",
"tools/ford_pscm_lab/damping_replay.py": "8c4f1f987b65e2be146c473a6e529168f08b1f7ce3c9514e22a9874450fa430e",
"tools/ford_pscm_lab/model_action_replay.py": "7cddac7ce9e88cc32bc7afbec7f9df79b66bb4dfa2fa5f9b36654891fae23a19",
"tools/ford_pscm_lab/stress_model_action.py": "66adef6cba120a1a3d8e9730ea987ef49f8fe283023362dbb99608dee0e3229f",
"tools/ford_pscm_lab/test_model_action_replay.py": "1f7d90c06ec09b405571565632653dc7d984b106b955877916d4a0836c11e3b5",
"openpilot/sunnypilot/sunnylink/settings_ui_src/pages/vehicle.yaml": "41ac31726b941e470f5be34e2523dc1fc2b25fb6a12705ab267a57ed4f3c27c8",
"openpilot/sunnypilot/sunnylink/settings_ui.json": "c44c75e02042a0485b5bd7896e0b794761d768bbe513b6dc9c2c986ddd22b44c"
},
"artifact_sha256": {
".cache/ford_prediction_uncapped/uncapped_red.txt": "09160dbc36320be886561cfdbdd21eef0c8bc420a11bae9bb1a8dd43f7b93a3c",
".cache/ford_prediction_uncapped/suite_run.txt": "99b3bd2af256c9a4a083cb015a51a458a26ac02a8cad73c28c47470f9a5eb910",
".cache/ford_prediction_uncapped/coverage.json": "50a282c90fa86147ed6925fdf37918471f8e5204a4c1e1ef9adb39335e220f42",
".cache/ford_prediction_uncapped/stress.json": "e4226f403a3a914b9f3a7868b14a2b763215ec1f77e7d45ac06604859acc9fb6",
".cache/ford_prediction_uncapped/route90/report.json": "c5cee3c56f9f7f292c8069be22dd1ffb1649055fccaf829b28d96bdec6558541",
".cache/ford_prediction_uncapped/route95/report.json": "6fcc8a3bd7abb5b513888cb64426670e7e05f8b2100b1876be196a800cec2c90",
".cache/ford_prediction_uncapped/final9b/report.json": "79faca3ff89e2015dad4823d4d988ea26ead55c7e51e4a594258102d32701b07",
".cache/ford_prediction_uncapped/final9e/report.json": "9b003ce97225e65e03e351c5e133f3fed08dc79e026f73308098d51a98dd67ae",
".cache/ford_prediction_uncapped/route90/commands.npz": "8787aabbe7007bcd7c68976c272085761aec6a4f360fa6cc8c8553bd1421c92b",
".cache/ford_prediction_uncapped/route95/commands.npz": "349b716bd910cd030716b9ef519ee24f620fae5280776ffa871bc43318939400",
".cache/ford_prediction_uncapped/final9b/commands.npz": "ff4a14fc74b144800f459c250be961ebb05a46a20e6ad1fc0394a659c9854394",
".cache/ford_prediction_uncapped/final9e/commands.npz": "59f6046acb10b00ef00e7b9feb5b8547139a67a2f8767d7cbfe3603a59e6c77d"
}
}
@@ -0,0 +1,94 @@
{
"version": "model-action-measured-pose-v6",
"base_commit": "72e9d94f62487d07e9b7c5cb17d5988d5ddfbedb",
"opendbc_revision": "c21a9013700734dd20b09e05aa68329ad8cc20f9",
"scope": "Offline runtime command replay on frozen recorded calibrated yaw, geometry and vehicle inputs; separate controlsd integration tests exercise health/freshness, calibration updates, publication and CAN. No counterfactual vehicle simulation or road-validation claim.",
"calibration_approved": false,
"source_sha256": {
"openpilot/selfdrive/controls/controlsd.py": "c12e8d9be37e21f27a2132974b0efe98f3e681fd07e12c263e897ad67a44871b",
"openpilot/selfdrive/controls/lib/ford_model_action.py": "326539389b04034932db41ca2c67794779403b2c0e97f408a81a9f5734114899",
"openpilot/selfdrive/controls/tests/test_ford_controlsd_logging.py": "2a55779b1a32f4dc3317b568365efbadc1f6aab06a0b3398514be891025d5e2d",
"openpilot/selfdrive/controls/tests/test_ford_model_action_adapter.py": "0e131ca17380a5a4fe37754659f77aeda550f5e3c113edb27b576673e57bf6b4",
"openpilot/selfdrive/controls/tests/test_ford_model_action_measured_pose.py": "4d81efa0f09350559ff4290fd2ec11e744be221623fd414de85adfe2fecb4289",
"openpilot/selfdrive/controls/tests/test_ford_model_action_selection.py": "4e87ff347046d8de7248b9272a0393dd5d12e10df5430f00540b675331d7fe58",
"openpilot/selfdrive/controls/tests/test_ford_model_action_yaw.py": "063b066aeddf0410dc208fa0017a58cda78effb1ca33c2af8b53422dbdad06f3",
"openpilot/sunnypilot/sunnylink/settings_ui.json": "6f8900aa642d7f3883b003fd9244f29997343808fc22119c5ca953c3e1946fde",
"openpilot/sunnypilot/sunnylink/settings_ui_src/pages/vehicle.yaml": "c8f7cc4c44e39027fecb52efc61dd5f3902e030193b10ac2f2c36c51cc6d4010"
},
"validation": {
"pytest": {
"passed": 427,
"subtests_passed": 26,
"skipped": 0,
"command": "python -m pytest -q openpilot/selfdrive/controls/tests/test_ford*.py tools/ford_pscm_lab opendbc_repo/opendbc/car/ford/tests/test_ford.py openpilot/sunnypilot/sunnylink/tests",
"output_sha256": "ed2ecdf280dadc52a834b2dfaa777e64505f7763d1a271849dadc729a86abb5d"
},
"type_check": "ty check openpilot/selfdrive/controls/lib/ford_model_action.py openpilot/selfdrive/controls/controlsd.py: pass",
"ruff_changed_python": "pass",
"settings_compiler_check": "pass",
"diff_check": "pass",
"regression_red_green": "49 measured-pose tests and 4 state-freshness/calibration tests failed before their implementation and passed afterward"
},
"replay": {
"cycles": 340757,
"wire_round_trips": 681514,
"command_matches_reviewed_candidate": true,
"baseline_matches_archived_v5": true,
"c1_eligibility_unchanged": true,
"checks": [
"finite values",
"caps",
"independent slew",
"inactive reset",
"Float32/CAN packing",
"zero C2 and C3",
"publication and filter-state freshness"
],
"routes": {
"a2": {
"cycles": 71111,
"wire_round_trips": 142222,
"runtime_matches_reviewed_lab_candidate": true,
"baseline_matches_archive": true,
"identical_c1_eligibility": true
},
"a0": {
"cycles": 114582,
"wire_round_trips": 229164,
"runtime_matches_reviewed_lab_candidate": true,
"baseline_matches_archive": true,
"identical_c1_eligibility": true
},
"9b": {
"cycles": 71396,
"wire_round_trips": 142792,
"runtime_matches_reviewed_lab_candidate": true,
"baseline_matches_archive": true,
"identical_c1_eligibility": true
},
"9e": {
"cycles": 83668,
"wire_round_trips": 167336,
"runtime_matches_reviewed_lab_candidate": true,
"baseline_matches_archive": true,
"identical_c1_eligibility": true
}
},
"report_sha256": {
"a2_pose_report.json": "35d709991507ac7c10fd3148fd61dcf33a9606f97cd134d2bd94cee999f9cbe1",
"a0_pose_report.json": "1e34fbcdb14d04b23ce755ddd2e66fa6ab12377ab925693474e9f719b6339530",
"9b_pose_report.json": "e601662de44a354247f3943c233ca14ce5c449dbb4289ab9564ced21d524f02b",
"9e_pose_report.json": "fb0ee49e7aab65d45b61251aa83ad2247cd90e91f6c077f86d2f8474af7013e4"
},
"reproduce": "PYTHONDONTWRITEBYTECODE=1 PYTHONPATH=.:opendbc_repo python .cache/ford_v6/replay.py a2 a0 9b 9e",
"reproduction_requirements": "Original route extracts in the local .cache directories and the matching scripts/reports in the ford-v6 artifact. Replay scripts and private recordings are not shipped on the device."
},
"limitations": [
"All route extracts came from one truck; cross-PSCM behavior is unverified.",
"C0 command changes are not percentages of physical steering improvement.",
"The older overshoot example has nearby driver influence.",
"The same C1 cap remains active during tight-turn peak misses.",
"Quiet-path per-cycle C0-change RMS increases about 28%; physical centering is unverified.",
"Hardware build, device boot and physical driving were not performed."
]
}
+67
View File
@@ -0,0 +1,67 @@
# Experimental Ford selected-action controller, v5
V5 removes the excess-yaw C0 attenuation at the user's request. The damping
function, 0.02 rad/s deadband and 0.2 s reduction scale are deleted. Valid measured
yaw no longer changes either command target. Existing yaw input-health checks
and logging remain. No replacement gain, increment gate or controller state is added.
The full 150 ms geometric prediction introduced in v4 remains, along with the
7 m offset station and one-second heading scale. This is not a return to v1:
v1 did not predict the offset. C0/C1 bounds, slew, quantization, service gates,
engagement, downstream driver arbitration and zero C2/C3 are unchanged.
The latest supplied route a0 ran v2, not v4. Prior same-input comparisons found
identical v1/v2 commands during its driver-clean minor-bend warning intervals
and the preceding five seconds. That evidence does not identify damping as the
cause of those misses. Removing damping can restore C0 demand where the damper
was active, including turn exits; it is not evidence of improved tracking or
reduced oversteer.
## Offline validation
The regression suite checks yaw-independent commands through mirrored turn
entry, release and reversal, including valid yaw extremes and small yaw offsets.
Six cases fail with v4 damping present and pass after removal. Invalid yaw still
resets the controller. Actual controlsd selection, upstream limiting, Float32
publication and downstream CAN tests cover both model and maneuver references.
Full-rlog comparisons run pinned v4 against production v5 on routes9b, 9e and a0.
They preserve original clocks, exact consumed model frames and causal carState;
publication times proxy computation time, and complete SubMaster health is
unavailable. The numerical stress run checks independent geometric targets,
scalar slew, mirrored turns and Float32/CAN packing. Recorded vehicle motion
stays fixed: none of these checks establishes counterfactual steering response,
closed-loop stability or a physical tracking improvement.
Results and source hashes are recorded in
`ford_model_action_no_yaw_damping_validation.json`. Earlier validation documents
remain archives of their specified controller versions.
Validation passes 356 tests and 26 subtests with 100% controller statement and
branch coverage, 280,636 recorded route cycles and 779,410 Float32/CAN round trips,
including 200,000 random stress cycles. C1 and input eligibility match v4 exactly
on all three routes. Commands during all 1,578 driver-clean ordinary-bend warning
cycles on route a0 also remain identical to v4. At the earlier right-turn exit,
removing damping increases C0 magnitude by a mean 0.079 m, maximum 0.13 m; these
are command offsets, not measured vehicle displacement.
The module is 171 total lines, or 111 code lines excluding blanks, comments and
docstrings, with two control states. No hardware build or device boot was performed.
## Reproduce and select
Use the dependency setup and combined suite in the
[drive-test guide](ford_model_action_drive_test.md). Replay and stress commands:
```sh
python -m tools.ford_pscm_lab.damping_replay /path/to/route9b/rlogs --baseline v4 --candidate current --window right_entry 637 640 --window right_exit 642.7 643.852 --output /path/to/separate/route9b-results
python -m tools.ford_pscm_lab.damping_replay /path/to/route9e/rlogs --baseline v4 --candidate current --window left_entry 173 175.4 --window left_peak 175.4 178.3 --window left_exit 178.3 180.5 --output /path/to/separate/route9e-results
python -m tools.ford_pscm_lab.damping_replay /path/to/routea0/rlogs --baseline v4 --candidate current --window bends_5min 298 338 --window bend_7min 449 458 --window bend_9min 579 588 --output /path/to/separate/routea0-results
python -m tools.ford_pscm_lab.stress_model_action --cycles 200000 --seed 20260908 --opendbc-revision c21a9013700734dd20b09e05aa68329ad8cc20f9 --output /path/to/stress.json
```
The same default-off **Selected-Action Path Tracking (Experimental)** Sunnylink
toggle selects v5 on the CAN FD F-150 Lightning. Deployment remains
`sunnypilot/sunnypilot`, branch `hiimisaac-dev`. After updating, restart controlsd
through a real offroad-to-onroad cycle. Diagnostics identify
`model-action-c0-c1-prediction-v5`; `calibration_approved=false` remains explicit.
@@ -0,0 +1,304 @@
{
"date": "2026-09-08",
"baseline_commit": "7e63449749d112f096c56cb848dd289054e5f85b",
"hypothesis": "model-action-c0-c1-prediction-v5",
"scope": "Remove yaw damping only; retain full path prediction and existing input-health gates. Fixed-input command checks, not physical tracking or stability evidence.",
"deployment_target": {
"repository": "sunnypilot/sunnypilot",
"branch": "hiimisaac-dev"
},
"calibration_approved": false,
"hardware_build_and_device_boot": "not performed",
"opendbc_revision": "c21a9013700734dd20b09e05aa68329ad8cc20f9",
"controller_size": {
"total_lines": 171,
"code_lines_excluding_blanks_comments_docstrings": 111,
"core_persistent_values": 2,
"adapter_timestamps": 3
},
"checks": {
"combined_ford_params_sunnylink_suite": "356 passed, 26 subtests passed; no skips",
"coverage": {
"covered_lines": 102,
"num_statements": 102,
"percent_covered": 100.0,
"percent_covered_display": "100",
"missing_lines": 0,
"excluded_lines": 0,
"percent_statements_covered": 100.0,
"percent_statements_covered_display": "100",
"num_branches": 30,
"num_partial_branches": 0,
"covered_branches": 30,
"missing_branches": 0,
"percent_branches_covered": 100.0,
"percent_branches_covered_display": "100"
},
"ruff": "pass",
"ty_controller_and_lab": "pass",
"settings_compiler_check": "pass",
"removal_regression_probe": "6 cases fail with v4 damping present; all 21 yaw tests pass after removal.",
"reviews": {
"standards": {
"remaining_findings": 0
},
"spec": {
"remaining_findings": 0,
"independent_focused_tests_passed": 107
},
"corrected_findings": [
"Removed stale damping claim from Sunnylink YAML and regenerated JSON.",
"Corrected replay yaw-use metadata to input-health checks and diagnostics."
]
}
},
"total_original_route_cycles": 280636,
"total_float32_can_round_trips": 779410,
"stress": {
"seed": 20260908,
"random_cycles": 200000,
"mirrored_core_updates": 200000,
"invalid_or_inactive_resets": 3537,
"field_boundary_cases": 18138,
"float32_can_round_trips": 218138,
"analytic_targets_scalar_slew_and_mirror_checks_pass": true,
"valid_yaw_does_not_affect_targets_checked": true,
"full_geometric_prediction_checked": true,
"direct_raw_float32_packing_matches_host_output": true,
"max_continuous_step_c0_c1": [
0.4000000000003041,
0.05000000000000002
],
"calibration_approved": false,
"scope": "Numerical construction only; no PSCM response or closed-loop performance claims.",
"opendbc_import_head": "c21a9013700734dd20b09e05aa68329ad8cc20f9"
},
"routes": {
"route9b": {
"cycles": 82386,
"eligible_cycles": 70703,
"same_validity": true,
"c1_exactly_unchanged": true,
"field_slew_zero_c2_c3_pass": true,
"float32_can_round_trips": 164772,
"baseline_revision": "7e63449749d112f096c56cb848dd289054e5f85b",
"baseline_source_sha256": "7a226cf3cdf6dc8c3b15829078a7b93e486ba4722c4a1d93ceb55dd2e6c77b21",
"candidate_source_sha256": "213a4dfa586092c28d8e6e2c27a7dd3622c5862615db9afd5a4e2d38144b1af8",
"source_rlog_sha256": {
"84865544361f55cb_0000009b--e4616dddaa--0--rlog.zst": "22746f7119109b73ed7f2c26ce8c99f87136e9124fb7fc14c9554409a28a7c3f",
"84865544361f55cb_0000009b--e4616dddaa--1--rlog.zst": "4c2e1d7083c31a2b37d0f8dd3be4d330898511b7e02c26f7d40ca9bc2779397d",
"84865544361f55cb_0000009b--e4616dddaa--2--rlog.zst": "62f3e049e220cd3681fadf386f2969537bd571998ae2f6ba2d08479428b5a28f",
"84865544361f55cb_0000009b--e4616dddaa--3--rlog.zst": "83bf0131b2d36b2ba7e5ba050bbc13c0a3350feb5c9b89dc9c87d3a37abebfb3",
"84865544361f55cb_0000009b--e4616dddaa--4--rlog.zst": "430985a80dd6e10f7abeb89457a17022e6bb6978617f415c905f584b1647603e",
"84865544361f55cb_0000009b--e4616dddaa--5--rlog.zst": "8c0c5ae6323ec33b3e14f84ca834f70cb56f6b29f471a350f1e3efc06b6ba553",
"84865544361f55cb_0000009b--e4616dddaa--6--rlog.zst": "db53dfa8156b9d66792c3eff0b2ce5d31b71ad41cc580dec85f528845593c184",
"84865544361f55cb_0000009b--e4616dddaa--7--rlog.zst": "91b0b3be10cb7d7d7f7dd2024d8f9ee99d1e9fd2204203a3a9a2f2f1c6e3fa03",
"84865544361f55cb_0000009b--e4616dddaa--8--rlog.zst": "687dbbfc49837efbfe8fa6bc091e40f7fad2908832234d7884f4616d1bc9ccff",
"84865544361f55cb_0000009b--e4616dddaa--9--rlog.zst": "a88ec4d25b04cdbf5844686fc77f6b28dca920c9b164e37ebf69844a3ae398fc",
"84865544361f55cb_0000009b--e4616dddaa--10--rlog.zst": "fe6b29580a6c94e1c236d13e18db4cd9f31cc1b25d52e1e6e19a5021125c9932",
"84865544361f55cb_0000009b--e4616dddaa--11--rlog.zst": "150d31b1944d7a1b8c562f3aee20b66cefa6c4e8d02660ec889907d835142f45",
"84865544361f55cb_0000009b--e4616dddaa--12--rlog.zst": "e105e5f703a70f36f1989c66fb46c35a65ff60a265b4332e10fa2e6875a2ced4",
"84865544361f55cb_0000009b--e4616dddaa--13--rlog.zst": "69304cf0c81401374f047c5022ca47d257ff9e59e30ec171325a2ee1e7ed13d4"
},
"focus_cohorts": {
"right_entry": {
"cycles": 298,
"seconds": 2.9932484459999387,
"changed_c0_cycles": 100,
"mean_absolute_c0_change_m": 0.007683282515599457,
"max_absolute_c0_change_m": 0.04999999999999982,
"increased_absolute_c0_cycles": 100,
"decreased_absolute_c0_cycles": 0,
"driver_input_percent": 20.395198628296086,
"baseline_peak_absolute_c0_m": 3.0300000000000002,
"candidate_peak_absolute_c0_m": 3.0300000000000002
},
"right_exit": {
"cycles": 115,
"seconds": 1.156770048999988,
"changed_c0_cycles": 115,
"mean_absolute_c0_change_m": 0.07934493768172877,
"max_absolute_c0_change_m": 0.13000000000000078,
"increased_absolute_c0_cycles": 115,
"decreased_absolute_c0_cycles": 0,
"driver_input_percent": 0.0,
"baseline_peak_absolute_c0_m": 0.7199999999999998,
"candidate_peak_absolute_c0_m": 0.7800000000000002
}
}
},
"route9e": {
"cycles": 83668,
"eligible_cycles": 74669,
"same_validity": true,
"c1_exactly_unchanged": true,
"field_slew_zero_c2_c3_pass": true,
"float32_can_round_trips": 167336,
"baseline_revision": "7e63449749d112f096c56cb848dd289054e5f85b",
"baseline_source_sha256": "7a226cf3cdf6dc8c3b15829078a7b93e486ba4722c4a1d93ceb55dd2e6c77b21",
"candidate_source_sha256": "213a4dfa586092c28d8e6e2c27a7dd3622c5862615db9afd5a4e2d38144b1af8",
"source_rlog_sha256": {
"84865544361f55cb_0000009e--592f7dc149--0--rlog.zst": "de63532ae6aedf5dc7fd3ac8e47a2d96a8f065614f4aca79ae2f120ea00390c7",
"84865544361f55cb_0000009e--592f7dc149--1--rlog.zst": "67b58197158b3e8f0581643f6657d2a85c47b0bb75dd3e00306d9ffae790008b",
"84865544361f55cb_0000009e--592f7dc149--2--rlog.zst": "a4840f338f51b5f1864c79ce3a4f2b11dc13d58d52b8459961c9da57237f2cc2",
"84865544361f55cb_0000009e--592f7dc149--3--rlog.zst": "ff3ea8d948006ab19c4dbfeeff59a509de3f193da47b91c88faa27ecd0b3f1fc",
"84865544361f55cb_0000009e--592f7dc149--4--rlog.zst": "5ff1996c2336299128a11e657c32bc21921716b7e3bd401de69a2d49484aa223",
"84865544361f55cb_0000009e--592f7dc149--5--rlog.zst": "238763457e89896933afaf9a5df325469ee8c02ddc53550df252531bc94fc540",
"84865544361f55cb_0000009e--592f7dc149--6--rlog.zst": "2ee86de80cfd762be10cd2dfb2895ddbee6b813706e9c7261460203e09b9bc4d",
"84865544361f55cb_0000009e--592f7dc149--7--rlog.zst": "017801f080861c63d799f87aebe30ab57cdf82f078ac882e6187d3870c403538",
"84865544361f55cb_0000009e--592f7dc149--8--rlog.zst": "3729016bd1f00bb1077613b63fe25b21ba7112822b994e0e0aa2b4cd93bdb940",
"84865544361f55cb_0000009e--592f7dc149--9--rlog.zst": "c40b3c1f6eb9252b85f176fee32cae16c23eaad3db830f6bbf37a730034a5ccf",
"84865544361f55cb_0000009e--592f7dc149--10--rlog.zst": "e0ce8f231798073e5bbc34551fbd9493f169a06a068adc1957ef8cd914b71fbb",
"84865544361f55cb_0000009e--592f7dc149--11--rlog.zst": "90863353f7c862a05618b4d0761dda3fcedd3cf6b4234c21c6dcc2eee3ec20b2",
"84865544361f55cb_0000009e--592f7dc149--12--rlog.zst": "e064f8abde9615b2daf00f469ef36b438cd4b3a1637069467c84de91bee3bdf1",
"84865544361f55cb_0000009e--592f7dc149--13--rlog.zst": "790fc438ce2675e0690fdce3b07bf54fdf6d06b17bbe7f5ff447adfa71189754",
"84865544361f55cb_0000009e--592f7dc149--14--rlog.zst": "b98f5a5be660013b1fe11250fdc24ee4fe1d9dd1301b787ddfdff1f2bcd3e27f"
},
"focus_cohorts": {
"left_entry": {
"cycles": 238,
"seconds": 2.4048367620016506,
"changed_c0_cycles": 0,
"mean_absolute_c0_change_m": 0.0,
"max_absolute_c0_change_m": 0.0,
"increased_absolute_c0_cycles": 0,
"decreased_absolute_c0_cycles": 0,
"driver_input_percent": 2.403816920672437,
"baseline_peak_absolute_c0_m": 2.6399999999999997,
"candidate_peak_absolute_c0_m": 2.6399999999999997
},
"left_peak": {
"cycles": 288,
"seconds": 2.891819394000777,
"changed_c0_cycles": 0,
"mean_absolute_c0_change_m": 0.0,
"max_absolute_c0_change_m": 0.0,
"increased_absolute_c0_cycles": 0,
"decreased_absolute_c0_cycles": 0,
"driver_input_percent": 6.41034524444729,
"baseline_peak_absolute_c0_m": 2.95,
"candidate_peak_absolute_c0_m": 2.95
},
"left_exit": {
"cycles": 219,
"seconds": 2.203320298998733,
"changed_c0_cycles": 129,
"mean_absolute_c0_change_m": 0.037433836245882854,
"max_absolute_c0_change_m": 0.11000000000000032,
"increased_absolute_c0_cycles": 128,
"decreased_absolute_c0_cycles": 1,
"driver_input_percent": 9.936619705158035,
"baseline_peak_absolute_c0_m": 2.51,
"candidate_peak_absolute_c0_m": 2.51
}
}
},
"routea0": {
"cycles": 114582,
"eligible_cycles": 105382,
"same_validity": true,
"c1_exactly_unchanged": true,
"field_slew_zero_c2_c3_pass": true,
"float32_can_round_trips": 229164,
"baseline_revision": "7e63449749d112f096c56cb848dd289054e5f85b",
"baseline_source_sha256": "7a226cf3cdf6dc8c3b15829078a7b93e486ba4722c4a1d93ceb55dd2e6c77b21",
"candidate_source_sha256": "213a4dfa586092c28d8e6e2c27a7dd3622c5862615db9afd5a4e2d38144b1af8",
"source_rlog_sha256": {
"84865544361f55cb_000000a0--5e86c30dae--0--rlog.zst": "57b8113783f70f9176e1f2703e70185f64df07b3283f739a6fd7b182a2c92417",
"84865544361f55cb_000000a0--5e86c30dae--1--rlog.zst": "a821ce7df80c6466110a6f6433b2c9482f5f3a5b4227e8459182c3ee9fb8c7a7",
"84865544361f55cb_000000a0--5e86c30dae--2--rlog.zst": "984cca8e311be8a61444ca2bc23bbf3937c6f00264303faefb7efb8f0a3e8aa2",
"84865544361f55cb_000000a0--5e86c30dae--3--rlog.zst": "c6ea25b226aa875a15a8f8356522ab3b12501d280f63ebe0438a9d50b2116279",
"84865544361f55cb_000000a0--5e86c30dae--4--rlog.zst": "99b129e2965674fb3406855d1b022c0876083984adf2f2ef51c07f1e699974ea",
"84865544361f55cb_000000a0--5e86c30dae--5--rlog.zst": "428e2ce9a1ac0ca7135c00221590c286ab32b4ce5cf24d376de1519b39b25fec",
"84865544361f55cb_000000a0--5e86c30dae--6--rlog.zst": "efca7fedd0aaed4be4500fd1a0c67f01d8f805cfdd681a0a9c0439061ec2f36f",
"84865544361f55cb_000000a0--5e86c30dae--7--rlog.zst": "1da238f57b63efbbf7dfd2b147637c1b846736e198999741817fc0646d4f60b7",
"84865544361f55cb_000000a0--5e86c30dae--8--rlog.zst": "b61209325da26751b98da85fe166dab65ab9eaaef129b859a59fe99e4de5ab65",
"84865544361f55cb_000000a0--5e86c30dae--9--rlog.zst": "cf555f1c57a9afdbed702b5934892acb5b2174ef48f1fcf494422aba9bd01321",
"84865544361f55cb_000000a0--5e86c30dae--10--rlog.zst": "13ff83c236cca64b57c11ffa05cb74511af1258cde3cf0554395922769a39683",
"84865544361f55cb_000000a0--5e86c30dae--11--rlog.zst": "848a1d26fd72bbe4e119339bd405aedb0b9d7e6b96329d0b868a27bd01b6b128",
"84865544361f55cb_000000a0--5e86c30dae--12--rlog.zst": "9e8bf8597942cd74d8d04c783148b8201f63f56e55b766f7887f27465de276f2",
"84865544361f55cb_000000a0--5e86c30dae--13--rlog.zst": "e37575ed1424d574015c20a3066754bcca6fdacbb104db2bedce782428fd48ef",
"84865544361f55cb_000000a0--5e86c30dae--14--rlog.zst": "b67c7ed7ddd957bf5dcbcdb0604ddc9c52cd7af9d6aa70c1eae3e7622cee160d",
"84865544361f55cb_000000a0--5e86c30dae--15--rlog.zst": "6baaf9fdf64c72a9be70d261580914e2489f363230d95a1291b8479bcc249ba2",
"84865544361f55cb_000000a0--5e86c30dae--16--rlog.zst": "f84803fa2a615caf59a6bd4e9b2848059dd9f4a7f0c01ac4ec7e2347e317b5e7",
"84865544361f55cb_000000a0--5e86c30dae--17--rlog.zst": "f0f5f36cb155c18f8f9f90f700e1e91e169ef0442fd70670323be4a1c658040e",
"84865544361f55cb_000000a0--5e86c30dae--18--rlog.zst": "8674d5822949bafaefc0d0f30b0e91603354b9981de7566419a2e5b780054e64",
"84865544361f55cb_000000a0--5e86c30dae--19--rlog.zst": "b5df55701d5bcfbfd750608a1dfbcba9b931a39fba7a82e87bc7915b8ca2333f"
},
"focus_cohorts": {
"bends_5min": {
"cycles": 3966,
"seconds": 39.99197329400005,
"changed_c0_cycles": 8,
"mean_absolute_c0_change_m": 1.950586094527418e-05,
"max_absolute_c0_change_m": 0.009999999999999787,
"increased_absolute_c0_cycles": 8,
"decreased_absolute_c0_cycles": 0,
"driver_input_percent": 14.825647085260368,
"baseline_peak_absolute_c0_m": 0.3099999999999996,
"candidate_peak_absolute_c0_m": 0.3099999999999996
},
"bend_7min": {
"cycles": 893,
"seconds": 8.995078784999919,
"changed_c0_cycles": 0,
"mean_absolute_c0_change_m": 0.0,
"max_absolute_c0_change_m": 0.0,
"increased_absolute_c0_cycles": 0,
"decreased_absolute_c0_cycles": 0,
"driver_input_percent": 0.0,
"baseline_peak_absolute_c0_m": 0.20000000000000018,
"candidate_peak_absolute_c0_m": 0.20000000000000018
},
"bend_9min": {
"cycles": 895,
"seconds": 8.998970718999999,
"changed_c0_cycles": 0,
"mean_absolute_c0_change_m": 0.0,
"max_absolute_c0_change_m": 0.0,
"increased_absolute_c0_cycles": 0,
"decreased_absolute_c0_cycles": 0,
"driver_input_percent": 0.22147398432951632,
"baseline_peak_absolute_c0_m": 0.2400000000000002,
"candidate_peak_absolute_c0_m": 0.2400000000000002
}
}
}
},
"routea0_ordinary_bend_warnings_v4_vs_v5": {
"definition": "Driver-clean +/-1s, speed>=8m/s, absolute desired wheel angle 3 to30deg. Proxy for ordinary bends, not map geometry.",
"cycles": 1578,
"seconds": 15.92979767199978,
"changed_c0_cycles": 0,
"changed_c1_cycles": 0,
"max_c0_change_m": 0.0
},
"source_sha256": {
"openpilot/selfdrive/controls/lib/ford_model_action.py": "213a4dfa586092c28d8e6e2c27a7dd3622c5862615db9afd5a4e2d38144b1af8",
"openpilot/sunnypilot/sunnylink/settings_ui_src/pages/vehicle.yaml": "be610c09e4bbd93e84d1920cfbb3e9609b3fbb7dca8eb229b8e67befb4b2ca84",
"openpilot/sunnypilot/sunnylink/settings_ui.json": "4d2dffdfd81190a871bd41e28e5832502aa95d8aacf91daa7e68382085588aca",
"openpilot/selfdrive/controls/tests/test_ford_model_action.py": "30cfffb86fcd3830320e9b2ec1f65cdfd219684a55d0bc0bb13f77966cc18b53",
"openpilot/selfdrive/controls/tests/test_ford_model_action_adapter.py": "7f8b13c4d85217cbb6bd32ec193ef18bdb30bdf1f2b59fa7fa9d40988cb20bea",
"openpilot/selfdrive/controls/tests/test_ford_model_action_prediction.py": "eb50e1d6acdb5e7332fadc3dcfcfa1b3809545e607c47c54b0ba3de80b2df208",
"openpilot/selfdrive/controls/tests/test_ford_model_action_selection.py": "f826c6328f0abac2a61f1a0a6f8d119fdbab858e363cb466d84ba9a7783059cd",
"openpilot/selfdrive/controls/tests/test_ford_model_action_yaw.py": "d646f89e2c4d4e112d03e77c5c34b7541b0bdba800419d472a4a69753637c515",
"openpilot/selfdrive/controls/tests/test_ford_controlsd_logging.py": "9697db696d5a01838ea5cdcf4f1771d813a647f5038cabe9f0460778a2936929",
"tools/ford_pscm_lab/__init__.py": "db4b8b7d2e317ed34ca0ec220bf9d53e7b80a23e766f4e1cb2c8224dada45f2e",
"tools/ford_pscm_lab/damping_replay.py": "d21565a29b9c2b4668fafe07f995c15e113888a1f4787f37d72d02d36998f9cf",
"tools/ford_pscm_lab/model_action_replay.py": "a90caed1c46c6f964fecb50bd0f531ae355505f1e84be9a1e37243d910b292b4",
"tools/ford_pscm_lab/stress_model_action.py": "123c6c0c9f53a5af2ab50bb568e47c0f8960b7d49fad2fb6ea74de5c63e03ad5",
"tools/ford_pscm_lab/test_model_action_replay.py": "bf1a1612474cb7308b3640cba535ee534999fdecdf055ae6d7bc83b9a8e66f63"
},
"artifact_sha256": {
".cache/ford_no_yaw_damping/removal_red.txt": "4aa79e5af1066d66db65853faad3923f8645f5dff8383fa3106489a3e53cae18",
".cache/ford_no_yaw_damping/suite_run.txt": "434d80389bc7d2859070c9f9df0cac1ac0f854151b5888dcedc2ce38df23d519",
".cache/ford_no_yaw_damping/coverage.json": "d99baeb8729a1394b41d9a4739b538372aad27b56d379f4b9cefe682ed99d662",
".cache/ford_no_yaw_damping/stress.json": "3e2c49dcc473d90ea03c95dc12905c2216ee573a691891b8a1f56ba82fd78251",
".cache/ford_no_yaw_damping/route9b/report.json": "446371173fed1ff632ece4286e53d0d941491be590d467af960d1931431212ca",
".cache/ford_no_yaw_damping/route9b/commands.npz": "d543a92991f06b56e20d0ed58daf6e4ef6a0021b79c027570b8d997658336b9d",
".cache/ford_no_yaw_damping/route9e/report.json": "fb8ddd8d2c242a14a63993593c96604e7306dcd97219d073bbcfcbeba68af45b",
".cache/ford_no_yaw_damping/route9e/commands.npz": "375a94227f3cb1a161a1295496908baa428d5ef0e0b79134a728f876477dbd6b",
".cache/ford_no_yaw_damping/routea0/report.json": "7959b77af715016b4da45b9bc8f095872702f662950324f64a0f4d332c814eb0",
".cache/ford_no_yaw_damping/routea0/commands.npz": "4f9dcde39ad9e20958ad04e53dbb32830c6a933dc7f743fdffe5a612252cb16b"
}
}
+126
View File
@@ -0,0 +1,126 @@
# Experimental Ford path prediction, v3
This document and its counts describe archived v3. The
[current v4 experiment](ford_model_action_full_prediction.md) removes the extra
15 cm / 25% prediction adjustment cap.
The latest driven route9e used v1 (`5fc16abc7`), before the v2 yaw damping.
It often follows the requested steering angle closely, but some tight turns
fall behind after a reasonable initial turn-in. The requested angle is replanned
from the car's changing position; a large late request may partly be a recovery
request after arriving wide. It is not proof that the original turn required
that much steering. The generic PSCM limit flag does not identify a torque,
rate or mechanical limit, and the miss starts before our C1 cap in the clearest
left turn.
Short measured-motion integrations against earlier frozen model paths are
consistent with a growing miss, but model uncertainty, reference timing and
some driver input prevent a conclusive causal attribution. V3 tests a bounded
change to initial path demand. No counterfactual physical tracking score is
claimed from replaying fixed logs.
## Change and bounds
Start with the current model's lateral offset at 7 m of path arc length, as in
v1/v2. Advance the reference pose by speed × 0.15 s along the **selected,
upstream-limited curvature**, and read the same model path 7 m beyond that
advance, expressed in the predicted ego frame. Bound the change from the
original offset to both ±0.15 m and ±25% of its magnitude. Prediction cannot
reverse that target or create C0 from a zero offset.
For advance `d`, selected curvature `k`, rotation `theta = k*d`, and model
point `(x, y)` at arc station `7+d`, the predicted lateral coordinate is:
```
y_predicted = cos(theta)*y - sin(theta)*x + (1-cos(theta))/k
```
The code evaluates the last term continuously at zero curvature without
cancellation. Available path horizon limits `d`; prediction tapers to zero as
the horizon approaches 7 m. Nonfinite prediction falls back to the validated
current offset. The existing endpoint hold remains for paths shorter than 7 m.
A matched constant-radius path retains essentially the same C0, subject to
sample interpolation. Developing and flattening bends can move the target
earlier. This is a geometric hypothesis assuming motion along selected
curvature, not an identified 150 ms actuator delay or a calibrated plant model.
Errors in that assumption can increase or reduce useful steering demand.
The predicted offset passes through the existing ±5.11 m clip, v2 excess-yaw
damping, independent 4 m/s slew and 0.01 m quantization. C1 construction, clip,
slew and quantization are unchanged. C2=C3=0. Input sanity, freshness, service
health and startup selection gates are unchanged. There are still only two
control states (C0 and C1 slew positions), plus three adapter timestamps.
The module is 193 total lines, including 125 code lines excluding blanks,
comments and docstrings (18 more code lines than v2). No model history, integral
or turn state machine is added. The bounds above
apply to the prediction target, not arbitrary differences between separately
slewed controllers after different histories.
## Offline results and tradeoff
All four supplied routes run at their original controls timestamps. Routes90/95
also reproduce the archived v1 command construction exactly. The newer routes
compare immutable v2 code with v3 using identical measured yaw, selected
curvature, exact consumed model and causal carState. Publication times proxy
computation time; complete SubMaster health is unavailable. Neither v2 nor v3
was driven on these recordings. V2-versus-recorded error is therefore not a
reconstruction accuracy measurement.
| Recorded interval | Command change versus v2 |
| --- | --- |
| route9e left entry, 173175.4 s | Mean C0 magnitude +0.143 m; same 2.0 m level reached 0.203 s earlier |
| route9e left peak, 175.4178.3 s | Mean magnitude +0.095 m; prediction also increases some late demand |
| route9e reversal, 728734 s | Peak C0 magnitude 0.24 → 0.22 m |
| route9b right exit, 642.7643.852 s | Mean C0 +0.024 m, partially offsetting v2 damping |
| Driver-clean low requests above 8 m/s, routes9b/9e | Mean absolute C0 change ≈0.0015 m; maximum 0.02 m |
The entry C0 crossings at 0.5, 1.0, 1.5, 2.0 and 2.4 m move earlier by 61, 64,
367, 203 and 90 ms respectively. These are command-level crossing times,
not measured improvements in wheel response. Several entry/peak windows
contain driver input, quantified in the validation record.
On all 115 earlier right-exit cycles before strong intervention, v3 remains
below the driven v1 reconstruction: mean C0 is 0.370 m for v1, 0.290 m for v2,
and 0.314 m for v3. This tradeoff is retained explicitly; v3 does not improve
every exit command relative to v2. There is no evidence yet that it reduces
the late model request or the physical miss.
Route9b now includes full rlogs 12/13, added after the archived v2 evaluation.
Its 14-rlog totals therefore differ from the historical 12-rlog report. The
focused segment-10 comparison uses identical timestamps and data.
Validation passes 372 tests and 26 subtests, including the real extracted
controlsd selection/limiter/publication path and downstream CAN builder, with
100% controller statement and branch coverage. Four routes cover 299,604
original cycles. Randomized testing adds 200,000 core updates plus their
mirrors against an independent analytic geometry/damping/slew oracle; boundary
and route checks total 817,346 Float32/CAN round trips. These checks establish
command construction and retained gates, not closed-loop vehicle behavior.
See `ford_model_action_prediction_validation.json` for provenance and counts.
## Reproduce and select
To reproduce the archived suite and stress results, use v3 commit
`01f8d51c82b3e863f1012d383b5994813ef01b81` with the native dependencies and
suite command in the [drive-test guide](ford_model_action_drive_test.md).
Current replay tooling can select the immutable v3 source explicitly. New-route comparisons use
the deployment opendbc pin `c21a9013700734dd20b09e05aa68329ad8cc20f9`:
```sh
python -m tools.ford_pscm_lab.damping_replay /path/to/route9e/rlogs --baseline v2 --candidate v3 --window left_entry 173 175.4 --window left_peak 175.4 178.3 --window left_exit 178.3 180.5 --window reversal 728 734 --window final_entry 822 825.5 --output /path/to/separate/route9e-results
python -m tools.ford_pscm_lab.damping_replay /path/to/route9b/rlogs --baseline v2 --candidate v3 --window right_entry 637 640 --window right_exit_before_strong_input 642.7 643.852 --output /path/to/separate/route9b-results
python -m tools.ford_pscm_lab.stress_model_action --cycles 200000 --seed 20260907 --opendbc-revision c21a9013700734dd20b09e05aa68329ad8cc20f9 --output /path/to/stress.json
```
Historical replay loads trusted controller source from immutable local Git
commits; those objects must exist in the checkout. The original replay tool
still requires its explicit historical opendbc pin. Neither tool downloads
code or drives the car.
The existing default-off Sunnylink **Selected-Action Path Tracking
(Experimental)** toggle selects v3 on the CAN FD F-150 Lightning. Updating
with that toggle already enabled selects v3 at the next controlsd startup.
Diagnostics identify `model-action-c0-c1-prediction-v3` and keep
`calibration_approved=false`. No new device installation, hardware build,
physical calibration or device boot is part of this offline validation.
@@ -0,0 +1,849 @@
{
"date": "2026-09-07",
"baseline_commit": "744a97d9bc08d8743b250eceff7c88585b5480de",
"deployment_target": {
"repository": "sunnypilot/sunnypilot",
"branch": "hiimisaac-dev"
},
"hypothesis": "model-action-c0-c1-prediction-v3",
"scope": "Bounded geometric prediction; fixed-input offline evidence only.",
"calibration_approved": false,
"hardware_build_and_device_boot": "not performed",
"controller_size": {
"total_lines": 193,
"code_lines_excluding_blanks_comments_docstrings": 125,
"core_persistent_values": 2,
"adapter_timestamps": 3
},
"checks": {
"combined_ford_params_sunnylink_suite": "372 passed, 26 subtests passed; no skips",
"suite_log_sha256": "b9546bd4cac8349ab9a699f175ba569ed41fc8a9e85e79a0018c79d3af24c11d",
"coverage": {
"covered_lines": 116,
"num_statements": 116,
"percent_covered": 100.0,
"percent_covered_display": "100",
"missing_lines": 0,
"excluded_lines": 0,
"percent_statements_covered": 100.0,
"percent_statements_covered_display": "100",
"num_branches": 32,
"num_partial_branches": 0,
"covered_branches": 32,
"missing_branches": 0,
"percent_branches_covered": 100.0,
"percent_branches_covered_display": "100"
},
"ruff": "pass",
"ty_controller_and_lab": "pass",
"settings_compiler_check": "pass",
"prediction_red_probe": "Disabling prediction fails all four mirrored developing/flattening-bend cases (4 failed, 34 passed); all 38 pass with prediction.",
"standards_review_remaining_findings": 0,
"spec_review_remaining_findings": 0,
"review_scope": "Tracked and untracked v3 changes since 744a97d9bc08d8743b250eceff7c88585b5480de; final loader unit test checked separately.",
"test_portability": "Archive loader unit test mocks Git source retrieval; full offline route runs use the actual immutable commit sources."
},
"stress": {
"seed": 20260907,
"random_cycles": 200000,
"mirrored_core_updates": 200000,
"invalid_or_inactive_resets": 3537,
"field_boundary_cases": 18138,
"float32_can_round_trips": 218138,
"analytic_targets_scalar_slew_and_mirror_checks_pass": true,
"bounded_excess_yaw_damping_checked": true,
"bounded_geometric_prediction_checked": true,
"direct_raw_float32_packing_matches_host_output": true,
"max_continuous_step_c0_c1": [
0.40000000000015523,
0.05000000000000002
],
"calibration_approved": false,
"scope": "Numerical construction only; no PSCM response or closed-loop performance claims.",
"opendbc_import_head": "c21a9013700734dd20b09e05aa68329ad8cc20f9",
"source_sha256": {
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/stress_model_action.py": "be77689e5327a13b7b4d0e18a3fc1863b57cab76be714339efc609124400489f",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/openpilot/selfdrive/controls/lib/ford_model_action.py": "5b9f7946f29b5fcd94ca93e53be14a12b18ae74d7ca27380e92b90c8c599dce0",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/model_action_replay.py": "a872cf4594e364988796394b09f531d18cc3d6732e0c92f45eed9fbf6f5cbee6"
}
},
"routes": {
"route90": {
"scope": "Command construction and adapter reconstruction only; no counterfactual closed-loop score.",
"calibration_approved": false,
"executes_live_selector": false,
"cycles": 78812,
"core_active_cycles": 73055,
"core_exact_archived_match": true,
"cohorts_reproduced": true,
"adapter_active_cycles": 73055,
"adapter_status_counts": {
"inactive": 5757,
"active": 73055
},
"adapter_matches_current_core_with_yaw_and_fresh_engagement_dt": true,
"core_active_path_shorter_than_7m_cycles": 0,
"adapter_validity_differs_from_archive_cycles": 0,
"adapter_command_differs_from_archive_cycles": 27264,
"adapter_max_absolute_command_difference_c0_c1": [
0.16000000000000014,
0.0020000000000000018
],
"field_slew_zero_c2_c3_pass": true,
"float32_can_round_trips": 157624,
"timing": "Original controls publication timestamps proxy computation time; repeated frames and gaps retained. No identified delay.",
"eligibility": "Adapter checks recorded services independently; full SubMaster health is unavailable. Core uses archived validity.",
"reference": "Recorded controlsState.desiredCurvature, already selected/limited. These two routes have no maneuver publications.",
"host_yaw": "Extract cs.yaw equals -carState.yawRate; current adapter uses it for bounded damping.",
"archived_core_revision": "5fc16abc7662020706e29f57d31a6d5e2bc1293a",
"archived_core_source_sha256": "8f3bc5d68e0051776f614a2ccffae84a88f7898dc95bdc12c23dcfe10dfe676a",
"cohorts": {
"small_request": {
"seconds": 475.24161910000026,
"core_c0_c1_rms": [
0.06125184365338734,
0.0057692154738982855
],
"recorded_v8_c0_c1_rms": [
0.057970374224682646,
0.007060795713496332
],
"adapter_eligible_seconds": 475.24161910000026,
"adapter_c0_c1_rms": [
0.06384544973437024,
0.0057692154738982855
]
},
"turn": {
"seconds": 47.85871389900012,
"core_c0_c1_rms": [
0.18725251016049418,
0.046449510236699354
],
"recorded_v8_c0_c1_rms": [
0.4773534845264775,
0.04315935101392594
],
"adapter_eligible_seconds": 47.85871389900012,
"adapter_c0_c1_rms": [
0.20375259142277982,
0.046449510236699354
]
},
"small_speed_2_8": {
"seconds": 38.696717235999785,
"core_c0_c1_rms": [
0.209264275431127,
0.014019080484620104
],
"recorded_v8_c0_c1_rms": [
0.1593558816527209,
0.01823849401244768
],
"adapter_eligible_seconds": 38.696717235999785,
"adapter_c0_c1_rms": [
0.21664870054721863,
0.014019080484620104
]
},
"turn_speed_2_8": {
"seconds": 0.30159887100000304,
"core_c0_c1_rms": [
2.090789090165622,
0.21878942570759013
],
"recorded_v8_c0_c1_rms": [
2.155728831937131,
0.38375302254248855
],
"adapter_eligible_seconds": 0.30159887100000304,
"adapter_c0_c1_rms": [
2.2043400076988204,
0.21878942570759013
]
},
"small_speed_8_15": {
"seconds": 79.2599167590001,
"core_c0_c1_rms": [
0.02250905681378238,
0.005843904113223639
],
"recorded_v8_c0_c1_rms": [
0.03477379324171293,
0.006545440153133947
],
"adapter_eligible_seconds": 79.2599167590001,
"adapter_c0_c1_rms": [
0.026080258136122546,
0.005843904113223639
]
},
"turn_speed_8_15": {
"seconds": 0.022264622000001566,
"core_c0_c1_rms": [
0.20999999999999996,
0.04349999999999998
],
"recorded_v8_c0_c1_rms": [
0.25,
0.04050000011920929
],
"adapter_eligible_seconds": 0.022264622000001566,
"adapter_c0_c1_rms": [
0.20999999999999996,
0.04349999999999998
]
},
"small_speed_15_55": {
"seconds": 357.2849851050004,
"core_c0_c1_rms": [
0.011622097035118838,
0.003925571829874466
],
"recorded_v8_c0_c1_rms": [
0.03809717934501699,
0.0045587590980714285
],
"adapter_eligible_seconds": 357.2849851050004,
"adapter_c0_c1_rms": [
0.013693372023947319,
0.003925571829874466
]
},
"turn_speed_15_55": {
"seconds": 47.53485040600012,
"core_c0_c1_rms": [
0.08686835454709245,
0.043216347944464766
],
"recorded_v8_c0_c1_rms": [
0.4471065308273131,
0.030663943784303503
],
"adapter_eligible_seconds": 47.53485040600012,
"adapter_c0_c1_rms": [
0.10462899220936993,
0.043216347944464766
]
},
"pose_quiet": {
"seconds": 427.99642020700026,
"core_c0_c1_rms": [
0.01261133542366393,
0.00422389167351435
],
"recorded_v8_c0_c1_rms": [
0.03724956081568198,
0.0046880581807907966
],
"adapter_eligible_seconds": 427.99642020700026,
"adapter_c0_c1_rms": [
0.014639447114479471,
0.00422389167351435
]
}
},
"workspace_head": "744a97d9bc08d8743b250eceff7c88585b5480de",
"opendbc_import_head": "72a775d35e54c21ff5c5798acef22016eedcc0a7",
"opendbc_import_path": "/Users/ibpersonal/.codex/worktrees/b926/sunnypilot/opendbc_repo",
"source_sha256": {
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/model_action_replay.py": "a872cf4594e364988796394b09f531d18cc3d6732e0c92f45eed9fbf6f5cbee6",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/openpilot/selfdrive/controls/lib/ford_model_action.py": "5b9f7946f29b5fcd94ca93e53be14a12b18ae74d7ca27380e92b90c8c599dce0",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/openpilot/selfdrive/controls/lib/ford_path.py": "383538fc7cdae3bc28dffb71fe12ac5f3f9866ffbe6adfb7457f3593e9fc903a",
"/Users/ibpersonal/.codex/worktrees/3548/sunnypilot/analysis/controller_search_20260904/route90/route.npz": "51e8c26eedde253e171af47d704c1967ba45ae6825d883393bec1fb9e00251c1",
"/Users/ibpersonal/.codex/worktrees/3548/sunnypilot/analysis/controller_search_20260904/route90/metadata.json": "742afec55ed629155b22d387f376f878f7b2765221fe94f515a829052bdd916f",
"/Users/ibpersonal/.codex/worktrees/3548/sunnypilot/analysis/controller_search_20260904/route90/encoder_comparison.npz": "7a625d3ed5cbd8013d1028aa3bc421740551dcae5c3d60981208bd047af9794c",
"/Users/ibpersonal/.codex/worktrees/3548/sunnypilot/analysis/controller_search_20260904/route90/encoder_comparison.json": "64e1cc4be84394ac7ec408d383b99b48ada9b3fd129cdef2fd846e5fac620d66",
"/Users/ibpersonal/.codex/worktrees/3548/sunnypilot/analysis/controller_search_20260904/route90/pose_candidate/pose_replay.npz": "4457ccc0868354749da5b72c1dea0faf750f783dfdc87038101288fdcba1e707"
}
},
"route95": {
"scope": "Command construction and adapter reconstruction only; no counterfactual closed-loop score.",
"calibration_approved": false,
"executes_live_selector": false,
"cycles": 54738,
"core_active_cycles": 37614,
"core_exact_archived_match": true,
"cohorts_reproduced": true,
"adapter_active_cycles": 37614,
"adapter_status_counts": {
"inactive": 17124,
"active": 37614
},
"adapter_matches_current_core_with_yaw_and_fresh_engagement_dt": true,
"core_active_path_shorter_than_7m_cycles": 44,
"adapter_validity_differs_from_archive_cycles": 0,
"adapter_command_differs_from_archive_cycles": 20110,
"adapter_max_absolute_command_difference_c0_c1": [
0.16999999999999993,
0.0010000000000000009
],
"field_slew_zero_c2_c3_pass": true,
"float32_can_round_trips": 109476,
"timing": "Original controls publication timestamps proxy computation time; repeated frames and gaps retained. No identified delay.",
"eligibility": "Adapter checks recorded services independently; full SubMaster health is unavailable. Core uses archived validity.",
"reference": "Recorded controlsState.desiredCurvature, already selected/limited. These two routes have no maneuver publications.",
"host_yaw": "Extract cs.yaw equals -carState.yawRate; current adapter uses it for bounded damping.",
"archived_core_revision": "5fc16abc7662020706e29f57d31a6d5e2bc1293a",
"archived_core_source_sha256": "8f3bc5d68e0051776f614a2ccffae84a88f7898dc95bdc12c23dcfe10dfe676a",
"cohorts": {
"small_request": {
"seconds": 160.93427628100014,
"core_c0_c1_rms": [
0.027283284164810895,
0.006054573235641941
],
"recorded_v8_c0_c1_rms": [
0.04189890174428264,
0.0075342351227490395
],
"adapter_eligible_seconds": 160.93427628100014,
"adapter_c0_c1_rms": [
0.02970169836480558,
0.006054573235641941
]
},
"turn": {
"seconds": 58.39837079799993,
"core_c0_c1_rms": [
0.5425346915921071,
0.09102985199238507
],
"recorded_v8_c0_c1_rms": [
0.63700479987207,
0.09691236120507696
],
"adapter_eligible_seconds": 58.39837079799993,
"adapter_c0_c1_rms": [
0.5622074838217578,
0.09102985199238507
]
},
"small_speed_2_8": {
"seconds": 8.083969085000064,
"core_c0_c1_rms": [
0.09941192804175551,
0.016572084086884566
],
"recorded_v8_c0_c1_rms": [
0.07597383189748476,
0.016403469943482765
],
"adapter_eligible_seconds": 8.083969085000064,
"adapter_c0_c1_rms": [
0.1039594708778471,
0.016572084086884566
]
},
"turn_speed_2_8": {
"seconds": 2.9056519089999995,
"core_c0_c1_rms": [
2.286598281178436,
0.3420570411209625
],
"recorded_v8_c0_c1_rms": [
2.4371443775668484,
0.37360610108051345
],
"adapter_eligible_seconds": 2.9056519089999995,
"adapter_c0_c1_rms": [
2.342145227147664,
0.3420570411209625
]
},
"small_speed_8_15": {
"seconds": 32.021085868,
"core_c0_c1_rms": [
0.028387860150467858,
0.0067602044219821
],
"recorded_v8_c0_c1_rms": [
0.036941169841952046,
0.007349671742584858
],
"adapter_eligible_seconds": 32.021085868,
"adapter_c0_c1_rms": [
0.032473217223426755,
0.0067602044219821
]
},
"turn_speed_8_15": {
"seconds": 22.160645306000006,
"core_c0_c1_rms": [
0.28668316323581683,
0.0671915746138579
],
"recorded_v8_c0_c1_rms": [
0.3391022553367454,
0.07297678510653233
],
"adapter_eligible_seconds": 22.160645306000006,
"adapter_c0_c1_rms": [
0.32040564199313326,
0.0671915746138579
]
},
"small_speed_15_55": {
"seconds": 120.82922132800007,
"core_c0_c1_rms": [
0.010802222799973712,
0.004282512931613862
],
"recorded_v8_c0_c1_rms": [
0.03987958989535526,
0.0065793876512313535
],
"adapter_eligible_seconds": 120.82922132800007,
"adapter_c0_c1_rms": [
0.013133007539405142,
0.004282512931613862
]
},
"turn_speed_15_55": {
"seconds": 33.332073582999925,
"core_c0_c1_rms": [
0.07257996778378971,
0.03628926246224237
],
"recorded_v8_c0_c1_rms": [
0.3416081588451539,
0.027322786376822172
],
"adapter_eligible_seconds": 33.332073582999925,
"adapter_c0_c1_rms": [
0.08556161843867907,
0.03628926246224237
]
},
"pose_quiet": {
"seconds": 145.24136911700015,
"core_c0_c1_rms": [
0.012566204405760934,
0.004614194153565963
],
"recorded_v8_c0_c1_rms": [
0.03855743841849719,
0.006621274926345545
],
"adapter_eligible_seconds": 145.24136911700015,
"adapter_c0_c1_rms": [
0.014652655970248427,
0.004614194153565963
]
}
},
"workspace_head": "744a97d9bc08d8743b250eceff7c88585b5480de",
"opendbc_import_head": "72a775d35e54c21ff5c5798acef22016eedcc0a7",
"opendbc_import_path": "/Users/ibpersonal/.codex/worktrees/b926/sunnypilot/opendbc_repo",
"source_sha256": {
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/model_action_replay.py": "a872cf4594e364988796394b09f531d18cc3d6732e0c92f45eed9fbf6f5cbee6",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/openpilot/selfdrive/controls/lib/ford_model_action.py": "5b9f7946f29b5fcd94ca93e53be14a12b18ae74d7ca27380e92b90c8c599dce0",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/openpilot/selfdrive/controls/lib/ford_path.py": "383538fc7cdae3bc28dffb71fe12ac5f3f9866ffbe6adfb7457f3593e9fc903a",
"/Users/ibpersonal/.codex/worktrees/3548/sunnypilot/analysis/controller_search_20260904/route95/route.npz": "6e5438867ea618e53ca395b60ff4b9b146256f1bf6f07dc8e92d663286074154",
"/Users/ibpersonal/.codex/worktrees/3548/sunnypilot/analysis/controller_search_20260904/route95/metadata.json": "f7c95a102e0e084bbcc9938399219080c1d3b26274b4ea3c3a9514ff7dc8ea8d",
"/Users/ibpersonal/.codex/worktrees/3548/sunnypilot/analysis/controller_search_20260904/route95/encoder_comparison.npz": "23bd05c4b6400299844acaba1d97051c96c23c682bf17b46e89e7f2cca5fce38",
"/Users/ibpersonal/.codex/worktrees/3548/sunnypilot/analysis/controller_search_20260904/route95/encoder_comparison.json": "1ab24a796d14817b8672ea6dfd39711d65cb06744eaaef7dcc02a840c392c4d3",
"/Users/ibpersonal/.codex/worktrees/3548/sunnypilot/analysis/controller_search_20260904/route95/pose_candidate/pose_replay.npz": "9cfbb3c6f0b1fdb7e3d38e6b64b8c94cffbcc9fabe41f6344d84a9b657b6af9b"
}
},
"route9b": {
"scope": "Fixed-input command replay only; no physical improvement or stability claim.",
"calibration_approved": false,
"cycles": 82386,
"eligible_cycles": 70703,
"status_counts": {
"inactive": 11683,
"active": 70703
},
"c1_exactly_unchanged": true,
"same_validity": true,
"field_slew_zero_c2_c3_pass": true,
"float32_can_round_trips": 164772,
"cohorts": {
"eligible": {
"cycles": 70703,
"seconds": 711.571703289,
"changed_c0_cycles": 20858,
"mean_absolute_c0_change_m": 0.006916798541216091,
"max_absolute_c0_change_m": 0.15000000000000124,
"increased_absolute_c0_cycles": 17700,
"decreased_absolute_c0_cycles": 3157,
"driver_input_percent": 3.0297623863274845,
"baseline_peak_absolute_c0_m": 3.89,
"candidate_peak_absolute_c0_m": 3.96
},
"driver_clean": {
"cycles": 63267,
"seconds": 636.7469998759999,
"changed_c0_cycles": 16024,
"mean_absolute_c0_change_m": 0.002923399405403562,
"max_absolute_c0_change_m": 0.15000000000000036,
"increased_absolute_c0_cycles": 13587,
"decreased_absolute_c0_cycles": 2437,
"driver_input_percent": 0.0,
"baseline_peak_absolute_c0_m": 2.16,
"candidate_peak_absolute_c0_m": 2.24
},
"driver_clean_low_request_above_8mps": {
"cycles": 40656,
"seconds": 409.13067679800054,
"changed_c0_cycles": 6007,
"mean_absolute_c0_change_m": 0.0014813245503935623,
"max_absolute_c0_change_m": 0.020000000000000462,
"increased_absolute_c0_cycles": 5099,
"decreased_absolute_c0_cycles": 908,
"driver_input_percent": 0.0,
"baseline_peak_absolute_c0_m": 0.08999999999999986,
"candidate_peak_absolute_c0_m": 0.11000000000000032
},
"turn": {
"cycles": 5459,
"seconds": 54.939139939000256,
"changed_c0_cycles": 3156,
"mean_absolute_c0_change_m": 0.007655026845650452,
"max_absolute_c0_change_m": 0.08999999999999986,
"increased_absolute_c0_cycles": 2938,
"decreased_absolute_c0_cycles": 218,
"driver_input_percent": 0.0,
"baseline_peak_absolute_c0_m": 2.16,
"candidate_peak_absolute_c0_m": 2.24
},
"right_entry": {
"cycles": 298,
"seconds": 2.9932484459999387,
"changed_c0_cycles": 298,
"mean_absolute_c0_change_m": 0.14871908015683724,
"max_absolute_c0_change_m": 0.15000000000000036,
"increased_absolute_c0_cycles": 298,
"decreased_absolute_c0_cycles": 0,
"driver_input_percent": 20.395198628296086,
"baseline_peak_absolute_c0_m": 2.8000000000000003,
"candidate_peak_absolute_c0_m": 2.95
},
"right_exit_before_strong_input": {
"cycles": 115,
"seconds": 1.156770048999988,
"changed_c0_cycles": 115,
"mean_absolute_c0_change_m": 0.02362929150320746,
"max_absolute_c0_change_m": 0.040000000000000036,
"increased_absolute_c0_cycles": 115,
"decreased_absolute_c0_cycles": 0,
"driver_input_percent": 0.0,
"baseline_peak_absolute_c0_m": 0.7000000000000002,
"candidate_peak_absolute_c0_m": 0.7199999999999998
}
},
"baseline_commands_vs_recorded_publications": {
"paired_cycles": 70703,
"within_one_quantum_cycles": 69862,
"maximum_absolute_error_c0_c1": [
0.20999998569488554,
0.0005000143051147043
]
},
"timing": "Publication-time proxy, causal carState, exact consumed model; full SubMaster health unavailable.",
"baseline_version": "v2",
"baseline_revision": "744a97d9bc08d8743b250eceff7c88585b5480de",
"candidate_version": "current",
"candidate_revision": "working_tree",
"focus_windows": [
[
"right_entry",
"637",
"640"
],
[
"right_exit_before_strong_input",
"642.7",
"643.852"
]
],
"baseline_source_sha256": "59d66297a017557f3d4f28b115be3f6220b800566c11935e2284b3814783fb7e",
"candidate_source_sha256": "5b9f7946f29b5fcd94ca93e53be14a12b18ae74d7ca27380e92b90c8c599dce0",
"source_rlog_sha256": {
"84865544361f55cb_0000009b--e4616dddaa--0--rlog.zst": "22746f7119109b73ed7f2c26ce8c99f87136e9124fb7fc14c9554409a28a7c3f",
"84865544361f55cb_0000009b--e4616dddaa--1--rlog.zst": "4c2e1d7083c31a2b37d0f8dd3be4d330898511b7e02c26f7d40ca9bc2779397d",
"84865544361f55cb_0000009b--e4616dddaa--2--rlog.zst": "62f3e049e220cd3681fadf386f2969537bd571998ae2f6ba2d08479428b5a28f",
"84865544361f55cb_0000009b--e4616dddaa--3--rlog.zst": "83bf0131b2d36b2ba7e5ba050bbc13c0a3350feb5c9b89dc9c87d3a37abebfb3",
"84865544361f55cb_0000009b--e4616dddaa--4--rlog.zst": "430985a80dd6e10f7abeb89457a17022e6bb6978617f415c905f584b1647603e",
"84865544361f55cb_0000009b--e4616dddaa--5--rlog.zst": "8c0c5ae6323ec33b3e14f84ca834f70cb56f6b29f471a350f1e3efc06b6ba553",
"84865544361f55cb_0000009b--e4616dddaa--6--rlog.zst": "db53dfa8156b9d66792c3eff0b2ce5d31b71ad41cc580dec85f528845593c184",
"84865544361f55cb_0000009b--e4616dddaa--7--rlog.zst": "91b0b3be10cb7d7d7f7dd2024d8f9ee99d1e9fd2204203a3a9a2f2f1c6e3fa03",
"84865544361f55cb_0000009b--e4616dddaa--8--rlog.zst": "687dbbfc49837efbfe8fa6bc091e40f7fad2908832234d7884f4616d1bc9ccff",
"84865544361f55cb_0000009b--e4616dddaa--9--rlog.zst": "a88ec4d25b04cdbf5844686fc77f6b28dca920c9b164e37ebf69844a3ae398fc",
"84865544361f55cb_0000009b--e4616dddaa--10--rlog.zst": "fe6b29580a6c94e1c236d13e18db4cd9f31cc1b25d52e1e6e19a5021125c9932",
"84865544361f55cb_0000009b--e4616dddaa--11--rlog.zst": "150d31b1944d7a1b8c562f3aee20b66cefa6c4e8d02660ec889907d835142f45",
"84865544361f55cb_0000009b--e4616dddaa--12--rlog.zst": "e105e5f703a70f36f1989c66fb46c35a65ff60a265b4332e10fa2e6875a2ced4",
"84865544361f55cb_0000009b--e4616dddaa--13--rlog.zst": "69304cf0c81401374f047c5022ca47d257ff9e59e30ec171325a2ee1e7ed13d4"
},
"opendbc_head": "c21a9013700734dd20b09e05aa68329ad8cc20f9",
"source_sha256": {
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/damping_replay.py": "663674fa9686291228766c4fd2990dcfc620b155f6d74e2f957bbb33e5593a5d",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/openpilot/selfdrive/controls/lib/ford_model_action.py": "5b9f7946f29b5fcd94ca93e53be14a12b18ae74d7ca27380e92b90c8c599dce0"
}
},
"route9e": {
"scope": "Fixed-input command replay only; no physical improvement or stability claim.",
"calibration_approved": false,
"cycles": 83668,
"eligible_cycles": 74669,
"status_counts": {
"inactive": 8999,
"active": 74669
},
"c1_exactly_unchanged": true,
"same_validity": true,
"field_slew_zero_c2_c3_pass": true,
"float32_can_round_trips": 167336,
"cohorts": {
"eligible": {
"cycles": 74669,
"seconds": 751.5479497329998,
"changed_c0_cycles": 23282,
"mean_absolute_c0_change_m": 0.00617904439256301,
"max_absolute_c0_change_m": 0.15000000000000036,
"increased_absolute_c0_cycles": 19880,
"decreased_absolute_c0_cycles": 3402,
"driver_input_percent": 2.7324748055386094,
"baseline_peak_absolute_c0_m": 4.33,
"candidate_peak_absolute_c0_m": 4.38
},
"driver_clean": {
"cycles": 66321,
"seconds": 667.5291971840106,
"changed_c0_cycles": 18123,
"mean_absolute_c0_change_m": 0.003409158953346422,
"max_absolute_c0_change_m": 0.15000000000000036,
"increased_absolute_c0_cycles": 16136,
"decreased_absolute_c0_cycles": 1987,
"driver_input_percent": 0.0,
"baseline_peak_absolute_c0_m": 2.3600000000000003,
"candidate_peak_absolute_c0_m": 2.4699999999999998
},
"driver_clean_low_request_above_8mps": {
"cycles": 39635,
"seconds": 398.9613925449994,
"changed_c0_cycles": 5866,
"mean_absolute_c0_change_m": 0.0014778774629256156,
"max_absolute_c0_change_m": 0.020000000000000462,
"increased_absolute_c0_cycles": 5377,
"decreased_absolute_c0_cycles": 489,
"driver_input_percent": 0.0,
"baseline_peak_absolute_c0_m": 0.05999999999999961,
"candidate_peak_absolute_c0_m": 0.07000000000000028
},
"turn": {
"cycles": 6219,
"seconds": 62.59491144700405,
"changed_c0_cycles": 5129,
"mean_absolute_c0_change_m": 0.011069748090899225,
"max_absolute_c0_change_m": 0.10999999999999943,
"increased_absolute_c0_cycles": 5098,
"decreased_absolute_c0_cycles": 31,
"driver_input_percent": 0.0,
"baseline_peak_absolute_c0_m": 2.3600000000000003,
"candidate_peak_absolute_c0_m": 2.4699999999999998
},
"left_entry": {
"cycles": 238,
"seconds": 2.4048367620016506,
"changed_c0_cycles": 238,
"mean_absolute_c0_change_m": 0.14261645650107144,
"max_absolute_c0_change_m": 0.15000000000000036,
"increased_absolute_c0_cycles": 238,
"decreased_absolute_c0_cycles": 0,
"driver_input_percent": 2.403816920672437,
"baseline_peak_absolute_c0_m": 2.46,
"candidate_peak_absolute_c0_m": 2.6100000000000003
},
"left_peak": {
"cycles": 288,
"seconds": 2.891819394000777,
"changed_c0_cycles": 288,
"mean_absolute_c0_change_m": 0.095420590363382,
"max_absolute_c0_change_m": 0.15000000000000036,
"increased_absolute_c0_cycles": 288,
"decreased_absolute_c0_cycles": 0,
"driver_input_percent": 6.41034524444729,
"baseline_peak_absolute_c0_m": 2.79,
"candidate_peak_absolute_c0_m": 2.9400000000000004
},
"left_exit": {
"cycles": 219,
"seconds": 2.203320298998733,
"changed_c0_cycles": 185,
"mean_absolute_c0_change_m": 0.0422943841629376,
"max_absolute_c0_change_m": 0.09000000000000075,
"increased_absolute_c0_cycles": 56,
"decreased_absolute_c0_cycles": 129,
"driver_input_percent": 9.936619705158035,
"baseline_peak_absolute_c0_m": 2.4299999999999997,
"candidate_peak_absolute_c0_m": 2.51
},
"reversal": {
"cycles": 554,
"seconds": 5.570295188001182,
"changed_c0_cycles": 167,
"mean_absolute_c0_change_m": 0.007060409634059627,
"max_absolute_c0_change_m": 0.040000000000000036,
"increased_absolute_c0_cycles": 20,
"decreased_absolute_c0_cycles": 147,
"driver_input_percent": 0.0,
"baseline_peak_absolute_c0_m": 0.2400000000000002,
"candidate_peak_absolute_c0_m": 0.21999999999999975
},
"final_entry": {
"cycles": 348,
"seconds": 3.4958847329980927,
"changed_c0_cycles": 348,
"mean_absolute_c0_change_m": 0.11950442609746584,
"max_absolute_c0_change_m": 0.15000000000000036,
"increased_absolute_c0_cycles": 348,
"decreased_absolute_c0_cycles": 0,
"driver_input_percent": 3.556656282920533,
"baseline_peak_absolute_c0_m": 3.31,
"candidate_peak_absolute_c0_m": 3.4400000000000004
}
},
"baseline_commands_vs_recorded_publications": {
"paired_cycles": 74669,
"within_one_quantum_cycles": 74156,
"maximum_absolute_error_c0_c1": [
0.29000000834465034,
0.0010000016689301061
]
},
"timing": "Publication-time proxy, causal carState, exact consumed model; full SubMaster health unavailable.",
"baseline_version": "v2",
"baseline_revision": "744a97d9bc08d8743b250eceff7c88585b5480de",
"candidate_version": "current",
"candidate_revision": "working_tree",
"focus_windows": [
[
"left_entry",
"173",
"175.4"
],
[
"left_peak",
"175.4",
"178.3"
],
[
"left_exit",
"178.3",
"180.5"
],
[
"reversal",
"728",
"734"
],
[
"final_entry",
"822",
"825.5"
]
],
"baseline_source_sha256": "59d66297a017557f3d4f28b115be3f6220b800566c11935e2284b3814783fb7e",
"candidate_source_sha256": "5b9f7946f29b5fcd94ca93e53be14a12b18ae74d7ca27380e92b90c8c599dce0",
"source_rlog_sha256": {
"84865544361f55cb_0000009e--592f7dc149--0--rlog.zst": "de63532ae6aedf5dc7fd3ac8e47a2d96a8f065614f4aca79ae2f120ea00390c7",
"84865544361f55cb_0000009e--592f7dc149--1--rlog.zst": "67b58197158b3e8f0581643f6657d2a85c47b0bb75dd3e00306d9ffae790008b",
"84865544361f55cb_0000009e--592f7dc149--2--rlog.zst": "a4840f338f51b5f1864c79ce3a4f2b11dc13d58d52b8459961c9da57237f2cc2",
"84865544361f55cb_0000009e--592f7dc149--3--rlog.zst": "ff3ea8d948006ab19c4dbfeeff59a509de3f193da47b91c88faa27ecd0b3f1fc",
"84865544361f55cb_0000009e--592f7dc149--4--rlog.zst": "5ff1996c2336299128a11e657c32bc21921716b7e3bd401de69a2d49484aa223",
"84865544361f55cb_0000009e--592f7dc149--5--rlog.zst": "238763457e89896933afaf9a5df325469ee8c02ddc53550df252531bc94fc540",
"84865544361f55cb_0000009e--592f7dc149--6--rlog.zst": "2ee86de80cfd762be10cd2dfb2895ddbee6b813706e9c7261460203e09b9bc4d",
"84865544361f55cb_0000009e--592f7dc149--7--rlog.zst": "017801f080861c63d799f87aebe30ab57cdf82f078ac882e6187d3870c403538",
"84865544361f55cb_0000009e--592f7dc149--8--rlog.zst": "3729016bd1f00bb1077613b63fe25b21ba7112822b994e0e0aa2b4cd93bdb940",
"84865544361f55cb_0000009e--592f7dc149--9--rlog.zst": "c40b3c1f6eb9252b85f176fee32cae16c23eaad3db830f6bbf37a730034a5ccf",
"84865544361f55cb_0000009e--592f7dc149--10--rlog.zst": "e0ce8f231798073e5bbc34551fbd9493f169a06a068adc1957ef8cd914b71fbb",
"84865544361f55cb_0000009e--592f7dc149--11--rlog.zst": "90863353f7c862a05618b4d0761dda3fcedd3cf6b4234c21c6dcc2eee3ec20b2",
"84865544361f55cb_0000009e--592f7dc149--12--rlog.zst": "e064f8abde9615b2daf00f469ef36b438cd4b3a1637069467c84de91bee3bdf1",
"84865544361f55cb_0000009e--592f7dc149--13--rlog.zst": "790fc438ce2675e0690fdce3b07bf54fdf6d06b17bbe7f5ff447adfa71189754",
"84865544361f55cb_0000009e--592f7dc149--14--rlog.zst": "b98f5a5be660013b1fe11250fdc24ee4fe1d9dd1301b787ddfdff1f2bcd3e27f"
},
"opendbc_head": "c21a9013700734dd20b09e05aa68329ad8cc20f9",
"source_sha256": {
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/damping_replay.py": "663674fa9686291228766c4fd2990dcfc620b155f6d74e2f957bbb33e5593a5d",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/openpilot/selfdrive/controls/lib/ford_model_action.py": "5b9f7946f29b5fcd94ca93e53be14a12b18ae74d7ca27380e92b90c8c599dce0"
}
}
},
"focus_comparison": {
"scope": "Same recorded inputs; crossing a C0 level earlier does not identify vehicle response time.",
"left_entry_crossings": [
{
"c0_magnitude_m": 0.5,
"v2_time_s": 173.04409075799958,
"v3_time_s": 172.98298211199835,
"earlier_ms": 61.10864600123023
},
{
"c0_magnitude_m": 1.0,
"v2_time_s": 173.41054691999852,
"v3_time_s": 173.3469171409979,
"earlier_ms": 63.629779000621056
},
{
"c0_magnitude_m": 1.5,
"v2_time_s": 174.09016323599826,
"v3_time_s": 173.7234842099988,
"earlier_ms": 366.6790259994741
},
{
"c0_magnitude_m": 2.0,
"v2_time_s": 174.69427409499804,
"v3_time_s": 174.49167145299907,
"earlier_ms": 202.60264199896483
},
{
"c0_magnitude_m": 2.4,
"v2_time_s": 175.08411706099832,
"v3_time_s": 174.99415714599854,
"earlier_ms": 89.95991499978118
}
],
"right_exit": {
"mean_c0_v1_m": 0.3704272817752263,
"mean_c0_v2_m": 0.29049595970311604,
"mean_c0_v3_m": 0.31412525120632345,
"v3_below_driven_v1_every_cycle": true,
"cycles": 115
}
},
"total_original_route_cycles": 299604,
"total_float32_can_round_trips": 817346,
"source_sha256": {
"openpilot/selfdrive/controls/lib/ford_model_action.py": "5b9f7946f29b5fcd94ca93e53be14a12b18ae74d7ca27380e92b90c8c599dce0",
"openpilot/selfdrive/controls/tests/test_ford_model_action.py": "8851104f811757f6a31b5e23ec5933c7bfc18c17012af9a0a78f851217eca525",
"openpilot/selfdrive/controls/tests/test_ford_model_action_adapter.py": "04daed84093330c05ae1669c155ed9c197f95ebb02fd4e7ca1c7a10ce70d03da",
"openpilot/selfdrive/controls/tests/test_ford_model_action_damping.py": "a7062d2c3c46b3abcd4c3facd97970644ce609fb55285daa0359681950948250",
"openpilot/selfdrive/controls/tests/test_ford_model_action_prediction.py": "c05ae173c809228a9f4782af32a174b1b928f506a76c454aa89a51ef0ed17bdd",
"openpilot/selfdrive/controls/tests/test_ford_model_action_selection.py": "f826c6328f0abac2a61f1a0a6f8d119fdbab858e363cb466d84ba9a7783059cd",
"openpilot/selfdrive/controls/tests/test_ford_controlsd_logging.py": "fd4095bc4f1363fd014c43a9e0776a2245105e8fb2703d87da367d0f33dc22dc",
"tools/ford_pscm_lab/__init__.py": "db4b8b7d2e317ed34ca0ec220bf9d53e7b80a23e766f4e1cb2c8224dada45f2e",
"tools/ford_pscm_lab/damping_replay.py": "663674fa9686291228766c4fd2990dcfc620b155f6d74e2f957bbb33e5593a5d",
"tools/ford_pscm_lab/model_action_replay.py": "a872cf4594e364988796394b09f531d18cc3d6732e0c92f45eed9fbf6f5cbee6",
"tools/ford_pscm_lab/stress_model_action.py": "be77689e5327a13b7b4d0e18a3fc1863b57cab76be714339efc609124400489f",
"tools/ford_pscm_lab/test_model_action_replay.py": "d2a3602e145db4f969ecf1402fe66684c3b1f884e3ed429e95f5fb5dcaddefb3",
"openpilot/sunnypilot/sunnylink/settings_ui_src/pages/vehicle.yaml": "b5951a90cb01bfcd79b82705600ad2d165b3cc5f7e33c7404536eb989bc3ce39",
"openpilot/sunnypilot/sunnylink/settings_ui.json": "6badb0080d8a999d63d063430c0a320f1a58884dfe0c390774f90a39cbf0e0e2"
},
"artifact_sha256": {
".cache/ford_model_action_prediction/prediction_red.txt": "81e765edd1fc73783855ef30aa3c4aa8f111c001d7e9e0951d5791e18a860f4e",
".cache/ford_model_action_prediction/stress.json": "e358a7caafe457a83637857cee7f0b1b2b60e0e4020f64b46630b8462e5edff5",
".cache/ford_model_action_prediction/coverage.json": "333ed4414632adbb12a8a1f3ef2f9dc7a62dd9fde08bbc6b8a3d372cf9a6869b",
".cache/ford_model_action_prediction/focus_comparison.json": "b0af320c0eb7e535420702ed78df30c43134f8f9e2a977e5f88f765ba0626c69",
".cache/ford_model_action_prediction/route90/report.json": "b58f738269d9fe176672670f5bf4116f3226d319f43b5b407ac221c288aba174",
".cache/ford_model_action_prediction/route95/report.json": "379a6f5bc9c98d8d1bccf687d61d46c54eda439284508e321d49b5312c1ca9f2",
".cache/ford_model_action_prediction/route9b/report.json": "650ce4056b663124e2f72fae94cd1b66423ff74de42611a99501271c2c846bc2",
".cache/ford_model_action_prediction/route9e/report.json": "196b3b242e3e056ed531cd6e657a235cc9bf8ed4357ce696b1bec1e5f0a49a39",
".cache/ford_model_action_prediction/route9b/commands.npz": "5d82318935253d4fa8a1b8c30cc7f2d465c151ae1cb9109fa09a622d5be49871",
".cache/ford_model_action_prediction/route9e/commands.npz": "2917bbe801a78711340fa4eae63d5bf1cb4a4464c45d8538c9a46a77b5511e06",
".cache/ford_model_action_prediction/plot_commands.py": "6a5bd97fedeb9bd951925dddc656e508082f6d457dbc1a6c9d4db769afcdd921",
"/Users/ibpersonal/.codex/visualizations/2026/09/07/01a07c33-1f6c-7292-8030-ad4b01923034/ford-route9e/prediction_command_comparison.png": "86e68a0497490352065997f73cd5a798ff93dcabc93152028669ccfb36a4fefe",
"/Users/ibpersonal/.codex/visualizations/2026/09/07/01a07c33-1f6c-7292-8030-ad4b01923034/ford-route9e/prediction_command_comparison.svg": "2310b2978b60b97619886c2e10ecca88c73b5fde3477452347e6db0a5bbcd5b3"
}
}
+220
View File
@@ -0,0 +1,220 @@
{
"date": "2026-09-07",
"baseline_commit": "c4b3c55c826fca1ce09618e418e95f0a24478d96",
"calibration_approved": false,
"production_selector_changed": false,
"vehicle_settings_changed": false,
"panda_safety_changed": false,
"scope": "Offline command construction, adapter integration, numerical fault probes and Ford regression tests. Physical response remains unvalidated.",
"controller_size": {
"total_lines": 131,
"code_lines_excluding_blanks_comments_docstrings": 86,
"core_persistent_values": 2,
"adapter_timestamps": 3
},
"opendbc_import_head": "72a775d35e54c21ff5c5798acef22016eedcc0a7",
"tests": {
"ford_suite": "264 passed, 150 subtests passed in 14.29s",
"new_core_adapter_tests": 107,
"new_replay_validator_tests": 13,
"controller_coverage": {
"statements": 78,
"missing_statements": 0,
"branches": 24,
"partial_branches": 0,
"percent": 100
},
"ruff": "pass",
"ty_controller_and_lab": "pass"
},
"routes": {
"route95": {
"cycles": 54738,
"core_active_cycles": 37614,
"core_exact_archived_match": true,
"cohorts_reproduced": true,
"adapter_active_cycles": 37614,
"adapter_status_counts": {
"inactive": 17124,
"active": 37614
},
"adapter_exact_match_with_fresh_engagement_dt": true,
"core_active_path_shorter_than_7m_cycles": 44,
"adapter_validity_differs_from_archive_cycles": 0,
"adapter_command_differs_from_archive_cycles": 59,
"adapter_max_absolute_command_difference_c0_c1": [
0.010000000000000675,
0.0010000000000000009
],
"field_slew_zero_c2_c3_pass": true,
"float32_can_round_trips": 109476,
"turn_speed_15_55": {
"seconds": 33.332073582999925,
"core_c0_c1_rms": [
0.07257996778378971,
0.03628926246224237
],
"recorded_v8_c0_c1_rms": [
0.3416081588451539,
0.027322786376822172
],
"adapter_eligible_seconds": 33.332073582999925,
"adapter_c0_c1_rms": [
0.07257996778378971,
0.03628926246224237
]
},
"input_sha256": {
"route.npz": "6e5438867ea618e53ca395b60ff4b9b146256f1bf6f07dc8e92d663286074154",
"encoder_comparison.npz": "23bd05c4b6400299844acaba1d97051c96c23c682bf17b46e89e7f2cca5fce38",
"pose_replay.npz": "9cfbb3c6f0b1fdb7e3d38e6b64b8c94cffbcc9fabe41f6344d84a9b657b6af9b"
},
"report_sha256": "cf4e3804f2ebdb0e50f3c49636bf60a30e3c1180411b582826a115970ab972fc"
},
"route90": {
"cycles": 78812,
"core_active_cycles": 73055,
"core_exact_archived_match": true,
"cohorts_reproduced": true,
"adapter_active_cycles": 73055,
"adapter_status_counts": {
"inactive": 5757,
"active": 73055
},
"adapter_exact_match_with_fresh_engagement_dt": true,
"core_active_path_shorter_than_7m_cycles": 0,
"adapter_validity_differs_from_archive_cycles": 0,
"adapter_command_differs_from_archive_cycles": 19,
"adapter_max_absolute_command_difference_c0_c1": [
0.020000000000000462,
0.0020000000000000018
],
"field_slew_zero_c2_c3_pass": true,
"float32_can_round_trips": 157624,
"turn_speed_15_55": {
"seconds": 47.53485040600012,
"core_c0_c1_rms": [
0.08686835454709245,
0.043216347944464766
],
"recorded_v8_c0_c1_rms": [
0.4471065308273131,
0.030663943784303503
],
"adapter_eligible_seconds": 47.53485040600012,
"adapter_c0_c1_rms": [
0.08686835454709245,
0.043216347944464766
]
},
"input_sha256": {
"route.npz": "51e8c26eedde253e171af47d704c1967ba45ae6825d883393bec1fb9e00251c1",
"encoder_comparison.npz": "7a625d3ed5cbd8013d1028aa3bc421740551dcae5c3d60981208bd047af9794c",
"pose_replay.npz": "4457ccc0868354749da5b72c1dea0faf750f783dfdc87038101288fdcba1e707"
},
"report_sha256": "b95247bdf6bbec16e5dc4781eaf7a678aca787418251cd161f6438e3173ad490"
}
},
"stress": {
"seed": 20260907,
"random_cycles": 200000,
"mirrored_core_updates": 200000,
"invalid_or_inactive_resets": 3537,
"field_boundary_cases": 18138,
"float32_can_round_trips": 218138,
"analytic_targets_scalar_slew_and_mirror_checks_pass": true,
"direct_raw_float32_packing_matches_host_output": true,
"max_continuous_step_c0_c1": [
0.40000000000000147,
0.05000000000000002
],
"calibration_approved": false,
"scope": "Numerical construction only; no PSCM response or closed-loop performance claims.",
"opendbc_import_head": "72a775d35e54c21ff5c5798acef22016eedcc0a7"
},
"mutation_checks": {
"mutations": [
{
"mutation": "halve_heading",
"detected_by_tests": true,
"failed_tests": 9
},
{
"mutation": "cap_heading_preview",
"detected_by_tests": true,
"failed_tests": 9
},
{
"mutation": "erase_centering",
"detected_by_tests": true,
"failed_tests": 15
},
{
"mutation": "slow_c0_to_c1_rate",
"detected_by_tests": true,
"failed_tests": 11
},
{
"mutation": "retain_invalid_state",
"detected_by_tests": true,
"failed_tests": 18
},
{
"mutation": "ignore_model_freshness",
"detected_by_tests": true,
"failed_tests": 2
},
{
"mutation": "ignore_model_clock_rollback",
"detected_by_tests": true,
"failed_tests": 1
},
{
"mutation": "reverse_c0_wire_sign",
"detected_by_tests": true,
"failed_tests": 11
}
],
"all_detected": true
},
"native_test_dependency": {
"scope": "Native dependency for inherited Params selection test only; copied existing local build, not rebuilt.",
"source_library": "/Users/ibpersonal/.codex/worktrees/3548/sunnypilot/openpilot/common/libparams_c.dylib",
"sha256": "ddde738108eab18b75f085c865c43aa197fd79a0384b390b1515ff90116e20e0",
"byte_identical_source_files": [
"openpilot/common/params.cc",
"openpilot/common/params.h",
"openpilot/common/params_c.cc",
"openpilot/common/params.py",
"openpilot/common/params_keys.h",
"openpilot/common/queue.h",
"openpilot/common/util.cc",
"openpilot/common/util.h",
"openpilot/common/hardware/hw.h"
]
},
"review": {
"standards_findings": 0,
"spec_findings": 0,
"method": "Independent parallel read-only reviews; 120 focused tests independently passed."
},
"source_sha256": {
"openpilot/selfdrive/controls/lib/ford_model_action.py": "cb6353f00f2f5c84df4e606c6b7e20650f8c1e908b9fd71890f72aa4a5e42592",
"openpilot/selfdrive/controls/tests/test_ford_model_action.py": "c3b971622cc4041575aeec1826d45b76cebab9a2f77b2b9525c85d4184961295",
"openpilot/selfdrive/controls/tests/test_ford_model_action_adapter.py": "5aaa29c1f11080b7df0165fd0202a29b065053809e5f9c4b9ed8abfcce47e41b",
"tools/ford_pscm_lab/__init__.py": "db4b8b7d2e317ed34ca0ec220bf9d53e7b80a23e766f4e1cb2c8224dada45f2e",
"tools/ford_pscm_lab/model_action_replay.py": "c114c479bd22e4fc61a3e8d3ee7fae5d71d1d80ec4b952faad8f8f3692fb1508",
"tools/ford_pscm_lab/stress_model_action.py": "a78a50eed1f801f3b096d694ab8c2fd70804b6c250465b4152f38d83770a982b",
"tools/ford_pscm_lab/test_model_action_replay.py": "09d024c59d44b83ec081d6416d0f946a7719a73f22213af1f4ddc03dc4e6f4ac"
},
"artifacts": {
"directory": ".cache/ford_model_action",
"route_reports": [
"route95/report.json",
"route90/report.json"
],
"stress_report": "stress.json",
"mutation_report": "mutations/report.json",
"test_log": "ford_suite.txt"
}
}
+134
View File
@@ -0,0 +1,134 @@
# Ford model-point candidate v7
This is the historical baseline at `4bd841ecc`. The driving branch now uses
the [C1 early-release experiment](ford_model_release.md). Results below remain
bound to their original sources and do not validate the newer request mapping.
## Decision
Use the model's own position and orientation at one shared path point. This
implements the request to follow model geometry through C0/C1, with C2/C3 zero,
without a fitted PSCM plant, new strength gain, yaw integral, or release mode.
Let `s(t)` be cumulative planar arc distance along model position. Choose:
```
station = min(path_end, max(7 metres, s(1 second)))
C0_target = model.position.y at station
C1_target = unwrapped model.orientation.z at station
C2 = C3 = 0
```
Use the published model timestamps, not `speed × 1 second`, so the point also
follows the model's predicted acceleration/braking. The seven-metre floor keeps
the existing low-speed preview distance; it can select a time beyond one second.
If the entire path is shorter, both fields hold the same endpoint. Interpolation
uses the same arc segment and weights for position and heading.
One second keeps C1 near the old one-second heading scale in ordinary driving.
Using that same point for C0 represents a meaningful change in faster bends.
This is a chosen local approximation; Ford's expected reference point and
preview are unknown. Matching metres/radians does not prove PSCM equivalence.
The former 150 ms vehicle-pose forecast is removed. C1 no longer uses
`max(7, speed) × selected desiredCurvature`. Measured yaw remains an input-health
check only; calibrated yaw has no command role. Freshness, independent slew,
packing, the shared startup toggle, 20 Hz sends, and Panda safety remain.
The upstream scalar curvature is still logged but does not limit this geometry;
C0/C1 retain their existing amplitude and slew limits. The scalar-only lateral
maneuver test reference is explicitly unsupported and disengages this candidate.
## Offline evidence
The broad run passed **585 tests and 9,146 subtests**, with 178 inherited safety
cases skipped as inapplicable. Tests of the removed yaw forecast were retired;
new tests cover actual model clocks, a nonconstant-speed trajectory, shared-point
sampling, the distance floor, endpoint holding, heading unwrap, source selection,
invalid geometry, reset, independent slew, and actual CAN delivery.
The stress run covers 200,000 random cycles plus mirrored turns, 18,138 field
boundary cases, and 218,138 Float32/CAN round trips. An independent scalar oracle
checks analytic model points and slew. No vehicle plant is simulated.
Five recorded routes (a5, a2, a0, 9b, 9e) supply **379,718 controller cycles** and
**76,294 model frames**. Original rlog hashes and position/orientation timestamps
were checked. An independently implemented segment-weight oracle matches every
sampled target and eligible slew state. Every cycle passed packing/bound checks;
all **75,947** scheduled 20 Hz requests passed the actual unchanged Panda TX hook.
This is TX acceptance with controlled eligibility, not a full Panda RX watchdog
or vehicle-response replay. The [validation manifest](ford_model_points_validation.json)
binds the results to their source and input hashes.
On a5's 61.24-second driver-clean ordinary-bend cohort, mean absolute C0 changes
from 0.075 m recorded to 0.167 m replayed; C1 changes from 0.02287 to 0.02224 rad.
Neither field target clips in that cohort. These are command differences, not
predicted changes in steering strength or tracking error.
| a5 time | Recorded C0 / C1 | Candidate C0 / C1 | Selected station / time |
| --- | --- | --- | --- |
| 107.995 s, ordinary bend | +0.20 m / +0.068 rad | +0.55 m / +0.0685 rad | 13.71 m / 1.00 s |
| 390.681 s, sustained bend | 0.52 m / 0.1235 rad | 0.75 m / 0.1215 rad | 9.37 m / 1.00 s |
| 374.889 s, tight turn | 3.76 m / 0.50 rad | 3.46 m / 0.50 rad | 7.00 m / 1.49 s |
| 17.607 s, exit overshoot | 0.01 m / 0.015 rad | 0.05 m / 0.012 rad | 7.00 m / 1.85 s |
The exit example retains more C0 into the turn than v6; physical unwind behavior
must be evaluated. Tight-turn C1 clipping remains (14.92 eligible seconds on a5).
No root cause or physical fix is proven by frozen inputs. The latest a5 road
logs used 100 Hz sends, whereas the immediately preceding code revision already
changed to 20 Hz; a comparison against that drive also includes the cadence change.
## Unwind comparison
The follow-up a5 comparison includes all seven clearly separated tight turns and
four completed ordinary bends identified in the full-route command plot. A fifth
bend runs into the next turn and is excluded from the summary. All scored windows
remain continuously paired-active, without control gaps over 30 ms. The tight
turns contain driver input; this compares instructions on frozen inputs, not
unassisted tracking or hypothetical truck motion.
Measure the first time each instruction falls below the same fixed level on
exit and stays below for 100 ms. All seven tight turns exceed these levels:
| Instruction | Candidate minus recorded clearance time |
| --- | --- |
| C0 below 0.5 m into the turn | 0.10 s later median; five later by 0.010.46 s, two unchanged |
| C1 below 0.1 rad into the turn | 0.10 s earlier median; all seven 0.040.17 s earlier |
These are control-publication times. Actual packet timing also includes the
20 Hz send phase; shifts of only a few tens of milliseconds should not be
interpreted as equally precise changes at the PSCM.
The report also compares half of each command's own peak: C1 reaches that level
0.25 s earlier in the median tight turn and C0 0.12 s later. Those normalized
crossings have different absolute thresholds when amplitudes differ. Comparing
half the smaller peak at an identical level instead gives C0 earlier in one
turn and later in six. No single threshold captures the whole release waveform.
For the ordinary bend around 108 s, C0 reaches half of its own peak 0.20 s later;
C1 reaches half-peak 0.24 s earlier. The larger C0 takes 0.95 s longer to fall
below the same 0.05 m threshold. On the last tight turn, C0 clears 0.05 m 0.23 s
later. Near-zero thresholds are sensitive to small residual model offsets; the
complete report retains 90%, 50%, 10%, common-level and near-zero crossings
rather than treating any one threshold as a physical success criterion.
The plotted candidate target and command largely coincide during these exits:
the longer C0 tail comes from the selected model point continuing to ask for
lateral offset, rather than a retained yaw correction. C1 can release sooner
because it now follows model orientation directly. These results do not show
that every command unwinds earlier, or that the vehicle will unwind earlier.
Ten additional end-to-end sender cases cover both signs and all five CAN send
phases. After a full-cap turn, a zero model target starts reducing both states on
the first control update and appears on the next scheduled CAN message, 040 ms
later at nominal 100 Hz calculation. There is no additional release hold. The
unchanged slew itself takes 1.28 s of updates to clear 5.11 m C0 and 1.00 s to
clear 0.5 rad C1, plus scheduling to transmit zero. These are software timing
checks, not measured PSCM dynamics. The [unwind report](ford_model_points_unwind.json)
records the episode timings, method and source hashes.
Older offline utilities now fail explicitly when their input lacks original
model clocks or their historical unchanged-C1 comparison cannot support v7.
This prevents missing model inputs from producing an all-invalid apparent match.
`calibration_approved=false` remains. Installation instructions and restore
behavior are in the [drive-test guide](ford_model_action_drive_test.md).
File diff suppressed because it is too large Load Diff
+632
View File
@@ -0,0 +1,632 @@
{
"baseline_root": "e1cd61166c529f39cb47c815db47e80de720a778",
"opendbc": "87ca78e6e641eefb2d654f260a6ab08df3058bd5",
"scope": "Model-point construction only; physical tracking not validated.",
"calibration_approved": false,
"tests": {
"passed": 585,
"subtests_passed": 9146,
"inapplicable_skips": 178
},
"source_sha256": {
"openpilot/selfdrive/controls/lib/ford_model_action.py": "e7502ab68d04aef52edcffcef4b1d42c04e9008ab6892789aac3a865c100cb6d",
"openpilot/selfdrive/controls/controlsd.py": "bea720b8ec68d6b8a6ad4376ae37324e2404833fb13097a8ab7710a1cedf37a5",
"tools/ford_pscm_lab/stress_model_action.py": "0221f85ea2f5757d3b54d077ed8be1faaa6a5d2ed8371de1137f3b6f3d70bf47",
"openpilot/sunnypilot/sunnylink/settings_ui_src/pages/vehicle.yaml": "ec5e0d9023f7260ae619a4cc6ec538d39053ac13e4515bd0de7d854b79d8abf6",
"openpilot/sunnypilot/sunnylink/settings_ui.json": "62e5178a8deee32ef5a88026094cedde359130741688e8a4bcfa82155ef4cca3",
"tools/ford_pscm_lab/model_action_replay.py": "95d546cc87c065fa7d581b41382e1ab78bacc4030931b81388cb69c61adf32f7",
"tools/ford_pscm_lab/damping_replay.py": "1c253b99342d8113739b0a0551ca66ec6ced9e0aa96338ce6cb42e09c04178a4",
"tools/ford_pscm_lab/test_model_action_replay.py": "7aa01cbf197155175215aa4494cfc909029526fbacad0a1e1a2a214583064c4b",
"openpilot/selfdrive/controls/tests/test_ford_model_action_cadence.py": "d647e33c83581257f2662c3fec725bb9cd245fb48d579c2e38388318592a2690"
},
"routes": {
"a5": {
"route": "a5",
"scope": "Frozen inputs; command construction and Panda TX acceptance only. No counterfactual vehicle response.",
"cycles": 38961,
"models": 7817,
"round_trips": 38961,
"panda_accepted_sends": 7793,
"source_model_clocks_verified": true,
"independent_point_and_slew_oracle_passed": true,
"cohorts": {
"eligible": {
"seconds": 359.186785903,
"old_mean_abs_c0": 0.34434291232781916,
"new_mean_abs_c0": 0.3706073530276498,
"old_mean_abs_c1": 0.05784747586810226,
"new_mean_abs_c1": 0.06183462577629167,
"c0_target_capped_s": 0.0,
"c1_target_capped_s": 14.919213676999902
},
"road_speed": {
"seconds": 261.730126543,
"old_mean_abs_c0": 0.10481662146430282,
"new_mean_abs_c0": 0.16319455033344274,
"old_mean_abs_c1": 0.025151988851597765,
"new_mean_abs_c1": 0.02471229299756163,
"c0_target_capped_s": 0.0,
"c1_target_capped_s": 0.0
},
"low_speed": {
"seconds": 97.45665935999997,
"old_mean_abs_c0": 0.9876160024355487,
"new_mean_abs_c0": 0.9276362872461182,
"old_mean_abs_c1": 0.14565465097969882,
"new_mean_abs_c1": 0.16153056158564297,
"c0_target_capped_s": 0.0,
"c1_target_capped_s": 14.919213676999902
},
"clean": {
"seconds": 249.91415584299997,
"old_mean_abs_c0": 0.08470877473478762,
"new_mean_abs_c0": 0.1191542994782463,
"old_mean_abs_c1": 0.017726187650852487,
"new_mean_abs_c1": 0.018543416153046232,
"c0_target_capped_s": 0.0,
"c1_target_capped_s": 1.056498005999913
},
"clean_ordinary_bends": {
"seconds": 61.23773978999962,
"old_mean_abs_c0": 0.07537829679720884,
"new_mean_abs_c0": 0.16659385323910988,
"old_mean_abs_c1": 0.022868833214909796,
"new_mean_abs_c1": 0.022235380107927534,
"c0_target_capped_s": 0.0,
"c1_target_capped_s": 0.0
}
},
"points": [
{
"t": 17.606574576,
"old_c0_c1": [
-0.009999999776482582,
-0.014999999664723873
],
"new_c0_c1": [
-0.04999999999999982,
-0.01200000000000001
],
"raw_target": [
-0.04535145975205448,
-0.012038111718131041
],
"sample_station_m": 7.0,
"sample_time_s": 1.8525775632377612
},
{
"t": 107.99483939600002,
"old_c0_c1": [
0.20000000298023224,
0.06800000369548798
],
"new_c0_c1": [
0.5499999999999998,
0.0685
],
"raw_target": [
0.548033630847931,
0.06853501158101219
],
"sample_station_m": 13.711669224561392,
"sample_time_s": 1.0
},
{
"t": 374.888921539,
"old_c0_c1": [
-3.759999990463257,
-0.5
],
"new_c0_c1": [
-3.46,
-0.5
],
"raw_target": [
-3.497294441592979,
-0.6981158781127829
],
"sample_station_m": 7.0,
"sample_time_s": 1.490557194102553
},
{
"t": 390.68080671100006,
"old_c0_c1": [
-0.5199999809265137,
-0.12349999696016312
],
"new_c0_c1": [
-0.75,
-0.12150000000000005
],
"raw_target": [
-0.7522029416901727,
-0.1214686983398029
],
"sample_station_m": 9.370794504969545,
"sample_time_s": 1.0
}
],
"hashes": {
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/openpilot/selfdrive/controls/lib/ford_model_action.py": "e7502ab68d04aef52edcffcef4b1d42c04e9008ab6892789aac3a865c100cb6d",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_model_points/replay.py": "762ebaac3b31737ad609d5b9788514147c696e548fd1ee09458314a0eea30434",
".cache/ford_routea5/route.npz": "815d1e248ff5c3e5e5cfc11dfbd0690d0d13ddafe1436890e239975a72dae9b8",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_model_points/a5_models.npz": "84cf4b6e0102d8d5a3f6c3e29bf332e929d7f28d0262eb46220fccc631b902aa"
}
},
"a2": {
"route": "a2",
"scope": "Frozen inputs; command construction and Panda TX acceptance only. No counterfactual vehicle response.",
"cycles": 71111,
"models": 14287,
"round_trips": 71111,
"panda_accepted_sends": 14223,
"source_model_clocks_verified": true,
"independent_point_and_slew_oracle_passed": true,
"cohorts": {
"eligible": {
"seconds": 437.0644468180001,
"old_mean_abs_c0": 0.2155311761983153,
"new_mean_abs_c0": 0.31863289785799315,
"old_mean_abs_c1": 0.04341179516514646,
"new_mean_abs_c1": 0.045887913019549746,
"c0_target_capped_s": 0.0,
"c1_target_capped_s": 11.38161301699995
},
"road_speed": {
"seconds": 328.02165902499996,
"old_mean_abs_c0": 0.06680861343584246,
"new_mean_abs_c0": 0.21392942512598495,
"old_mean_abs_c1": 0.025146318618058133,
"new_mean_abs_c1": 0.025146126320953914,
"c0_target_capped_s": 0.0,
"c1_target_capped_s": 0.0
},
"low_speed": {
"seconds": 109.04278779300012,
"old_mean_abs_c0": 0.6629172230811673,
"new_mean_abs_c0": 0.6336010633494195,
"old_mean_abs_c1": 0.09835785845941926,
"new_mean_abs_c1": 0.10828319309008405,
"c0_target_capped_s": 0.0,
"c1_target_capped_s": 11.38161301699995
},
"clean": {
"seconds": 332.6767952400001,
"old_mean_abs_c0": 0.07338523837198829,
"new_mean_abs_c0": 0.1946808147540538,
"old_mean_abs_c1": 0.023104103590461375,
"new_mean_abs_c1": 0.022903078905402382,
"c0_target_capped_s": 0.0,
"c1_target_capped_s": 2.902278352000053
},
"clean_ordinary_bends": {
"seconds": 111.90846122999926,
"old_mean_abs_c0": 0.07917757541079931,
"new_mean_abs_c0": 0.3589047266588909,
"old_mean_abs_c1": 0.03830142702025842,
"new_mean_abs_c1": 0.03807353528971872,
"c0_target_capped_s": 0.0,
"c1_target_capped_s": 0.0
}
},
"points": [],
"hashes": {
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/openpilot/selfdrive/controls/lib/ford_model_action.py": "e7502ab68d04aef52edcffcef4b1d42c04e9008ab6892789aac3a865c100cb6d",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_model_points/replay.py": "762ebaac3b31737ad609d5b9788514147c696e548fd1ee09458314a0eea30434",
".cache/ford_routea2/route.npz": "af4128d278be6be0042f72a02255ddbec96a8a5aa435b9852c8202e4a5cb39e5",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_model_points/a2_models.npz": "c3a366796ca5fb27f8c0dc85732ccd6f0ec61b4c8646af3dfe55600e026c0810"
}
},
"a0": {
"route": "a0",
"scope": "Frozen inputs; command construction and Panda TX acceptance only. No counterfactual vehicle response.",
"cycles": 114582,
"models": 23038,
"round_trips": 114582,
"panda_accepted_sends": 22917,
"source_model_clocks_verified": true,
"independent_point_and_slew_oracle_passed": true,
"cohorts": {
"eligible": {
"seconds": 1061.1945591,
"old_mean_abs_c0": 0.16681197800600925,
"new_mean_abs_c0": 0.25477442195222594,
"old_mean_abs_c1": 0.036905620742387873,
"new_mean_abs_c1": 0.0381744756199198,
"c0_target_capped_s": 0.0,
"c1_target_capped_s": 14.894412850999998
},
"road_speed": {
"seconds": 901.0129238640001,
"old_mean_abs_c0": 0.038282177648169546,
"new_mean_abs_c0": 0.13938686570168032,
"old_mean_abs_c1": 0.016746723820478274,
"new_mean_abs_c1": 0.01614716305741802,
"c0_target_capped_s": 0.0,
"c1_target_capped_s": 0.0
},
"low_speed": {
"seconds": 160.18163523600006,
"old_mean_abs_c0": 0.889785064486564,
"new_mean_abs_c0": 0.9038231052276234,
"old_mean_abs_c1": 0.15029831167554059,
"new_mean_abs_c1": 0.1620769021843944,
"c0_target_capped_s": 0.0,
"c1_target_capped_s": 14.894412850999998
},
"clean": {
"seconds": 884.2159145080002,
"old_mean_abs_c0": 0.05133685312289168,
"new_mean_abs_c0": 0.13569127462997554,
"old_mean_abs_c1": 0.017060990972111164,
"new_mean_abs_c1": 0.01680535356441334,
"c0_target_capped_s": 0.0,
"c1_target_capped_s": 4.094436286000047
},
"clean_ordinary_bends": {
"seconds": 229.50590253700045,
"old_mean_abs_c0": 0.06277285849992259,
"new_mean_abs_c0": 0.29272144832989233,
"old_mean_abs_c1": 0.032915757288861924,
"new_mean_abs_c1": 0.03226126900978203,
"c0_target_capped_s": 0.0,
"c1_target_capped_s": 0.0
}
},
"points": [],
"hashes": {
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/openpilot/selfdrive/controls/lib/ford_model_action.py": "e7502ab68d04aef52edcffcef4b1d42c04e9008ab6892789aac3a865c100cb6d",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_model_points/replay.py": "762ebaac3b31737ad609d5b9788514147c696e548fd1ee09458314a0eea30434",
".cache/ford_routea0/route.npz": "01ce8b0361c3154d07d5449525a650baa2cc261675095cb1f04ec9e37a9174d3",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_model_points/a0_models.npz": "72a72c950d244f5afcffe8cb1cd09b9731297f7565c93814197c728d4b25e8cb"
}
},
"9b": {
"route": "9b",
"scope": "Frozen inputs; command construction and Panda TX acceptance only. No counterfactual vehicle response.",
"cycles": 71396,
"models": 14346,
"round_trips": 71396,
"panda_accepted_sends": 14280,
"source_model_clocks_verified": true,
"independent_point_and_slew_oracle_passed": true,
"cohorts": {
"eligible": {
"seconds": 651.159063169,
"old_mean_abs_c0": 0.09686840630728118,
"new_mean_abs_c0": 0.16616909035237892,
"old_mean_abs_c1": 0.020769878239136775,
"new_mean_abs_c1": 0.02325818302804746,
"c0_target_capped_s": 0.0,
"c1_target_capped_s": 4.1034965929999885
},
"road_speed": {
"seconds": 549.860784016,
"old_mean_abs_c0": 0.025487998809066218,
"new_mean_abs_c0": 0.10707029577162376,
"old_mean_abs_c1": 0.011795500864550536,
"new_mean_abs_c1": 0.01141539602134342,
"c0_target_capped_s": 0.0,
"c1_target_capped_s": 0.0
},
"low_speed": {
"seconds": 101.29827915299992,
"old_mean_abs_c0": 0.484330929447169,
"new_mean_abs_c0": 0.48696535455636186,
"old_mean_abs_c1": 0.06948401455523531,
"new_mean_abs_c1": 0.08754243546441211,
"c0_target_capped_s": 0.0,
"c1_target_capped_s": 4.1034965929999885
}
},
"points": [],
"hashes": {
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/openpilot/selfdrive/controls/lib/ford_model_action.py": "e7502ab68d04aef52edcffcef4b1d42c04e9008ab6892789aac3a865c100cb6d",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_model_points/replay.py": "762ebaac3b31737ad609d5b9788514147c696e548fd1ee09458314a0eea30434",
".cache/ford_route9b/route.npz": "8942b281b76eac012ee660f8ae21bbcad0df95d515291d2497ce5897d5738647",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_model_points/9b_models.npz": "fa444360a2775b0df38f274ab4c99568bbe47a3d2777c31ab0193705fce2b1fb"
}
},
"9e": {
"route": "9e",
"scope": "Frozen inputs; command construction and Panda TX acceptance only. No counterfactual vehicle response.",
"cycles": 83668,
"models": 16806,
"round_trips": 83668,
"panda_accepted_sends": 16734,
"source_model_clocks_verified": true,
"independent_point_and_slew_oracle_passed": true,
"cohorts": {
"eligible": {
"seconds": 751.5479497329998,
"old_mean_abs_c0": 0.09489001052394747,
"new_mean_abs_c0": 0.16056913479509022,
"old_mean_abs_c1": 0.02256825583285058,
"new_mean_abs_c1": 0.022300154628921095,
"c0_target_capped_s": 0.0,
"c1_target_capped_s": 7.398116462998587
},
"road_speed": {
"seconds": 624.6602415809975,
"old_mean_abs_c0": 0.023757802428838078,
"new_mean_abs_c0": 0.10203005034223132,
"old_mean_abs_c1": 0.01124063950870769,
"new_mean_abs_c1": 0.010770200237869725,
"c0_target_capped_s": 0.0,
"c1_target_capped_s": 0.0
},
"low_speed": {
"seconds": 126.88770815200223,
"old_mean_abs_c0": 0.4450694167092398,
"new_mean_abs_c0": 0.4487533818653312,
"old_mean_abs_c1": 0.07833340166635812,
"new_mean_abs_c1": 0.07906139809550021,
"c0_target_capped_s": 0.0,
"c1_target_capped_s": 7.398116462998587
},
"clean": {
"seconds": 639.296275887018,
"old_mean_abs_c0": 0.02396620128468532,
"new_mean_abs_c0": 0.09600545527466989,
"old_mean_abs_c1": 0.01021397220255641,
"new_mean_abs_c1": 0.010198642694698857,
"c0_target_capped_s": 0.0,
"c1_target_capped_s": 0.0
},
"clean_ordinary_bends": {
"seconds": 168.17771330400137,
"old_mean_abs_c0": 0.05087679617819314,
"new_mean_abs_c0": 0.2527786285635614,
"old_mean_abs_c1": 0.02663914589176354,
"new_mean_abs_c1": 0.026386933889696897,
"c0_target_capped_s": 0.0,
"c1_target_capped_s": 0.0
}
},
"points": [],
"hashes": {
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/openpilot/selfdrive/controls/lib/ford_model_action.py": "e7502ab68d04aef52edcffcef4b1d42c04e9008ab6892789aac3a865c100cb6d",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_model_points/replay.py": "762ebaac3b31737ad609d5b9788514147c696e548fd1ee09458314a0eea30434",
".cache/ford_route9e/route.npz": "9f169338caf2b5a5377332d15914d1dde57be8b922e25c1bbe16264e0f1be6fb",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_model_points/9e_models.npz": "acacf8f0d16fd986b6d74e4a02d704fa5b5b3327fe66dcbe149806f2a5bc3d65"
}
}
},
"stress": {
"seed": 20260908,
"random_cycles": 200000,
"mirrored_core_updates": 200000,
"invalid_or_inactive_resets": 3537,
"field_boundary_cases": 18138,
"float32_can_round_trips": 218138,
"analytic_targets_scalar_slew_and_mirror_checks_pass": true,
"valid_yaw_does_not_affect_targets_checked": true,
"shared_model_point_checked": true,
"direct_raw_float32_packing_matches_host_output": true,
"max_continuous_step_c0_c1": [
0.40000000000000147,
0.05000000000000002
],
"calibration_approved": false,
"scope": "Numerical construction only; no PSCM response or closed-loop performance claims.",
"opendbc_import_head": "87ca78e6e641eefb2d654f260a6ab08df3058bd5",
"source_sha256": {
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/stress_model_action.py": "0221f85ea2f5757d3b54d077ed8be1faaa6a5d2ed8371de1137f3b6f3d70bf47",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/openpilot/selfdrive/controls/lib/ford_model_action.py": "e7502ab68d04aef52edcffcef4b1d42c04e9008ab6892789aac3a865c100cb6d",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/model_action_replay.py": "95d546cc87c065fa7d581b41382e1ab78bacc4030931b81388cb69c61adf32f7"
}
},
"unwind": {
"report": "docs/ford_model_points_unwind.json",
"sha256": "7395a4e66a275fd72d744f5a8244f1b9c8840bb7efff2eec225f15b9c033cd6e",
"full_cap_release_send_phase_cases": 10,
"summary": {
"tight": {
"episodes": 7,
"c0": {
"below_90pct_s": {
"n": 7,
"median_s": 0.0,
"min_s": -0.12269046900000546,
"max_s": 0.04266988000000538,
"earlier": 2,
"later": 2
},
"below_50pct_s": {
"n": 7,
"median_s": 0.12129962300002717,
"min_s": 0.0848213179999675,
"max_s": 0.8246327600000001,
"earlier": 0,
"later": 7
},
"below_10pct_s": {
"n": 7,
"median_s": 0.1376267710000434,
"min_s": 0.0,
"max_s": 0.45046578099999124,
"earlier": 0,
"later": 6
},
"near_zero_s": {
"n": 5,
"median_s": 0.1399018539999588,
"min_s": -0.5224231490000193,
"max_s": 1.9510819790000085,
"earlier": 1,
"later": 4
},
"common_half_s": {
"n": 7,
"median_s": 0.07685077599995793,
"min_s": -0.2591174110000338,
"max_s": 0.5661551039999893,
"earlier": 1,
"later": 6
},
"fixed_clearance_s": {
"n": 7,
"median_s": 0.10080577899998389,
"min_s": 0.0,
"max_s": 0.4609723959999883,
"earlier": 0,
"later": 5
}
},
"c1": {
"below_90pct_s": {
"n": 7,
"median_s": -0.3856273459999784,
"min_s": -0.5246007750000103,
"max_s": 0.5545458859999997,
"earlier": 6,
"later": 1
},
"below_50pct_s": {
"n": 7,
"median_s": -0.2540558009999927,
"min_s": -0.46442294700000275,
"max_s": -0.21144918100003451,
"earlier": 7,
"later": 0
},
"below_10pct_s": {
"n": 6,
"median_s": -0.15774414349999688,
"min_s": -0.20159088100001554,
"max_s": 0.0,
"earlier": 5,
"later": 0
},
"near_zero_s": {
"n": 5,
"median_s": 0.0,
"min_s": -0.148710938000022,
"max_s": 1.5534367700000047,
"earlier": 2,
"later": 2
},
"common_half_s": {
"n": 7,
"median_s": -0.2528352759999848,
"min_s": -0.45502116000000115,
"max_s": -0.15168304499999863,
"earlier": 7,
"later": 0
},
"fixed_clearance_s": {
"n": 7,
"median_s": -0.10267424500000288,
"min_s": -0.1688626670000417,
"max_s": -0.040530361999969955,
"earlier": 7,
"later": 0
}
}
},
"ordinary": {
"episodes": 4,
"c0": {
"below_90pct_s": {
"n": 4,
"median_s": 0.49771416100000465,
"min_s": 0.2530698499999744,
"max_s": 3.195503137000003,
"earlier": 0,
"later": 4
},
"below_50pct_s": {
"n": 4,
"median_s": 0.3417062340000143,
"min_s": 0.19507780300000377,
"max_s": 0.4595077099999685,
"earlier": 0,
"later": 4
},
"below_10pct_s": {
"n": 4,
"median_s": 0.11352378200000146,
"min_s": 0.09310488600004874,
"max_s": 1.1537201169999776,
"earlier": 0,
"later": 4
},
"near_zero_s": {
"n": 4,
"median_s": 0.6988682280000234,
"min_s": 0.40513631699997177,
"max_s": 3.1885321799999815,
"earlier": 0,
"later": 4
},
"common_half_s": {
"n": 4,
"median_s": 1.0263607459999946,
"min_s": 0.7940629349999995,
"max_s": 2.846801904000017,
"earlier": 0,
"later": 4
},
"fixed_clearance_s": {
"n": 2,
"median_s": 1.97568246000003,
"min_s": 1.5486082510000188,
"max_s": 2.4027566690000413,
"earlier": 0,
"later": 2
}
},
"c1": {
"below_90pct_s": {
"n": 4,
"median_s": -0.0037014085000066643,
"min_s": -0.25255103900002496,
"max_s": 0.3999567310000316,
"earlier": 2,
"later": 1
},
"below_50pct_s": {
"n": 4,
"median_s": -0.17102558000001977,
"min_s": -0.2489269939999872,
"max_s": -0.046780566999984785,
"earlier": 4,
"later": 0
},
"below_10pct_s": {
"n": 4,
"median_s": -0.10130154349999998,
"min_s": -0.29764589699999533,
"max_s": 0.05121605200002932,
"earlier": 2,
"later": 1
},
"near_zero_s": {
"n": 4,
"median_s": -0.022888650000027155,
"min_s": -0.10325871399999187,
"max_s": 0.24679704999999785,
"earlier": 2,
"later": 1
},
"common_half_s": {
"n": 4,
"median_s": -0.22263683349999042,
"min_s": -0.2573995380000156,
"max_s": -0.09939024700003074,
"earlier": 4,
"later": 0
},
"fixed_clearance_s": {
"n": 2,
"median_s": -0.17009618150001415,
"min_s": -0.24998850800000127,
"max_s": -0.09020385500002703,
"earlier": 2,
"later": 0
}
}
}
}
}
}
+101
View File
@@ -0,0 +1,101 @@
# Ford C1 early-release experiment
The a9 recording contains holds followed by rapid steering release while C0/C1
requests were already decreasing. Several occur without a PSCM limit flag or
detected driver input. The experiment changes C1 alone to ask for less turning
when the model's path straightens ahead. It does not establish which internal
PSCM mechanism caused those holds.
## Request mapping
Keep the v7 sample station `s`: model arc distance at one second, with the
existing seven-metre minimum and endpoint hold. Let `psi` be unwrapped model
heading at that station and `psi0` its initial heading. Using the same model
segment enclosing that point:
```
terminal_curvature = change in heading / change in arc distance
release_heading = psi0 + s * terminal_curvature
C0_target = model lateral position at s
C1_target = release_heading bounded between zero and psi
C2 = C3 = 0
```
The C1 interval respects the sign of `psi`. The correction cannot amplify the
raw heading target or manufacture an opposite-direction target. A real model
sign reversal continues through the normal slew. At an exact model knot the
incoming segment supplies the slope. A duplicate selected station, zero span,
or nonfinite derived slope retains the original heading.
Heading linear in the sampled cumulative arc distance, and constant heading,
retain the original mapping. Arc distance uses model-point chords, so even a
true circle can differ slightly under uneven sampling; exact preservation is
a statement about the discrete heading slope, not every sampled physical arc.
When the terminal curvature is gentler than the average over the preview, C1
can decrease sooner; it can reach zero even while the future pose still has a
nonzero heading. C0 continues requesting that future lateral position.
There is no temporal turn-mode latch, new strength multiplier, measured-yaw
correction, fitted PSCM model, or foreign firmware command cap. Only the two
existing C0/C1 slew positions persist. Limits remain ±5.11 m / ±0.5 rad and
4 m/s / 0.5 rad/s; transmission remains 20 Hz.
## Physical question and tradeoff
The question is whether unloading C1 sooner makes the measured wheel release
sooner, without unacceptable loss of turning authority. Constant discrete
heading slope is preserved; not every real turn-in is guaranteed unchanged. A model
point can already lie on the straight after a corner while the truck still
needs heading change to reach it. This rule can then remove useful C1. C0 and
the PSCM's physical response determine the result, which passive replay cannot
establish.
The prior ML3V firmware's internal contribution limits are not treated as
Lightning limits. `LimitReached` is not used as a release trigger. No claim is
made that this fixes the a9 hang, tracking error, or stability across PSCMs.
`calibration_approved=false` remains.
## Recorded command comparison
The a9 comparison uses 7,199 original-clock models and 35,775 control cycles
from six recorded segments. Candidate C0 is exactly equal to baseline C0 at
raw target, publication, and sampled send times. Raw C1 never exceeds the
original heading magnitude or invents a reversal. Independent slew histories
can let the candidate reach a genuine opposite-direction target sooner, so
published C1 magnitude is not universally smaller on reversal.
Entry and plateau changes are included, rather than scoring only exits:
| Recorded window | Baseline mean absolute C1 | Candidate mean absolute C1 |
| --- | ---: | ---: |
| Right entry, 38:4738:50 | 0.13643 rad | 0.12561 rad |
| Large left entry, 40:0140:05 | unchanged | unchanged |
| Right plateau, 45:2645:28.4 | 0.11442 rad | 0.10506 rad |
| Left entry, 45:3445:36 | 0.16580 rad | 0.15962 rad |
C1 crosses a common 0.1 rad level about 0.800.85 seconds earlier in three
selected exits. The level is a comparison marker, not a PSCM limit. On the
right exits near 38:51 and 45:29, it stops requesting the original turn
direction 0.454 and 0.356 seconds earlier. These are proposed command times
on frozen inputs, not measured improvements in wheel release.
The baseline replay agrees with recorded publication within one wire quantum
after cache-boundary warmup; logged publication times do not provide every
internal controller-entry time. Original clocks, eligibility, and that
reconstruction tolerance are retained in the validation record.
## Selection and verification
Use the existing default-off Sunnylink **Selected-Action Path Tracking
(Experimental)** toggle (`FordModelActionController`). An already enabled
setting selects this revision after updating and an offroad-to-onroad cycle.
The diagnostic identity is `model-pose-terminal-c1-v1`; active diagnostics also
report `c1_release=terminal_spatial_curvature`. Turning the toggle off restores
the previous nonexperimental selection after another offroad-to-onroad cycle.
See the [drive-test guide](ford_model_action_drive_test.md) and the
[validation record](ford_model_release_validation.json). Unit/integration checks
cover command construction, bounds, reset, sender cadence, and encoding. Route
comparisons report changes in commands, including entry attenuation; they do
not predict wheel motion under the changed commands. The next recording must
compare desired versus actual steering and release timing on the actual PSCM.
File diff suppressed because it is too large Load Diff
+327
View File
@@ -0,0 +1,327 @@
# Ford C2-free model-pose tracking with measured feedback
This experiment is retired. Its implementation, setting and dedicated tests
were removed from the selected-action drive-test branch. For current setup,
see [Ford selected-action drive testing](ford_model_action_drive_test.md).
The material below is historical; it does not describe an available toggle.
Hypothesis `model-pose-c0-c1-feedback-v8` retains the model-pose C0/C1 base
and adds two guarded release policies. When measured turning exceeds both
current and delayed requests, a separate output guard prevents same-direction
C0/C1 growth, including while feedback history rebuilds after driver input.
When turning instead falls below both requests and is no longer increasing,
bounded C1 tracking can use remaining release-entry command headroom.
Existing opposing-bias recovery still stops at zero bias. Geometry, blending,
feedback gain, slew rates and field limits are unchanged; C2/C3 remain zero.
This is an experimental outer controller around the multivariable PSCM.
Its geometry does not define a calibrated C0/C1-to-wheel mapping or an angle
servo. V8 has offline validation only. Command replay cannot establish the
truck's response, closed-loop stability, or an overshoot improvement.
## Evidence and scope
Route80 ran v3 and contains both sustained under-response and over-response.
Representative eligible windows had median CAN response/request ratios of
0.78, 1.77 and 0.69 with a declared 0.2-second comparison interval. These
are descriptive tracking ratios, not identified controller gains.
V4 replaced separate model-heading C1 with selected-curvature C1 and reduced
heading demand in several large maneuvers. The user subsequently reported
weak turning and steering repeatedly stopping near 85 degrees. Older logs
contain larger wheel angles; the inspected host code has no fixed 85-degree
wheel stop, although upstream curvature limits depend on speed.
Route83 had the Sunnylink toggle on, but omitted EPS firmware responses.
The former firmware gate selected the default `FordPathController`; replay
reproduced its recorded C0/C1/C2 requests. Its favorable turns are evidence
for the existing model-pose construction, not validation of v5 or v6.
V6 reuses that construction while replacing its remaining C2 request with
C0/C1 geometry. Removing C2 changes the request received by the PSCM, so
matching large C0/C1 commands does not guarantee matching vehicle motion.
Route8a ran v6 and was reported as the best drive. Route8e ran v7 throughout
with the experiment enabled; it includes entry lag and excessive turning
while requests release. Fixed-input v6/v7 replay produced identical commands
in the main reversal and over-response examples, so the v7 recovery change
does not directly explain their command behavior. In the over-response
example, model C0/C1 grew while selected curvature fell and driver resets
repeatedly removed feedback history. Another exit remained deficient after
opposing bias reached zero. These observations motivate the v8 guards; they
do not isolate an EPS transfer function or demonstrate the proposed response.
## Base request
controlsd selects valid `lateralManeuverPlan.desiredCurvature`, otherwise
`modelV2.action.desiredCurvature`, after the existing curvature limiter.
This action already includes upstream delay handling; it receives no extra
response advance here.
The model contribution uses the existing allocator's raw forward pose and
bounded short-pose correction. `_model_pose` advances 0.1 seconds, retains
the model's remaining forward geometry, and separately corrects the short
pose using measured curvature and its recent change. Its offset preview is
up to 7 m and its heading preview is up to max(7 m, speed × 1 s), bounded by
available path length. This raw pose is not passed through a second model
filter. The filtered, ego-aligned reference remains available for comparison
and the existing geometry-validity checks.
```text
share(k) = clip((k - 0.006/m) / (0.012/m - 0.006/m), 0, 1)
aligned = desired_curvature × model_forward_heading > 0
model_share = min(share(abs(desired_curvature)), share(model_curvature_demand))
if aligned, otherwise 0
model_pair = existing_pose_encoder(model_pose, model_share, C2=0)
remaining_curvature = desired_curvature × (1 - model_share)
L0 = max(8 m, speed × 1 s)
L1 = max(7 m, speed × 1 s)
curvature_C0 = 0.5 × remaining_curvature × L0²
curvature_C1 = remaining_curvature × L1
C0_base = clip(model_pair.C0 + curvature_C0, ±5.11 m)
C1_base = clip(model_pair.C1 + curvature_C1, ±0.5 rad)
```
`model_curvature_demand` is the larger absolute curvature implied by the
forward offset and heading previews. The share uses the existing allocator's
0.0060.012/m thresholds. Both model and action must request a substantial
turn in the same direction before model pose supplies the full base.
Small, flat, opposed or zero requests use the curvature contribution; zero
action produces a zero base. Partial shares combine both contributions.
The existing pose encoder retains its quantization and field-allocation rules.
The residual-curvature lift is geometric, not a claim of EPS equivalence to C2.
The inherited pose encoder allocates heading overflow using its asymmetric
limits (+0.5235/0.5 rad), before the symmetric final ±0.5 rad
heading bound. On clipped tails, this can leave mirrored C0 requests differing
by up to 0.0235 rad × 7 m = 0.1645 m. The favorable comparison anchors lie
below that heading cap; full model-base odd symmetry is not claimed.
## Measured feedback and limits
```text
past_request = selected curvature held at or before (measurement_time - delay)
yaw_error = measured_speed × past_request - measured_yaw_rate
bias_trial = released_bias + feedback_gain × yaw_error × measurement_dt
C1_unconstrained = clip(C1_base + accepted_bias, ±0.5 rad)
C1_target = temporary_backoff_ceiling(C1_unconstrained) if backoff_active
otherwise C1_unconstrained
```
Measured yaw is negated Ford CAN yaw, matching the control sign convention.
The historical request uses zero-order hold; it never interpolates toward a
future publication. Nominal comparison delay is `CP.steerActuatorDelay`
(0.2 seconds on the source vehicle). Feedback compares against selected
curvature, not curvature inferred from the model-pose coefficients.
| Quantity | Value |
|---|---:|
| C0 / C1 final bounds | ±5.11 m / ±0.5 rad |
| Independent C0 / C1 slew | 4 m/s / 0.5 rad/s |
| Feedback integration scale | 1.0 |
| Feedback minimum speed | 2 m/s |
| Maximum PSCM/core input age | 150 ms |
| Allowed timestamp lead | 5 ms |
| Release comparison tolerance | one C1 wire quantum, 0.0005 rad |
The integration scale, preview distances and blend thresholds are effective
gains; none establishes stability. No wheel-response gain is fitted.
Zero yaw error retains acquired bias while an eligible turn continues.
Host anti-windup admits reachable correction within the combined C1 field
and slew limits. Feedback overflow is not transferred into C0.
The release logic scales bias as the bounded base decreases and resets on
zero/reversal. When delayed curvature still represents a stronger or opposing
request, or PSCM reports LimitReached, new integration is normally frozen.
One exception permits measured-error backoff: measured turning must exceed
both the delayed and current selected yaw requests in the base's direction,
and total heading must still have the base's sign. Exceeding only an older,
smaller request during turn-in does not qualify. The accepted increment may
only reduce that existing total toward zero; it cannot grow the request or
carry it through zero. Existing host field and slew limits still apply.
The existing release-recovery exception requires fresh valid PSCM status with
limit below 2, retained bias opposing the base, and both current and delayed
requests aligned with that base. Measured turning must be below both requests
in their direction. It then uses the current yaw deficit × the existing
feedback gain × measurement interval to unwind only the opposing bias toward
zero. The increment is clipped so recovery cannot cross zero bias or create
demand beyond the existing base. Common host anti-windup still limits what
can be accepted. A separate release-tracking exception is described below;
other constrained cases remain frozen. PSCM limit 2 never permits either
request-increasing exception.
The no-new-bias restriction applies to `release_recovery`. It does not apply
to the separate bounded `release_tracking` branch. Once release ends,
ordinary eligible integration can add correction beyond the base as before;
its existing limits and guards are unchanged.
`release_recovery` and `feedback_recovery_active=true` indicate that the
recovery branch actually changed bias on that update. If host anti-windup
blocks the entire increment, the status remains `host_limit` and the flag is
false. Recovery is evaluated only on fresh measurements; the flag is false
on repeated-measurement updates and after reset.
Diagnostics distinguish `release_backoff` and `pscm_backoff`; a release takes
precedence when both conditions apply. While `feedback_backoff_active` is
true, total C1 is also capped at the preceding continuous heading request in
the current request direction and at zero in the opposite direction. This
ceiling affects the output only: it is not stored or projected into bias.
The measured-error increment can still update bias under the normal limits,
but a changing model base does not create persistent integral suppression.
The ceiling persists between repeated measurements; C1 cannot grow or reverse
while it applies. The next fresh measurement clears it unless backoff is
again warranted. It does not cap C0, and normal feedback has its own rules
outside backoff. Independent slew remains 0.5 rad/s for C1 and 4 m/s for C0.
Backoff still compares against the delayed reference, so response lag remains.
Reducing a request does not demonstrate that physical overshoot is resolved.
## V8 release guard and tracking
`ReleaseGuard` retains selected-request history independently of feedback
bias history. Driver-related feedback resets do not erase that reference,
but the guard still requires current fresh valid PSCM status, no current
driver override, and the existing input and speed eligibility. Invalid core
input or disengagement resets its history with the controller.
During release, measured yaw must exceed both the current and delay-matched
requests in the requested turn direction. Only then does the guard cap
same-direction C0/C1 growth at each preceding continuous request. Terms
already reducing the turn, including an opposing C0 centering offset, remain
available. The guard follows base allocation and C1 feedback, so changing
model geometry cannot bypass it. Its ceilings affect outputs, never stored
bias. No scalar-curvature cap replaces strong model geometry during turn-in
or undertracking. Existing independent slew and field limits still apply.
`release_tracking` addresses an eligible release deficit once bias is zero
or already in the base's direction. Both current and delayed requests must
align with that base, measured turning must be below both, and measured
curvature must not be rising in the turn direction across the response
interval by more than one C1 wire quantum after scaling by heading preview.
Fresh valid PSCM status with limit below 2 is required. The current yaw deficit
uses the existing integration gain and measurement interval;
new C1 tracking increments are limited by command headroom captured at
release entry, tapered with remaining desired curvature. The allowance is
`max(0, entry_command_magnitude - abs(base)) × min(1, abs(desired) / entry_reference)`
above the current base; any existing same-direction bias consumes it first.
This limits new tracking integration, not the existing model base or bias.
Only that additional allowance is tapered; strong model geometry remains
available. A brief pause does not reacquire a higher entry
ceiling; a full response interval without release ends the retained episode.
Common host anti-windup, field and slew bounds still apply. Opposing bias
continues through `release_recovery`, which stops at zero, before any separate
tracking exception can be considered.
Neither exception relaxes the PSCM LimitReached growth restriction. The
reference delay and finite response time remain; these output policies are
command-construction changes, not evidence of improved physical tracking.
## PSCM status and driver handling
card publishes `Lane_Assist_Data3_FD1` in `carStateSP.fordPscmStatus`, retaining
the original CAN receipt timestamp. Republishing carStateSP or receiving
unrelated frames cannot refresh it. The opendbc submodule is unchanged.
Feedback requires valid fresh status, InProgress lateral state (2), capability
LimitedModeAvailable or ExtendedModeAvailable (1 or 2), and no denial.
Missing, malformed, stale, backward-timestamped, denied or unavailable status
clears feedback bias/history and disables the separate release guard,
leaving the base subject to its core validity gates.
LimitReached (2) permits only the bounded request-reducing backoff described
above and otherwise freezes integration. LimitWithDriverActive (3) clears
feedback. Backoff still requires fresh, valid, InProgress status with an
available capability and no denial. These generic PSCM reports do not identify
a specific torque or rate limit.
`steeringPressed`, raw torque above the existing Ford driver allowance, or
nonfinite torque clear feedback. Below 2 m/s feedback also clears. A fresh
feedback reference interval is required after override; the independent
release guard can use retained valid request history once its current gates
are satisfied. Base requests retain normal
PSCM driver arbitration while lateral control remains authorized; an unset
override flag cannot rule out subthreshold driver influence.
## Gates and Sunnylink selection
Core model/action/car-state freshness, finite-value, clock and speed checks
remain in place. Invalid core inputs reset both commands and clear latActive.
Raw model geometry is validated on every update, including repeated model
timestamps; an invalid raw path cannot reuse the cached valid reference.
Missing PSCM status disables feedback, not an otherwise valid base request.
Vehicle → Ford → **C2-Free Path Tracking (Experimental)** retains the
`FordVirtualAngleController` key, default-off setting and offroad/onroad cycle
requirement. Enabled selects v8 on Ford CAN FD `FORD_F_150_LIGHTNING_MK1`
regardless of missing or different EPS firmware-query results. Other platforms
retain their existing controller. V8 takes priority over PSCM Coefficient
Observer while selected; disabling and cycling offroad/onroad restores the
previous selection. Controller selection does not force lateral engagement.
The analyzed firmware is `RL38-14D003-AA`; removing the eligibility check
is not validation of other firmware. No live device setting is changed.
## Diagnostics and verification
The 5 Hz `Ford C2-free path tracking` event keeps its name and identifies v8.
`model_offset_base` / `model_heading_base` report the already weighted and
encoded model contribution; `curvature_offset_base` / `curvature_heading_base`
report the residual-curvature contribution. `model_share` and `base_guard`
identify model-pose, blended, curvature-only, opposed-model and zero-request
cases. `heading_base` is the bounded pre-feedback C1. `offset_target` and
`heading_target` are the final targets after the independent release guard;
`offset_target_unguarded` and `heading_target_unguarded` retain the inputs to
that guard. The latter C1 already includes its normal feedback/backoff policy.
The event retains source timestamps, measured curvature/yaw, final commands,
slew scales, feedback bias/status/history, raw torque and PSCM status/age.
`feedback_backoff_active` records the persistent heading ceiling, including
cycles whose feedback status is `no_new_measurement`.
`release_guard_active` and `release_guard_reference_curvature` expose the
independent C0/C1 guard and its retained delayed reference.
`feedback_release_tracking_active`, `feedback_release_ceiling` and
`feedback_curvature_delta` identify accepted release
tracking, the total-heading threshold used to admit new bias, and the
measured-curvature change across the response interval (1/m). The tracking
flag is true only when the branch accepts a bias change on a new measurement;
it is false on repeated measurements. The ceiling/trend fields can describe
an evaluated condition even when no increment is accepted.
`feedback_recovery_active` records an accepted recovery increment on this
update only; it does not persist between measurements.
`feedback_yaw_error` retains its delayed-reference meaning. Recovery instead
uses current error, reconstructed from logged `desired_curvature`,
synchronized car-state speed and `yaw_rate`; those two errors can differ.
During backoff or the independent release guard, `heading_target` can be lower in the request direction than
the bounded sum of `heading_base` and `heading_bias`, because the temporary
ceiling is not part of the stored bias.
`model_heading_target` remains a filtered comparison reference; it is not the
weighted model contribution. `angleState.saturated` is not an EPS-limit signal.
Validation must cover large recorded maneuvers, flat-model centering, both
turn directions, model/action disagreement, share transitions, release and
reversal, release/limit backoff without growth or zero crossing, status/driver
resets, reference causality, bounds, slew and CAN packing with C2/C3 zero.
Recovery checks cover both directions, stopping at zero bias, repeated
measurements, current-and-delayed agreement, and rejection at PSCM limit 2.
Old v3/v4 command-equality expectations do not define
v8 success. Guard checks also cover driver reset/history rebuilding,
same-direction growth, opposing coefficients, repeated measurements,
undertracking and invalid-status inhibition. Tracking checks cover delayed
curvature trends and tapered release-entry headroom. Historical v5v7 replay
results remain historical observations.
The v8 recorded-input fixture contains 15,273 cycles with 4,879 selected
evidence samples. Base allocation and output eligibility match v7. In the
clean deficient exit, median absolute C1 changes from 0.0665 to 0.0845 rad
while C0 stays unchanged. The growth guard also acts while feedback history
rebuilds; the largest over-growth witness includes nearby driver input and
is excluded from the strict autonomous tracking score. Both good comparison
curves in that fixture retain their median requests, and the older large-turn
fixtures retain their required command scale.
On the earlier good drive, one comparison curve retains extra C1 after
eligible release tracking: median magnitude changes from 0.121 to 0.128 rad.
In its 103110 s interval, tracking increments occur only while measured
turning falls short, with a median current response/request ratio of 0.895.
Acquired bias can persist after matching, as with ordinary integral feedback.
This collateral command change remains a reason to compare new vehicle logs.
Replay fixes recorded motion and planner outputs, so enabled vehicle logs
are still required to assess tracking error, oscillation and interventions.
+3 -1
View File
@@ -24,7 +24,9 @@ function agnos_init {
if $AGNOS_PY --verify $MANIFEST; then
sudo reboot
fi
$DIR/openpilot/common/hardware/comma/updater $AGNOS_PY $MANIFEST
while true; do
$DIR/openpilot/common/hardware/comma/updater $AGNOS_PY $MANIFEST
done
fi
}
+1 -1
View File
@@ -16,7 +16,7 @@ export VECLIB_MAXIMUM_THREADS=1
export QCOM_PRIORITY=12
if [ -z "$AGNOS_VERSION" ]; then
export AGNOS_VERSION="19.6"
export AGNOS_VERSION="19.7"
fi
export STAGING_ROOT="/data/safe_staging"
+45 -1
View File
@@ -131,6 +131,7 @@ struct ModelManagerSP @0xaedffd8f31e7b55d {
downloaded @2;
cached @3;
failed @4;
verifying @5;
}
struct DownloadProgress {
@@ -352,6 +353,7 @@ struct OnroadEventSP @0xda96579883444c35 {
speedLimitPending @22;
e2eChime @23;
laneChangeRoadEdge @24;
bigModelReady @25;
}
}
@@ -381,6 +383,7 @@ struct CarControlSP @0xa5cd762cd951a455 {
leadOne @2 :LeadData;
leadTwo @3 :LeadData;
intelligentCruiseButtonManagement @4 :IntelligentCruiseButtonManagement;
fordLateralPath @5 :FordLateralPath;
struct Param {
key @0 :Text;
@@ -401,6 +404,14 @@ struct CarControlSP @0xa5cd762cd951a455 {
}
}
struct FordLateralPath {
pathOffset @0 :Float32; # c0 [m]
pathAngle @1 :Float32; # c1 [rad]
curvature @2 :Float32; # c2 [1/m]
curvatureRate @3 :Float32; # c3 [1/m^2]
valid @4 :Bool;
}
struct BackupManagerSP @0xf98d843bfd7004a3 {
backupStatus @0 :Status;
restoreStatus @1 :Status;
@@ -445,6 +456,16 @@ struct BackupManagerSP @0xf98d843bfd7004a3 {
struct CarStateSP @0xb86e6369214c01c8 {
speedLimit @0 :Float32;
fordPscmStatus @1 :FordPscmStatus;
struct FordPscmStatus {
valid @0 :Bool;
canMonoTime @1 :UInt64; # Last accepted Lane_Assist_Data3_FD1 CAN receipt, not carStateSP publication time.
lateralState @2 :UInt8; # LatCtlSte_D_Stat
limit @3 :UInt8; # LatCtlLim_D_Stat: generic lateral limit, not a torque/rate diagnosis.
capability @4 :UInt8; # LatCtlCpblty_D_Stat
denied @5 :Bool; # LaActDeny_B_Actl
}
}
struct LiveMapDataSP @0xf416ec09499d9d19 {
@@ -468,7 +489,30 @@ struct ModelDataV2SP @0xa1680744031fdb2d {
}
}
struct CustomReserved10 @0xcb9fd56c7057593a {
struct AssistedDrivingMilestoneState @0xcb9fd56c7057593a {
enabled @0 :Bool;
madsDistanceMeters @1 :Float64;
fullAssistDistanceMeters @2 :Float64;
event @3 :Event;
struct Event {
id @0 :UInt64;
category @1 :Category;
distanceMeters @2 :Float64;
previousDistanceMeters @3 :Float64;
unit @4 :Unit;
}
enum Category {
none @0;
mads @1;
fullAssist @2;
}
enum Unit {
imperial @0;
metric @1;
}
}
struct CustomReserved11 @0xc2243c65e0340384 {
+3 -1
View File
@@ -725,6 +725,7 @@ struct ChestnutState {
pcieLtssm @7 :UInt8;
supplyVoltage @8 :UInt16; # mV
supplyCurrent @9 :Int16; # mA
supplyFault @10 :Bool;
}
struct RadarState @0x9a185389d6fdd05f {
@@ -1004,6 +1005,7 @@ struct DrivingModelData {
frameIdExtra @1 :UInt32;
frameDropPerc @6 :Float32;
modelExecutionTime @7 :Float32;
big @8 :Bool;
action @2 :ModelDataV2.Action;
@@ -2640,7 +2642,7 @@ struct Event {
carStateSP @114 :Custom.CarStateSP;
liveMapDataSP @115 :Custom.LiveMapDataSP;
modelDataV2SP @116 :Custom.ModelDataV2SP;
customReserved10 @136 :Custom.CustomReserved10;
assistedDrivingMilestoneState @136 :Custom.AssistedDrivingMilestoneState;
customReserved11 @137 :Custom.CustomReserved11;
customReserved12 @138 :Custom.CustomReserved12;
customReserved13 @139 :Custom.CustomReserved13;
+1
View File
@@ -90,6 +90,7 @@ _services: dict[str, tuple] = {
"carParamsSP": (True, 0.02, 1),
"carControlSP": (True, 100., 10),
"carStateSP": (True, 100., 10),
"assistedDrivingMilestoneState": (True, 10., 1),
"liveMapDataSP": (True, 1., 1),
"modelDataV2SP": (True, 20., None, QueueSize.BIG),
"liveLocationKalman": (True, 20.),
+11 -11
View File
@@ -56,29 +56,29 @@
},
{
"name": "boot",
"url": "https://commadist.azureedge.net/agnosupdate/boot-b30f5eef65ec3878f3aa3dcaf2cc95c09e2c1e661cd3a38e94da37dee76f68bd.img.xz",
"hash": "b30f5eef65ec3878f3aa3dcaf2cc95c09e2c1e661cd3a38e94da37dee76f68bd",
"hash_raw": "b30f5eef65ec3878f3aa3dcaf2cc95c09e2c1e661cd3a38e94da37dee76f68bd",
"url": "https://commadist.azureedge.net/agnosupdate/boot-6ecf6f987cd11968104abcccabbe268485d329cdb73012dfd3c381a6b8deb27d.img.xz",
"hash": "6ecf6f987cd11968104abcccabbe268485d329cdb73012dfd3c381a6b8deb27d",
"hash_raw": "6ecf6f987cd11968104abcccabbe268485d329cdb73012dfd3c381a6b8deb27d",
"size": 46897152,
"sparse": false,
"full_check": true,
"has_ab": true,
"ondevice_hash": "6650e4c46df99ae6dfd6ee895a34b8a2a3cc490a8ce18e16cc3c451c3f822b6e"
"ondevice_hash": "d12e1e5b9455b62a1464558716493b33e470d7a7e88da1c4105a3b21d0961808"
},
{
"name": "system",
"url": "https://commadist.azureedge.net/agnosupdate/system-5b6ce7965904a157fd3a134ccfcb854f9ca5c1cc2a26b7cb80a4fa4e1cc4aaa3.img.xz",
"hash": "b134fd04e9da27fa1d359ea0f2742c216fa21a08b5c47e9be22ab3b0563d9b9b",
"hash_raw": "5b6ce7965904a157fd3a134ccfcb854f9ca5c1cc2a26b7cb80a4fa4e1cc4aaa3",
"url": "https://commadist.azureedge.net/agnosupdate/system-3c271e2b3d20d2f0a8bf6555a1319f3efb12845490967d6151195174a01e912f.img.xz",
"hash": "74ffc9c551e1f29cda897ace8a69080fe644f8039977c6885f2b48362e39b744",
"hash_raw": "3c271e2b3d20d2f0a8bf6555a1319f3efb12845490967d6151195174a01e912f",
"size": 4718592000,
"sparse": true,
"full_check": false,
"has_ab": true,
"ondevice_hash": "91242772af771ae96fe2eebc105f2b80a7e1dbaaf6003c2574b62d51b806f468",
"ondevice_hash": "6a992680183685eea9db99d915219a37935f45989330d9b619e880450257f448",
"alt": {
"hash": "5b6ce7965904a157fd3a134ccfcb854f9ca5c1cc2a26b7cb80a4fa4e1cc4aaa3",
"url": "https://commadist.azureedge.net/agnosupdate/system-5b6ce7965904a157fd3a134ccfcb854f9ca5c1cc2a26b7cb80a4fa4e1cc4aaa3.img",
"hash": "3c271e2b3d20d2f0a8bf6555a1319f3efb12845490967d6151195174a01e912f",
"url": "https://commadist.azureedge.net/agnosupdate/system-3c271e2b3d20d2f0a8bf6555a1319f3efb12845490967d6151195174a01e912f.img",
"size": 4718592000
}
}
]
]
+2 -1
View File
@@ -5,6 +5,7 @@ import logging
import os
import select
import signal
import string
import struct
import subprocess
import tempfile
@@ -354,7 +355,7 @@ class Modem:
imei = ""
iccid = (self._atv("AT+QCCID", "+QCCID:") or "").rstrip("F")
if not iccid.isdigit():
if not all(c in string.hexdigits for c in iccid):
iccid = ""
imsi = first_line("AT+CIMI")
+7 -1
View File
@@ -4,11 +4,17 @@ from pathlib import Path
CHESTNUT_FW_VERSION = "ed4e39b7"
CHESTNUT_USB_IDS = ((0xADD1, 0x0001), (0x3801, 0x0001))
CHESTNUT_ROM_USB_IDS = ((0x174C, 0x2464), (0x174C, 0x2463))
CHESTNUT_USB_PRODUCT = f"custom {CHESTNUT_FW_VERSION}-CLEAN"
USB_DEVICES_PATH = Path("/sys/bus/usb/devices")
TYPEC_CC_ORIENTATION_PATH = Path("/sys/class/power_supply/usb/typec_cc_orientation")
PRIMARY_USB_CONTROLLER = "a600000.ssusb"
def is_chestnut_usb_id(vendor_id: int, product_id: int, include_bootloader: bool = False) -> bool:
ids = CHESTNUT_USB_IDS + CHESTNUT_ROM_USB_IDS if include_bootloader else CHESTNUT_USB_IDS
return (vendor_id, product_id) in ids
def get_usb_topology() -> set[str]:
try:
return set(os.listdir(USB_DEVICES_PATH))
@@ -81,7 +87,7 @@ def set_usb_state(device_state, devices: list[dict]) -> None:
entry.linkErrorCount = device["linkErrorCount"]
entry.usb3Lane = device.get("usb3Lane", "unknown")
if (entry.vendorId, entry.productId) in CHESTNUT_USB_IDS:
if is_chestnut_usb_id(entry.vendorId, entry.productId):
chestnut_present = True
device_state.chestnutPresent = chestnut_present
+4
View File
@@ -97,6 +97,10 @@ Params::Params(const std::string &path) {
}
Params::~Params() {
flushNonBlockingWrites();
}
void Params::flushNonBlockingWrites() {
if (future.valid()) {
future.wait();
}
+1
View File
@@ -75,6 +75,7 @@ public:
return put(key.c_str(), val ? "1" : "0", 1);
}
void putNonBlocking(const std::string &key, const std::string &val);
void flushNonBlockingWrites();
inline void putBoolNonBlocking(const std::string &key, bool val) {
putNonBlocking(key, val ? "1" : "0");
}
+5
View File
@@ -73,6 +73,7 @@ params_get = _bind("params_get", [ParamsHandle, ctypes.c_char_p, ctypes.c_bool],
params_get_bool = _bind("params_get_bool", [ParamsHandle, ctypes.c_char_p, ctypes.c_bool], ctypes.c_bool)
params_put = _bind("params_put", [ParamsHandle, ctypes.c_char_p, ctypes.c_char_p, ctypes.c_size_t, ctypes.c_bool], ctypes.c_int)
params_put_bool = _bind("params_put_bool", [ParamsHandle, ctypes.c_char_p, ctypes.c_bool, ctypes.c_bool], ctypes.c_int)
params_flush = _bind("params_flush", [ParamsHandle])
params_remove = _bind("params_remove", [ParamsHandle, ctypes.c_char_p], ctypes.c_int)
params_get_path = _bind("params_get_path", [ParamsHandle, ctypes.c_char_p, ctypes.c_size_t], ParamsBuffer)
params_keys_size = _bind("params_keys_size", [ParamsHandle], ctypes.c_size_t)
@@ -178,6 +179,10 @@ class Params:
def put_bool(self, key, val, block=False):
params_put_bool(self.p, self.check_key(key), val, block)
def flush(self):
"""Wait for all prior nonblocking writes from this Params instance."""
params_flush(self.p)
def remove(self, key):
params_remove(self.p, self.check_key(key))
+14 -5
View File
@@ -133,6 +133,12 @@ int params_put_bool(ParamsHandle *handle, const char *key, bool value, bool bloc
});
}
void params_flush(ParamsHandle *handle) noexcept {
translate_exceptions([&]() {
handle->params.flushNonBlockingWrites();
});
}
int params_remove(ParamsHandle *handle, const char *key) noexcept {
return translate_exceptions(-1, [&]() {
return handle->params.remove(key);
@@ -162,12 +168,15 @@ ParamsBuffer params_key_at(ParamsHandle *handle, size_t index) noexcept {
size_t params_keys_by_flag(ParamsHandle *handle, uint32_t flag, ParamsBuffer *out, size_t out_size) noexcept {
return translate_exceptions(size_t{0}, [&]() {
auto filtered = handle->params.allKeys(static_cast<ParamKeyFlag>(flag));
size_t count = std::min(filtered.size(), out_size);
for (size_t i = 0; i < count; i++) {
out[i] = return_string(filtered[i]);
size_t count = 0;
for (const auto &key : handle->keys) {
if (flag == ALL || (handle->params.getKeyFlag(key) & flag)) {
// Each buffer borrows a different string, stable for the handle's lifetime.
if (count < out_size) out[count] = {key.data(), key.size()};
++count;
}
}
return filtered.size();
return count;
});
}
+24 -7
View File
@@ -59,7 +59,7 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
{"IsDriverViewEnabled", {CLEAR_ON_MANAGER_START, BOOL}},
{"IsEngaged", {PERSISTENT, BOOL}},
{"IsLdwEnabled", {PERSISTENT | BACKUP, BOOL}},
{"IsLiveStreaming", {CLEAR_ON_MANAGER_START, BOOL}},
{"IsLiveStreaming", {CLEAR_ON_MANAGER_START | CLEAR_ON_IGNITION_ON, BOOL}},
{"IsMetric", {PERSISTENT | BACKUP, BOOL}},
{"IsOffroad", {CLEAR_ON_MANAGER_START, BOOL}},
{"IsRhdDetected", {PERSISTENT, BOOL}},
@@ -92,6 +92,12 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
{"ObdMultiplexingEnabled", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, BOOL}},
{"Offroad_CarUnrecognized", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, JSON}},
{"Offroad_ChestnutBranch", {CLEAR_ON_MANAGER_START, JSON}},
{"Offroad_ChestnutNotDetected", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, JSON}},
{"Offroad_ChestnutOverheated", {CLEAR_ON_MANAGER_START, JSON}},
{"Offroad_ChestnutPcieUnavailable", {CLEAR_ON_MANAGER_START, JSON}},
{"Offroad_ChestnutUncompiled", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, JSON}},
{"Offroad_ChestnutUpdateFailed", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, JSON}},
{"Offroad_ChestnutUsbSlow", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, JSON}},
{"Offroad_ConnectivityNeeded", {CLEAR_ON_MANAGER_START, JSON}},
{"Offroad_ConnectivityNeededPrompt", {CLEAR_ON_MANAGER_START, JSON}},
{"Offroad_ExcessiveActuation", {PERSISTENT, JSON}},
@@ -130,12 +136,15 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
{"UpdaterLastFetchTime", {PERSISTENT, TIME}},
{"UptimeOffroad", {PERSISTENT, FLOAT, "0.0"}},
{"UptimeOnroad", {PERSISTENT, FLOAT, "0.0"}},
{"UsbGpuActive", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION | CLEAR_ON_IGNITION_ON, BOOL}},
{"UsbGpuLoading", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION | CLEAR_ON_IGNITION_ON, BOOL}},
{"ChestnutActive", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION | CLEAR_ON_IGNITION_ON, BOOL}},
{"ChestnutLoading", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION | CLEAR_ON_IGNITION_ON, BOOL}},
{"ChestnutModelError", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION | CLEAR_ON_IGNITION_ON, BOOL}},
{"Version", {PERSISTENT, STRING}},
// --- sunnypilot params --- //
{"ApiCache_DriveStats", {PERSISTENT, JSON}},
{"AssistedDrivingMilestonesEnabled", {PERSISTENT | BACKUP, BOOL, "1"}},
{"AssistedDrivingMilestoneState", {PERSISTENT, JSON, "{}"}},
{"AutoLaneChangeBsmDelay", {PERSISTENT | BACKUP, BOOL, "0"}},
{"AutoLaneChangeTimer", {PERSISTENT | BACKUP, INT, "0"}},
{"BlinkerLateralReengageDelay", {PERSISTENT | BACKUP, INT, "0"}}, // seconds
@@ -156,6 +165,7 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
{"DevUIInfo", {PERSISTENT | BACKUP, INT, "0"}},
{"EnableCopyparty", {PERSISTENT | BACKUP, BOOL}},
{"EnableGithubRunner", {PERSISTENT | BACKUP, BOOL}},
{"FullAssistDrivenDistanceMeters", {PERSISTENT, FLOAT, "0.0"}},
{"GreenLightAlert", {PERSISTENT | BACKUP, BOOL, "0"}},
{"GithubRunnerSufficientVoltage", {CLEAR_ON_MANAGER_START , BOOL}},
{"HasAcceptedTermsSP", {PERSISTENT, STRING, "0"}},
@@ -165,7 +175,9 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
{"IsDevelopmentBranch", {CLEAR_ON_MANAGER_START, BOOL}},
{"IsReleaseSpBranch", {CLEAR_ON_MANAGER_START, BOOL}},
{"LastGPSPositionLLK", {PERSISTENT, STRING}},
{"LastDriveAssistedDrivingSummary", {PERSISTENT, JSON, "{}"}},
{"LeadDepartAlert", {PERSISTENT | BACKUP, BOOL, "0"}},
{"MadsDrivenDistanceMeters", {PERSISTENT, FLOAT, "0.0"}},
{"MaxTimeOffroad", {PERSISTENT | BACKUP, INT, "1800"}},
{"ModelRunnerTypeCache", {CLEAR_ON_ONROAD_TRANSITION, INT}},
{"OffroadMode", {CLEAR_ON_MANAGER_START, BOOL}},
@@ -195,14 +207,16 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
// Model Manager params
{"ModelManager_ActiveBundle", {PERSISTENT, JSON}},
{"ModelManager_ActiveJson", {CLEAR_ON_MANAGER_START, STRING}},
{"ModelManager_ActiveBundleUSBGPU", {PERSISTENT, JSON}}, //TODO-SP: kept for migration, remove on next sync?
{"ModelManager_ActiveBundleChestnut", {PERSISTENT, JSON}},
{"ModelManager_ActiveJson", {CLEAR_ON_MANAGER_START, JSON}},
{"ModelManager_ClearCache", {CLEAR_ON_MANAGER_START, BOOL}},
{"ModelManager_DownloadIndex", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, INT}},
{"ModelManager_DownloadRef", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, STRING}},
{"ModelManager_Favs", {PERSISTENT | BACKUP, STRING}},
{"ModelManager_LastSyncTime", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION, INT, "0"}},
{"ModelManager_LastSyncTime_USBGPU", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION, INT, "0"}},
{"ModelManager_LastSyncTime_Chestnut", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION, INT, "0"}},
{"ModelManager_ModelsCache", {PERSISTENT | BACKUP, JSON}},
{"ModelManager_ModelsCache_USBGPU", {PERSISTENT | BACKUP, JSON}},
{"ModelManager_ModelsCache_Chestnut", {PERSISTENT | BACKUP, JSON}},
// Neural Network Lateral Control
{"NeuralNetworkLateralControl", {PERSISTENT | BACKUP, BOOL, "0"}},
@@ -223,6 +237,8 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
{"BackupManager_RestoreVersion", {PERSISTENT, STRING}},
// sunnypilot car specific params
{"FordPscmObserver", {PERSISTENT | BACKUP, BOOL, "0"}},
{"FordModelActionController", {PERSISTENT | BACKUP, BOOL, "0"}},
{"HyundaiLongitudinalTuning", {PERSISTENT | BACKUP, INT, "0"}},
{"SubaruStopAndGo", {PERSISTENT | BACKUP, BOOL, "0"}},
{"SubaruStopAndGoManualParkingBrake", {PERSISTENT | BACKUP, BOOL, "0"}},
@@ -245,6 +261,7 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
// mapd
{"MapAdvisorySpeedLimit", {CLEAR_ON_ONROAD_TRANSITION, FLOAT}},
{"Mapd_ClearCache", {CLEAR_ON_MANAGER_START, BOOL}},
{"MapdVersion", {PERSISTENT, STRING}},
{"MapSpeedLimit", {CLEAR_ON_ONROAD_TRANSITION, FLOAT, "0.0"}},
{"NextMapSpeedLimit", {CLEAR_ON_ONROAD_TRANSITION, JSON}},
+4 -4
View File
@@ -27,14 +27,14 @@ public:
auto param_path = Params().getParamPath();
if (util::file_exists(param_path)) {
std::string real_path = util::readlink(param_path);
util::check_system(util::string_format("rm %s -rf", real_path.c_str()));
util::check_system(util::string_format("rm -rf %s", real_path.c_str()));
unlink(param_path.c_str());
}
if (getenv("COMMA_CACHE") == nullptr) {
util::check_system(util::string_format("rm %s -rf", Path::download_cache_root().c_str()));
util::check_system(util::string_format("rm -rf %s", Path::download_cache_root().c_str()));
}
util::check_system(util::string_format("rm %s -rf", Path::comma_home().c_str()));
util::check_system(util::string_format("rm %s -rf", msgq_path.c_str()));
util::check_system(util::string_format("rm -rf %s", Path::comma_home().c_str()));
util::check_system(util::string_format("rm -rf %s", msgq_path.c_str()));
unsetenv("OPENPILOT_PREFIX");
}
+17
View File
@@ -106,6 +106,13 @@ class TestParams(OpenpilotTestCase):
assert q.get("CarParams") is None
assert q.get("CarParams", True) == b"1"
def test_flush_non_blocking_writes(self):
self.params.put("DongleId", "first")
self.params.put("DongleId", "last")
self.params.flush()
assert self.params.get("DongleId") == "last"
def test_params_all_keys(self):
keys = Params().all_keys()
@@ -126,6 +133,16 @@ class TestParams(OpenpilotTestCase):
assert self.params.get("LiveParametersV2") is None
assert self.params.get("LiveParametersV2", return_default=True) is None
def test_filtered_keys_are_distinct_registered_strings(self):
registered = set(self.params.all_keys())
for flag in (ParamKeyFlag.PERSISTENT, ParamKeyFlag.BACKUP, ParamKeyFlag.CLEAR_ON_MANAGER_START):
filtered = self.params.all_keys(flag)
assert len(filtered) > 1
assert len(filtered) == len(set(filtered))
assert set(filtered) <= registered
assert all(key.decode('utf-8') for key in filtered)
assert self.params.all_keys(flag) == filtered
def test_params_get_type(self):
# json
self.params.put("ApiCache_FirehoseStats", {"a": 0}, block=True)
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:845c40ff0d37612e8f2f482a36845744b5ae91ce2fcfc8117990d7d278b59820
size 13079
@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:8a8c5fece2a1c7587feb41cbe04c6aee08e768ecd9b5d00da6af9832a4ccc842
size 2034
@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:7409c53d7c72681c24982fd83b56ce70f80797c9c0f936d9296a5c18557ac472
size 7279
@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:58bd6155433f623b1f75d134bd8ca4745d9aa71f6767eb807cdbcf7deb3089a1
size 10876
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:07bda2fe5d6be0b2854044053c384fe002e96406da119863a443b9344258b500
size 1544
Binary file not shown.
+2
View File
@@ -21,6 +21,7 @@ from opendbc.car.interfaces import CarInterfaceBase, RadarInterfaceBase
from openpilot.selfdrive.pandad import can_capnp_to_list, can_list_to_can_capnp
from openpilot.selfdrive.car.cruise import VCruiseHelper
from openpilot.selfdrive.car.helpers import convert_carControlSP, convert_to_capnp
from openpilot.selfdrive.car.ford_pscm_status import populate_ford_pscm_status
from openpilot.sunnypilot.mads.helpers import set_alternative_experience, set_car_specific_params
from openpilot.sunnypilot.selfdrive.car import interfaces as sunnypilot_interfaces
@@ -198,6 +199,7 @@ class Car:
# Update carState from CAN
CS, CS_SP = self.CI.update(can_list)
CS_SP = convert_to_capnp(CS_SP)
populate_ford_pscm_status(self.CP, self.CI.can_parsers, CS_SP, CS.canValid)
# Update radar tracks from CAN
RD: structs.RadarDataT | None = self.RI.update(can_list)
@@ -0,0 +1,36 @@
"""Publish the Ford PSCM's actual CAN status without changing opendbc structs."""
import math
from opendbc.car import Bus
from opendbc.car.ford.values import FordFlags
MESSAGE = 'Lane_Assist_Data3_FD1'
SIGNALS = ('LatCtlSte_D_Stat', 'LatCtlLim_D_Stat', 'LatCtlCpblty_D_Stat', 'LaActDeny_B_Actl')
def populate_ford_pscm_status(CP, can_parsers, CS_SP, can_valid):
if CP.brand != 'ford' or not CP.flags & FordFlags.CANFD:
return
status = CS_SP.init('fordPscmStatus')
parser = can_parsers.get(Bus.pt)
if parser is None:
return
values = parser.vl.get(MESSAGE, {})
timestamps = parser.ts_nanos.get(MESSAGE, {})
if any(signal not in values or signal not in timestamps for signal in SIGNALS):
return
received = timestamps[SIGNALS[0]]
if received <= 0 or any(timestamps[signal] != received for signal in SIGNALS):
return
decoded = [values[signal] for signal in SIGNALS]
if any(not math.isfinite(value) or int(value) != value or not 0 <= value <= maximum
for value, maximum in zip(decoded, (7, 3, 3, 1), strict=True)):
return
status.canMonoTime = received
status.lateralState, status.limit, status.capability = map(int, decoded[:3])
status.denied = bool(decoded[3])
# CI.update already checked all parser validity. Reading can_valid again here
# would advance the parser's invalid-message counter a second time per tick.
# Age is evaluated by the feedback consumer using this original CAN timestamp.
status.valid = bool(can_valid)
+1
View File
@@ -63,5 +63,6 @@ def convert_carControlSP(struct: capnp.lib.capnp._DynamicStructReader) -> struct
struct_dataclass.intelligentCruiseButtonManagement = structs.IntelligentCruiseButtonManagement(
**remove_deprecated(struct_dict.get('intelligentCruiseButtonManagement', {}))
)
struct_dataclass.fordLateralPath = structs.FordLateralPath(**remove_deprecated(struct_dict.get('fordLateralPath', {})))
return struct_dataclass
@@ -0,0 +1,109 @@
import ast
from pathlib import Path
from types import SimpleNamespace
import unittest
from openpilot.cereal import custom
from openpilot.selfdrive.car.ford_pscm_status import MESSAGE, SIGNALS, populate_ford_pscm_status
from openpilot.selfdrive.car.helpers import convert_to_capnp
from opendbc.can import CANPacker, CANParser
from opendbc.car import Bus, structs
from opendbc.car.ford.values import FordFlags
class TestFordPscmStatus(unittest.TestCase):
def setUp(self):
self.cp = SimpleNamespace(brand='ford', flags=FordFlags.CANFD)
self.packer = CANPacker('ford_lincoln_base_pt')
self.parser = CANParser('ford_lincoln_base_pt', [(MESSAGE, 33), ('Yaw_Data_FD1', 100)], 0)
def update_status(self, timestamp, *, lateral_state=2, limit=0, capability=2, denied=False):
status = self.packer.make_can_msg(MESSAGE, 0, dict(zip(SIGNALS, (lateral_state, limit, capability, denied), strict=True)))
yaw = self.packer.make_can_msg('Yaw_Data_FD1', 0, {'VehYaw_W_Actl': 0.1})
self.parser.update([(timestamp, [status, yaw])])
def publish(self, *, can_valid=True):
state_sp = convert_to_capnp(structs.CarStateSP(speedLimit=13.5))
populate_ford_pscm_status(self.cp, {Bus.pt: self.parser}, state_sp, can_valid)
return state_sp
def test_decodes_status_and_preserves_receipt_time_across_other_can_messages(self):
self.update_status(1_000_000_000, limit=2, capability=1, denied=True)
original = self.publish()
self.assertEqual(original.speedLimit, 13.5)
status = original.fordPscmStatus
self.assertTrue(status.valid)
self.assertEqual(status.canMonoTime, 1_000_000_000)
self.assertEqual((status.lateralState, status.limit, status.capability, status.denied), (2, 2, 1, True))
# carStateSP may publish at 100 Hz while this 33 Hz message is absent. New
# unrelated CAN must not freshen the timestamp of an old PSCM status.
yaw = self.packer.make_can_msg('Yaw_Data_FD1', 0, {'VehYaw_W_Actl': .2})
self.parser.update([(1_080_000_000, [yaw])])
copied = self.publish().fordPscmStatus
self.assertEqual(copied.canMonoTime, 1_000_000_000)
self.assertEqual((copied.limit, copied.capability, copied.denied), (2, 1, True))
self.update_status(1_090_000_000, lateral_state=3, limit=3, capability=2)
next_state = self.publish()
with custom.CarStateSP.from_bytes(next_state.to_bytes()) as decoded:
latest = decoded.fordPscmStatus
self.assertTrue(latest.valid)
self.assertEqual(latest.canMonoTime, 1_090_000_000)
self.assertEqual((latest.lateralState, latest.limit, latest.capability, latest.denied), (3, 3, 2, False))
def test_absent_parser_unseen_message_and_invalid_can_do_not_claim_valid_status(self):
state = custom.CarStateSP.new_message()
populate_ford_pscm_status(self.cp, {}, state, True)
self.assertFalse(state.fordPscmStatus.valid)
self.assertEqual(state.fordPscmStatus.canMonoTime, 0)
self.assertFalse(self.publish().fordPscmStatus.valid)
self.update_status(1_000_000_000)
invalid = self.publish(can_valid=False).fordPscmStatus
self.assertFalse(invalid.valid)
self.assertEqual(invalid.canMonoTime, 1_000_000_000)
def test_mixed_timestamps_or_malformed_status_cannot_enable_feedback(self):
self.update_status(1_000_000_000)
self.parser.ts_nanos[MESSAGE][SIGNALS[-1]] = 990_000_000
self.assertFalse(self.publish().fordPscmStatus.valid)
self.parser.ts_nanos[MESSAGE][SIGNALS[-1]] = 1_000_000_000
for value in (float('nan'), -1, 1.5, 4):
self.parser.vl[MESSAGE]['LatCtlLim_D_Stat'] = value
self.assertFalse(self.publish().fordPscmStatus.valid)
def test_other_vehicles_and_legacy_messages_default_to_unavailable(self):
for cp in (SimpleNamespace(brand='toyota'), SimpleNamespace(brand='ford', flags=0)):
state = custom.CarStateSP.new_message(speedLimit=10.)
populate_ford_pscm_status(cp, {}, state, True)
self.assertFalse(state.fordPscmStatus.valid)
self.assertEqual(state.fordPscmStatus.canMonoTime, 0)
self.assertEqual(state.speedLimit, 10.)
# Old recordings/readers have no appended status pointer; defaults must
# remain unavailable rather than interpreting zeroed enums as fresh data.
self.assertFalse(custom.CarStateSP.new_message().fordPscmStatus.valid)
def test_actual_card_update_populates_status_after_dataclass_conversion(self):
self.update_status(1_000_000_000, limit=1)
source_path = Path(__file__).resolve().parents[1] / 'card.py'
source = ast.parse(source_path.read_text())
car_class = next(n for n in source.body if isinstance(n, ast.ClassDef) and n.name == 'Car')
method = next(n for n in car_class.body if isinstance(n, ast.FunctionDef) and n.name == 'state_update')
statements = method.body
first = next(i for i, n in enumerate(statements) if isinstance(n, ast.Assign) and ast.unparse(n.value) == 'self.CI.update(can_list)')
last = next(i for i, n in enumerate(statements) if isinstance(n, ast.Expr) and isinstance(n.value, ast.Call)
and isinstance(n.value.func, ast.Name) and n.value.func.id == 'populate_ford_pscm_status')
self.assertGreater(last, first)
code = compile(ast.Module(body=statements[first:last + 1], type_ignores=[]), str(source_path), 'exec')
ci = SimpleNamespace(update=lambda _: (SimpleNamespace(canValid=True), structs.CarStateSP(speedLimit=11.)),
can_parsers={Bus.pt: self.parser})
environment = {'self': SimpleNamespace(CP=self.cp, CI=ci), 'can_list': [], 'convert_to_capnp': convert_to_capnp,
'populate_ford_pscm_status': populate_ford_pscm_status}
exec(code, environment)
self.assertTrue(environment['CS_SP'].fordPscmStatus.valid)
self.assertEqual(environment['CS_SP'].fordPscmStatus.canMonoTime, 1_000_000_000)
self.assertEqual(environment['CS_SP'].fordPscmStatus.limit, 1)
if __name__ == '__main__':
unittest.main()
+46 -1
View File
@@ -1,5 +1,6 @@
#!/usr/bin/env python3
import math
import time
from numbers import Number
from openpilot.cereal import log
@@ -11,8 +12,11 @@ from openpilot.common.realtime import config_realtime_process, DT_CTRL, Priority
from openpilot.common.swaglog import cloudlog
from opendbc.car.car_helpers import interfaces
from opendbc.car.ford.values import FordFlags, FordFlagsSP
from opendbc.car.vehicle_model import VehicleModel
from openpilot.selfdrive.controls.lib.drive_helpers import clip_curvature
from openpilot.selfdrive.controls.lib.ford_model_action import FordModelActionController, select_model_action_controller
from openpilot.selfdrive.controls.lib.ford_path import FordPath, FordPathController, FordPscmObserverPathController
from openpilot.selfdrive.controls.lib.latcontrol import LatControl
from openpilot.selfdrive.controls.lib.latcontrol_pid import LatControlPID
from openpilot.selfdrive.controls.lib.latcontrol_angle import LatControlAngle, STEER_ANGLE_SATURATION_THRESHOLD
@@ -44,7 +48,7 @@ class Controls(ControlsExt):
self.CI = interfaces[self.CP.carFingerprint](self.CP, self.CP_SP)
self.sm = messaging.SubMaster(['lateralDelay', 'vehicleParameters', 'lateralTorqueParameters', 'modelV2', 'selfdriveState',
'extrinsicsCalibration', 'deviceMotion', 'longitudinalPlan', 'lateralManeuverPlan', 'carState', 'carOutput',
'extrinsicsCalibration', 'deviceMotion', 'longitudinalPlan', 'lateralManeuverPlan', 'carState', 'carStateSP', 'carOutput',
'driverMonitoringState', 'onroadEvents', 'driverAssistance'] + self.sm_services_ext,
poll='selfdriveState')
self.pm = messaging.PubMaster(['carControl', 'controlsState'] + self.pm_services_ext)
@@ -52,6 +56,15 @@ class Controls(ControlsExt):
self.steer_limited_by_safety = False
self.curvature = 0.0
self.desired_curvature = 0.0
self.ford_pscm_observer = (self.CP.brand == "ford" and self.CP.flags & FordFlags.CANFD and
self.params.get_bool("FordPscmObserver"))
self.ford_path_controller = FordPscmObserverPathController() if self.ford_pscm_observer else FordPathController()
self.ford_path_controller = select_model_action_controller(self.CP, bool(self.CP_SP.flags & FordFlagsSP.MODEL_ACTION),
self.ford_path_controller)
self.ford_model_action = isinstance(self.ford_path_controller, FordModelActionController)
if self.CP.brand == "ford":
cloudlog.event("Ford path controller selected", controller=type(self.ford_path_controller).__name__)
self.ford_path = FordPath()
self.pose_calibrator = PoseCalibrator()
self.calibrated_pose: Pose | None = None
@@ -155,6 +168,38 @@ class Controls(ControlsExt):
actuators.curvature = float(lateral_output)
else:
actuators.steeringAngleDeg = float(lateral_output)
if self.CP.brand == "ford":
ford_model = model_v2 if self.sm.valid['modelV2'] else None
if self.ford_model_action:
assert isinstance(self.ford_path_controller, FordModelActionController)
reference_service = 'lateralManeuverPlan' if self.sm.valid['lateralManeuverPlan'] else 'modelV2'
now = time.monotonic()
self.ford_path = self.ford_path_controller.update(
ford_model, self.desired_curvature, yaw_rate=-CS.yawRate, speed=CS.vEgo, now=now,
measurement_time=self.sm.logMonoTime['carState'] * 1e-9,
model_time=self.sm.logMonoTime['modelV2'] * 1e-9,
reference_time=self.sm.logMonoTime[reference_service] * 1e-9,
reference_source=reference_service,
active=CC.latActive, valid=CS.canValid and self.sm.all_checks(['carState', 'vehicleParameters', 'modelV2', reference_service]),
)
if not self.ford_path.valid:
CC.latActive = False
if self.sm.frame % 20 == 0:
cloudlog.event("Ford C2-free path tracking", model_mono_time=self.sm.logMonoTime['modelV2'],
measurement_mono_time=self.sm.logMonoTime['carState'],
reference_service=reference_service, reference_mono_time=self.sm.logMonoTime[reference_service],
measured_curvature=self.curvature,
**self.ford_path_controller.diagnostics)
elif self.ford_pscm_observer:
assert isinstance(self.ford_path_controller, FordPscmObserverPathController)
self.ford_path = self.ford_path_controller.update(ford_model, self.desired_curvature,
current_curvature=self.curvature, v_ego=CS.vEgo,
v_ego_raw=CS.vEgoRaw, active=CC.latActive)
else:
self.ford_path = self.ford_path_controller.update(ford_model, self.desired_curvature,
current_curvature=self.curvature, v_ego=CS.vEgo,
active=CC.latActive)
actuators.curvature = float(self.ford_path.curvature)
# Ensure no NaNs/Infs
for p in ACTUATOR_FIELDS:
attr = getattr(actuators, p)
@@ -0,0 +1,163 @@
"""Experimental Ford model-point controller with earlier C1 release.
The one-second preview and seven-metre minimum are engineering choices,
not identified Ford reference points or PSCM calibration.
"""
import math
import struct
import numpy as np
from opendbc.car.ford.values import FordFlags
from openpilot.selfdrive.controls.lib.ford_path import FordPath, _model_path
MIN_STATION_M = 7.0
PREVIEW_TIME_S = 1.0
CALIBRATION_APPROVED = False
def _packed(value, resolution, offset):
"""Mirror Float32 carControlSP and sign-reversed CANPacker rounding."""
value = struct.unpack("f", struct.pack("f", value))[0]
return -(math.floor((-value - offset) / resolution + 0.5) * resolution + offset)
def _finite(*values):
try:
return all(math.isfinite(value) for value in values)
except (TypeError, ValueError, OverflowError):
return False
def encode_model_action(model, desired_curvature, speed):
"""Sample a model pose and unload C1 when its path is straightening ahead.
Use the model's distance at one second, with a seven-metre minimum and an
endpoint hold. Terminal spatial curvature bounds the heading contribution
toward zero. A constant heading slope in sampled arc distance retains C1;
decreasing curvature can release C1 before that heading returns to zero.
This is an experimental request mapping, not an identified PSCM response.
Selected curvature remains a health/diagnostic input only.
"""
if not _finite(desired_curvature, speed) or not .3 <= speed <= 55 or abs(desired_curvature) > 1:
return FordPath()
try:
path = _model_path(model)
times = [float(t) for t in model.position.t]
heading_times = [float(t) for t in model.orientation.t]
except (AttributeError, TypeError, ValueError, OverflowError):
return FordPath()
if path is None or not all(_finite(*values) for values in path):
return FordPath()
station, _, lateral, heading = path
if (len(times) != len(station) or not times or times[0] != 0. or times != heading_times or
not _finite(*times) or any(b <= a for a, b in zip(times, times[1:], strict=False))):
return FordPath()
sample_station = min(station[-1], max(MIN_STATION_M, float(np.interp(PREVIEW_TIME_S, times, station))))
c0 = float(np.interp(sample_station, station, lateral))
c1 = float(np.interp(sample_station, station, heading))
# Use the same enclosing model segment as the pose interpolation. At an
# exact knot use its incoming segment; an ambiguous duplicate keeps C1.
upper = max(1, int(np.searchsorted(station, sample_station, side='left')))
span = station[upper] - station[upper - 1]
duplicate = (upper + 1 < len(station) and station[upper] == sample_station == station[upper + 1])
if span > 0. and not duplicate:
terminal_heading = heading[0] + sample_station * ((heading[upper] - heading[upper - 1]) / span)
if _finite(terminal_heading):
# One-sided: never amplify C1 or invent a reversal ahead of the model.
direction = math.copysign(1., c1)
c1 = direction * float(np.clip(direction * terminal_heading, 0., abs(c1)))
return FordPath(True, c0, c1, 0., 0.) if _finite(c0, c1) else FordPath()
class ModelActionController:
"""Only two states: independently slewed C0/C1 model-point requests."""
__slots__ = ('c0', 'c1')
def __init__(self):
self.reset()
def reset(self):
self.c0 = self.c1 = 0.
def update(self, model, desired_curvature, *, speed, dt, yaw_rate=0., active=True, valid=True):
# Raw Ford yaw remains an input-health check, not a pose measurement.
if not active or not valid or not _finite(dt, yaw_rate) or not .002 <= dt <= .1 or abs(yaw_rate) > 3:
self.reset()
return FordPath()
target = encode_model_action(model, desired_curvature, speed)
if not target.valid:
self.reset()
return FordPath()
c0 = float(np.clip(target.path_offset, -5.11, 5.11))
c1 = float(np.clip(target.path_angle, -.5, .5))
self.c0 += float(np.clip(c0-self.c0, -4.*dt, 4.*dt))
self.c1 += float(np.clip(c1-self.c1, -.5*dt, .5*dt))
return FordPath(True, _packed(self.c0, .01, -5.12), _packed(self.c1, .0005, -.5), 0., 0.)
class FordModelActionController:
"""Freshness, engagement and reference checks for model-point tracking.
Scalar-only maneuver references cannot supply this controller's model pose.
Reject them explicitly rather than silently following a different reference.
Measured yaw checks input health only; it never modifies valid geometry.
"""
def __init__(self):
self.core = ModelActionController()
self.reset()
def reset(self, status='inactive'):
self.core.reset()
self.last_time = self.last_measurement_time = self.last_model_time = None
self.diagnostics = {'status': status, 'hypothesis': 'model-pose-terminal-c1-v1',
'calibration_approved': CALIBRATION_APPROVED, 'command': (0., 0., 0., 0.)}
def update(self, model, desired_curvature, *, yaw_rate, speed, now, measurement_time, model_time, reference_time,
active, valid=True, reference_source="modelV2"):
reason = None
if not active:
reason = 'inactive'
elif reference_source != 'modelV2':
reason = 'unsupported_reference'
elif not valid:
reason = 'invalid_service'
elif not _finite(desired_curvature, yaw_rate, speed, now, measurement_time, model_time, reference_time):
reason = 'nonfinite'
elif not all(-.005 <= now - timestamp <= .15 for timestamp in (measurement_time, model_time, reference_time)):
reason = 'stale_input'
elif not .3 <= speed <= 55 or abs(yaw_rate) > 3 or abs(desired_curvature) > 1:
reason = 'input_range'
if reason is not None:
self.reset(reason)
return FordPath()
dt = .01 if self.last_time is None else now - self.last_time
if not .002 <= dt <= .1 or (self.last_measurement_time is not None and measurement_time < self.last_measurement_time) or (
self.last_model_time is not None and model_time < self.last_model_time
):
self.reset('timing_reset')
return FordPath()
command = self.core.update(model, desired_curvature, speed=speed, dt=dt, yaw_rate=yaw_rate)
if not command.valid:
self.reset('invalid_path')
return command
self.last_time, self.last_measurement_time, self.last_model_time = now, measurement_time, model_time
self.diagnostics = {'status': 'active', 'hypothesis': 'model-pose-terminal-c1-v1',
'calibration_approved': CALIBRATION_APPROVED, 'desired_curvature': desired_curvature,
'yaw_rate': yaw_rate, 'pose_source': 'model',
'preview_time_s': PREVIEW_TIME_S, 'minimum_station_m': MIN_STATION_M,
'c1_release': 'terminal_spatial_curvature',
'model_age': now - model_time, 'measurement_age': now - measurement_time, 'reference_age': now - reference_time,
'dt': dt, 'offset_request': self.core.c0, 'heading_request': self.core.c1,
'command': (command.path_offset, command.path_angle, 0., 0.)}
return command
def select_model_action_controller(CP, enabled, previous_controller):
"""The separate default-off toggle takes priority on the CAN FD Lightning."""
compatible = CP.brand == 'ford' and CP.flags & FordFlags.CANFD and CP.carFingerprint == 'FORD_F_150_LIGHTNING_MK1'
if enabled and compatible:
return FordModelActionController()
return previous_controller
@@ -0,0 +1,368 @@
from collections import deque
from dataclasses import dataclass
import math
import numpy as np
from opendbc.car.ford.values import CarControllerParams
DBC_OFFSET = (-5.12, 5.11)
DBC_ANGLE = (-0.5, 0.5235)
DBC_CURVATURE = (-0.02, 0.02)
DBC_CURVATURE_RATE = (-0.001024, 0.001023)
DBC_OFFSET_RESOLUTION = 0.01
DBC_ANGLE_RESOLUTION = 0.0005
DBC_CURVATURE_RESOLUTION = 0.00002
DBC_CURVATURE_RATE_RESOLUTION = 0.000001
_PATH_MIN_LOOKAHEAD = 7.0
_POSE_PREDICTION_TIME = 0.1
_POSE_BLEND_CURVATURE = (0.006, 0.012)
_PATH_OFFSET_RATE = 4.0
_PATH_ANGLE_RATE = 1.0
_PSCM_DT = 0.004
_PSCM_C0_RATE = 1.5
_PSCM_C1_RATE = 0.100006103515625
_PSCM_C2_RATE = 0.0030059814453125
_PSCM_SPEED_KPH = (0.0, 15.0, 40.0, 70.0, 100.0, 150.0, 200.0, 250.0)
_PSCM_SPEED_GAIN = (32.0, 32.0, 32.0, 30.0, 30.0, 24.0, 12.0, 0.0)
_PSCM_C0_EFFECTIVE_LIMIT = 1.0
_PSCM_C1_EFFECTIVE_LIMIT = 0.349609375 / 10.0
@dataclass(frozen=True)
class FordPath:
valid: bool = False
path_offset: float = 0.0
path_angle: float = 0.0
curvature: float = 0.0
curvature_rate: float = 0.0
@dataclass(frozen=True)
class FordPscmState:
path_offset: float = 0.0
path_angle: float = 0.0
curvature: float = 0.0
@dataclass(frozen=True)
class FordModelPose:
path_offset: float
path_angle: float
offset_horizon: float
curvature_demand: float
forward_angle: float
def _finite(value: float) -> float:
return float(value) if math.isfinite(value) else 0.0
def _sample(distance: float, distances: list[float], values: list[float]) -> float:
return float(np.interp(distance, distances, values))
def _blend_share(demand: float) -> float:
lower, upper = _POSE_BLEND_CURVATURE
return float(np.clip((demand - lower) / (upper - lower), 0.0, 1.0))
def _model_path(model) -> tuple[list[float], list[float], list[float], list[float]] | None:
try:
x = [float(value) for value in model.position.x]
y = [float(value) for value in model.position.y]
heading = [float(value) for value in model.orientation.z]
except (AttributeError, TypeError, ValueError):
return None
if len(x) < 2 or len(x) != len(y) or len(x) != len(heading):
return None
if not all(math.isfinite(value) for values in (x, y, heading) for value in values):
return None
distance = [0.0]
for i in range(1, len(x)):
distance.append(distance[-1] + math.hypot(x[i] - x[i - 1], y[i] - y[i - 1]))
if distance[-1] <= 0.0:
return None
unwrapped_heading = [heading[0]]
for value in heading[1:]:
delta = (value - unwrapped_heading[-1] + math.pi) % (2.0 * math.pi) - math.pi
unwrapped_heading.append(unwrapped_heading[-1] + delta)
return distance, x, y, unwrapped_heading
def _predicted_pose(distance: float, current_curvature: float,
curvature_delta: float) -> tuple[float, float, float]:
curvature = current_curvature + 0.5 * curvature_delta
heading = curvature * distance
if abs(curvature) < 1e-9:
return distance, 0.0, 0.0
return math.sin(heading) / curvature, (1.0 - math.cos(heading)) / curvature, heading
def _relative_pose(target_distance: float, path: tuple[list[float], list[float], list[float], list[float]],
vehicle_pose: tuple[float, float, float]) -> tuple[float, float]:
distance, x, y, heading = path
vehicle_x, vehicle_y, vehicle_heading = vehicle_pose
dx = _sample(target_distance, distance, x) - vehicle_x
dy = _sample(target_distance, distance, y) - vehicle_y
cosine = math.cos(vehicle_heading)
sine = math.sin(vehicle_heading)
offset = -sine * dx + cosine * dy
angle = math.atan2(math.sin(_sample(target_distance, distance, heading) - vehicle_heading),
math.cos(_sample(target_distance, distance, heading) - vehicle_heading))
return offset, angle
def _path_pose(target_distance: float,
path: tuple[list[float], list[float], list[float], list[float]]) -> tuple[float, float, float]:
distance, x, y, heading = path
return (_sample(target_distance, distance, x), _sample(target_distance, distance, y),
_sample(target_distance, distance, heading))
def _bounded_feedback(feedforward: float, feedback: float, resolution: float, zero_path_limit: float) -> float:
quantization_threshold = 0.5 * resolution
limit = max(abs(feedforward) - resolution, 0.0) if abs(feedforward) >= quantization_threshold else zero_path_limit
return float(np.clip(feedback, -limit, limit))
def _model_pose(path: tuple[list[float], list[float], list[float], list[float]],
current_curvature: float, curvature_delta: float, v_ego: float) -> FordModelPose:
distance, _, _, _ = path
advance = min(v_ego * _POSE_PREDICTION_TIME, distance[-1])
offset_horizon = min(_PATH_MIN_LOOKAHEAD, distance[-1] - advance)
angle_horizon = min(max(v_ego, _PATH_MIN_LOOKAHEAD), distance[-1] - advance)
# Keep the model's remaining path as feedforward. Measured vehicle motion is
# a separate, short delay-aligned correction, so catching the requested
# curvature cannot erase a turn that is still present in the model path.
model_pose = _path_pose(advance, path)
model_offset, _ = _relative_pose(advance + offset_horizon, path, model_pose)
_, model_angle = _relative_pose(advance + angle_horizon, path, model_pose)
vehicle_pose = _predicted_pose(advance, current_curvature, curvature_delta)
feedback_offset, feedback_angle = _relative_pose(advance, path, vehicle_pose)
gentle_curvature = _POSE_BLEND_CURVATURE[0]
feedback_offset = _bounded_feedback(model_offset, feedback_offset, DBC_OFFSET_RESOLUTION,
0.5 * gentle_curvature * advance ** 2)
feedback_angle = _bounded_feedback(model_angle, feedback_angle, DBC_ANGLE_RESOLUTION,
gentle_curvature * advance)
offset_curvature = 2.0 * model_offset / max(offset_horizon, 1e-3) ** 2
angle_curvature = model_angle / max(angle_horizon, 1e-3)
return FordModelPose(model_offset + feedback_offset, model_angle + feedback_angle, offset_horizon,
max(abs(offset_curvature), abs(angle_curvature)), model_angle)
def _encode_pose(pose: FordModelPose, pose_share: float, curvature: float) -> FordPath:
path_offset = pose_share * pose.path_offset
path_angle = pose_share * pose.path_angle
if abs(path_offset) < 0.5 * DBC_OFFSET_RESOLUTION:
path_offset = 0.0
if abs(path_angle) < 0.5 * DBC_ANGLE_RESOLUTION:
path_angle = 0.0
limited_path_angle = float(np.clip(path_angle, *DBC_ANGLE))
path_offset += (path_angle - limited_path_angle) * pose.offset_horizon
return FordPath(
valid=True,
path_offset=float(np.clip(path_offset, *DBC_OFFSET)),
path_angle=limited_path_angle,
curvature=float(np.clip(curvature, *DBC_CURVATURE)),
curvature_rate=0.0,
)
def _encode_path(path: tuple[list[float], list[float], list[float], list[float]], desired_curvature: float,
current_curvature: float, curvature_delta: float, v_ego: float) -> FordPath:
pose = _model_pose(path, current_curvature, curvature_delta, v_ego)
pose_share = _blend_share(max(pose.curvature_demand, abs(desired_curvature)))
# Match upstream's C2-only normal driving, then continuously transfer the
# command to the model pose for larger maneuvers. An opposing/finished model
# path must unload sticky C2 and retain the fast pose needed to unwind it.
c2_opposes_path = desired_curvature != 0.0 and desired_curvature * pose.forward_angle <= 0.0
if c2_opposes_path:
pose_share = 1.0
curvature = 0.0
else:
curvature = desired_curvature * (1.0 - pose_share)
return _encode_pose(pose, pose_share, curvature)
class FordPathController:
"""Blend normal C2 following into the model's forward C0/C1 pose."""
def __init__(self, dt: float = 0.01):
self.dt = dt
self._last_path = FordPath(valid=True)
self._curvature_history = deque(maxlen=max(round(_POSE_PREDICTION_TIME / dt) + 1, 2))
def _limit(self, target: FordPath) -> FordPath:
offset_delta = target.path_offset - self._last_path.path_offset
angle_delta = target.path_angle - self._last_path.path_angle
scale = min(
1.0,
_PATH_OFFSET_RATE * self.dt / abs(offset_delta) if offset_delta else 1.0,
_PATH_ANGLE_RATE * self.dt / abs(angle_delta) if angle_delta else 1.0,
)
self._last_path = FordPath(
True,
self._last_path.path_offset + scale * offset_delta,
self._last_path.path_angle + scale * angle_delta,
self._last_path.curvature + scale * (target.curvature - self._last_path.curvature),
0.0,
)
return self._last_path
def update(self, model, desired_curvature: float, *, current_curvature: float = 0.0,
v_ego: float = 0.0, active: bool = True) -> FordPath:
if not active:
self._last_path = FordPath(valid=True)
self._curvature_history.clear()
return FordPath()
current_curvature = _finite(current_curvature)
self._curvature_history.append(current_curvature)
curvature_delta = (current_curvature - self._curvature_history[0]
if len(self._curvature_history) == self._curvature_history.maxlen else 0.0)
path = _model_path(model) if model is not None else None
if path is None:
return self._limit(FordPath(valid=True))
return self._limit(_encode_path(path, _finite(desired_curvature), current_curvature, curvature_delta,
max(_finite(v_ego), 0.0)))
def _pscm_slew(value: float, target: float, rate: float, ticks: int) -> float:
step = rate * _PSCM_DT * ticks
return float(np.clip(target, value - step, value + step))
def _pscm_speed_gain(v_ego: float) -> float:
return float(np.interp(max(v_ego, 0.0) * 3.6, _PSCM_SPEED_KPH, _PSCM_SPEED_GAIN))
def _wire_path(path: FordPath) -> FordPath:
return FordPath(
valid=path.valid,
path_offset=round(path.path_offset / DBC_OFFSET_RESOLUTION) * DBC_OFFSET_RESOLUTION,
path_angle=round(path.path_angle / DBC_ANGLE_RESOLUTION) * DBC_ANGLE_RESOLUTION,
curvature=round(path.curvature / DBC_CURVATURE_RESOLUTION) * DBC_CURVATURE_RESOLUTION,
curvature_rate=round(path.curvature_rate / DBC_CURVATURE_RATE_RESOLUTION) * DBC_CURVATURE_RATE_RESOLUTION,
)
def _pscm_contributions(state: FordPscmState, v_ego: float) -> tuple[float, float, float]:
gain = _pscm_speed_gain(v_ego)
return (
float(np.clip(0.5 * gain * state.path_offset, -0.5 * gain, 0.5 * gain)),
float(np.clip(10.0 * gain * state.path_angle, -0.349609375 * gain, 0.349609375 * gain)),
float(np.clip(0.30078125 * gain * state.curvature * v_ego ** 2, -0.5 * gain, 0.5 * gain)),
)
class FordPscmObserver:
"""Mirror the firmware's held-command coefficient states at its 250 Hz step."""
def __init__(self):
self.state = FordPscmState()
self.command = FordPath(valid=True)
self._phase = 0.0
def reset(self) -> None:
self.state = FordPscmState()
self.command = FordPath(valid=True)
self._phase = 0.0
def advance(self, elapsed: float) -> None:
self._phase += max(elapsed, 0.0)
ticks = int((self._phase + 1e-12) / _PSCM_DT)
self._phase -= ticks * _PSCM_DT
if ticks == 0:
return
self.state = FordPscmState(
_pscm_slew(self.state.path_offset, self.command.path_offset, _PSCM_C0_RATE, ticks),
_pscm_slew(self.state.path_angle, self.command.path_angle, _PSCM_C1_RATE, ticks),
_pscm_slew(self.state.curvature, self.command.curvature + 10.0 * self.command.curvature_rate,
_PSCM_C2_RATE, ticks),
)
def set_command(self, command: FordPath) -> None:
self.command = _wire_path(command)
class FordPscmObserverPathController:
"""Compensate model-path commands for the PSCM coefficient state it still carries."""
def __init__(self, dt: float = 0.01):
self.dt = dt
self._last_path = FordPath(valid=True)
self._curvature_history = deque(maxlen=max(round(_POSE_PREDICTION_TIME / dt) + 1, 2))
self.observer = FordPscmObserver()
self._sent_c2 = 0.0
def _reset(self) -> None:
self._last_path = FordPath(valid=True)
self._curvature_history.clear()
self.observer.reset()
self._sent_c2 = 0.0
def _command_for_state(self, target: FordPath, v_ego: float) -> FordPath:
# The target describes the desired fully-settled PSCM contribution. C0 keeps
# the remaining C1-saturated residual. C1 supplies the primary contribution
# that the known slow C2 state does not yet provide, without a guessed gain.
target_state = FordPscmState(target.path_offset, target.path_angle, target.curvature)
target_contribution = sum(_pscm_contributions(target_state, v_ego))
_, _, observed_c2 = _pscm_contributions(self.observer.state, v_ego)
gain = _pscm_speed_gain(v_ego)
required_fast = target_contribution - observed_c2
c1_contribution = float(np.clip(required_fast, -0.349609375 * gain, 0.349609375 * gain))
c0_contribution = required_fast - c1_contribution
path_offset = c0_contribution / (0.5 * gain) if gain > 0.0 else 0.0
path_angle = c1_contribution / (10.0 * gain) if gain > 0.0 else 0.0
return FordPath(
valid=True,
path_offset=float(np.clip(path_offset, -_PSCM_C0_EFFECTIVE_LIMIT, _PSCM_C0_EFFECTIVE_LIMIT)),
path_angle=float(np.clip(path_angle, -_PSCM_C1_EFFECTIVE_LIMIT, _PSCM_C1_EFFECTIVE_LIMIT)),
curvature=target.curvature,
curvature_rate=target.curvature_rate,
)
def _limit(self, target: FordPath, v_ego_raw: float) -> FordPath:
path_offset = float(np.clip(target.path_offset,
self._last_path.path_offset - _PATH_OFFSET_RATE * self.dt,
self._last_path.path_offset + _PATH_OFFSET_RATE * self.dt))
path_angle = float(np.clip(target.path_angle,
self._last_path.path_angle - _PATH_ANGLE_RATE * self.dt,
self._last_path.path_angle + _PATH_ANGLE_RATE * self.dt))
curvature = CarControllerParams.CURVATURE_LIMITS.apply_limits(
target.curvature, self._sent_c2, v_ego_raw, 0.0, True, CarControllerParams.LMC2_STEP,
)
self._sent_c2 = curvature
self._last_path = FordPath(True, path_offset, path_angle, curvature, target.curvature_rate)
self.observer.set_command(self._last_path)
return self._last_path
def update(self, model, desired_curvature: float, *, current_curvature: float = 0.0,
v_ego: float = 0.0, v_ego_raw: float = 0.0, active: bool = True) -> FordPath:
if not active:
self._reset()
return FordPath()
self.observer.advance(self.dt)
current_curvature = _finite(current_curvature)
self._curvature_history.append(current_curvature)
curvature_delta = (current_curvature - self._curvature_history[0]
if len(self._curvature_history) == self._curvature_history.maxlen else 0.0)
path = _model_path(model) if model is not None else None
if path is None:
target = FordPath(valid=True)
else:
target = _encode_path(path, _finite(desired_curvature), current_curvature, curvature_delta,
max(_finite(v_ego), 0.0))
v_ego_raw = max(_finite(v_ego_raw), 0.0)
command = self._command_for_state(target, v_ego_raw)
return self._limit(command, v_ego_raw)
@@ -0,0 +1,229 @@
{
"description": "Curvature-driven C0 and full-heading C1 command regression; does not predict counterfactual wheel response. Contains geometry and control signals only, no GPS.",
"fixture_sha256": "12782ac1b0d0637945f729a46ad03af16cd58188872b6a65f104e32c4db70e9b",
"episodes": [
{
"name": "left_large",
"route": "84865544361f55cb_00000077--4b55791ce6",
"range_seconds": [
809.5,
815.0
],
"evidence_seconds": [
812.1,
814.0
],
"samples": 532
},
{
"name": "right_large",
"route": "84865544361f55cb_00000077--4b55791ce6",
"range_seconds": [
866.5,
872.0
],
"evidence_seconds": [
869.5,
871.0
],
"samples": 547
},
{
"name": "left_very_large",
"route": "84865544361f55cb_00000077--4b55791ce6",
"range_seconds": [
880.0,
884.0
],
"evidence_seconds": [
882.9,
883.32
],
"samples": 397
},
{
"name": "right_plateau",
"route": "84865544361f55cb_00000077--4b55791ce6",
"range_seconds": [
964.0,
970.0
],
"evidence_seconds": [
967.2,
968.93
],
"samples": 583
},
{
"name": "oscillation",
"route": "84865544361f55cb_00000078--349f5b8695",
"range_seconds": [
29.0,
37.0
],
"evidence_seconds": [
32.5,
36.1
],
"samples": 795
},
{
"name": "weak_first",
"route": "84865544361f55cb_0000007a--5a95fc717e",
"range_seconds": [
90.5,
95.2
],
"evidence_seconds": [
93.5,
95.08
],
"samples": 467
},
{
"name": "weak_second",
"route": "84865544361f55cb_0000007a--5a95fc717e",
"range_seconds": [
119.5,
125.3
],
"evidence_seconds": [
122.5,
125.2
],
"samples": 576
}
],
"sources": {
"84865544361f55cb_00000077--4b55791ce6": [
{
"name": "84865544361f55cb_00000077--4b55791ce6--0--rlog.zst",
"bytes": 10342855,
"sha256": "2f072eff3076f4d32dbc1a077f24fc85818abeafd13a4b970b1ba558a0d2ca52"
},
{
"name": "84865544361f55cb_00000077--4b55791ce6--1--rlog.zst",
"bytes": 11688902,
"sha256": "bbbbf7fc79b1c7133b83678a5202ac41cd8b3dcdd0bb358843d43fdb38f25e1e"
},
{
"name": "84865544361f55cb_00000077--4b55791ce6--2--rlog.zst",
"bytes": 11875525,
"sha256": "18d7e0224a61d068aeeef0e176da071f5e67d77fcb7ebaa7b5854756d4438add"
},
{
"name": "84865544361f55cb_00000077--4b55791ce6--3--rlog.zst",
"bytes": 12489313,
"sha256": "7869f95c87849018df07c680e0584145a31c407608f96f2fc77f35b729f02d2c"
},
{
"name": "84865544361f55cb_00000077--4b55791ce6--4--rlog.zst",
"bytes": 12301118,
"sha256": "2c2125fb2320b9bc6620fc586cedb6558b33e8475d0cce3fb361db4eea6a1c28"
},
{
"name": "84865544361f55cb_00000077--4b55791ce6--5--rlog.zst",
"bytes": 12976655,
"sha256": "b18c448786daf46cfa05bf0352396ce5851a09603d34ee5b989e096da5ee5180"
},
{
"name": "84865544361f55cb_00000077--4b55791ce6--6--rlog.zst",
"bytes": 13223857,
"sha256": "8c08b8d47ebca38c70aa94a1cdda25443ed3be94727bb487406b518471c88dd1"
},
{
"name": "84865544361f55cb_00000077--4b55791ce6--7--rlog.zst",
"bytes": 13043701,
"sha256": "638948d7e5853046773f82df8a531c518c42883c77f060e61ff683ccb59d52af"
},
{
"name": "84865544361f55cb_00000077--4b55791ce6--8--rlog.zst",
"bytes": 12569024,
"sha256": "41e5c83d2205964889579cf24967712dd0340da5f30f213409f2b1e04e6eb78a"
},
{
"name": "84865544361f55cb_00000077--4b55791ce6--9--rlog.zst",
"bytes": 11926213,
"sha256": "6034a90e817424c02755c2c0d9bdbad088c4286edfb887d9f2acbd60d7818da7"
},
{
"name": "84865544361f55cb_00000077--4b55791ce6--10--rlog.zst",
"bytes": 13010961,
"sha256": "23958a1ac8277977952c73e889fbfd9245bc2cf82b3af58233c245dbf1e76545"
},
{
"name": "84865544361f55cb_00000077--4b55791ce6--11--rlog.zst",
"bytes": 13204208,
"sha256": "f78d65b7b5927a8570f52d765f9af45431f8ab72260987f718321ad1b03c70af"
},
{
"name": "84865544361f55cb_00000077--4b55791ce6--12--rlog.zst",
"bytes": 12562994,
"sha256": "485621d1cf71605fccc8c679cb146b1f0954078849db7c74b1b65b166d741025"
},
{
"name": "84865544361f55cb_00000077--4b55791ce6--13--rlog.zst",
"bytes": 12610836,
"sha256": "0b4b0c01caae39a7dc4ff2219ab168029ea12c43b1966b8c204972743627c24b"
},
{
"name": "84865544361f55cb_00000077--4b55791ce6--14--rlog.zst",
"bytes": 13114068,
"sha256": "f18769800bd08c7614280916b50ec0274eca9b4765ad26d6549b72912321dac0"
},
{
"name": "84865544361f55cb_00000077--4b55791ce6--15--rlog.zst",
"bytes": 13113162,
"sha256": "abf0adc702db6f5fdd78a145709504c7061255bc9b9a61329a575707d5bd2f74"
},
{
"name": "84865544361f55cb_00000077--4b55791ce6--16--rlog.zst",
"bytes": 13029294,
"sha256": "a76ed74b889bda60e2d929123186df83653591d1e058fbd1497551fbbf23941e"
},
{
"name": "84865544361f55cb_00000077--4b55791ce6--17--rlog.zst",
"bytes": 12429846,
"sha256": "51d3aff2afa1a7ce3c5372a499b14394f22ac730e952273a69496c65ef726ed2"
},
{
"name": "84865544361f55cb_00000077--4b55791ce6--18--rlog.zst",
"bytes": 9241845,
"sha256": "2a1646227f9cdb1ab7eaf79fa653b7f4443a629a703577ff6c38a715dbced3a4"
}
],
"84865544361f55cb_00000078--349f5b8695": [
{
"name": "84865544361f55cb_00000078--349f5b8695--0--rlog.zst",
"bytes": 10919702,
"sha256": "2d35f6c9ac9b09f8b86b3fe2fbe864e4af0d50c411e3dd56572b5aa113bf1973"
},
{
"name": "84865544361f55cb_00000078--349f5b8695--1--rlog.zst",
"bytes": 10600112,
"sha256": "6cabb48ea0caeb2fddd58be35b5c7e7c42faf87fc01991c4603573bffe33eaeb"
}
],
"84865544361f55cb_0000007a--5a95fc717e": [
{
"name": "84865544361f55cb_0000007a--5a95fc717e--0--rlog.zst",
"bytes": 10841486,
"sha256": "bfc9e3308e5241e76cba57f2441043710fe8b18ade314221ee5e423f640c211a"
},
{
"name": "84865544361f55cb_0000007a--5a95fc717e--1--rlog.zst",
"bytes": 12567837,
"sha256": "ab9eb05c5805e286a6ec639bbbbc1cf086bfcf1b440801ad000713db95dfa7fc"
},
{
"name": "84865544361f55cb_0000007a--5a95fc717e--2--rlog.zst",
"bytes": 11015857,
"sha256": "af4f87f39be37f0a5b23b58de50c2ed8801bdbda6544cd6cd292525fc3bd4fb3"
}
]
},
"pairing": "controlsState cycle time; causal carState speed/yaw/pressed; exact consumed model timestamp and geometry; nearest same-cycle carControl and carControlSP within 5ms.",
"yaw_rate": "Negative carState.yawRate, matching the model/control curvature coordinate sign; no wheel-to-curvature conversion.",
"desired_curvature": "Exact controlsState.desiredCurvature from the matching controlsState cycle. This is the post-selection, post-limiting request consumed by controlsd; it is not a wheel-angle-to-curvature fit.",
"reference_time": "Exact consumed modelV2 publication time, in the same relative seconds as each episode. The extraction cache does not retain consumed lateralManeuverPlan timestamps or validity; model time is an explicit replay assumption and cannot verify alternate-reference freshness."
}
@@ -0,0 +1,67 @@
{
"description": "Real route80 turn-command regressions. Signal-only fixture; no GPS. Counterfactual commands do not predict physical vehicle response.",
"route": "84865544361f55cb_00000080--1643deea7e",
"source_commit": "98662df401217a00ec9fc8e73b16857b6c220150",
"frozen_v3_controller_sha256": "576f4ec6f2dbc93f7e6c93a69839f69447eb5a0c2f834bd48b24f84a163dc2eb",
"fixture_sha256": "c1460e2cf1d3fd52b1a036d923fec7835a7d361126ee0c2decbc3f101ee6653c",
"episodes": [
{
"name": "under_333_339",
"range_seconds": [
331.5,
339.0
],
"evidence_seconds": [
333.0,
339.0
],
"samples": 745
},
{
"name": "over_417_420",
"range_seconds": [
415.5,
420.0
],
"evidence_seconds": [
417.0,
420.0
],
"samples": 447
},
{
"name": "under_430_435",
"range_seconds": [
428.5,
435.0
],
"evidence_seconds": [
430.0,
435.0
],
"samples": 646
}
],
"sources": [
{
"name": "84865544361f55cb_00000080--1643deea7e--5--rlog.zst",
"bytes": 12531711,
"sha256": "059482830794cb0eabe6069b75a9610b900bf2a93d7a6624f53c575cef997157"
},
{
"name": "84865544361f55cb_00000080--1643deea7e--6--rlog.zst",
"bytes": 12560505,
"sha256": "147276789f5b14913adc4cd16db18f3d4bd27ce8497c9ff96fdf0315c219339f"
},
{
"name": "84865544361f55cb_00000080--1643deea7e--7--rlog.zst",
"bytes": 12660797,
"sha256": "b311b6ace75819db52b9618154d68c7d12e2751d5046b6d174adb89ef87a223c"
}
],
"pairing": "Exact controlsState desiredCurvature and consumed model publication timestamp; causal carState speed, negative CAN yaw, and steeringPressed; nearest same-cycle carControl/carControlSP within 5 ms.",
"reference_time": "Consumed modelV2 publication time. Controller audit confirms route80 used modelV2 as reference throughout.",
"preroll": "Each episode starts from reset 1.5 s before evidence; v3_replay stores those exact cold-start commands and gates, while recorded stores original live path fields.",
"benchmark_clean": "Existing route80 benchmark mask: whole interval request minus 0.5 s through response (0.2 s) plus 0.25 s active, unpressed, valid, fresh, and speed >= 2 m/s.",
"expected_common_c1": "Independent shadow: clip(desiredCurvature * max(7 m, vEgo * 1 s), +/-0.5 rad), independently slewed at 0.5 rad/s and packed to Float32/sign-reversed CAN semantics. No subtraction of measured curvature."
}
@@ -0,0 +1,13 @@
{
"description": "PSCM status and raw driver-torque overlay for the existing three route80 request windows. No GPS. No counterfactual vehicle response.",
"fixture_sha256": "a9defdc5abdf26724358d606beb16becbdf30faa972974d49b179a9e004d7629",
"base_fixture": "ford_curvature_heading_route80.npz",
"base_fixture_sha256": "c1460e2cf1d3fd52b1a036d923fec7835a7d361126ee0c2decbc3f101ee6653c",
"source_route": "84865544361f55cb_00000080--1643deea7e",
"source_commit": "98662df401217a00ec9fc8e73b16857b6c220150",
"samples": 1838,
"source_cache_sha256": "1cd3e0c00805869ace1c5954dc682644f36f5eddb71785b69e4cb9da40f7f04f",
"pairing": "Latest actual bus-0 EPS 972 frame at or before each controlsState cycle; raw steering torque from the exact causal carState used by the base fixture.",
"timestamp_policy": "Actual CAN event logMonoTime in route-relative seconds, not the benchmark response-shifted status. The old route predates the new carStateSP status telemetry; source CAN timestamps are an explicit replay approximation.",
"validity": "Replay validity uses the paired carState valid and canValid values; enum validity, availability and age are checked by the production feedback controller."
}
@@ -0,0 +1,40 @@
{
"description": "Signal-only v6 turn-exit recovery regression; no location, device identity, or predicted new vehicle response.",
"recorded_controller_revision": "61dac4977bf9c36504398e8a4959dfed79cf6f05",
"baseline_revision": "61dac4977bf9c36504398e8a4959dfed79cf6f05",
"response_delay": 0.20000000298023224,
"samples": 9134,
"models": 1843,
"fixture_sha256": "41d5e3efcee03a9e02fcaf7bf456c050c6a671a5b7f7fc9706ddf4d27bad71b8",
"windows": [
{
"name": "overturn_then_underturn",
"range_s": [
19.99615067150053,
29.48070058550053
],
"samples": 521
},
{
"name": "well_tracked_curve_a",
"range_s": [
56.19474309950053,
63.68808603150053
],
"samples": 729
},
{
"name": "well_tracked_curve_b",
"range_s": [
101.19780691350051,
116.33885445450052
],
"samples": 810
}
],
"selection": "One previously identified overturn-then-underturn event and two previously reported well-tracked curves; selected before recovery implementation.",
"mask": "Whole t-0.5 through t+0.65 interval active, valid, fresh, unpressed, raw driver torque magnitude <=1 Nm; requested |curvature|*speed\u00b2 >=.5 m/s\u00b2.",
"timing": "Exact consumed model publication; causal CAN/PSCM at estimated control computation time. Subtract observed median computation-to-publication delay; unsampled tick timing remains approximate.",
"context": "At least 20 seconds prior context or the available start, extended before the latest observed reset. Overlapping episodes are merged.",
"coordinates": "Times are local elapsed seconds; models contain only relative position.x/y and orientation.z arrays."
}
@@ -0,0 +1,247 @@
{
"description": "Signal-only historical fallback evidence and frozen-v5 comparison; no GPS or inferred counterfactual vehicle response.",
"route": "route83",
"recorded_commit": "79a4caa1f6b71488949108aee9ae6ae6566347b1",
"fixture_sha256": "d00312c430ace47000c05b8284ee8d56df56ec24bb17a9ea8f4dce83133527c3",
"samples": 11744,
"model_count": 2367,
"source_cache_sha256": "53d786aff2e0b6338e1991320145305fda3101f7b76e50bd2929adbcaea95b28",
"response_delay": 0.20000000298023224,
"episodes": [
[
1861.2933736250002,
1874.756970279
],
[
1878.07372132,
1892.07372132
],
[
1950.874232409,
1964.874232409
],
[
2440.9020600930003,
2456.964158177
],
[
2580.722658577,
2611.364366768
],
[
2734.478264791,
2764.574374172
]
],
"windows": [
{
"name": "successful_large_early",
"role": "authority_target",
"range_s": [
1866.722720383,
1874.756970279
],
"samples": 426,
"substantial_demand_required": true,
"recorded_can_ratio_02s_median": 1.0233371460413845,
"published_median_abs_c0_c1": [
1.6002928018569946,
0.2796146124601364
],
"send_clamped_median_abs_c0_c1": [
1.6002928018569946,
0.2796146124601364
],
"phase_samples": {
"phase_turn_in": 15,
"phase_held": 122,
"phase_release": 402,
"phase_reversal": 0
}
},
{
"name": "centering_reversal_positive_to_negative",
"role": "reversal",
"range_s": [
1888.07372132,
1892.07372132
],
"samples": 396,
"substantial_demand_required": false,
"recorded_can_ratio_02s_median": null,
"published_median_abs_c0_c1": [
0.0,
0.0
],
"send_clamped_median_abs_c0_c1": [
0.0,
0.0
],
"phase_samples": {
"phase_turn_in": 283,
"phase_held": 48,
"phase_release": 104,
"phase_reversal": 21
}
},
{
"name": "centering_reversal_negative_to_positive",
"role": "reversal",
"range_s": [
1960.874232409,
1964.874232409
],
"samples": 397,
"substantial_demand_required": false,
"recorded_can_ratio_02s_median": null,
"published_median_abs_c0_c1": [
0.0,
0.0
],
"send_clamped_median_abs_c0_c1": [
0.0,
0.0
],
"phase_samples": {
"phase_turn_in": 154,
"phase_held": 0,
"phase_release": 183,
"phase_reversal": 21
}
},
{
"name": "clean_release",
"role": "release",
"range_s": [
2453.714158177,
2456.964158177
],
"samples": 323,
"substantial_demand_required": false,
"recorded_can_ratio_02s_median": null,
"published_median_abs_c0_c1": [
0.0,
0.0
],
"send_clamped_median_abs_c0_c1": [
0.0,
0.0
],
"phase_samples": {
"phase_turn_in": 4,
"phase_held": 0,
"phase_release": 305,
"phase_reversal": 17
}
},
{
"name": "successful_smaller_positive",
"role": "sign_coverage_only",
"range_s": [
2590.722658577,
2600.918740146
],
"samples": 175,
"substantial_demand_required": true,
"recorded_can_ratio_02s_median": 1.0960646334373787,
"published_median_abs_c0_c1": [
0.42173025012016296,
0.1222948431968689
],
"send_clamped_median_abs_c0_c1": [
0.42173025012016296,
0.1222948431968689
],
"phase_samples": {
"phase_turn_in": 170,
"phase_held": 61,
"phase_release": 0,
"phase_reversal": 0
}
},
{
"name": "large_under_response",
"role": "under_response_challenge",
"range_s": [
2604.2254721,
2611.364366768
],
"samples": 128,
"substantial_demand_required": true,
"recorded_can_ratio_02s_median": 0.7322859508492778,
"published_median_abs_c0_c1": [
2.4204851388931274,
0.42145511507987976
],
"send_clamped_median_abs_c0_c1": [
2.4204851388931274,
0.42145511507987976
],
"phase_samples": {
"phase_turn_in": 68,
"phase_held": 96,
"phase_release": 56,
"phase_reversal": 0
}
},
{
"name": "successful_large_181deg",
"role": "authority_target",
"range_s": [
2744.478264791,
2750.573209708
],
"samples": 207,
"substantial_demand_required": true,
"recorded_can_ratio_02s_median": 1.0087938914780248,
"published_median_abs_c0_c1": [
2.1044259071350098,
0.3815947473049164
],
"send_clamped_median_abs_c0_c1": [
2.1044259071350098,
0.3815947473049164
],
"phase_samples": {
"phase_turn_in": 137,
"phase_held": 94,
"phase_release": 64,
"phase_reversal": 0
}
},
{
"name": "large_over_response_290deg",
"role": "over_response_challenge_not_target",
"range_s": [
2760.493612962,
2764.574374172
],
"samples": 181,
"substantial_demand_required": true,
"recorded_can_ratio_02s_median": 1.2515789463064766,
"published_median_abs_c0_c1": [
4.737145900726318,
0.5235000252723694
],
"send_clamped_median_abs_c0_c1": [
4.737145900726318,
0.5
],
"phase_samples": {
"phase_turn_in": 139,
"phase_held": 90,
"phase_release": 41,
"phase_reversal": 0
}
}
],
"selection": "Authority targets require automatic turn windows with >=1 second strict torque eligibility, eligible |wheel|>=150 degrees, and whole-window CAN response ratio median 0.90..1.10 at fixed 0.2 s. No positive-request large turn qualifies.",
"non_targets": "Positive smaller turn supplies sign coverage only. Under/over response and release/reversal windows are regression challenges, not authority targets.",
"context": "At least 10 s pre-roll or available route start, extended to include the preceding feedback reset/sign reversal. Overlapping intervals are merged. First episode begins at the partial route boundary with unobserved earlier history.",
"phase_policy": "Held means request curvature range over +/-0.25 s times speed squared <0.15 m/s2 at demand>=0.5. Turn-in/release compare current absolute curvature with the historical held request at measurement_time-delay, scaled by max(7,speed), using +/-0.0005 rad. These masks can overlap held; reversal means opposing delayed/current signs.",
"wire_policy": "Published coefficients preserve Float32 values. Send-clamped copy caps C0 to +/-5.11 and C1 to +/-0.5 before packing. Actual decoded wire is normalized to controller sign, nearest within 15 ms; wire_time/fresh/mode expose timing approximation.",
"model_schema": "models[model_index] contains position.x, position.y, orientation.z; Float32 conversion preserves the original model payload precision.",
"v5_reference": "Frozen full sequential replay from command_replay.npz, whose source hash and limitations are recorded in command_replay.json.",
"frozen_v5_revision": "09acf8ec2f327769f00ee53563ad2dd9225e37a7",
"preroll_validation": "Compact reset replay exactly matches full sequential frozen-v5 C0/C1, gates and bias on all 2233 evidence samples."
}
@@ -0,0 +1,156 @@
{
"description": "Anonymous recorded-input turn-exit regression fixture; command construction only, not simulated vehicle response.",
"baseline_revision": "dfcfddb91ce2409511f5b2dbce25d06d5056b3d6",
"baseline_hypothesis": "model-pose-c0-c1-feedback-v7",
"baseline_source_hashes": {
"controller_sha256": "4951a6352d89fcd66277bbfe682bd22e935a31b5a4db33e617ad21189b6705fd",
"allocator_sha256": "383538fc7cdae3bc28dffb71fe12ac5f3f9866ffbe6adfb7457f3593e9fc903a"
},
"fixture_sha256": "87a030c309061b7dc218715d05440c2077e465a8138079b46e8e8cee94201e54",
"source_fixture_sha256": "d476110b83dc628ffbd094220e464d6d3114b709bda2977813c3217964d41086",
"response_delay": 0.20000000298023224,
"publication_latency_estimate_s": 0.0015483515003040793,
"samples": 15273,
"model_count": 3078,
"evidence_samples": 4879,
"context_policy": "At least twenty seconds prior context, extended before the last observed reset. Overlapping intervals are merged.",
"provenance": "Selected from a recorded drive running the pinned baseline; request, model, driver and PSCM observations stay fixed during replay.",
"baseline_policy": "Stored commands, validity and bias exactly match the complete baseline replay on evidence samples. Context outside evidence initializes state and is not an exact-output target.",
"compact_full_baseline_evidence_parity": {
"commands": {
"exact": true,
"max_difference": 0.0
},
"valid": {
"exact": true,
"max_difference": 0.0
},
"heading_bias": {
"exact": true,
"max_difference": 0.0
}
},
"measurement_policy": "Controller computation time is estimated from publication time using the recorded median latency; exact vehicle motion under changed commands is unknown.",
"clean_policy": "Every sample from request time minus 0.5 s through plus 0.65 s is active, valid, fresh, unpressed and within 1 Nm raw driver torque. Demand is absolute desired curvature times current speed squared; substantial means at least 0.5 m/s2.",
"driver_policy": "All replay inputs retain driver interference; only comparison metrics use the clean mask. History-reset failures intentionally retain nearby driver context.",
"coordinates": "Elapsed seconds shifted to the first fixture control cycle; model x/y/heading are vehicle-relative, not global position.",
"retained_fields": [
"t",
"episode",
"model_index",
"models",
"desired_curvature",
"yaw_rate",
"speed",
"measurement_time",
"model_time",
"reference_time",
"active",
"valid",
"pressed",
"steering_torque",
"pscm_timestamp",
"pscm_valid",
"pscm_lateral_state",
"pscm_limit",
"pscm_capability",
"pscm_denied",
"clean_rawtorque",
"demand",
"window_masks",
"evidence",
"baseline_commands",
"baseline_valid",
"baseline_heading_base",
"baseline_heading_target",
"baseline_heading_bias",
"baseline_feedback_yaw_error",
"baseline_feedback_reference_curvature",
"baseline_status",
"baseline_offset_target"
],
"omitted_data": "No route/device identifiers, VIN, GPS, private paths, raw wheel angle, wheel rate, EPS torque, or absolute clock origins.",
"baseline_status_meaning": "feedback_status from the pinned baseline",
"windows": [
{
"name": "good_curve_a",
"role": "comparison",
"range_s": [
20.0002130975003,
25.0002130975003
],
"samples": 496,
"clean_substantial_samples": 259
},
{
"name": "first_reversal",
"role": "reversal",
"range_s": [
83.0002130975003,
92.7002130975003
],
"samples": 964,
"clean_substantial_samples": 167
},
{
"name": "good_curve_b",
"role": "comparison",
"range_s": [
121.0002130975003,
128.0002130975003
],
"samples": 695,
"clean_substantial_samples": 308
},
{
"name": "second_reversal",
"role": "reversal",
"range_s": [
133.5002130975003,
138.9002130975003
],
"samples": 537,
"clean_substantial_samples": 191
},
{
"name": "large_turn_driver_context_a",
"role": "driver_context",
"range_s": [
150.0002130975003,
157.0002130975003
],
"samples": 695,
"clean_substantial_samples": 0
},
{
"name": "over_growth",
"role": "over_response",
"range_s": [
182.0002130975003,
191.0002130975003
],
"samples": 897,
"clean_substantial_samples": 66
},
{
"name": "large_turn_driver_context_b",
"role": "driver_context",
"range_s": [
199.0002130975003,
205.0002130975003
],
"samples": 595,
"clean_substantial_samples": 281
},
{
"name": "zero_bias_release",
"role": "under_response",
"range_s": [
202.0002130975003,
205.0002130975003
],
"samples": 297,
"clean_substantial_samples": 279
}
]
}
@@ -0,0 +1,64 @@
import ast
import io
import json
import logging
from pathlib import Path
from types import SimpleNamespace
import unittest
from openpilot.common.logging_extra import SwagFormatter, SwagLogger
from openpilot.selfdrive.controls.lib.ford_model_action import FordModelActionController
from openpilot.selfdrive.controls.lib.ford_path import FordPathController, FordPscmObserverPathController
from openpilot.selfdrive.controls.tests.test_ford_model_action import circle
class TestFordControlsLogging(unittest.TestCase):
def emit_controls_event(self, event, controls):
# Execute the actual controlsd call with the real logger and formatter,
# without launching hardware-dependent Controls or opening logging IPC.
source_path = Path(__file__).resolve().parents[1] / 'controlsd.py'
source = ast.parse(source_path.read_text())
calls = [node for node in ast.walk(source) if isinstance(node, ast.Call)
and isinstance(node.func, ast.Attribute) and isinstance(node.func.value, ast.Name)
and node.func.value.id == 'cloudlog' and node.args
and isinstance(node.args[0], ast.Constant) and node.args[0].value == event]
self.assertEqual(len(calls), 1)
logger = SwagLogger()
logger.setLevel(logging.INFO) # disabled INFO logging would hide this crash
stream = io.StringIO()
handler = logging.StreamHandler(stream)
handler.setFormatter(SwagFormatter(logger))
logger.addHandler(handler)
try:
expression = ast.Expression(body=calls[0])
eval(compile(expression, str(source_path), 'eval'), {'cloudlog': logger, 'self': controls, 'reference_service': 'modelV2'})
record = json.loads(stream.getvalue())
finally:
handler.close()
self.assertEqual(record['level'], 'INFO')
self.assertEqual(record['msg']['event'], event)
return record['msg']
def test_startup_logs_selected_controller_without_crashing(self):
for controller in (FordPathController(), FordPscmObserverPathController(), FordModelActionController()):
with self.subTest(controller=type(controller).__name__):
record = self.emit_controls_event('Ford path controller selected', SimpleNamespace(ford_path_controller=controller))
self.assertEqual(record['controller'], type(controller).__name__)
def test_candidate_diagnostics_identify_the_experiment_and_do_not_claim_calibration(self):
controller = FordModelActionController()
for active, valid in ((False, True), (True, True), (True, False)):
controller.update(circle(.01), .005, yaw_rate=.05, speed=20., now=1.,
measurement_time=1., model_time=1., reference_time=1., active=active, valid=valid)
controls = SimpleNamespace(ford_path_controller=controller, desired_curvature=.005, curvature=.0025,
sm=SimpleNamespace(logMonoTime={'modelV2': 123456789, 'carState': 123450000}))
record = self.emit_controls_event('Ford C2-free path tracking', controls)
self.assertEqual(record['hypothesis'], 'model-pose-terminal-c1-v1')
self.assertIs(record['calibration_approved'], False)
self.assertEqual(record['command'][2:], [0., 0.])
self.assertEqual(record['status'], controller.diagnostics['status'])
if active and valid:
self.assertEqual(record['pose_source'], 'model')
self.assertEqual(record['preview_time_s'], 1.)
self.assertEqual(record['minimum_station_m'], 7.)
self.assertEqual(record['c1_release'], 'terminal_spatial_curvature')
@@ -0,0 +1,194 @@
import math
from types import SimpleNamespace
import numpy as np
import pytest
from opendbc.can import CANPacker, CANParser
from opendbc.car.ford.fordcan import CanBus, create_lat_ctl2_msg
from openpilot.cereal import custom
from openpilot.selfdrive.controls.lib.ford_path import FordPath
from openpilot.selfdrive.controls.lib.ford_model_action import ModelActionController, encode_model_action
def make_model(x, y, heading):
times = np.linspace(0., 3., len(x))
return SimpleNamespace(position=SimpleNamespace(t=times, x=x, y=y), orientation=SimpleNamespace(t=times, z=heading))
def circle(curvature):
s = np.linspace(0., 60., 601)
return make_model(np.sin(curvature*s)/curvature, (1-np.cos(curvature*s))/curvature, curvature*s)
def straight(offset=0., heading=0.):
x = np.linspace(0., 60., 121)
return make_model(x*np.cos(heading), offset+x*np.sin(heading), np.full_like(x, heading))
def test_model_heading_is_used_even_when_scalar_action_differs():
model = circle(.02)
for desired in (0., -.004, .004):
target = encode_model_action(model, desired, 20.)
assert target.path_angle == pytest.approx(.4)
assert target.path_offset == pytest.approx((1-math.cos(.4))/.02)
def test_straight_centering_and_matched_model_circles():
for speed in (2., 7., 20., 35.):
assert encode_model_action(straight(.4), 0., speed) == FordPath(True, .4, 0., 0., 0.)
for sign in (-1, 1):
target = encode_model_action(circle(sign*.01), sign*.01, 20.)
assert target.path_offset == pytest.approx(sign*(1-math.cos(.2))/.01)
assert target.path_angle == pytest.approx(sign*.2)
def test_two_actuator_positions_are_sufficient_for_every_next_output():
controller = ModelActionController()
assert not hasattr(controller, '__dict__')
for i in range(300):
copied = ModelActionController()
copied.c0, copied.c1 = controller.c0, controller.c1
model = straight(.2*math.sin(i*.1))
kwargs = {'speed': 20., 'dt': .01}
desired = .005*math.cos(i*.03)
assert controller.update(model, desired, **kwargs) == copied.update(model, desired, **kwargs)
def test_held_turn_releases_using_new_model_geometry_without_retained_bias():
for sign in (-1., 1.):
controller = ModelActionController()
for _ in range(400):
out = controller.update(circle(sign*.01), sign*.01, speed=20., dt=.01)
assert out.path_angle == pytest.approx(sign*.2)
previous = np.array([controller.c0, controller.c1])
for _ in range(100):
out = controller.update(straight(), sign*.01, speed=20., dt=.01)
expected = previous+np.clip(-previous, [-.04, -.005], [.04, .005])
values = np.array([controller.c0, controller.c1])
np.testing.assert_allclose(values, expected, atol=1e-10)
previous = values
assert out == FordPath(True, 0., 0., 0., 0.)
def test_current_model_replacement_leaves_only_independent_actuator_slew():
controller = ModelActionController()
for _ in range(150):
controller.update(straight(1.-20*math.sin(.4), .4), .04, speed=20., dt=.01)
for _ in range(25):
out = controller.update(straight(), 0., speed=20., dt=.01)
assert out.path_offset == pytest.approx(0.)
assert out.path_angle > 0. # C1 cannot hold C0 during its longer release.
for _ in range(75):
out = controller.update(straight(), 0., speed=20., dt=.01)
assert out == FordPath(True, 0., 0., 0., 0.)
@pytest.mark.parametrize('overrides', [{'active': False}, {'valid': False}, {'dt': .2}, {'speed': math.nan}])
def test_invalid_or_inactive_input_clears_state_before_reengagement(overrides):
controller = ModelActionController()
for _ in range(100):
controller.update(straight(.5), .01, speed=20., dt=.01)
kwargs = {'speed': 20., 'dt': .01, 'active': True, 'valid': True}
kwargs.update(overrides)
assert controller.update(straight(), 0., **kwargs) == FordPath()
assert (controller.c0, controller.c1) == (0., 0.)
assert controller.update(straight(), 0., speed=20., dt=.01) == FordPath(True, 0., 0., 0., 0.)
def test_malformed_geometry_and_nonfinite_action_never_create_an_active_command():
for model, desired in ((None, 0.), (straight(), math.nan), (straight(), math.inf)):
assert not encode_model_action(model, desired, 20.).valid
def test_selected_core_reversal_through_float32_and_wire_keeps_sign_and_zero_c2():
controller = ModelActionController()
packer = CANPacker('ford_lincoln_base_pt')
parser = CANParser('ford_lincoln_base_pt', [('LateralMotionControl2', 100)], 0)
bus = CanBus(fingerprint={0: {}})
previous = np.zeros(2)
for i in range(600):
sign = 1. if i < 300 else -1.
out = controller.update(straight(sign*8., sign*.8), sign*.1, speed=30., dt=.01)
fields = np.array([out.path_offset, out.path_angle])
assert (abs(fields) <= [5.1100001, .5000001]).all()
assert (abs(fields-previous) <= [.0500001, .0055001]).all()
previous = fields
message = custom.CarControlSP.new_message()
message.fordLateralPath.pathOffset = out.path_offset
message.fordLateralPath.pathAngle = out.path_angle
packet = create_lat_ctl2_msg(packer, bus, 2, -message.fordLateralPath.pathOffset,
-message.fordLateralPath.pathAngle, out.curvature, out.curvature_rate, i % 16)
parser.update([i*10_000_000, [packet]])
decoded = parser.vl['LateralMotionControl2']
assert decoded['LatCtlPathOffst_L_Actl'] == pytest.approx(-out.path_offset)
assert decoded['LatCtlPath_An_Actl'] == pytest.approx(-out.path_angle)
assert decoded['LatCtlCurv_No_Actl'] == decoded['LatCtlCrv_NoRate2_Actl'] == 0.
def test_short_path_holds_available_endpoint_without_extrapolation():
model = make_model([0., 1.], [0., .1], [0., 0.])
assert encode_model_action(model, .01, 20.) == FordPath(True, .1, 0., 0., 0.)
def test_overflowing_arc_resets_instead_of_publishing_invalid_geometry():
model = make_model([0., 1e308, -1e308], [0., 0., 0.], [0., 0., 0.])
controller = ModelActionController()
controller.update(straight(.4), .01, speed=20., dt=.01)
assert controller.update(model, .01, speed=20., dt=.01) == FordPath()
assert (controller.c0, controller.c1) == (0., 0.)
@pytest.mark.parametrize('value', [None, 'bad', 10**400])
@pytest.mark.parametrize('field', ['dt', 'speed', 'desired_curvature'])
def test_malformed_numeric_input_resets_without_throwing(field, value):
controller = ModelActionController()
kwargs = {'speed': 20., 'dt': .01, 'desired_curvature': .01}
controller.update(straight(.4), **kwargs)
kwargs[field] = value
assert controller.update(straight(.4), **kwargs) == FordPath()
assert (controller.c0, controller.c1) == (0., 0.)
@pytest.mark.parametrize('model', [
make_model([], [], []), make_model([0.], [0.], [0.]),
make_model([0., 10.], [0.], [0., 0.]), make_model([0., 10.], [0., 0.], [0.]),
make_model([0., 0.], [0., 0.], [0., 0.]),
make_model([0., 10.], [0., math.nan], [0., 0.]), make_model([0., math.inf], [0., 0.], [0., 0.]),
make_model([0., 10.], [0., 0.], [0., math.inf]),
make_model([0., 10**400], [0., 0.], [0., 0.]),
make_model([0., 10.], [0., 0.], [1e308, -1e308]),
])
def test_malformed_model_arrays_cannot_reuse_a_previous_valid_command(model):
controller = ModelActionController()
controller.update(straight(.4), .01, speed=20., dt=.01)
assert controller.update(model, .01, speed=20., dt=.01) == FordPath()
assert (controller.c0, controller.c1) == (0., 0.)
@pytest.mark.parametrize('field,value,valid', [
('speed', .2999, False), ('speed', .3, True), ('speed', 55., True), ('speed', 55.0001, False),
('desired_curvature', -1., True), ('desired_curvature', 1., True), ('desired_curvature', -1.0001, False),
('dt', .001999, False), ('dt', .002, True), ('dt', .1, True), ('dt', .100001, False), ('dt', 0., False),
])
def test_domain_and_elapsed_time_boundaries(field, value, valid):
kwargs = {'speed': 20., 'desired_curvature': .01, 'dt': .01}
kwargs[field] = value
assert ModelActionController().update(straight(.4), **kwargs).valid == valid
def test_arc_station_floor_not_forward_x_determines_offset():
x = np.array([0., 6., 12.])
y = .4+x*.75
target = encode_model_action(make_model(x, y, [.4, .4, .4]), -.01, 20.)
# At one second arc station is 5 m; the 7 m minimum gives x=5.6, y=4.6.
assert target.path_offset == pytest.approx(4.6)
assert target.path_angle == pytest.approx(.4)
def test_duplicate_stations_keep_valid_geometry_and_first_cycle_slew():
model = make_model([0., 0., 10.], [.4, .4, .4], [0., 0., 0.])
assert encode_model_action(model, 0., 20.) == FordPath(True, .4, 0., 0., 0.)
out = ModelActionController().update(model, .01, speed=20., dt=.002)
assert out.path_offset == pytest.approx(.01)
assert out.path_angle == 0.
@@ -0,0 +1,269 @@
"""Exercise the candidate through existing selection, publication and CAN code.
Tests enable the candidate through controlsd's real startup selection.
No hardware, IPC or CAN transmission is involved.
"""
import ast
from collections import defaultdict
import json
import math
from pathlib import Path
from types import SimpleNamespace
import pytest
from opendbc.can import CANParser
from opendbc.car import Bus, structs
from opendbc.car.ford.carcontroller import CarController
from opendbc.car.ford.values import FordFlags
from openpilot.cereal import custom
from openpilot.selfdrive.car.helpers import convert_carControlSP
from openpilot.selfdrive.controls.lib.drive_helpers import clip_curvature
from openpilot.selfdrive.controls.lib.ford_model_action import FordModelActionController
from openpilot.selfdrive.controls.lib.ford_path import FordPath
from openpilot.selfdrive.controls.tests.test_ford_model_action import circle, straight
from openpilot.selfdrive.controls.tests.test_ford_model_action_selection import startup
def update(controller, now=1., **overrides):
kwargs = {'model': straight(.4, .1), 'desired_curvature': .01, 'speed': 20., 'yaw_rate': 0., 'now': now,
'model_time': now, 'measurement_time': now, 'reference_time': now, 'active': True}
kwargs.update(overrides)
return controller.update(**kwargs)
@pytest.mark.parametrize('field', ['model_time', 'measurement_time', 'reference_time'])
@pytest.mark.parametrize('age', [.151, -.006])
def test_stale_or_future_service_clears_commands_and_reengages_from_zero(field, age):
controller = FordModelActionController()
update(controller)
assert update(controller, 1.01, **{field: 1.01-age}) == FordPath()
assert controller.diagnostics['status'] == 'stale_input'
assert update(controller, 1.02).path_offset == pytest.approx(.04)
@pytest.mark.parametrize('change,reason', [
({'now': 1.}, 'timing_reset'),
({'now': .99}, 'timing_reset'),
({'now': 1.001}, 'timing_reset'),
({'now': 1.101}, 'timing_reset'),
({'model_time': .999}, 'timing_reset'),
({'measurement_time': .999}, 'timing_reset'),
({'active': False}, 'inactive'),
({'valid': False}, 'invalid_service'),
({'model': None}, 'invalid_path'),
({'yaw_rate': math.nan}, 'nonfinite'),
({'yaw_rate': 3.01}, 'input_range'),
({'speed': 55.01}, 'input_range'),
({'desired_curvature': 1.01}, 'input_range'),
])
def test_invalid_cycle_never_keeps_a_previous_active_request(change, reason):
controller = FordModelActionController()
update(controller)
now = change.get('now', 1.01)
assert update(controller, **dict(change, now=now)) == FordPath()
assert controller.diagnostics['status'] == reason
assert (controller.core.c0, controller.core.c1) == (0., 0.)
assert update(controller, now+1.).path_angle == pytest.approx(.005)
@pytest.mark.parametrize('field', ['now', 'measurement_time', 'model_time', 'reference_time', 'speed', 'yaw_rate', 'desired_curvature'])
@pytest.mark.parametrize('value', [math.nan, math.inf, -math.inf, None])
def test_nonfinite_input_never_raises_or_leaks_into_diagnostics(field, value):
controller = FordModelActionController()
update(controller)
assert update(controller, **{field: value}) == FordPath()
assert controller.diagnostics['status'] == 'nonfinite'
json.dumps(controller.diagnostics, allow_nan=False)
def test_repeated_measurements_do_not_freeze_slew_or_cache_invalid_model_geometry():
controller = FordModelActionController()
for i in range(10):
result = update(controller, 1.+i*.01, measurement_time=1., model_time=1., reference_time=1.)
assert result.path_offset == pytest.approx(.4)
assert result.path_angle == pytest.approx(.05)
broken = straight(.4)
broken.position.y[5] = math.nan
assert update(controller, 1.1, model=broken, model_time=1., measurement_time=1.) == FordPath()
assert controller.diagnostics['status'] == 'invalid_path'
def test_yaw_offset_does_not_change_the_base():
controllers = [FordModelActionController() for _ in range(3)]
variants = [{}, {'yaw_rate': .0072}, {'yaw_rate': -.0072}]
for i in range(100):
outputs = [update(c, 1.+i*.01, **kwargs) for c, kwargs in zip(controllers, variants, strict=True)]
assert all(out == outputs[0] for out in outputs)
assert outputs[0].path_angle == pytest.approx(.1)
def test_reference_source_can_change_to_an_older_but_fresh_publication():
controller = FordModelActionController()
update(controller, reference_time=.99)
assert update(controller, 1.01, reference_time=.98).valid
def test_current_model_geometry_controls_both_fields_independently_of_scalar_action():
for sign in (-1., 1.):
controller = FordModelActionController()
for i in range(100):
before = update(controller, 1.+i*.01, model=circle(sign*.01), desired_curvature=sign*.005)
for i in range(100):
after = update(controller, 2.+i*.01, model=circle(sign*.02), desired_curvature=sign*.004)
assert abs(after.path_offset) > abs(before.path_offset)
assert abs(after.path_angle) > abs(before.path_angle)
for i in range(100):
released = update(controller, 3.+i*.01, model=circle(sign*.02), desired_curvature=0.)
assert released == after # A scalar reference change does not fabricate a different model pose.
def _method(filename, class_name, method):
tree = ast.parse(filename.read_text())
cls = next(node for node in tree.body if isinstance(node, ast.ClassDef) and node.name == class_name)
return next(node for node in cls.body if isinstance(node, ast.FunctionDef) and node.name == method)
@pytest.fixture
def pipeline():
root = Path(__file__).resolve().parents[3]
controls_file = root/'selfdrive/controls/controlsd.py'
body = _method(controls_file, 'Controls', 'state_control').body
# Execute the actual source choice, upstream limiter and Ford integration.
selection = next(n for n in body if isinstance(n, ast.If) and ast.unparse(n.test) == "self.sm.valid['lateralManeuverPlan']")
limiter = next(n for n in body if isinstance(n, ast.Assign) and isinstance(n.value, ast.Call) and
isinstance(n.value.func, ast.Name) and n.value.func.id == 'clip_curvature')
branch = next(n for n in body if isinstance(n, ast.If) and ast.unparse(n.test) == "self.CP.brand == 'ford'")
call = compile(ast.Module(body=[selection, limiter, branch], type_ignores=[]), str(controls_file), 'exec')
publication_file = root/'sunnypilot/selfdrive/controls/controlsd_ext.py'
body = _method(publication_file, 'ControlsExt', 'state_control_ext').body
publish = [n for n in body if (isinstance(n, ast.Assign) and ast.unparse(n.targets[0]) == 'ford_path') or
(isinstance(n, ast.If) and ast.unparse(n.test) == 'ford_path is not None')]
assert len(publish) == 2
publication = compile(ast.Module(body=publish, type_ignores=[]), str(publication_file), 'exec')
return call, publication
class Subscriptions:
frame = 1
def __init__(self, maneuver):
self.valid = {'lateralManeuverPlan': maneuver, 'modelV2': True}
self.logMonoTime = {'carState': 995_000_000, 'modelV2': 980_000_000, 'lateralManeuverPlan': 990_000_000,
'deviceMotion': 980_000_000, 'extrinsicsCalibration': 750_000_000}
self.failed = set()
self.messages = {'carStateSP': custom.CarStateSP.new_message(), 'lateralManeuverPlan': SimpleNamespace(desiredCurvature=-.1),
'deviceMotion': SimpleNamespace(angularVelocityDevice=SimpleNamespace(valid=True), sensorsOK=True, inputsOK=True,
timestamp=970_000_000)}
def __getitem__(self, service):
return self.messages[service]
def all_checks(self, services):
return not self.failed.intersection(services) and all(self.valid.get(s, True) for s in services)
@pytest.mark.parametrize('maneuver', [False, True])
@pytest.mark.parametrize('host_yaw', [.0072, .3])
@pytest.mark.parametrize('initial_curvature', [0., .005])
def test_actual_controlsd_selection_limiting_publication_and_downstream_can(pipeline, maneuver, host_yaw, initial_curvature):
call, publication = pipeline
sm = Subscriptions(maneuver)
controls = startup()
controller = controls.ford_path_controller
initial_curvature *= -1 if maneuver else 1
controls.sm, controls.desired_curvature, controls.curvature = sm, initial_curvature, 0.
if initial_curvature:
# Start at the old target so startup slew cannot hide prediction on the real call path.
controller.core.c0, controller.core.c1 = .4, .1
model = straight(.4, .1)
model.action = SimpleNamespace(desiredCurvature=.1)
cc = structs.CarControl(latActive=True)
cs = SimpleNamespace(vEgo=20., yawRate=-host_yaw, canValid=True, steeringPressed=False, steeringTorque=0.)
environment = {'FordModelActionController': FordModelActionController, 'self': controls, 'CS': cs, 'CC': cc,
'actuators': cc.actuators, 'model_v2': model, 'lp': SimpleNamespace(roll=0.),
'clip_curvature': clip_curvature, 'time': SimpleNamespace(monotonic=lambda: 1.)}
exec(call, environment)
expected_curvature = initial_curvature+(-1 if maneuver else 1)*.000125
assert controls.desired_curvature == pytest.approx(expected_curvature)
if maneuver:
assert controls.ford_path == FordPath() and not cc.latActive
assert controller.diagnostics['status'] == 'unsupported_reference'
else:
assert controls.ford_path.path_offset == pytest.approx(.44 if initial_curvature else .04)
assert controls.ford_path.path_angle == pytest.approx(.1 if initial_curvature else .005)
assert controller.diagnostics['yaw_rate'] == host_yaw
assert controller.diagnostics['pose_source'] == 'model'
assert cc.latActive and cc.actuators.curvature == 0.
assert controller.diagnostics['reference_age'] == pytest.approx(.02)
cp = structs.CarParams(flags=int(FordFlags.CANFD), carFingerprint='FORD_F_150_LIGHTNING_MK1')
downstream = CarController({Bus.pt: 'ford_lincoln_base_pt'}, cp, controls.CP_SP)
vehicle = SimpleNamespace(out=structs.CarState(vEgo=20., vEgoRaw=20.), acc_tja_status_stock_values=defaultdict(int),
lkas_status_stock_values=defaultdict(int), buttons_stock_values=defaultdict(int))
parser = CANParser('ford_lincoln_base_pt', [('LateralMotionControl2', 20)], downstream.CAN.main)
for i, fail in enumerate((False, True)):
if fail:
sm.failed.add('modelV2')
exec(call, environment)
assert not cc.latActive and controls.ford_path == FordPath()
msg = custom.CarControlSP.new_message()
exec(publication, {'self': controls, 'CC_SP': msg})
for tick in range(5 if fail else 1):
now_nanos = (i + tick + 1) * 10_000_000
_, packets = downstream.update(cc.as_reader(), convert_carControlSP(msg.as_reader()), vehicle, now_nanos)
lateral = [p for p in packets if p[0] == 0x3d6]
assert len(lateral) == int(not fail or tick == 4)
parser.update([now_nanos, packets])
wire = parser.vl['LateralMotionControl2']
assert wire['LatCtlPathOffst_L_Actl'] == pytest.approx(-controls.ford_path.path_offset)
assert wire['LatCtlPath_An_Actl'] == pytest.approx(-controls.ford_path.path_angle)
assert wire['LatCtlCurv_No_Actl'] == wire['LatCtlCrv_NoRate2_Actl'] == 0.
assert wire['LatCtl_D2_Rq'] == (0 if fail or maneuver else 2)
@pytest.mark.parametrize('maneuver', [False, True])
@pytest.mark.parametrize('failed', ['carState', 'modelV2', 'vehicleParameters', 'lateralManeuverPlan'])
def test_actual_controlsd_service_gates(pipeline, maneuver, failed):
sm = Subscriptions(maneuver)
sm.failed.add(failed)
controls = startup()
controls.sm, controls.desired_curvature, controls.curvature = sm, 0., 0.
cc = structs.CarControl(latActive=True)
cs = SimpleNamespace(vEgo=20., yawRate=0., canValid=True, steeringPressed=False, steeringTorque=0.)
model = straight()
model.action = SimpleNamespace(desiredCurvature=.1)
exec(pipeline[0], {'FordModelActionController': FordModelActionController, 'self': controls, 'CS': cs, 'CC': cc,
'actuators': cc.actuators, 'model_v2': model, 'lp': SimpleNamespace(roll=0.),
'clip_curvature': clip_curvature, 'time': SimpleNamespace(monotonic=lambda: 1.)})
assert controls.ford_path.valid == cc.latActive == (failed == 'lateralManeuverPlan' and not maneuver)
@pytest.mark.parametrize('service', ['deviceMotion', 'extrinsicsCalibration'])
def test_optional_pose_services_do_not_modify_model_point_requests(pipeline, service):
sm = Subscriptions(False)
sm.failed.add(service)
controls = startup()
controls.sm, controls.desired_curvature, controls.curvature = sm, .01, 0.
controls.calibrated_pose = None
controls.ford_path_controller.core.c0, controls.ford_path_controller.core.c1 = .4, .1
cc = structs.CarControl(latActive=True)
cs = SimpleNamespace(vEgo=20., yawRate=-.3, canValid=True, steeringPressed=False, steeringTorque=0.)
model = straight(.4, .1)
model.action = SimpleNamespace(desiredCurvature=.01)
exec(pipeline[0], {'FordModelActionController': FordModelActionController, 'self': controls, 'CS': cs, 'CC': cc,
'actuators': cc.actuators, 'model_v2': model, 'lp': SimpleNamespace(roll=0.),
'clip_curvature': clip_curvature, 'time': SimpleNamespace(monotonic=lambda: 1.)})
assert cc.latActive and controls.ford_path.valid
assert controls.ford_path.path_offset == pytest.approx(.44)
assert controls.ford_path.path_angle == pytest.approx(.1)
assert controls.ford_path_controller.diagnostics['pose_source'] == 'model'
def test_maneuver_reference_clears_existing_model_point_requests():
controller = FordModelActionController()
update(controller)
assert update(controller, 1.01, reference_source='lateralManeuverPlan') == FordPath()
assert controller.diagnostics['status'] == 'unsupported_reference'
assert (controller.core.c0, controller.core.c1) == (0., 0.)
assert update(controller, 1.02).path_angle == pytest.approx(.005)
@@ -0,0 +1,146 @@
"""Exercise 100Hz calculation and 20Hz transmission through the real CAN sender."""
from collections import defaultdict
from types import SimpleNamespace
import pytest
from opendbc.can import CANParser
from opendbc.car import Bus, structs
from opendbc.car.ford.carcontroller import CarController
from opendbc.car.ford.fordcan import calculate_lat_ctl2_checksum
from opendbc.car.ford.values import FordFlags, FordFlagsSP, FordSafetyFlags
from opendbc.safety.tests.libsafety import libsafety_py
from openpilot.selfdrive.controls.lib.ford_model_action import ModelActionController
from openpilot.selfdrive.controls.tests.test_ford_model_action import straight
def sender(canfd=True, selected=True):
cp = structs.CarParams(flags=int(FordFlags.CANFD) if canfd else 0, carFingerprint='FORD_F_150_LIGHTNING_MK1',
safetyConfigs=[structs.CarParams.SafetyConfig()])
cp_sp = structs.CarParamsSP(flags=int(FordFlagsSP.MODEL_ACTION) if selected else 0)
controller = CarController({Bus.pt: 'ford_lincoln_base_pt'}, cp, cp_sp)
cs = SimpleNamespace(out=structs.CarState(vEgo=20., vEgoRaw=20.), acc_tja_status_stock_values=defaultdict(int),
lkas_status_stock_values=defaultdict(int), buttons_stock_values=defaultdict(int))
return controller, cs
@pytest.mark.parametrize('canfd,selected,step', [(True, True, 5), (True, False, 1), (False, True, 5), (False, False, 5)])
def test_send_intervals_latest_sample_counter_and_checksum(canfd, selected, step):
controller, cs = sender(canfd, selected)
cc, sp = structs.CarControl(latActive=True), structs.CarControlSP()
sp.fordLateralPath.valid = True
name = 'LateralMotionControl2' if canfd else 'LateralMotionControl'
address = 0x3d6 if canfd else 0x3d3
parser = CANParser('ford_lincoln_base_pt', [(name, 0)], controller.CAN.main)
sent = []
for frame in range(1000):
sp.fordLateralPath.pathOffset = (frame % 101 - 50) * .01
sp.fordLateralPath.pathAngle = (frame % 101 - 50) * .0005
_, packets = controller.update(cc.as_reader(), sp, cs, frame * 10_000_000)
lateral = [p for p in packets if p[0] == address]
assert len(lateral) == int(frame % step == 0)
if not lateral:
continue
sent.append(frame)
parser.update([frame * 10_000_000, lateral])
wire = parser.vl[name]
assert wire['LatCtlPathOffst_L_Actl'] == pytest.approx(-sp.fordLateralPath.pathOffset)
assert wire['LatCtlPath_An_Actl'] == pytest.approx(-sp.fordLateralPath.pathAngle)
if canfd:
counter = (len(sent) - 1) % 16
assert wire['LatCtlPath_No_Cnt'] == counter
assert wire['LatCtlPath_No_Cs'] == calculate_lat_ctl2_checksum(2, counter, lateral[0][1])
assert sent == list(range(0, 1000, step))
@pytest.mark.parametrize('failure_frame', range(1, 6))
@pytest.mark.parametrize('disengage', [False, True])
def test_next_scheduled_frame_clears_invalid_or_inactive_path(failure_frame, disengage):
controller, cs = sender()
cc, sp = structs.CarControl(latActive=True), structs.CarControlSP()
sp.fordLateralPath.valid = True
sp.fordLateralPath.pathOffset, sp.fordLateralPath.pathAngle = .4, .1
parser = CANParser('ford_lincoln_base_pt', [('LateralMotionControl2', 0)], controller.CAN.main)
for frame in range(11):
if frame == failure_frame:
if disengage:
cc.latActive = False
else:
sp.fordLateralPath.valid = False
_, packets = controller.update(cc.as_reader(), sp, cs, frame * 10_000_000)
lateral = [p for p in packets if p[0] == 0x3d6]
assert len(lateral) == int(frame % 5 == 0)
if lateral and frame >= failure_frame:
parser.update([frame * 10_000_000, lateral])
wire = parser.vl['LateralMotionControl2']
assert wire['LatCtl_D2_Rq'] == (0 if disengage else 2)
assert all(wire[k] == 0. for k in ('LatCtlPathOffst_L_Actl', 'LatCtlPath_An_Actl', 'LatCtlCurv_No_Actl', 'LatCtlCrv_NoRate2_Actl'))
def test_core_slew_per_second_and_actual_panda_acceptance():
controller, cs = sender()
core = ModelActionController()
cc, sp = structs.CarControl(latActive=True), structs.CarControlSP()
parser = CANParser('ford_lincoln_base_pt', [('LateralMotionControl2', 0)], controller.CAN.main)
safety = libsafety_py.libsafety
assert safety.set_safety_hooks(structs.CarParams.SafetyModel.ford, FordSafetyFlags.CANFD) == 0
safety.init_tests()
safety.set_controls_allowed(True)
frames = []
for frame in range(100):
command = core.update(straight(10., 1.), .1, speed=20., dt=.01)
assert core.c0 == pytest.approx((frame + 1) * .04)
assert core.c1 == pytest.approx((frame + 1) * .005)
sp.fordLateralPath.valid = command.valid
sp.fordLateralPath.pathOffset, sp.fordLateralPath.pathAngle = command.path_offset, command.path_angle
_, packets = controller.update(cc.as_reader(), sp, cs, frame * 10_000_000)
for address, data, bus in packets:
if address != 0x3d6:
continue
frames.append(frame)
safety.set_timer(frame * 10_000)
assert safety.safety_tx_hook(libsafety_py.make_CANPacket(address, bus, data))
parser.update([frame * 10_000_000, [(address, data, bus)]])
wire = parser.vl['LateralMotionControl2']
assert wire['LatCtlPathOffst_L_Actl'] == pytest.approx(-command.path_offset)
assert wire['LatCtlPath_An_Actl'] == pytest.approx(-command.path_angle)
assert wire['LatCtlCurv_No_Actl'] == wire['LatCtlCrv_NoRate2_Actl'] == 0.
assert frames == list(range(0, 100, 5))
@pytest.mark.parametrize('sign', [-1., 1.])
@pytest.mark.parametrize('phase', range(5))
def test_saturated_turn_unwinds_on_first_update_and_next_scheduled_can_frame(sign, phase):
controller, cs = sender()
core = ModelActionController()
cc, sp = structs.CarControl(latActive=True), structs.CarControlSP()
parser = CANParser('ford_lincoln_base_pt', [('LateralMotionControl2', 0)], controller.CAN.main)
change_frame = 200 + phase
first_unwind = first_zero_c0 = first_zero_c1 = None
turn, released = straight(sign*10., sign), straight()
for frame in range(change_frame+135):
command = core.update(turn if frame < change_frame else released, sign*.1, speed=20., dt=.01)
if frame >= change_frame:
elapsed = (frame-change_frame+1)*.01
assert sign*core.c0 == pytest.approx(max(0., 5.11-4.*elapsed), abs=1e-10)
assert sign*core.c1 == pytest.approx(max(0., .5-.5*elapsed), abs=1e-10)
sp.fordLateralPath.valid = command.valid
sp.fordLateralPath.pathOffset, sp.fordLateralPath.pathAngle = command.path_offset, command.path_angle
_, packets = controller.update(cc.as_reader(), sp, cs, frame*10_000_000)
lateral = [p for p in packets if p[0] == 0x3d6]
if not lateral or frame < change_frame:
continue
parser.update([frame*10_000_000, lateral])
wire = parser.vl['LateralMotionControl2']
c0, c1 = -sign*wire['LatCtlPathOffst_L_Actl'], -sign*wire['LatCtlPath_An_Actl']
assert 0. <= c0 < 5.11 and 0. <= c1 < .5
assert wire['LatCtlCurv_No_Actl'] == wire['LatCtlCrv_NoRate2_Actl'] == 0.
if first_unwind is None:
first_unwind = frame
if c0 == 0. and first_zero_c0 is None:
first_zero_c0 = frame
if c1 == 0. and first_zero_c1 is None:
first_zero_c1 = frame
assert first_unwind == ((change_frame+4)//5)*5
assert first_zero_c0 == ((change_frame+127+4)//5)*5
assert first_zero_c1 == ((change_frame+99+4)//5)*5
@@ -0,0 +1,125 @@
"""Exercise real startup selection and Sunnylink writes without starting hardware."""
import ast
import base64
import itertools
from pathlib import Path
from types import SimpleNamespace
import pytest
from opendbc.car import structs
from opendbc.car.ford.values import FordFlags, FordFlagsSP
from openpilot.cereal import custom
from openpilot.common.params import Params, ParamKeyFlag, ParamKeyType
from openpilot.selfdrive.car.helpers import convert_to_capnp
from openpilot.selfdrive.controls.lib.ford_model_action import FordModelActionController, select_model_action_controller
from openpilot.selfdrive.controls.lib.ford_path import FordPath, FordPathController, FordPscmObserverPathController
from openpilot.sunnypilot.mads.helpers import set_car_specific_params
def car_params(**overrides):
return SimpleNamespace(**({'brand': 'ford', 'flags': FordFlags.CANFD, 'carFingerprint': 'FORD_F_150_LIGHTNING_MK1',
'carFw': []} | overrides))
def startup(cp=None, params=None, cp_sp=None):
filename = Path(__file__).resolve().parents[1]/'controlsd.py'
tree = ast.parse(filename.read_text())
cls = next(n for n in tree.body if isinstance(n, ast.ClassDef) and n.name == 'Controls')
body = next(n for n in cls.body if isinstance(n, ast.FunctionDef) and n.name == '__init__').body
start = next(i for i, n in enumerate(body) if isinstance(n, ast.Assign) and ast.unparse(n.targets[0]) == 'self.ford_pscm_observer')
end = next(i for i, n in enumerate(body) if isinstance(n, ast.Assign) and ast.unparse(n.targets[0]) == 'self.ford_path')
if params is None:
params = SimpleNamespace(get_bool=lambda key: key == 'FordModelActionController')
cp = cp or car_params()
# card snapshots the toggle into CarParamsSP before controlsd starts.
if cp_sp is None:
cp_sp = structs.CarParamsSP()
if cp.brand == 'ford':
set_car_specific_params(cp, cp_sp, params)
controls = SimpleNamespace(CP=cp, CP_SP=cp_sp, params=params, calibrated_pose=None,
pose_calibrator=SimpleNamespace(calib_valid=False))
environment = {'self': controls, 'FordFlags': FordFlags, 'FordFlagsSP': FordFlagsSP, 'FordPath': FordPath,
'FordPathController': FordPathController, 'FordPscmObserverPathController': FordPscmObserverPathController,
'FordModelActionController': FordModelActionController,
'select_model_action_controller': select_model_action_controller,
'cloudlog': SimpleNamespace(event=lambda *args, **kwargs: None)}
exec(compile(ast.Module(body=body[start:end+1], type_ignores=[]), str(filename), 'exec'), environment)
return controls
@pytest.mark.parametrize('candidate,observer', list(itertools.product((False, True), repeat=2)))
def test_actual_startup_priority(candidate, observer):
settings = {'FordModelActionController': candidate, 'FordPscmObserver': observer}
selected = startup(params=SimpleNamespace(get_bool=settings.__getitem__))
previous = FordPscmObserverPathController if observer else FordPathController
expected = FordModelActionController if candidate else previous
assert type(selected.ford_path_controller) is expected
assert selected.ford_model_action == candidate
assert bool(selected.CP_SP.flags & FordFlagsSP.MODEL_ACTION) == candidate
assert selected.ford_path == FordPath()
@pytest.mark.parametrize('selected', [False, True])
def test_controller_and_sender_share_card_snapshot_when_stored_toggle_changes(selected):
cp, cp_sp = car_params(), structs.CarParamsSP(flags=128)
set_car_specific_params(cp, cp_sp, SimpleNamespace(get_bool=lambda key: selected))
with custom.CarParamsSP.from_bytes(convert_to_capnp(cp_sp).to_bytes()) as snapshot:
controls = startup(cp, SimpleNamespace(get_bool=lambda key: not selected), snapshot)
assert controls.ford_model_action == selected
assert bool(controls.CP_SP.flags & FordFlagsSP.MODEL_ACTION) == selected
assert controls.CP_SP.flags & 128
set_car_specific_params(cp, cp_sp, SimpleNamespace(get_bool=lambda key: False))
assert cp_sp.flags == 128
@pytest.mark.parametrize('overrides', [{'brand': 'tesla'}, {'flags': 0}, {'carFingerprint': 'FORD_F_150_MK14'}])
@pytest.mark.parametrize('observer', [False, True])
def test_other_vehicles_keep_their_previous_selection(overrides, observer):
settings = {'FordModelActionController': False, 'FordPscmObserver': observer}
params = SimpleNamespace(get_bool=settings.__getitem__)
before = startup(car_params(**overrides), params)
settings['FordModelActionController'] = True
after = startup(car_params(**overrides), params)
assert type(after.ford_path_controller) is type(before.ford_path_controller)
assert not after.ford_model_action
@pytest.mark.parametrize('firmware', [[], [SimpleNamespace(ecu='eps', fwVersion=b'other')]])
def test_candidate_does_not_depend_on_eps_firmware_query(firmware):
assert isinstance(startup(car_params(carFw=firmware)).ford_path_controller, FordModelActionController)
@pytest.mark.parametrize('observer', [False, True])
def test_sunnylink_write_takes_effect_on_restart_and_restores_stored_selection(tmp_path, monkeypatch, observer):
from openpilot.sunnypilot.sunnylink import utils
params = Params(str(tmp_path))
monkeypatch.setattr(utils, 'Params', lambda: params)
assert params.get_default_value('FordModelActionController') is False
assert params.get_type('FordModelActionController') == ParamKeyType.BOOL
assert b'FordModelActionController' in params.all_keys(ParamKeyFlag.PERSISTENT)
assert b'FordModelActionController' in params.all_keys(ParamKeyFlag.BACKUP)
params.put_bool('FordPscmObserver', observer, block=True)
old = startup(params=params)
assert not isinstance(old.ford_path_controller, FordModelActionController)
utils.save_param_from_base64_encoded_string('FordModelActionController', base64.b64encode(b'true').decode())
enabled = startup(params=params)
assert isinstance(enabled.ford_path_controller, FordModelActionController)
assert not isinstance(old.ford_path_controller, FordModelActionController)
utils.save_param_from_base64_encoded_string('FordModelActionController', base64.b64encode(b'false').decode())
assert isinstance(enabled.ford_path_controller, FordModelActionController)
assert type(startup(params=params).ford_path_controller) is type(old.ford_path_controller)
assert params.get_bool('FordPscmObserver') == observer
def test_stored_retired_toggle_cannot_enable_the_candidate(tmp_path):
params = Params(str(tmp_path))
Path(params.get_param_path('FordVirtualAngleController')).write_text('1')
assert b'FordVirtualAngleController' not in params.all_keys()
assert params.get_bool('FordModelActionController') is False
assert type(startup(params=params).ford_path_controller) is FordPathController
params.put_bool('FordModelActionController', True, block=True)
params.clear_all(ParamKeyFlag.CLEAR_ON_MANAGER_START)
assert not Path(params.get_param_path('FordVirtualAngleController')).exists()
assert params.get_bool('FordModelActionController') is True
@@ -0,0 +1,29 @@
"""Raw Ford yaw gates input health but cannot change path demand."""
import math
import pytest
from openpilot.selfdrive.controls.lib.ford_model_action import ModelActionController
from openpilot.selfdrive.controls.lib.ford_path import FordPath
from openpilot.selfdrive.controls.tests.test_ford_model_action import straight
@pytest.mark.parametrize('sign', [-1., 1.])
@pytest.mark.parametrize('yaw', [-3., -.2, -.008, 0., .008, .2, 3.])
def test_valid_yaw_cannot_change_commands_during_entry_release_or_reversal(sign, yaw):
reference, measured = ModelActionController(), ModelActionController()
for i in range(400):
offset, desired = ((.4, .02), (.4, .001), (.12, -.0004078), (-.4, -.02))[i//100]
model = straight(sign*offset)
expected = reference.update(model, sign*desired, speed=10., dt=.01)
actual = measured.update(model, sign*desired, speed=10., dt=.01, yaw_rate=yaw)
assert actual == expected
assert actual.curvature == actual.curvature_rate == 0.
@pytest.mark.parametrize('yaw', [math.nan, math.inf, -math.inf, None, 'bad', 3.001, -3.001])
def test_invalid_yaw_still_resets_core(yaw):
controller = ModelActionController()
controller.update(straight(.4), .01, speed=10., dt=.01)
assert controller.update(straight(.4), .01, speed=10., dt=.01, yaw_rate=yaw) == FordPath()
assert controller.c0 == controller.c1 == 0.
@@ -0,0 +1,56 @@
"""Model geometry contract, independent of any PSCM response model."""
from types import SimpleNamespace
import numpy as np
import pytest
from openpilot.selfdrive.controls.lib.ford_model_action import encode_model_action
def model_points(t, x, y, heading):
return SimpleNamespace(position=SimpleNamespace(t=t, x=x, y=y), orientation=SimpleNamespace(t=t, z=heading))
def test_both_fields_sample_the_same_model_time_without_constant_speed_assumption():
# Accelerating plan: one second is 12 m along this straight inclined path.
s = np.array([0., 4., 12., 30.])
heading = .1
m = model_points([0., .5, 1., 2.], s*np.cos(heading), .3+s*np.sin(heading), np.full(4, heading))
out = encode_model_action(m, -.01, 20.)
assert out.path_offset == pytest.approx(.3+12*np.sin(heading))
assert out.path_angle == pytest.approx(.1)
assert out.curvature == out.curvature_rate == 0.
assert encode_model_action(m, .01, 30.) == out
def test_low_speed_floor_uses_one_shared_seven_metre_station():
m = model_points([0., 1., 2.], [0., 3., 9.], [0., 0., 0.], [0., .03, .09])
out = encode_model_action(m, .01, 3.)
assert out.path_offset == 0.
assert out.path_angle == pytest.approx(.07)
def test_short_plan_holds_both_endpoint_values_without_extrapolation():
m = model_points([0., .5], [0., 2.], [0., .4], [0., .2])
out = encode_model_action(m, -.01, 20.)
assert out.path_offset == .4
assert out.path_angle == pytest.approx(.2)
@pytest.mark.parametrize('times', [[], [0.], [0., 0., 1.], [0., 1., .5], [0., float('nan'), 1.], [.1, .5, 1.]])
def test_invalid_model_clock_cannot_publish_an_active_path(times):
m = model_points(times, [0., 10., 20.], [0., .1, .4], [0., .02, .04])
assert not encode_model_action(m, .01, 20.).valid
def test_orientation_and_position_must_describe_the_same_times():
m = model_points([0., .5, 1.], [0., 10., 20.], [0., .1, .4], [0., .02, .04])
m.orientation.t = [0., .6, 1.]
assert not encode_model_action(m, .01, 20.).valid
def test_model_heading_unwraps_before_interpolation_and_terminal_slope():
m = model_points([0., .5, 1.5], [0., 10., 30.], [0., 0., 0.], [3., 3.1, -3.1])
# Heading at 20 m is pi, but the terminal slope is gentler than the
# average slope. Its release bound is 3 + 20 * ((2*pi - 3.1) - 3.1)/20.
assert encode_model_action(m, 0., 20.).path_angle == pytest.approx(2*np.pi - 3.2)
@@ -0,0 +1,148 @@
"""C1 release geometry checks; none predicts a PSCM or vehicle response."""
import math
from types import SimpleNamespace
import numpy as np
import pytest
from openpilot.selfdrive.controls.lib.ford_model_action import ModelActionController, encode_model_action
def model_path(headings, *, origin=0., offset=.3, sign=1.):
# Known chord lengths make spatial heading profiles independent of speed.
station = (0., 6., 18., 30.)
heading = [sign*(origin+value) for value in headings]
x, y = [0.], [sign*offset]
for i in range(1, len(station)):
direction = (heading[i-1]+heading[i])/2
distance = station[i]-station[i-1]
x.append(x[-1]+distance*math.cos(direction))
y.append(y[-1]+distance*math.sin(direction))
times = [0., .5, 1.5, 2.5]
return SimpleNamespace(position=SimpleNamespace(t=times, x=x, y=y),
orientation=SimpleNamespace(t=times, z=heading))
@pytest.mark.parametrize('sign', [-1., 1.])
@pytest.mark.parametrize('origin', [0., .15])
def test_constant_spatial_curvature_preserves_original_heading(sign, origin):
model = model_path([0., .12, .36, .60], origin=origin, sign=sign)
out = encode_model_action(model, 0., 20.)
# The selected point is halfway from 6 m to 18 m: psi = psi0 + .02*12.
assert out.valid
assert out.path_angle == pytest.approx(sign*(origin+.24))
assert out.curvature == out.curvature_rate == 0.
@pytest.mark.parametrize('heading', [-.3, .3])
def test_constant_heading_line_keeps_its_nonzero_heading(heading):
out = encode_model_action(model_path([0.]*4, origin=heading), 0., 20.)
assert out.valid
assert out.path_angle == pytest.approx(heading)
@pytest.mark.parametrize('sign', [-1., 1.])
def test_increasing_spatial_curvature_cannot_amplify_c1(sign):
# Selected heading .24; terminal curvature .03/m would yield .36 rad.
model = model_path([0., .06, .42, .90], sign=sign)
out = encode_model_action(model, 0., 20.)
assert out.path_angle == pytest.approx(sign*.24)
@pytest.mark.parametrize('sign', [-1., 1.])
@pytest.mark.parametrize('origin', [0., .15])
def test_decreasing_spatial_curvature_unloads_c1_without_erasing_origin(sign, origin):
# Selected heading is psi0+.36; terminal curvature .02/m gives psi0+.24.
model = model_path([0., .24, .48, .54], origin=origin, sign=sign)
out = encode_model_action(model, 0., 20.)
assert out.path_angle == pytest.approx(sign*(origin+.24))
assert abs(out.path_angle) < origin+.36
@pytest.mark.parametrize('sign', [-1., 1.])
def test_opposite_terminal_curvature_releases_without_inventing_a_reversal(sign):
model = model_path([0., .30, .10, -.30], sign=sign)
out = encode_model_action(model, 0., 20.)
# The selected model heading is still sign*.20, though its slope has reversed.
assert out.valid
assert out.path_angle == 0.
def test_exact_model_knot_uses_incoming_segment():
model = SimpleNamespace(position=SimpleNamespace(t=[0., .5, 1., 2.], x=[0., 6., 12., 24.], y=[.3]*4),
orientation=SimpleNamespace(t=[0., .5, 1., 2.], z=[0., .24, .48, .48]))
assert encode_model_action(model, 0., 20.).path_angle == pytest.approx(.48)
@pytest.mark.parametrize('sign', [-1., 1.])
def test_duplicate_selected_station_retains_original_c1(sign):
times = [0., .5, 1., 2.]
model = SimpleNamespace(position=SimpleNamespace(t=times, x=[0., 7., 7., 14.], y=[sign*.4]*4),
orientation=SimpleNamespace(t=times, z=[0., sign*.3, sign*.2, sign*.2]))
out = encode_model_action(model, 0., 20.)
assert out.valid
assert out.path_offset == sign*.4
assert out.path_angle == pytest.approx(sign*.2)
def test_actual_model_reversal_uses_existing_slew_and_reaches_opposite_c1():
controller = ModelActionController()
positive = model_path([0., .12, .36, .60])
negative = model_path([0., .12, .36, .60], sign=-1.)
for _ in range(50):
controller.update(positive, 0., speed=20., dt=.01)
before = controller.c1
first = controller.update(negative, 0., speed=20., dt=.01)
assert controller.c1 == pytest.approx(before-.005)
assert first.path_angle > 0.
for _ in range(100):
final = controller.update(negative, 0., speed=20., dt=.01)
assert final.path_angle == pytest.approx(-.24)
def test_release_preserves_c0_exactly_through_target_slew_and_packing():
actual, reference = ModelActionController(), ModelActionController()
for i in range(240):
model = model_path([0., .24, .48, .54], offset=8.*math.sin(i*.04), sign=1. if i < 120 else -1.)
# C0's reference has byte-for-byte identical position and clocks, but no C1.
zero_heading = SimpleNamespace(position=model.position,
orientation=SimpleNamespace(t=model.orientation.t, z=[0.]*4))
target = encode_model_action(model, 0., 20.)
c0_target = encode_model_action(zero_heading, 0., 20.)
assert target.path_offset == c0_target.path_offset
output = actual.update(model, 0., speed=20., dt=.01)
c0_output = reference.update(zero_heading, 0., speed=20., dt=.01)
assert actual.c0 == reference.c0
assert output.path_offset == c0_output.path_offset
@pytest.mark.parametrize('sign', [-1., 1.])
def test_large_geometry_keeps_existing_field_caps_and_zero_c2_c3(sign):
controller = ModelActionController()
model = model_path([0.]*4, origin=.8, offset=20., sign=sign)
for _ in range(150):
out = controller.update(model, 0., speed=20., dt=.01)
assert out.valid
assert abs(out.path_offset) <= 5.11+1e-12
assert abs(out.path_angle) <= .5+1e-12
assert out.curvature == out.curvature_rate == 0.
assert out.path_offset == pytest.approx(sign*5.11)
assert out.path_angle == pytest.approx(sign*.5)
@pytest.mark.parametrize('field', ['x', 'y', 'z'])
def test_nonfinite_geometry_cannot_publish_a_release_request(field):
model = model_path([0., .24, .48, .54])
values = getattr(model.orientation if field == 'z' else model.position, field)
values[2] = math.nan
assert not encode_model_action(model, 0., 20.).valid
def test_unrepresentable_terminal_slope_falls_back_to_finite_original_heading():
times = [0., .5, 1.]
model = SimpleNamespace(position=SimpleNamespace(t=times, x=[0., 5e-320, 1e-319], y=[0.]*3),
orientation=SimpleNamespace(t=times, z=[0., .2, .4]))
out = encode_model_action(model, 0., 20.)
assert out.valid
assert np.isfinite(out.path_angle)
assert out.path_angle == pytest.approx(.4)
@@ -0,0 +1,422 @@
import math
from types import SimpleNamespace
import numpy as np
from openpilot.cereal import custom
from openpilot.selfdrive.car.helpers import convert_carControlSP
from openpilot.selfdrive.controls.lib.ford_path import (DBC_ANGLE, DBC_CURVATURE, DBC_OFFSET, FordPath, FordPathController,
FordPscmObserver, FordPscmObserverPathController, FordPscmState,
_bounded_feedback, _encode_path, _model_path, _predicted_pose,
_pscm_contributions, _relative_pose)
def _path(curvature: float, speed: float = 8.0):
t = np.linspace(0.0, 3.0, 61)
distance = speed * t
heading = curvature * distance
x = np.zeros_like(distance)
y = np.zeros_like(distance)
for i in range(1, len(distance)):
ds = distance[i] - distance[i - 1]
average_heading = 0.5 * (heading[i] + heading[i - 1])
x[i] = x[i - 1] + ds * math.cos(average_heading)
y[i] = y[i - 1] + ds * math.sin(average_heading)
return SimpleNamespace(
position=SimpleNamespace(t=t.tolist(), x=x.tolist(), y=y.tolist()),
orientation=SimpleNamespace(z=heading.tolist()),
)
def _changing_path(start_curvature: float, end_curvature: float, speed: float = 8.0):
t = np.linspace(0.0, 3.0, 61)
distance = speed * t
curvature = np.interp(distance, [distance[0], min(distance[-1], 7.0)], [start_curvature, end_curvature])
heading = np.zeros_like(distance)
x = np.zeros_like(distance)
y = np.zeros_like(distance)
for i in range(1, len(distance)):
ds = distance[i] - distance[i - 1]
heading[i] = heading[i - 1] + 0.5 * (curvature[i] + curvature[i - 1]) * ds
average_heading = 0.5 * (heading[i] + heading[i - 1])
x[i] = x[i - 1] + ds * math.cos(average_heading)
y[i] = y[i - 1] + ds * math.sin(average_heading)
return SimpleNamespace(
position=SimpleNamespace(t=t.tolist(), x=x.tolist(), y=y.tolist()),
orientation=SimpleNamespace(z=heading.tolist()),
)
def _command(model, desired_curvature: float, *, current_curvature: float = 0.0, v_ego: float = 8.0):
return FordPathController(dt=1.0).update(model, desired_curvature, current_curvature=current_curvature, v_ego=v_ego)
def _equivalent_curvature(command) -> float:
return 2.0 * command.path_offset / 7.0 ** 2 + 2.0 * command.path_angle / 7.0 + command.curvature
def test_gentle_path_uses_only_c2():
command = _command(_path(0.004, speed=20.0), 0.004, current_curvature=0.004, v_ego=20.0)
assert command.valid
assert command.path_offset == 0.0
assert command.path_angle == 0.0
assert np.isclose(command.curvature, 0.004, atol=1e-6)
assert command.curvature_rate == 0.0
def test_gentle_path_uses_only_c2_when_model_and_action_disagree():
command = _command(_path(0.005), 0.002, current_curvature=0.005)
assert command.path_offset == 0.0
assert command.path_angle == 0.0
assert np.isclose(command.curvature, 0.002, atol=1e-6)
def test_spatially_growing_path_adds_fast_pose_before_action_becomes_large():
controller = FordPathController(dt=1.0)
command = controller.update(_changing_path(0.0, 0.04), 0.012, current_curvature=0.0, v_ego=8.0)
assert command.path_offset > 0.0
assert command.path_angle > 0.0
assert command.curvature < 0.012
assert command.curvature_rate == 0.0
def test_growing_model_pose_adds_authority_but_c3_is_never_transmitted():
constant = _command(_path(0.012), 0.012)
growing = _command(_changing_path(0.0, 0.04), 0.012)
assert _equivalent_curvature(growing) > _equivalent_curvature(constant)
assert constant.curvature_rate == 0.0
assert growing.curvature_rate == 0.0
def test_local_tracking_error_corrects_without_replacing_forward_pose():
model = _changing_path(0.0, 0.04)
local_curvature = 0.5 * 0.04 * 2.0 / 7.0
aligned = _command(model, 0.012, current_curvature=local_curvature)
under = _command(model, 0.012, current_curvature=0.0)
assert aligned.path_offset > 0.0
assert aligned.path_angle > 0.0
assert under.path_offset > aligned.path_offset
assert under.path_angle > aligned.path_angle
def test_large_maneuver_uses_fast_pose_and_zeros_c2():
command = _command(_path(0.04), 0.04)
assert command.path_offset > 0.5
assert command.path_angle > 0.2
assert command.curvature == 0.0
assert command.curvature_rate == 0.0
def test_model_pose_can_trigger_maneuver_when_action_is_late():
command = _command(_path(0.04), 0.002)
assert command.path_offset > 0.5
assert command.path_angle > 0.2
assert command.curvature == 0.0
def test_gentle_model_pose_does_not_replace_a_collapsed_action():
command = _command(_path(0.005), 0.0, current_curvature=0.005)
assert command.path_offset == 0.0
assert command.path_angle == 0.0
assert command.curvature == 0.0
def test_changing_gentle_curve_keeps_upstream_strength_c2():
command = _command(_changing_path(0.0, 0.008), 0.004, current_curvature=0.0)
assert np.isclose(command.curvature, 0.004)
assert command.path_offset == 0.0
assert command.path_angle == 0.0
def test_action_only_maneuver_cannot_invent_large_model_pose():
command = _command(_path(0.002), 0.04)
assert 0.0 < command.path_offset < 0.1
assert 0.0 < command.path_angle < 0.03
assert command.curvature == 0.0
def test_nearby_demands_blend_continuously_without_a_mode_threshold():
low = _command(_path(0.0119), 0.0119)
high = _command(_path(0.0121), 0.0121)
assert abs(high.path_offset - low.path_offset) < 0.05
assert abs(high.path_angle - low.path_angle) < 0.03
assert abs(high.curvature - low.curvature) < 0.001
def test_leaving_c2_normal_band_does_not_drop_total_authority():
normal = _command(_path(0.006), 0.006)
transition = _command(_path(0.0061), 0.0061)
assert transition.curvature <= normal.curvature
assert _equivalent_curvature(transition) >= _equivalent_curvature(normal)
def test_low_speed_still_uses_available_model_pose():
command = _command(_path(0.04, speed=2.0), 0.04, v_ego=2.0)
assert command.path_offset > 0.0
assert command.path_angle > 0.0
def test_higher_speed_advances_predicted_pose_and_extends_heading_horizon():
model = _changing_path(0.0, 0.015, speed=20.0)
slow = _command(model, 0.012, v_ego=7.0)
fast = _command(model, 0.012, v_ego=20.0)
assert fast.path_offset > slow.path_offset
assert fast.path_angle > slow.path_angle
def test_short_model_uses_available_endpoint():
model = _path(0.04, speed=1.0)
command = _command(model, 0.04, v_ego=1.0)
assert command.valid
assert command.path_offset > 0.0
assert command.path_angle > 0.0
def test_turn_entry_coordinates_c2_release_with_fast_pose_attack():
controller = FordPathController(dt=0.01)
for _ in range(20):
assert controller.update(_path(0.004), 0.004, v_ego=8.0).curvature > 0.0
outputs = [controller.update(_path(0.04), 0.04, current_curvature=0.01, v_ego=8.0) for _ in range(100)]
assert 0.0 < outputs[0].curvature < 0.004
assert outputs[0].path_offset > 0.0
assert outputs[0].path_angle > 0.0
assert outputs[-1].curvature == 0.0
def test_turn_exit_allows_c2_to_take_over_while_fast_pose_drains():
controller = FordPathController(dt=0.01)
for _ in range(20):
controller.update(_path(0.04), 0.04, current_curvature=0.02, v_ego=8.0)
outputs = [controller.update(_path(0.004), 0.004, current_curvature=0.004, v_ego=8.0) for _ in range(100)]
assert 0.0 < outputs[0].curvature < 0.004
assert outputs[0].path_offset != 0.0 or outputs[0].path_angle != 0.0
assert outputs[-1].path_offset == 0.0
assert outputs[-1].path_angle == 0.0
def test_100hz_handoff_preserves_total_authority_without_entry_drop_or_exit_overshoot():
controller = FordPathController(dt=0.01)
normal = controller.update(_path(0.006), 0.006, current_curvature=0.006, v_ego=8.0)
entries = [controller.update(_path(0.04), 0.04, current_curvature=0.01, v_ego=8.0) for _ in range(100)]
entry_authority = np.asarray([_equivalent_curvature(command) for command in entries])
assert np.all(np.diff(entry_authority) >= -1e-9)
assert entry_authority[0] >= _equivalent_curvature(normal)
exits = [controller.update(_path(0.004), 0.004, current_curvature=0.004, v_ego=8.0) for _ in range(100)]
exit_authority = np.asarray([_equivalent_curvature(command) for command in exits])
assert np.all(np.diff(exit_authority) <= 1e-9)
assert np.all(exit_authority >= 0.004 - 1e-9)
def test_measured_tracking_error_closes_bidirectionally_without_abandoning_the_turn():
model = _path(0.04)
under = _command(model, 0.04, current_curvature=0.005)
on_target = _command(model, 0.04, current_curvature=0.04)
over = _command(model, 0.04, current_curvature=0.05)
assert under.path_offset > on_target.path_offset
assert under.path_angle > on_target.path_angle
assert 0.0 < over.path_offset < on_target.path_offset
assert 0.0 < over.path_angle < on_target.path_angle
def test_gentle_curve_does_not_add_fast_tracking_trim():
model = _path(0.004)
under = _command(model, 0.004, current_curvature=0.002)
on_target = _command(model, 0.004, current_curvature=0.004)
over = _command(model, 0.004, current_curvature=0.006)
assert under.path_offset == on_target.path_offset == over.path_offset == 0.0
assert under.path_angle == on_target.path_angle == over.path_angle == 0.0
assert np.allclose([under.curvature, on_target.curvature, over.curvature], 0.004, atol=2e-6)
def test_overshoot_trim_cannot_erase_a_modeled_turn():
model = _path(0.04)
on_target = _command(model, 0.04, current_curvature=0.04)
over = _command(model, 0.04, current_curvature=0.06)
assert over.path_offset > 0.95 * on_target.path_offset
assert over.path_angle > 0.9 * on_target.path_angle
def test_corrupt_measured_curvature_cannot_reverse_a_modeled_turn():
command = _command(_path(0.04), 0.04, current_curvature=0.5)
assert command.path_offset > 0.0
assert command.path_angle > 0.0
assert command.curvature == 0.0
def test_feedback_preserves_half_lsb_feedforward_direction():
for feedforward, resolution in ((0.006, 0.01), (0.0004, 0.0005)):
result = feedforward + _bounded_feedback(feedforward, -1.0, resolution, 1.0)
assert result >= 0.5 * resolution
def test_recent_curvature_trend_advances_vehicle_pose_without_a_response_gain():
model = _model_path(_path(0.04))
assert model is not None
constant = _encode_path(model, 0.04, current_curvature=0.02, curvature_delta=0.0, v_ego=8.0)
rising = _encode_path(model, 0.04, current_curvature=0.02, curvature_delta=0.01, v_ego=8.0)
assert 0.0 < rising.path_offset < constant.path_offset
assert 0.0 < rising.path_angle < constant.path_angle
def test_model_path_exit_zeros_lingering_c2_and_countersteers():
command = _command(_path(0.0), 0.004, current_curvature=0.006)
assert command.path_offset <= 0.0
assert command.path_angle < 0.0
assert command.curvature == 0.0
def test_model_path_reversal_zeros_opposing_lingering_c2():
command = _command(_path(-0.004), 0.004, current_curvature=0.002)
assert command.path_offset < 0.0
assert command.path_angle < 0.0
assert command.curvature == 0.0
def test_s_turn_reverses_model_pose_without_slow_c2():
controller = FordPathController(dt=0.05)
for _ in range(10):
controller.update(_path(0.04), 0.04, v_ego=8.0)
outputs = [controller.update(_path(-0.04), -0.04, v_ego=8.0) for _ in range(10)]
assert all(command.curvature == 0.0 for command in outputs)
assert np.all(np.diff([command.path_offset for command in outputs]) < 0.0)
assert np.all(np.diff([command.path_angle for command in outputs]) < 0.0)
assert outputs[-1].path_offset < 0.0
assert outputs[-1].path_angle < 0.0
def test_output_limits_and_rates_are_bounded():
controller = FordPathController()
outputs = [controller.update(_path(0.2), 0.2, v_ego=8.0) for _ in range(100)]
assert all(DBC_OFFSET[0] <= command.path_offset <= DBC_OFFSET[1] for command in outputs)
assert all(DBC_ANGLE[0] <= command.path_angle <= DBC_ANGLE[1] for command in outputs)
assert all(DBC_CURVATURE[0] <= command.curvature <= DBC_CURVATURE[1] for command in outputs)
assert np.max(np.abs(np.diff([command.path_offset for command in outputs]))) <= 0.04 + 1e-9
assert np.max(np.abs(np.diff([command.path_angle for command in outputs]))) <= 0.01 + 1e-9
def test_clipped_path_angle_uses_available_offset_to_preserve_endpoint():
horizon = 7.0
for curvature, angle_limit in ((-0.1, DBC_ANGLE[0]), (0.1, DBC_ANGLE[1])):
model = _path(curvature)
command = _command(model, curvature, current_curvature=curvature, v_ego=horizon)
path = _model_path(model)
assert path is not None
advance = 0.1 * horizon
model_offset, model_angle = _relative_pose(advance + horizon, path,
_predicted_pose(advance, curvature, 0.0))
assert command.path_angle == angle_limit
assert np.isclose(command.path_offset + horizon * command.path_angle,
model_offset + horizon * model_angle)
def test_invalid_model_ramps_pose_to_zero_and_inactive_resets():
controller = FordPathController(dt=0.01)
for _ in range(20):
active = controller.update(_path(0.04), 0.04, v_ego=8.0)
invalid = controller.update(None, 0.0, v_ego=8.0)
assert invalid.valid
assert abs(invalid.path_offset) < abs(active.path_offset)
assert abs(invalid.path_angle) < abs(active.path_angle)
assert not controller.update(_path(0.0), 0.0, v_ego=8.0, active=False).valid
def test_sunnypilot_path_message_round_trip():
message = custom.CarControlSP.new_message()
message.fordLateralPath.pathOffset = 0.3
message.fordLateralPath.pathAngle = -0.2
message.fordLateralPath.curvature = 0.008
message.fordLateralPath.curvatureRate = -0.0004
message.fordLateralPath.valid = True
path = convert_carControlSP(message.as_reader()).fordLateralPath
assert np.isclose(path.pathOffset, 0.3)
assert np.isclose(path.pathAngle, -0.2)
assert np.isclose(path.curvature, 0.008)
assert np.isclose(path.curvatureRate, -0.0004)
assert path.valid
def test_pscm_observer_mirrors_exact_250hz_slew_and_c3_target():
observer = FordPscmObserver()
observer.set_command(FordPath(True, 1.0, 0.5, 0.0, 0.001))
observer.advance(1.0)
assert np.isclose(observer.state.path_offset, 1.0)
assert np.isclose(observer.state.path_angle, 0.100006103515625)
assert np.isclose(observer.state.curvature, 0.0030059814453125)
def test_pscm_observer_tracks_wire_quantized_commands():
observer = FordPscmObserver()
observer.set_command(FordPath(True, 0.006, 0.0004, 0.000011, 0.0))
assert observer.command.path_offset == 0.01
assert observer.command.path_angle == 0.0005
assert observer.command.curvature == 0.00002
def test_pscm_c2_contribution_is_speed_scheduled():
state = FordPscmObserver().state
state = type(state)(curvature=0.004)
low = _pscm_contributions(state, 5.0)[2]
high = _pscm_contributions(state, 20.0)[2]
assert high > low * 10.0
def test_pscm_observer_fills_missing_gentle_c2_with_fast_fields():
controller = FordPscmObserverPathController(dt=0.01)
command = controller.update(_path(0.004, speed=20.0), 0.004, current_curvature=0.004,
v_ego=20.0, v_ego_raw=20.0)
assert command.path_offset > 0.0
assert command.path_angle > 0.0
assert command.curvature > 0.0
def test_pscm_observer_uses_c0_only_after_c1_reaches_its_effective_limit():
controller = FordPscmObserverPathController(dt=0.01)
small = controller._command_for_state(FordPath(True, 0.2, 0.0, 0.0, 0.0), 8.0)
large = controller._command_for_state(FordPath(True, 1.0, 0.5, 0.0, 0.0), 8.0)
assert small.path_offset == 0.0
assert small.path_angle > 0.0
assert large.path_offset > 0.0
assert large.path_angle == 0.349609375 / 10.0
def test_pscm_observer_preserves_c2_residual_across_c0_c1_headroom():
controller = FordPscmObserverPathController(dt=0.01)
target = FordPath(True, 0.0, 0.0, 0.004, 0.0)
command = controller._command_for_state(target, 20.0)
target_contribution = sum(_pscm_contributions(FordPscmState(curvature=target.curvature), 20.0))
command_contributions = _pscm_contributions(FordPscmState(command.path_offset, command.path_angle), 20.0)
assert np.isclose(sum(command_contributions), target_contribution)
controller.observer.state = FordPscmState(curvature=0.004)
unwind = controller._command_for_state(FordPath(valid=True), 20.0)
unwind_contributions = _pscm_contributions(FordPscmState(unwind.path_offset, unwind.path_angle), 20.0)
lingering_c2 = _pscm_contributions(controller.observer.state, 20.0)[2]
assert np.isclose(sum(unwind_contributions) + lingering_c2, 0.0)
def test_pscm_observer_unloads_fast_residual_as_c2_loads():
controller = FordPscmObserverPathController(dt=0.01)
outputs = [controller.update(_path(0.004, speed=20.0), 0.004, current_curvature=0.004,
v_ego=20.0, v_ego_raw=20.0) for _ in range(200)]
assert outputs[0].path_angle > outputs[-1].path_angle >= 0.0
assert controller.observer.state.curvature > 0.003
def test_pscm_observer_counters_lingering_c2_during_model_exit():
controller = FordPscmObserverPathController(dt=0.01)
for _ in range(200):
controller.update(_path(0.004, speed=20.0), 0.004, current_curvature=0.004,
v_ego=20.0, v_ego_raw=20.0)
command = controller.update(_path(0.0, speed=20.0), 0.0, current_curvature=0.004,
v_ego=20.0, v_ego_raw=20.0)
assert command.path_angle < 0.0
assert command.curvature < controller.observer.state.curvature
def test_pscm_observer_avoids_ineffective_c0_c1_windup():
controller = FordPscmObserverPathController(dt=1.0)
command = controller.update(_path(0.2), 0.2, v_ego=8.0, v_ego_raw=8.0)
assert abs(command.path_offset) <= 1.0
assert abs(command.path_angle) <= 0.349609375 / 10.0
+51 -54
View File
@@ -7,14 +7,9 @@ from openpilot.common.file_chunker import chunk_file, get_chunk_targets, get_exi
from openpilot.common.transformations.camera import _ar_ox_fisheye, _os_fisheye
from openpilot.common.transformations.model import MEDMODEL_INPUT_SIZE, DM_INPUT_SIZE
from openpilot.selfdrive.modeld.constants import ModelConstants
from openpilot.selfdrive.modeld.helpers import TG_INPUT_DEVICES_PATH, usbgpu_present, modeld_pkl_path
from openpilot.selfdrive.modeld.helpers import TG_INPUT_DEVICES_PATH, chestnut_present, modeld_pkl_path
CAMERA_CONFIGS = [
(_ar_ox_fisheye.width, _ar_ox_fisheye.height), # tici: 1928x1208
(_os_fisheye.width, _os_fisheye.height), # mici: 1344x760
]
Import('env', 'arch')
chunker_file = File("#openpilot/common/file_chunker.py")
lenv = env.Clone()
@@ -24,30 +19,32 @@ tinygrad_files = ["#"+x for x in glob.glob(env.Dir("#tinygrad_repo").relpath + "
if 'pycache' not in x and os.path.isfile(os.path.join(tinygrad_root, x))]
def estimate_pickle_max_size(onnx_size):
return 1.2 * onnx_size + 10 * 1024 * 1024 # 20% + 10MB is plenty
# QCOM programs for models with spatial recurrent features can approach 2x
# the ONNX size. Overestimating only adds an empty trailing chunk.
return 2.0 * onnx_size + 10 * 1024 * 1024
if arch == 'comma_arm64':
from openpilot.common.hardware import HARDWARE
camera = _os_fisheye if HARDWARE.get_device_type() == "mici" else _ar_ox_fisheye
camera_configs = [(camera.width, camera.height)]
tg_backend = 'QCOM'
tg_flags = f'DEV={tg_backend} IMAGE=1 FLOAT16=1 NOLOCALS=1 JIT_BATCH_SIZE=0 OPENPILOT_HACKS=1'
else:
camera_configs = [(c.width, c.height) for c in (_ar_ox_fisheye, _os_fisheye)]
tg_backend = 'CPU'
tg_flags = f'DEV=CPU' if arch == 'Darwin' else 'DEV=CPU:LLVM'
tg_devices = { # which device to put jit inputs to at runtime
'openpilot.selfdrive.modeld.modeld': {
'default': {'WARP_DEV': tg_backend, 'QUEUE_DEV': tg_backend},
'usbgpu': {'WARP_DEV': tg_backend, 'QUEUE_DEV': 'AMD'}
},
'openpilot.selfdrive.modeld.dmonitoringmodeld': {
'default': {'DEV': tg_backend}
},
}
USBGPU = usbgpu_present()
if USBGPU:
usbgpu_tg_flags = f'DEBUG=2 DEV=USB+AMD:LLVM WARP_DEV={tg_backend} FLOAT16=1 JIT_BATCH_SIZE=0 GMMU=0 TC_OPT=2'
CHESTNUT = chestnut_present()
if CHESTNUT:
chestnut_tg_flags = 'DEBUG=1 DEV=USB+AMD:LLVM FRAME_DEV=CPU FLOAT16=1 JIT_BATCH_SIZE=0 GMMU=0 TC_OPT=2 TC_OCCUPANCY_OPT=1'
# the USB+AMD GPU takes an exclusive flock; serialize all targets that touch it
usbgpu_lock = File("models/.usb_gpu.lock").abspath
chestnut_lock = File("models/.chestnut.lock").abspath
def write_tg_devices(target, source, env):
with open(str(target[0]), "w") as f:
@@ -73,44 +70,44 @@ compile_modeld_script = [
model_w, model_h = MEDMODEL_INPUT_SIZE
frame_skip = ModelConstants.MODEL_RUN_FREQ // ModelConstants.MODEL_CONTEXT_FREQ
for usbgpu in [False, True] if USBGPU else [False]:
target_pkl_path = File(modeld_pkl_path(usbgpu)).abspath
# BIG_INTO_SMALL=1 builds the default target from the big model, e.g. to test it without a USB GPU
file_prefix, cmd_flags = ('big_', usbgpu_tg_flags) if usbgpu else ('big_' if os.getenv('BIG_INTO_SMALL') else '', tg_flags)
driving_onnx_deps = get_existing_chunks(File(f"models/{file_prefix}driving_supercombo.onnx").abspath)
camera_res_args = ' '.join(f'{cw}x{ch}' for cw, ch in CAMERA_CONFIGS)
# CPU 7 is isolated with isolcpus on AGNOS, so explicitly pin the compiler to it.
taskset = 'taskset -c 7 ' if arch == 'comma_arm64' else ''
cmd = (f'{cmd_flags} {mac_brew_string} {taskset}python3 {modeld_dir}/compile_modeld.py '
f'--model-size {model_w}x{model_h} '
f'--camera-resolutions {camera_res_args} '
f'--onnx {File(f"models/{file_prefix}driving_supercombo.onnx").abspath} '
f'--output {target_pkl_path} --frame-skip {frame_skip}')
onnx_sizes_sum = sum(os.path.getsize(f) for f in driving_onnx_deps)
chunk_targets = get_chunk_targets(target_pkl_path, estimate_pickle_max_size(onnx_sizes_sum))
def do_compile(target, source, env, command=cmd, pkl=target_pkl_path, chunks=chunk_targets):
from openpilot.system.hardware.chestnut.flash import link_up
# chestnut can enumerate before its PCIe link is up due to varying 12V power behavior across cars
for _ in range(10):
if link_up():
break
time.sleep(1)
else:
print("Chestnut not ready, skipping big model build")
return
if ret := env.Execute(command):
return ret
chunk_file(pkl, chunks)
def do_chunk(target, source, env, pkl=target_pkl_path, chunks=chunk_targets):
chunk_file(pkl, chunks)
actions = Action(do_compile, " [USBGPU] $TARGET") if usbgpu else [cmd, Action(do_chunk, " [CHUNK] $TARGET")]
node = lenv.Command(
chunk_targets,
tinygrad_files + compile_modeld_script + driving_onnx_deps + [Value(chunk_targets), chunker_file],
actions,
)
if usbgpu:
lenv.SideEffect(usbgpu_lock, node)
if not os.getenv('SKIP_TINYGRAD_COMPILE'):
for chestnut in [False, True] if CHESTNUT else [False]:
target_pkl_path = File(modeld_pkl_path(chestnut)).abspath
file_prefix, cmd_flags = ('big_', chestnut_tg_flags) if chestnut else ('', tg_flags)
driving_onnx_deps = get_existing_chunks(File(f"models/{file_prefix}driving_supercombo.onnx").abspath)
camera_res_args = ' '.join(f'{cw}x{ch}' for cw, ch in camera_configs)
# CPU 7 is isolated with isolcpus on AGNOS, so explicitly pin the compiler to it.
taskset = 'taskset -c 7 ' if arch == 'comma_arm64' else ''
cmd = (f'{cmd_flags} {mac_brew_string} {taskset}python3 {modeld_dir}/compile_modeld.py '
f'--model-size {model_w}x{model_h} '
f'--camera-resolutions {camera_res_args} '
f'--onnx {File(f"models/{file_prefix}driving_supercombo.onnx").abspath} '
f'--output {target_pkl_path} --frame-skip {frame_skip}')
onnx_sizes_sum = sum(os.path.getsize(f) for f in driving_onnx_deps)
chunk_targets = get_chunk_targets(target_pkl_path, estimate_pickle_max_size(onnx_sizes_sum))
def do_compile(target, source, env, command=cmd, pkl=target_pkl_path, chunks=chunk_targets):
from openpilot.system.hardware.chestnut.flash import link_up
# chestnut can enumerate before its PCIe link is up due to varying 12V power behavior across cars
for _ in range(10):
if link_up():
break
time.sleep(1)
else:
print("Chestnut not ready, skipping big model build")
return
if ret := env.Execute(command):
return ret
chunk_file(pkl, chunks)
def do_chunk(target, source, env, pkl=target_pkl_path, chunks=chunk_targets):
chunk_file(pkl, chunks)
actions = Action(do_compile, " [CHESTNUT] $TARGET") if chestnut else [cmd, Action(do_chunk, " [CHUNK] $TARGET")]
node = lenv.Command(
chunk_targets,
tinygrad_files + compile_modeld_script + driving_onnx_deps + [Value(camera_res_args), Value(chunk_targets), chunker_file],
actions,
)
if chestnut:
lenv.SideEffect(chestnut_lock, node)
# get model metadata
fn = File(f"models/dmonitoring_model").abspath
@@ -120,7 +117,7 @@ lenv.Command(fn + "_metadata.pkl", [fn + ".onnx"] + tinygrad_files + script_file
dm_w, dm_h = DM_INPUT_SIZE
compile_dm_warp_script = [File(f"{modeld_dir}/compile_dm_warp.py")]
for cam_w, cam_h in CAMERA_CONFIGS:
for cam_w, cam_h in camera_configs:
dm_pkl_path = File(f"models/dm_warp_{cam_w}x{cam_h}_tinygrad.pkl").abspath
cmd = (f'{tg_flags} {mac_brew_string} python3 {modeld_dir}/compile_dm_warp.py '
f'--camera-resolution {cam_w}x{cam_h} --warp-to {dm_w}x{dm_h} '
+86 -73
View File
@@ -37,17 +37,12 @@ from tinygrad.engine.jit import TinyJit
NV12Frame = namedtuple("NV12Frame", ['width', 'height', 'stride', 'y_height', 'uv_height', 'size'])
WARP_INPUTS = ['tfm', 'big_tfm']
POLICY_INPUTS = ['img_q', 'big_img_q', 'feat_q', 'desire_q', 'packed_npy_inputs']
UV_SCALE_MATRIX = np.array([[0.5, 0, 0], [0, 0.5, 0], [0, 0, 1]], dtype=np.float32)
UV_SCALE_MATRIX_INV = np.linalg.inv(UV_SCALE_MATRIX)
WARP_DEV = os.getenv('WARP_DEV')
MODELD_INPUTS = ['img_q', 'big_img_q', 'feat_q', 'desire_q', 'packed_npy_inputs']
def make_random_images(keys, shape, device=None):
return {k: Tensor.randint(shape, low=0, high=256, dtype='uint8', device=device).realize() for k in keys}
def nv12_copy_size(stride: int, y_height: int, uv_height: int) -> int:
# Retain the padded Y and UV plane storage, but skip the trailing kernel/guard allocation.
return stride * (y_height + uv_height)
def warp_perspective_tinygrad(src_flat, M_inv, dst_shape, src_shape, stride_pad, border_fill_val=None):
@@ -99,7 +94,7 @@ def make_frame_prepare(nv12: NV12Frame, model_w, model_h):
def frame_prepare_tinygrad(input_frame, M_inv):
# UV_SCALE @ M_inv @ UV_SCALE_INV simplifies to elementwise scaling
M_inv_uv = M_inv * Tensor([[1.0, 1.0, 0.5], [1.0, 1.0, 0.5], [2.0, 2.0, 1.0]], device=WARP_DEV)
M_inv_uv = M_inv * Tensor([[1.0, 1.0, 0.5], [1.0, 1.0, 0.5], [2.0, 2.0, 1.0]], device=Device.DEFAULT)
# deinterleave NV12 UV plane (UVUV... -> separate U, V)
uv = input_frame[uv_offset:uv_offset + uv_height * stride].reshape(uv_height, stride)
with Context(SPLIT_REDUCEOP=0):
@@ -118,49 +113,43 @@ def make_frame_prepare(nv12: NV12Frame, model_w, model_h):
return frame_prepare_tinygrad
def make_warp_input_queues(vision_input_shapes, frame_skip, device):
img = vision_input_shapes['img'] # (1, 12, 128, 256)
n_frames = img[1] // 6
img_buf_shape = (frame_skip * (n_frames - 1) + 1, 6, img[2], img[3])
npy = {
'tfm': np.zeros((3, 3), dtype=np.float32),
'big_tfm': np.zeros((3, 3), dtype=np.float32),
}
input_queues = {
'img_q': Tensor(np.zeros(img_buf_shape, dtype=np.uint8), device=device).contiguous().realize(),
'big_img_q': Tensor(np.zeros(img_buf_shape, dtype=np.uint8), device=device).contiguous().realize(),
**{k: Tensor(v, device='NPY').realize() for k, v in npy.items()},
}
return input_queues, npy
def get_policy_npy_shapes(input_shapes):
dp = input_shapes['desire_pulse'] # (1, 25, 8)
tc = input_shapes['traffic_convention'] # (1, 2)
at = input_shapes['action_t'] # (1, 2)
fb = input_shapes['features_buffer'] # (1, 24, 512)
fb = input_shapes['features_buffer'] # (1, T-1, ...) e.g. (1, 24, 32, 512) with spatial features
feat_dim = math.prod(fb[2:])
# TODO prev_feat shouldn't exist and be handled inside the JIT, but corrupt on QCOM for now
shapes = {'desire': (dp[2],), 'traffic_convention': tuple(tc), 'action_t': tuple(at), 'prev_feat': (fb[0], fb[2])}
shapes = {'desire': (dp[2],), 'traffic_convention': tuple(tc), 'action_t': tuple(at), 'prev_feat': (fb[0], feat_dim)}
return shapes, [math.prod(s) for s in shapes.values()]
def make_input_queues(input_shapes, frame_skip, device):
input_queues, npy = make_warp_input_queues(input_shapes, frame_skip, device)
fb = input_shapes['features_buffer'] # (1, 24, 512), past features only; the model appends the current frame's feature
def make_input_queues(input_shapes, frame_skip, device, frame_copy_size):
img = input_shapes['img'] # (1, 12, 128, 256)
fb = input_shapes['features_buffer'] # (1, T-1, ...), past features only; the model appends the current frame's feature
feat_dim = math.prod(fb[2:])
dp = input_shapes['desire_pulse'] # (1, 25, 8)
n_frames = img[1] // 6
img_buf_shape = (frame_skip * (n_frames - 1) + 1, 6, img[2], img[3])
shapes, sizes = get_policy_npy_shapes(input_shapes)
packed_npy_inputs = np.zeros(sum(sizes), dtype=np.float32)
policy_shapes, _ = get_policy_npy_shapes(input_shapes)
shapes = {'tfm': (3, 3), 'big_tfm': (3, 3)} | policy_shapes
sizes = [math.prod(s) for s in shapes.values()]
packed_npy_size = sum(sizes) * np.dtype(np.float32).itemsize
packed_input = np.zeros(packed_npy_size + 2 * frame_copy_size, dtype=np.uint8)
packed_npy_inputs = packed_input[:packed_npy_size].view(np.float32)
frames = packed_input[packed_npy_size:]
frame_views = {'img': frames[:frame_copy_size], 'big_img': frames[frame_copy_size:]}
# views into the packed inputs, to be refilled at runtime
npy.update({k: v.reshape(s) for (k, s), v in zip(shapes.items(), np.split(packed_npy_inputs, np.cumsum(sizes[:-1])), strict=True)})
input_queues.update({
'feat_q': Tensor(np.zeros((frame_skip * fb[1], fb[0], fb[2]), dtype=np.float32), device=device).contiguous().realize(),
npy = {k: v.reshape(s) for (k, s), v in zip(shapes.items(), np.split(packed_npy_inputs, np.cumsum(sizes[:-1])), strict=True)}
input_queues = {
'img_q': Tensor(np.zeros(img_buf_shape, dtype=np.uint8), device=device).contiguous().realize(),
'big_img_q': Tensor(np.zeros(img_buf_shape, dtype=np.uint8), device=device).contiguous().realize(),
'feat_q': Tensor(np.zeros((frame_skip * fb[1], fb[0], feat_dim), dtype=np.float32), device=device).contiguous().realize(),
'desire_q': Tensor(np.zeros((frame_skip * dp[1], dp[0], dp[2]), dtype=np.float32), device=device).contiguous().realize(),
'packed_npy_inputs': Tensor(packed_npy_inputs, device='NPY').realize(),
})
return input_queues, npy
'packed_npy_inputs': Tensor(packed_input, device='NPY').realize(),
}
return input_queues, npy, frame_views
def shift_and_sample(buf, new_val, sample_fn):
@@ -176,13 +165,15 @@ def sample_desire(buf, frame_skip):
return buf.reshape(-1, frame_skip, *buf.shape[1:]).max(1).flatten(0, 1).unsqueeze(0)
def make_warp(nv12, model_w, model_h, frame_skip):
def make_warp(nv12, model_w, model_h):
frame_prepare = make_frame_prepare(nv12, model_w, model_h)
def warp(tfm, big_tfm, frame, big_frame):
tfm = tfm.to(WARP_DEV)
big_tfm = big_tfm.to(WARP_DEV)
Tensor.realize(tfm, big_tfm)
tfm = tfm.to(Device.DEFAULT)
big_tfm = big_tfm.to(Device.DEFAULT)
frame = frame.to(Device.DEFAULT)
big_frame = big_frame.to(Device.DEFAULT)
Tensor.realize(tfm, big_tfm, frame, big_frame)
warped_frame = frame_prepare(frame, tfm).unsqueeze(0)
warped_big_frame = frame_prepare(big_frame, big_tfm).unsqueeze(0)
@@ -195,10 +186,10 @@ def make_run_policy(model_runner, model_metadata, frame_skip):
sample_desire_fn = partial(sample_desire, frame_skip=frame_skip)
sample_skip_fn = partial(sample_skip, frame_skip=frame_skip)
npy_shapes, npy_sizes = get_policy_npy_shapes(model_metadata['input_shapes'])
model_input_dtypes = {name: spec.dtype for name, spec in model_runner.graph_inputs.items()}
def run_policy(warped, img_q, big_img_q, feat_q, desire_q, packed_npy_inputs):
packed_npy_inputs = packed_npy_inputs.to(Device.DEFAULT)
warped = warped.to(Device.DEFAULT)
Tensor.realize(packed_npy_inputs, warped)
img = shift_and_sample(img_q, warped[0:1], sample_skip_fn)
@@ -211,33 +202,50 @@ def make_run_policy(model_runner, model_metadata, frame_skip):
inputs = {
'img': img,
'big_img': big_img,
'features_buffer': feat_buf,
'features_buffer': feat_buf.reshape(model_metadata['input_shapes']['features_buffer']),
'desire_pulse': desire_buf,
'traffic_convention': traffic_convention,
'action_t': action_t,
}
inputs = {name: value.cast(model_input_dtypes[name]) for name, value in inputs.items()}
out = next(iter(model_runner(inputs).values())).cast('float32')
return out,
return run_policy
def compile_jit(jit, make_random_inputs, input_keys, make_queues):
SEED = 42
def random_inputs_run(fn, seed, test_val=None, test_buffers=None, expect_match=True):
input_queues, npy = make_queues(Device.DEFAULT)
rng = np.random.default_rng(seed)
Tensor.manual_seed(seed)
def make_run_model(warp, run_policy, model_metadata, frame_copy_size):
_, policy_sizes = get_policy_npy_shapes(model_metadata['input_shapes'])
packed_npy_size = (18 + sum(policy_sizes)) * np.dtype(np.float32).itemsize
testing = test_val is not None or test_buffers is not None
n_runs = 1 if testing else 3
def run_model(img_q, big_img_q, feat_q, desire_q, packed_npy_inputs):
packed_input = packed_npy_inputs.to(Device.DEFAULT)
Tensor.realize(packed_input)
packed_npy_inputs = packed_input[:packed_npy_size].bitcast('float32')
frame = packed_input[packed_npy_size:packed_npy_size + frame_copy_size]
big_frame = packed_input[packed_npy_size + frame_copy_size:]
tfm, big_tfm, policy_inputs = packed_npy_inputs.split([9, 9, sum(policy_sizes)])
warped = warp(tfm.reshape(3, 3), big_tfm.reshape(3, 3), frame, big_frame)
return run_policy(warped, img_q, big_img_q, feat_q, desire_q, policy_inputs)
return run_model
def compile_jit(jit, input_keys, make_queues, benchmark_runs):
if benchmark_runs < 1:
raise ValueError("benchmark_runs must be at least 1")
SEED = 42
def random_inputs_run(fn, seed, n_runs, test_val=None, test_buffers=None, expect_match=True):
input_queues, npy, frame_views = make_queues(Device.DEFAULT)
rng = np.random.default_rng(seed)
for i in range(n_runs):
for v in npy.values():
v[:] = rng.standard_normal(v.shape).astype(v.dtype)
for v in frame_views.values():
v[:] = rng.integers(0, 256, size=v.shape, dtype=np.uint8)
Device.default.synchronize()
random_inputs = make_random_inputs()
st = time.perf_counter()
outs = fn(**{k: input_queues[k] for k in input_keys}, **random_inputs)
outs = fn(**{k: input_queues[k] for k in input_keys})
mt = time.perf_counter()
Device.default.synchronize()
et = time.perf_counter()
@@ -256,14 +264,15 @@ def compile_jit(jit, make_random_inputs, input_keys, make_queues):
return val, buffers
print('capture + replay')
test_val, test_buffers = random_inputs_run(jit, SEED)
print('pickle round trip')
test_val, test_buffers = random_inputs_run(jit, SEED, 3)
print(f'pickle round trip ({benchmark_runs} runs per seed)')
with tempfile.TemporaryFile(dir=".") as f:
dump_oob(jit, f)
f.seek(0)
jit = load_oob(f)
random_inputs_run(jit, SEED, test_val, test_buffers, expect_match=True)
random_inputs_run(jit, SEED+1, test_val, test_buffers, expect_match=False)
loaded_jit = load_oob(f)
random_inputs_run(loaded_jit, SEED, benchmark_runs, test_val, test_buffers, expect_match=True)
random_inputs_run(loaded_jit, SEED+1, benchmark_runs, test_val, test_buffers, expect_match=False)
# Keep the original so per-resolution JITs share model weight buffers in the final pickle.
return jit
@@ -292,27 +301,31 @@ if __name__ == "__main__":
p.add_argument('--onnx', required=True)
p.add_argument('--output', required=True)
p.add_argument('--frame-skip', type=int, required=True)
p.add_argument('--benchmark-runs', type=int, default=1,
help='timed loaded-JIT runs for each correctness seed')
args = p.parse_args()
model_path = read_file_chunked_to_disk(args.onnx)
model_w, model_h = args.model_size
model_runner = OnnxRunner(model_path)
out = {'metadata': make_metadata_dict(model_path)}
out = {
'metadata': make_metadata_dict(model_path),
'input_devices': {'model': Device.DEFAULT},
'run_model': {},
}
run_policy_jit = TinyJit(make_run_policy(model_runner, out['metadata'], args.frame_skip), prune=True)
make_policy_queues = partial(make_input_queues, out['metadata']['input_shapes'], args.frame_skip)
make_random_model_inputs = partial(make_random_images, keys=['warped'], shape=(2, 6, *out['metadata']['input_shapes']['img'][2:]), device=WARP_DEV)
out['run_policy'] = compile_jit(run_policy_jit, make_random_model_inputs, POLICY_INPUTS,
make_policy_queues)
run_policy = make_run_policy(model_runner, out['metadata'], args.frame_skip)
for cam_w, cam_h in args.camera_resolutions:
nv12 = NV12Frame(cam_w, cam_h, *get_nv12_info(cam_w, cam_h))
make_random_warp_inputs = partial(make_random_images, keys=['frame', 'big_frame'], shape=nv12.size, device=WARP_DEV)
warp = TinyJit(make_warp(nv12, model_w, model_h, args.frame_skip), prune=True)
make_warp_queues = partial(make_warp_input_queues, out['metadata']['input_shapes'], args.frame_skip)
out[(cam_w,cam_h)] = compile_jit(warp, make_random_warp_inputs, WARP_INPUTS, make_warp_queues)
frame_copy_size = nv12_copy_size(nv12.stride, nv12.y_height, nv12.uv_height)
make_model_queues = partial(make_input_queues, out['metadata']['input_shapes'], args.frame_skip,
frame_copy_size=frame_copy_size)
warp = make_warp(nv12, model_w, model_h)
run_model_jit = TinyJit(make_run_model(warp, run_policy, out['metadata'], frame_copy_size), prune=True)
out['run_model'][(cam_w,cam_h)] = compile_jit(run_model_jit, MODELD_INPUTS, make_model_queues,
args.benchmark_runs)
with open(args.output, "wb") as f:
dump_oob(out, f)
@@ -29,7 +29,7 @@ class ModelState:
output: np.ndarray
def __init__(self, cam_w: int, cam_h: int):
self.DEV = get_tg_input_devices(PROCESS_NAME, usbgpu=False)['DEV']
self.DEV = get_tg_input_devices(PROCESS_NAME, chestnut=False)['DEV']
with open(METADATA_PATH, 'rb') as f:
model_metadata = pickle.load(f)
self.input_shapes = model_metadata['input_shapes']
@@ -64,6 +64,7 @@ def fill_driving_model_data(msg: capnp._DynamicStructBuilder, modelv2_send: capn
driving_model_data.frameIdExtra = modelV2.frameIdExtra
driving_model_data.frameDropPerc = modelV2.frameDropPerc
driving_model_data.modelExecutionTime = modelV2.modelExecutionTime
driving_model_data.big = modelV2.big
driving_model_data.action = modelV2.action
driving_model_data.meta.laneChangeState = modelV2.meta.laneChangeState
driving_model_data.meta.laneChangeDirection = modelV2.meta.laneChangeDirection
+15 -9
View File
@@ -7,18 +7,20 @@ import tempfile
from pathlib import Path
from openpilot.common.file_chunker import get_manifest_path
from openpilot.common.hardware.usb import CHESTNUT_FW_VERSION, CHESTNUT_USB_IDS, USB_DEVICES_PATH
from openpilot.common.hardware.usb import CHESTNUT_USB_PRODUCT, USB_DEVICES_PATH, is_chestnut_usb_id
MODELS_DIR = Path(__file__).resolve().parent / 'models'
TG_INPUT_DEVICES_PATH = MODELS_DIR / 'tg_input_devices.json'
CHESTNUT_POWERED_VOLTAGE = 5000
CHESTNUT_PCIE_READY = 0x78
def get_tg_input_devices(process_name: str, usbgpu: bool):
def get_tg_input_devices(process_name: str, chestnut: bool):
with open(TG_INPUT_DEVICES_PATH) as f:
return json.load(f)[process_name]['default' if not usbgpu else 'usbgpu']
return json.load(f)[process_name]['default' if not chestnut else 'chestnut']
def modeld_pkl_path(usbgpu: bool):
prefix = 'big_' if usbgpu else ''
def modeld_pkl_path(chestnut: bool):
prefix = 'big_' if chestnut else ''
return MODELS_DIR / f'{prefix}driving_tinygrad.pkl'
def dump_oob(obj, f):
@@ -45,16 +47,20 @@ def load_oob(f):
yield pb
return pickle.load(io.BytesIO(opcodes), buffers=buffers())
def usbgpu_present() -> bool:
def chestnut_present() -> bool:
for d in USB_DEVICES_PATH.glob("*"):
try:
usb_id = (int((d / "idVendor").read_text(), 16), int((d / "idProduct").read_text(), 16))
product = (d / "product").read_text().strip()
if usb_id in CHESTNUT_USB_IDS and product == f"custom {CHESTNUT_FW_VERSION}-CLEAN":
if is_chestnut_usb_id(*usb_id) and product == CHESTNUT_USB_PRODUCT:
return True
except Exception:
pass
return False
def usbgpu_compiled() -> bool:
return Path(get_manifest_path(modeld_pkl_path(usbgpu=True))).is_file()
def chestnut_compiled() -> bool:
return Path(get_manifest_path(modeld_pkl_path(chestnut=True))).is_file()
def chestnut_ready(state) -> bool:
return state.supplyVoltage >= CHESTNUT_POWERED_VOLTAGE and not state.supplyFault and state.pcieLtssm == CHESTNUT_PCIE_READY
+105 -66
View File
@@ -1,9 +1,11 @@
#!/usr/bin/env python3
from collections.abc import Callable
import ctypes
from functools import cached_property
import os
os.environ['GMMU'] = '0' # for usbgpu fast loading, noop for qcom
from tinygrad.tensor import Tensor
os.environ['GMMU'] = '0' # for chestnut fast loading, noop for qcom
from tinygrad.device import Device
import usb1
import struct
import threading
import time
@@ -26,17 +28,17 @@ from openpilot.common.transformations.model import get_warp_matrix
from openpilot.selfdrive.controls.lib.desire_helper import DesireHelper
from openpilot.selfdrive.controls.lib.drive_helpers import get_accel_from_plan, should_stop, smooth_value, get_curvature_from_plan
from openpilot.selfdrive.modeld.parse_model_outputs import Parser
from openpilot.selfdrive.modeld.compile_modeld import make_input_queues, WARP_INPUTS, POLICY_INPUTS
from openpilot.selfdrive.modeld.compile_modeld import make_input_queues, nv12_copy_size, MODELD_INPUTS
from openpilot.selfdrive.modeld.fill_model_msg import fill_model_msg, fill_driving_model_data, fill_pose_msg, PublishState
from openpilot.common.file_chunker import open_file_chunked
from openpilot.common.hardware.usb import CHESTNUT_USB_IDS
from openpilot.selfdrive.modeld.constants import ModelConstants, Plan
from openpilot.selfdrive.modeld.helpers import usbgpu_present, usbgpu_compiled, modeld_pkl_path, get_tg_input_devices, load_oob
from openpilot.selfdrive.modeld.helpers import chestnut_present, chestnut_compiled, chestnut_ready, modeld_pkl_path, load_oob
from openpilot.sunnypilot.livedelay.helpers import get_lat_delay
from openpilot.sunnypilot.modeld_v2.modeld_base import ModelStateBase
from openpilot.sunnypilot.selfdrive.controls.lib.relc import RoadEdgeLaneChangeController
PROCESS_NAME = "openpilot.selfdrive.modeld.modeld"
SEND_RAW_PRED = os.getenv('SEND_RAW_PRED')
LAT_SMOOTH_SECONDS = 0.0
@@ -81,6 +83,37 @@ class ChestnutState:
self.valid = True
self.sends = 0
self.metrics = {}
self._asm_usb = None
def _close_asm_usb(self) -> None:
if self._asm_usb is not None:
self._asm_usb.close()
self._asm_usb = None
def _open_asm_usb(self):
context = usb1.USBContext()
for vendor_id, product_id in CHESTNUT_USB_IDS:
if (handle := context.openByVendorIDAndProductID(vendor_id, product_id, skip_on_error=True)) is not None:
return handle
context.close()
def _read_ina(self) -> tuple[int, int, bool]:
if "AMD" in Device._opened_devices and self._asm_usb is None:
try:
raw = Device["AMD"].iface.pci_dev.usb.usb.control_read(0xC0, 5)
return struct.unpack('<Hh?', bytes(raw))
except Exception:
pass
if self._asm_usb is None:
self._asm_usb = self._open_asm_usb()
if self._asm_usb is None:
raise usb1.USBErrorNoDevice
try:
raw = self._asm_usb.controlRead(0xC0, 0xC0, 0, 0, 5, timeout=100)
except usb1.USBError:
self._close_asm_usb()
raise
return struct.unpack('<Hh?', bytes(raw))
@cached_property
def power_limit(self) -> int:
@@ -94,8 +127,10 @@ class ChestnutState:
if self.big and "AMD" in Device._opened_devices and self.sends % 100 == 1:
try:
smu = Device["AMD"].iface.dev_impl.smu
metrics_t = smu.smu_mod.SmuMetricsExternal_t
smu._send_msg(smu.smu_mod.PPSMC_MSG_TransferTableSmu2Dram, smu.smu_mod.TABLE_SMU_METRICS, timeout=100)
metrics = smu.read_table(smu.smu_mod.SmuMetricsExternal_t, smu.smu_mod.TABLE_SMU_METRICS).SmuMetrics
metrics_buf = bytearray(smu.adev.vram.view(smu.driver_table_paddr, ctypes.sizeof(metrics_t))[:])
metrics = metrics_t.from_buffer(metrics_buf).SmuMetrics
self.metrics = {'tempC': metrics.AvgTemperature[smu.smu_mod.TEMP_HOTSPOT],
'memoryTempC': metrics.AvgTemperature[smu.smu_mod.TEMP_MEM],
'powerDrawW': metrics.AverageSocketPower,
@@ -114,13 +149,15 @@ class ChestnutState:
setattr(state, k, v)
asm_valid = False
try:
# ASM runs on USB-C power, these still read without a gpu
state.supplyVoltage, state.supplyCurrent, state.supplyFault = self._read_ina()
asm_valid = True
except Exception:
pass
if "AMD" in Device._opened_devices:
try:
# ASM runs on USB-C power, these still read without a gpu
asm = Device["AMD"].iface.pci_dev.usb
state.pcieLtssm = asm.read(0xB450, 1)[0]
state.supplyVoltage, state.supplyCurrent = struct.unpack('<Hh', bytes(asm.usb.control_read(0xC0, 5))[:4])
asm_valid = True
state.pcieLtssm = Device["AMD"].iface.pci_dev.usb.read(0xB450, 1)[0]
except Exception:
pass
@@ -141,42 +178,34 @@ class FrameMeta:
class ModelState(ModelStateBase):
prev_desire: np.ndarray # for tracking the rising edge of the pulse
def __init__(self, cam_w: int, cam_h: int, usbgpu: bool):
def __init__(self, cam_w: int, cam_h: int, chestnut: bool):
ModelStateBase.__init__(self)
input_devices = get_tg_input_devices(PROCESS_NAME, usbgpu)
self.WARP_DEV, self.QUEUE_DEV = input_devices['WARP_DEV'], input_devices['QUEUE_DEV']
jits = load_oob(open_file_chunked(modeld_pkl_path(usbgpu)))
jits = load_oob(open_file_chunked(modeld_pkl_path(chestnut)))
input_devices = jits['input_devices']
self.model_device = input_devices['model']
metadata = jits['metadata']
self.input_shapes = metadata['input_shapes']
self.vision_input_names = [k for k in self.input_shapes if 'img' in k]
self.output_slices = metadata['output_slices']
self.prev_desire = np.zeros(ModelConstants.DESIRE_LEN, dtype=np.float32)
self.usbgpu = usbgpu
self.chestnut = chestnut
self.frame_skip = ModelConstants.MODEL_RUN_FREQ // ModelConstants.MODEL_CONTEXT_FREQ
self.input_queues, self.npy = make_input_queues(self.input_shapes, self.frame_skip, device=self.QUEUE_DEV)
self.full_frames: dict[str, Tensor] = {}
self._blob_cache: dict[tuple[str, int], Tensor] = {}
self.frame_copy_size = nv12_copy_size(*get_nv12_info(cam_w, cam_h)[:3])
self.input_queues, self.npy, self.frame_views = make_input_queues(
self.input_shapes, self.frame_skip, device=self.model_device, frame_copy_size=self.frame_copy_size)
self.parser = Parser()
self.frame_buf_params = {k: get_nv12_info(cam_w, cam_h) for k in ('img', 'big_img')}
self.run_policy = jits['run_policy']
self.warp = jits[(cam_w,cam_h)]
self.run_model = jits['run_model'][(cam_w,cam_h)]
def slice_outputs(self, model_outputs: np.ndarray, output_slices: dict[str, slice]) -> dict[str, np.ndarray]:
parsed_model_outputs = {k: model_outputs[np.newaxis, v] for k,v in output_slices.items()}
return parsed_model_outputs
def run(self, bufs: dict[str, VisionBuf], transforms: dict[str, np.ndarray],
inputs: dict[str, np.ndarray]) -> dict[str, np.ndarray] | None:
for key in bufs.keys():
ptr = np.frombuffer(bufs[key].data, dtype=np.uint8).ctypes.data
yuv_size = self.frame_buf_params[key][3]
# There is a ringbuffer of imgs, just cache tensors pointing to all of them
cache_key = (key, ptr)
if cache_key not in self._blob_cache:
self._blob_cache[cache_key] = Tensor.from_blob(ptr, (yuv_size,), dtype='uint8', device=self.WARP_DEV)
self.full_frames[key] = self._blob_cache[cache_key]
inputs: dict[str, np.ndarray], after_enqueue: Callable[[], None] | None = None) -> dict[str, np.ndarray]:
for key, buf in bufs.items():
np.copyto(self.frame_views[key], np.frombuffer(buf.data, dtype=np.uint8, count=self.frame_copy_size))
# Model decides when action is completed, so desire input is just a pulse triggered on rising edge
inputs['desire_pulse'][0] = 0
@@ -187,16 +216,12 @@ class ModelState(ModelStateBase):
self.npy['tfm'][:,:] = transforms['img'][:,:]
self.npy['big_tfm'][:,:] = transforms['big_img'][:,:]
warped = self.warp(**{k: self.input_queues[k] for k in WARP_INPUTS}, frame=self.full_frames['img'], big_frame=self.full_frames['big_img'])
outs, = self.run_policy(
**{k: self.input_queues[k] for k in POLICY_INPUTS if k in self.input_queues}, warped=warped
)
outs, = self.run_model(**{k: self.input_queues[k] for k in MODELD_INPUTS})
if after_enqueue is not None:
after_enqueue()
model_output = outs.numpy()[0]
if self.usbgpu and not np.all(np.isfinite(model_output)):
# TODO remove with prev_feat
cloudlog.error("model output not finite, dropping frame")
return None
if self.chestnut and not np.all(np.isfinite(model_output)):
raise RuntimeError("model output not finite")
outputs_dict = self.parser.parse_outputs(self.slice_outputs(model_output, self.output_slices))
self.npy['prev_feat'][:] = model_output[self.output_slices['hidden_state']]
@@ -205,25 +230,37 @@ class ModelState(ModelStateBase):
return outputs_dict
def warmup(self) -> None:
dummy_frames = {k: np.zeros(self.frame_buf_params[k][3], dtype=np.uint8) for k in self.vision_input_names}
dummy_frames = {k: np.zeros(self.frame_copy_size, dtype=np.uint8) for k in self.vision_input_names}
eye = np.eye(3, dtype=np.float32)
dims = {'desire_pulse': ModelConstants.DESIRE_LEN, 'traffic_convention': 2, 'action_t': 2}
self.run(dummy_frames, dict.fromkeys(self.vision_input_names, eye), {k: np.zeros(v, dtype=np.float32) for k, v in dims.items()})
self.input_queues, self.npy = make_input_queues(self.input_shapes, self.frame_skip, device=self.QUEUE_DEV)
self.input_queues, self.npy, self.frame_views = make_input_queues(
self.input_shapes, self.frame_skip, device=self.model_device, frame_copy_size=self.frame_copy_size)
self.prev_desire[:] = 0
self.full_frames.clear()
self._blob_cache.clear()
def main(demo=False):
cloudlog.warning("modeld init")
USBGPU = usbgpu_present() and usbgpu_compiled()
if USBGPU:
chestnut_available = chestnut_present() and chestnut_compiled()
CHESTNUT = False
if chestnut_available:
poller = messaging.Poller()
sock = messaging.sub_sock("chestnutState", poller=poller, conflate=True)
deadline = time.monotonic() + 4. / SERVICE_LIST['deviceState'].frequency
while not CHESTNUT and (remaining := deadline - time.monotonic()) > 0.:
if not poller.poll(round(remaining * 1000)):
break
msg = messaging.recv_one_or_none(sock)
CHESTNUT = msg is not None and msg.valid and chestnut_ready(msg.chestnutState)
if CHESTNUT:
os.environ['HCQDEV_WAIT_TIMEOUT_MS'] = '3000'
params = Params()
params.put_bool("UsbGpuLoading", USBGPU)
params.remove("UsbGpuActive")
params.put_bool("ChestnutLoading", CHESTNUT)
if chestnut_available and not CHESTNUT:
params.put_bool("ChestnutActive", False)
else:
params.remove("ChestnutActive")
config_realtime_process(7, 54)
@@ -253,7 +290,7 @@ def main(demo=False):
st = time.monotonic()
cloudlog.warning("loading model")
model = None
if USBGPU:
if CHESTNUT:
big_model = None
def load_big():
nonlocal big_model
@@ -267,23 +304,27 @@ def main(demo=False):
loader.start()
loader.join(BIG_MODEL_TIMEOUT)
model = big_model
params.put_bool("UsbGpuActive", model is not None)
if model is None:
params.put_bool("ChestnutModelError", True)
params.put_bool("ChestnutActive", model is not None)
if model is not None:
params.remove("ChestnutModelError")
small_model = ModelState(vipc_client_main.width, vipc_client_main.height, False) if model is None or USBGPU else None
small_model = ModelState(vipc_client_main.width, vipc_client_main.height, False) if model is None or CHESTNUT else None
if model is None:
model = small_model
params.put_bool("UsbGpuLoading", False)
params.put_bool("ChestnutLoading", False)
assert model is not None
cloudlog.warning(f"models loaded in {time.monotonic() - st:.1f}s, modeld starting")
# messaging
pub_socks = ["modelV2", "drivingModelData", "cameraOdometry", "modelDataV2SP"] + (["chestnutState"] if USBGPU else [])
pub_socks = ["modelV2", "drivingModelData", "cameraOdometry", "modelDataV2SP"] + (["chestnutState"] if CHESTNUT else [])
pm = PubMaster(pub_socks)
sm = SubMaster(["deviceState", "carState", "narrowRoadCameraState", "extrinsicsCalibration", "driverMonitoringState", "carControl", "lateralDelay"])
publish_state = PublishState()
params = Params()
chestnut_state = ChestnutState(pm, model.usbgpu) if USBGPU else None
chestnut_state = ChestnutState(pm, model.chestnut) if CHESTNUT else None
# setup filter to track dropped frames
frame_dropped_filter = FirstOrderFilter(0., 10., 1. / ModelConstants.MODEL_RUN_FREQ)
@@ -393,13 +434,16 @@ def main(demo=False):
mt1 = time.perf_counter()
try:
model_output = model.run(bufs, transforms, inputs)
send_chestnut = (chestnut_state is not None and
run_count % round(ModelConstants.MODEL_RUN_FREQ / SERVICE_LIST['chestnutState'].frequency) == 0)
model_output = model.run(bufs, transforms, inputs, chestnut_state.send if send_chestnut else None)
except Exception:
if not params.get_bool("UsbGpuActive"):
if not params.get_bool("ChestnutActive"):
raise
# fallback to small model
cloudlog.exception("big model failed, fall back to small")
params.put_bool("UsbGpuActive", False)
params.put_bool("ChestnutModelError", True)
params.put_bool("ChestnutActive", False)
assert small_model is not None
model = small_model
if chestnut_state is not None:
@@ -419,18 +463,17 @@ def main(demo=False):
fill_model_msg(modelv2_send, model_output, action,
publish_state, meta_main.frame_id, meta_extra.frame_id, frame_id,
frame_drop_ratio, meta_main.timestamp_eof, model_execution_time, extrinsics_calibration_seen)
modelv2_send.modelV2.big = model.usbgpu
modelv2_send.modelV2.big = model.chestnut
desire_state = modelv2_send.modelV2.meta.desireState
l_lane_change_prob = desire_state[log.Desire.laneChangeLeft]
r_lane_change_prob = desire_state[log.Desire.laneChangeRight]
lane_change_prob = l_lane_change_prob + r_lane_change_prob
DH.update(sm['carState'], sm['carControl'].latActive, lane_change_prob)
modelv2_send.modelV2.meta.laneChangeState = DH.lane_change_state
modelv2_send.modelV2.meta.laneChangeDirection = DH.lane_change_direction
mdv2sp_send = messaging.new_message('modelDataV2SP')
left_edge, right_edge = RELC.update_and_fill(modelv2_send.modelV2, mdv2sp_send.modelDataV2SP, v_ego)
DH.update(sm['carState'], sm['carControl'].latActive, lane_change_prob, left_edge, right_edge)
modelv2_send.modelV2.meta.laneChangeState = DH.lane_change_state
modelv2_send.modelV2.meta.laneChangeDirection = DH.lane_change_direction
mdv2sp_send.modelDataV2SP.laneTurnDirection = DH.lane_turn_direction
fill_driving_model_data(drivingdata_send, modelv2_send)
@@ -441,10 +484,6 @@ def main(demo=False):
pm.send('modelDataV2SP', mdv2sp_send)
last_vipc_frame_id = meta_main.frame_id
if chestnut_state is not None and run_count % round(ModelConstants.MODEL_RUN_FREQ / SERVICE_LIST['chestnutState'].frequency) == 0:
chestnut_state.send()
if __name__ == "__main__":
try:
import argparse
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:a501760a9d1d5fef0eab2b8c5d122d06124fc26dc8e0782e0aa94b82a208f0ff
size 1757355221
oid sha256:1791d5940b2c048d0639813426dd2cf1d6f2a6727ed51e17c8bcea8bbe754123
size 765950064
+10 -10
View File
@@ -123,22 +123,22 @@ void fill_panda_state(cereal::PandaState::Builder &ps, cereal::PandaState::Panda
ps.setUptime(health.uptime_pkt);
ps.setSafetyTxBlocked(health.safety_tx_blocked_pkt);
ps.setSafetyRxInvalid(health.safety_rx_invalid_pkt);
ps.setIgnitionLine(health.ignition_line_pkt);
ps.setIgnitionCan(health.ignition_can_pkt);
ps.setControlsAllowed(health.controls_allowed_pkt);
ps.setIgnitionLine((health.flags_pkt & HEALTH_FLAG_IGNITION_LINE) != 0U);
ps.setIgnitionCan((health.flags_pkt & HEALTH_FLAG_IGNITION_CAN) != 0U);
ps.setControlsAllowed((health.flags_pkt & HEALTH_FLAG_CONTROLS_ALLOWED) != 0U);
ps.setTxBufferOverflow(health.tx_buffer_overflow_pkt);
ps.setRxBufferOverflow(health.rx_buffer_overflow_pkt);
ps.setPandaType(hw_type);
ps.setSafetyModel(cereal::CarParams::SafetyModel(health.safety_mode_pkt));
ps.setSafetyParam(health.safety_param_pkt);
ps.setFaultStatus(cereal::PandaState::FaultStatus(health.fault_status_pkt));
ps.setPowerSaveEnabled((bool)(health.power_save_enabled_pkt));
ps.setHeartbeatLost((bool)(health.heartbeat_lost_pkt));
ps.setPowerSaveEnabled((health.flags_pkt & HEALTH_FLAG_POWER_SAVE_ENABLED) != 0U);
ps.setHeartbeatLost((health.flags_pkt & HEALTH_FLAG_HEARTBEAT_LOST) != 0U);
ps.setAlternativeExperience(health.alternative_experience_pkt);
ps.setHarnessStatus(cereal::PandaState::HarnessStatus(health.car_harness_status_pkt));
ps.setInterruptLoad(health.interrupt_load_pkt);
ps.setInterruptLoad(health.interrupt_load_pkt / 255.0f);
ps.setFanPower(health.fan_power);
ps.setSafetyRxChecksInvalid((bool)(health.safety_rx_checks_invalid_pkt));
ps.setSafetyRxChecksInvalid((health.flags_pkt & HEALTH_FLAG_SAFETY_RX_CHECKS_INVALID) != 0U);
ps.setSpiErrorCount(health.spi_error_count_pkt);
ps.setSbu1Voltage(health.sbu1_voltage_mV / 1000.0f);
ps.setSbu2Voltage(health.sbu2_voltage_mV / 1000.0f);
@@ -198,10 +198,10 @@ std::optional<bool> send_panda_states(PubMaster *pm, Panda *panda, bool is_onroa
}
if (spoofing_started) {
health.ignition_line_pkt = 1;
health.flags_pkt |= HEALTH_FLAG_IGNITION_LINE;
}
bool ignition_local = ((health.ignition_line_pkt != 0) || (health.ignition_can_pkt != 0)) && !always_offroad;
bool ignition_local = ((health.flags_pkt & (HEALTH_FLAG_IGNITION_LINE | HEALTH_FLAG_IGNITION_CAN)) != 0U) && !always_offroad;
// Make sure CAN buses are live: safety_setter_thread does not work if Panda CAN are silent and there is only one other CAN node
if (health.safety_mode_pkt == (uint8_t)(cereal::CarParams::SafetyModel::SILENT)) {
@@ -209,7 +209,7 @@ std::optional<bool> send_panda_states(PubMaster *pm, Panda *panda, bool is_onroa
}
bool power_save_desired = !ignition_local;
if (health.power_save_enabled_pkt != power_save_desired) {
if (((health.flags_pkt & HEALTH_FLAG_POWER_SAVE_ENABLED) != 0U) != power_save_desired) {
panda->set_power_saving(power_save_desired);
}
@@ -19,6 +19,30 @@
},
"Offroad_ChestnutBranch": {
"text": "Chestnut detected! Switch to the %1 branch to use chestnut-class models.",
"severity": -1
},
"Offroad_ChestnutNotDetected": {
"text": "Chestnut not detected. Check USB and 12V connections.",
"severity": 0
},
"Offroad_ChestnutOverheated": {
"text": "Chestnut overheated. Ensure good airflow. Current GPU temperature is %1.",
"severity": 0
},
"Offroad_ChestnutPcieUnavailable": {
"text": "%1",
"severity": 0
},
"Offroad_ChestnutUncompiled": {
"text": "Chestnut model not compiled. Keep ignition on and reboot the comma.",
"severity": 0
},
"Offroad_ChestnutUpdateFailed": {
"text": "Chestnut update failed. Check the USB cable.",
"severity": 0
},
"Offroad_ChestnutUsbSlow": {
"text": "Chestnut USB link is slow. Check the USB cable. The current speed is %1.",
"severity": 0
},
"Offroad_UnregisteredHardware": {
+71 -5
View File
@@ -32,7 +32,14 @@ from openpilot.sunnypilot.selfdrive.car.car_specific import CarSpecificEventsSP
from openpilot.sunnypilot.selfdrive.car.cruise_helpers import CruiseHelper
from openpilot.sunnypilot.selfdrive.car.intelligent_cruise_button_management.controller import IntelligentCruiseButtonManagement
from openpilot.sunnypilot.selfdrive.selfdrived.button_state_tracker import ButtonStateTracker
from openpilot.sunnypilot.selfdrive.selfdrived.assisted_driving_milestones import (
AssistCategory,
AssistedDrivingMilestones,
MilestoneEvent,
MilestoneStore,
)
from openpilot.sunnypilot.selfdrive.selfdrived.events import EventsSP
from openpilot.sunnypilot.system.statsd import statlog
REPLAY = "REPLAY" in os.environ
SIMULATION = "SIMULATION" in os.environ
@@ -88,7 +95,8 @@ class SelfdriveD(CruiseHelper):
self.big_model_ready_t = 0.
# Setup sockets
self.pm = messaging.PubMaster(['selfdriveState', 'onroadEvents'] + ['selfdriveStateSP', 'onroadEventsSP'])
self.pm = messaging.PubMaster(['selfdriveState', 'onroadEvents'] +
['selfdriveStateSP', 'onroadEventsSP', 'assistedDrivingMilestoneState'])
self.gps_location_service = get_gps_location_service(self.params)
self.gps_packets = [self.gps_location_service]
@@ -127,6 +135,7 @@ class SelfdriveD(CruiseHelper):
self.params.remove("ExperimentalMode")
self.CS_prev = car.CarState.new_message()
self.car_state_log_mono_time = 0
self.AM = AlertManager()
self.events = Events()
@@ -137,6 +146,11 @@ class SelfdriveD(CruiseHelper):
self.cruise_mismatch_counter = 0
self.last_steering_pressed_frame = 0
self.distance_traveled = 0
self.assisted_driving_milestones = AssistedDrivingMilestones(MilestoneStore(self.params))
self.assisted_driving_milestones_enabled = bool(self.params.get("AssistedDrivingMilestonesEnabled", return_default=True))
self.assisted_driving_milestone_drive_id = ""
self._milestone_event: MilestoneEvent | None = None
self._milestone_event_expires_ns = 0
self.last_functional_fan_frame = 0
self.events_prev = []
self.logged_comm_issue = None
@@ -195,17 +209,18 @@ class SelfdriveD(CruiseHelper):
self.events.add(EventName.joystickDebug)
self.startup_event = None
loading = self.params.get_bool("UsbGpuLoading")
loading = self.params.get_bool("ChestnutLoading")
if self.big_model_loading and not loading:
self.big_model_ready_t = time.monotonic()
self.events_sp.add(custom.OnroadEventSP.EventName.bigModelReady)
self.big_model_loading = loading
if self.big_model_loading:
self.events.add(EventName.bigModelLoading)
big_active = self.params.get("UsbGpuActive")
usbgpu_present = self.sm['deviceState'].chestnutPresent
big_active = self.params.get("ChestnutActive")
chestnut_present = self.sm['deviceState'].chestnutPresent
model_unavailable = big_active is True and self.sm.seen['modelV2'] and not self.sm.alive['modelV2']
big_failed = big_active is False or model_unavailable or (self.big_model_active and not usbgpu_present)
big_failed = big_active is False or model_unavailable or (self.big_model_active and not chestnut_present)
if big_failed and not self.big_model_failed:
self.events.add(EventName.bigModelFailed)
self.big_model_failed = big_failed
@@ -527,6 +542,8 @@ class SelfdriveD(CruiseHelper):
def data_sample(self):
_car_state = messaging.recv_one(self.car_state_sock)
CS = _car_state.carState if _car_state else self.CS_prev
if _car_state is not None:
self.car_state_log_mono_time = _car_state.logMonoTime
self.sm.update(0)
@@ -645,6 +662,31 @@ class SelfdriveD(CruiseHelper):
self.pm.send('onroadEventsSP', ce_send_sp)
self.events_sp_prev = self.events_sp.names.copy()
def publish_assisted_driving_milestones(self, now_ns: int, event: MilestoneEvent | None) -> None:
if event is not None:
self._milestone_event = event
self._milestone_event_expires_ns = now_ns + 1_000_000_000
elif now_ns >= self._milestone_event_expires_ns:
self._milestone_event = None
if event is None and self.sm.frame % 10 != 0:
return
snapshot = self.assisted_driving_milestones.snapshot()
msg = messaging.new_message("assistedDrivingMilestoneState")
msg.valid = True
state = msg.assistedDrivingMilestoneState
state.enabled = self.assisted_driving_milestones_enabled
state.madsDistanceMeters = snapshot.distances_meters[AssistCategory.MADS]
state.fullAssistDistanceMeters = snapshot.distances_meters[AssistCategory.FULL_ASSIST]
if self._milestone_event is not None:
state.event.id = self._milestone_event.event_id
state.event.category = self._milestone_event.category.value
state.event.distanceMeters = self._milestone_event.distance_meters
state.event.previousDistanceMeters = self._milestone_event.previous_distance_meters
state.event.unit = self._milestone_event.unit.value
self.pm.send("assistedDrivingMilestoneState", msg)
def step(self):
CS = self.data_sample()
self.update_events(CS)
@@ -654,6 +696,28 @@ class SelfdriveD(CruiseHelper):
self.mads.update(CS)
self.update_alerts(CS)
now_ns = time.monotonic_ns()
if not self.assisted_driving_milestone_drive_id:
self.assisted_driving_milestone_drive_id = self.params.get("CurrentRoute") or ""
self.assisted_driving_milestones.set_drive_id(self.assisted_driving_milestone_drive_id)
car_control = self.sm['carControl']
milestone_event = self.assisted_driving_milestones.update(
self.car_state_log_mono_time,
CS.vEgo,
lat_active=car_control.latActive,
long_active=car_control.longActive,
is_metric=self.is_metric,
enabled=self.assisted_driving_milestones_enabled,
)
if milestone_event is not None:
cloudlog.event("assisted_driving_milestone_reached",
event_id=milestone_event.event_id,
category=milestone_event.category.value,
distance_meters=milestone_event.distance_meters)
statlog.gauge(f"assisted_driving_milestone.{milestone_event.category.value}.meters",
milestone_event.distance_meters)
self.publish_assisted_driving_milestones(now_ns, milestone_event)
self.button_state_tracker.update(CS)
self.publish_selfdriveState(CS)
@@ -666,6 +730,7 @@ class SelfdriveD(CruiseHelper):
self.disengage_on_accelerator = self.params.get_bool("DisengageOnAccelerator")
self.experimental_mode = self.params.get_bool("ExperimentalMode") and self.CP.openpilotLongitudinalControl
self.personality = self.params.get("LongitudinalPersonality", return_default=True)
self.assisted_driving_milestones_enabled = bool(self.params.get("AssistedDrivingMilestonesEnabled", return_default=True))
self.mads.read_params()
time.sleep(0.1)
@@ -679,6 +744,7 @@ class SelfdriveD(CruiseHelper):
self.step()
self.rk.monitor_time()
finally:
self.assisted_driving_milestones.close()
e.set()
t.join()
@@ -152,7 +152,7 @@ def migrate_drivingModelData(msgs):
add_ops = []
for _, msg in msgs:
dmd = messaging.new_message('drivingModelData', valid=msg.valid, logMonoTime=msg.logMonoTime)
for field in ["frameId", "frameIdExtra", "frameDropPerc", "modelExecutionTime", "action"]:
for field in ["frameId", "frameIdExtra", "frameDropPerc", "modelExecutionTime", "big", "action"]:
setattr(dmd.drivingModelData, field, getattr(msg.modelV2, field))
for meta_field in ["laneChangeState", "laneChangeState"]:
setattr(dmd.drivingModelData.meta, meta_field, getattr(msg.modelV2.meta, meta_field))
@@ -33,9 +33,9 @@ MODEL_REPLAY_BUCKET="model_replay_master"
GITHUB = GithubUtils(API_TOKEN, DATA_TOKEN)
EXEC_TIMINGS = [
# model, instant max, average max
("modelV2", 0.05, 0.028),
("driverStateV2", 0.05, 0.018),
# model, instant max, average max, chestnut average max
("modelV2", 0.05, 0.03, 0.05),
("driverStateV2", 0.05, 0.018, 0.018),
]
def get_log_fn(test_route, ref="master"):
@@ -169,11 +169,13 @@ def model_replay(lr, frs):
dmonitoringmodeld_msgs = replay_process(dmonitoringmodeld, dmodeld_logs, frs)
msgs = modeld_msgs + dmonitoringmodeld_msgs
chestnut = any(m.modelV2.big for m in modeld_msgs if m.which() == "modelV2")
header = ['model', 'max instant', 'max instant allowed', 'average', 'max average allowed', 'test result']
rows = []
timings_ok = True
for (s, instant_max, avg_max) in EXEC_TIMINGS:
for (s, instant_max, avg_max, chestnut_avg_max) in EXEC_TIMINGS:
avg_max = chestnut_avg_max if chestnut else avg_max
ts = [getattr(m, s).modelExecutionTime for m in msgs if m.which() == s]
# TODO some init can happen in first iteration
ts = ts[1:]
@@ -1,7 +1,7 @@
import time
import pyray as rl
from openpilot.system.ui.lib.application import gui_app, FontWeight
from openpilot.system.ui.lib.application import gui_app, FontWeight, TextAlignment, TextAlignmentVertical
from openpilot.system.ui.widgets import Widget
from openpilot.system.ui.widgets.label import UnifiedLabel
from openpilot.selfdrive.ui.ui_state import ui_state
@@ -26,8 +26,8 @@ class BodyLayout(Widget):
self._last_input_time = time.monotonic()
self._was_active = False
self._offroad_label = UnifiedLabel("turn on ignition to use", 95 if gui_app.big_ui() else 45, FontWeight.DISPLAY,
alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER,
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE)
alignment=TextAlignment.CENTER,
alignment_vertical=TextAlignmentVertical.MIDDLE)
def draw_dot_grid(self, rect: rl.Rectangle, dots: list[tuple[int, int]], color: rl.Color):
spacing = min(rect.height / GRID_ROWS, rect.width / GRID_COLS)
+2 -2
View File
@@ -8,7 +8,7 @@ from openpilot.selfdrive.ui.widgets.exp_mode_button import ExperimentalModeButto
from openpilot.selfdrive.ui.widgets.prime import PrimeWidget
from openpilot.selfdrive.ui.widgets.setup import SetupWidget
from openpilot.system.ui.lib.text_measure import measure_text_cached
from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos
from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos, TextAlignment
from openpilot.system.ui.lib.multilang import tr, trn
from openpilot.system.ui.widgets.label import gui_label
from openpilot.system.ui.widgets import Widget
@@ -178,7 +178,7 @@ class HomeLayout(Widget):
version_rect = rl.Rectangle(self.header_rect.x + self.header_rect.width - version_text_width, self.header_rect.y,
version_text_width, self.header_rect.height)
gui_label(version_rect, self._version_text, 48, rl.WHITE, alignment=rl.GuiTextAlignment.TEXT_ALIGN_RIGHT)
gui_label(version_rect, self._version_text, 48, rl.WHITE, alignment=TextAlignment.RIGHT)
def _render_home_content(self):
self._render_left_column()
+4 -4
View File
@@ -5,7 +5,7 @@ from enum import IntEnum
import pyray as rl
from openpilot.common.basedir import BASEDIR
from openpilot.system.ui.lib.application import FontWeight, gui_app
from openpilot.system.ui.lib.application import FontWeight, TextAlignment, gui_app
from openpilot.system.ui.lib.multilang import tr
from openpilot.system.ui.widgets import Widget
from openpilot.system.ui.widgets.button import Button, ButtonStyle
@@ -115,9 +115,9 @@ class TermsPage(Widget):
self._on_accept = on_accept
self._on_decline = on_decline
self._title = Label(tr("Welcome to sunnypilot"), font_size=90, font_weight=FontWeight.BOLD, text_alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT)
self._title = Label(tr("Welcome to sunnypilot"), font_size=90, font_weight=FontWeight.BOLD, text_alignment=TextAlignment.LEFT)
self._desc = Label(tr("You must accept the Terms of Service to use sunnypilot. Read the latest terms at https://sunnypilot.ai/terms before continuing."),
font_size=90, font_weight=FontWeight.MEDIUM, text_alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT)
font_size=90, font_weight=FontWeight.MEDIUM, text_alignment=TextAlignment.LEFT)
self._decline_btn = Button(tr("Decline"), click_callback=on_decline)
self._accept_btn = Button(tr("Agree"), button_style=ButtonStyle.PRIMARY, click_callback=on_accept)
@@ -150,7 +150,7 @@ class DeclinePage(Widget):
def __init__(self, back_callback=None):
super().__init__()
self._text = Label(tr("You must accept the Terms of Service in order to use sunnypilot."),
font_size=90, font_weight=FontWeight.MEDIUM, text_alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT)
font_size=90, font_weight=FontWeight.MEDIUM, text_alignment=TextAlignment.LEFT)
self._back_btn = Button(tr("Back"), click_callback=back_callback)
self._uninstall_btn = Button(tr("Decline, uninstall sunnypilot"), button_style=ButtonStyle.DANGER,
click_callback=self._on_uninstall_clicked)
@@ -199,6 +199,9 @@ class SoftwareLayout(Widget):
selection = self._branch_dialog.selection
ui_state.params.put("UpdaterTargetBranch", selection, block=True)
self._branch_btn.action_item.set_value(selection)
self._download_btn.action_item.set_enabled(False)
self._waiting_for_updater = True
self._waiting_start_ts = time.monotonic()
subprocess.run("pkill -SIGUSR1 -f openpilot.system.updated.updated", shell=True)
self._branch_dialog = None
+8 -1
View File
@@ -168,9 +168,16 @@ class Sidebar(Widget, SidebarSP):
# Home/Flag button
flag_pressed = mouse_down and rl.check_collision_point_rec(mouse_pos, HOME_BTN)
button_img = self._flag_img if ui_state.started else self._home_img
button_pos = rl.Vector2(HOME_BTN.x, HOME_BTN.y)
icon_opacity = 1.0
if gui_app.sunnypilot_ui():
button_img, button_pos, icon_opacity = SidebarSP._get_home_icon(self, button_img)
tint = Colors.BUTTON_PRESSED if (ui_state.started and flag_pressed) else Colors.BUTTON_NORMAL
rl.draw_texture_ex(button_img, rl.Vector2(HOME_BTN.x, HOME_BTN.y), 0.0, 1.0, tint)
if icon_opacity < 1.0:
tint = rl.Color(tint[0], tint[1], tint[2], int(255 * icon_opacity))
rl.draw_texture_ex(button_img, button_pos, 0.0, 1.0, tint)
# Microphone button
if self._recording_audio:
+27 -11
View File
@@ -1,4 +1,5 @@
import datetime
import math
import time
from openpilot.cereal import log
@@ -8,8 +9,8 @@ from openpilot.system.ui.widgets import Widget
from openpilot.system.ui.widgets.layouts import HBoxLayout
from openpilot.system.ui.widgets.icon_widget import IconWidget
from openpilot.system.ui.widgets.label import UnifiedLabel, gui_label
from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos
from openpilot.selfdrive.ui.ui_state import ui_state
from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos, TextAlignment, TextAlignmentVertical
from openpilot.selfdrive.ui.ui_state import ui_state, ChestnutState
from openpilot.common.version import RELEASE_BRANCHES
HEAD_BUTTON_FONT_SIZE = 40
@@ -69,8 +70,8 @@ class AlertsPill(Widget):
count_rect = rl.Rectangle(self.rect.x + self.COUNT_OFFSET, self.rect.y, pill_w - self.COUNT_OFFSET, pill_h)
gui_label(count_rect, str(alert_count), font_size=36,
alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER,
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE)
alignment=TextAlignment.CENTER,
alignment_vertical=TextAlignmentVertical.MIDDLE)
class NetworkIcon(Widget):
@@ -139,8 +140,10 @@ class MiciHomeLayout(Widget):
self._version_text = self._get_version_text()
self._experimental_icon = IconWidget("icons_mici/experimental_mode.png", (48, 48))
self._egpu_icon = IconWidget("icons_mici/egpu_green.png", (50, 37))
self._egpu_icon_gray = IconWidget("icons_mici/egpu_gray.png", (50, 37))
self._usb_icon = IconWidget("icons_mici/usb.png", (62, 40))
self._chestnut_icon = IconWidget("icons_mici/chestnut_green.png", (68, 40))
self._chestnut_loading_icon = IconWidget("icons_mici/chestnut.png", (68, 40))
self._chestnut_failed_icon = IconWidget("icons_mici/chestnut_orange.png", (68, 40))
self._mic_icon = IconWidget("icons_mici/microphone.png", (32, 46))
self._body_icon = IconWidget("icons_mici/body.png", (54, 37))
@@ -150,13 +153,15 @@ class MiciHomeLayout(Widget):
IconWidget("icons_mici/settings.png", (48, 48), opacity=0.9),
NetworkIcon(),
self._experimental_icon,
self._egpu_icon,
self._egpu_icon_gray,
self._usb_icon,
self._chestnut_icon,
self._chestnut_loading_icon,
self._chestnut_failed_icon,
self._body_icon,
self._mic_icon,
], spacing=18)
self._openpilot_label = UnifiedLabel("sunnypilot", font_size=96, font_weight=FontWeight.DISPLAY, max_width=480, wrap_text=False)
self._openpilot_label = UnifiedLabel("openpilot", font_size=96, font_weight=FontWeight.DISPLAY, max_width=480, wrap_text=False)
self._version_label = UnifiedLabel("", font_size=36, font_weight=FontWeight.ROMAN, max_width=480, wrap_text=False)
self._large_version_label = UnifiedLabel("", font_size=64, text_color=rl.GRAY, font_weight=FontWeight.ROMAN, max_width=480, wrap_text=False)
self._date_label = UnifiedLabel("", font_size=36, text_color=rl.GRAY, font_weight=FontWeight.ROMAN, max_width=480, wrap_text=False)
@@ -247,9 +252,20 @@ class MiciHomeLayout(Widget):
self._version_commit_label.render()
# ***** Center-aligned bottom section icons *****
usb_connected = ui_state.usb_connected
usb_unknown = ui_state.usb_unknown
chestnut_state = ui_state.chestnut_state
self._experimental_icon.set_visible(ui_state.experimental_mode)
self._egpu_icon.set_visible(ui_state.sm["deviceState"].chestnutPresent and ui_state.usbgpu_compiled)
self._egpu_icon_gray.set_visible(ui_state.sm["deviceState"].chestnutPresent and not ui_state.usbgpu_compiled)
if gui_app.sunnypilot_ui():
self._set_chestnut_visibility()
else:
self._usb_icon.set_visible(usb_connected and usb_unknown)
self._chestnut_icon.set_visible(not usb_unknown and chestnut_state not in
(ChestnutState.LOADING, ChestnutState.UNCOMPILED, ChestnutState.FAILED) and
(usb_connected or chestnut_state in (ChestnutState.READY, ChestnutState.ACTIVE)))
self._chestnut_loading_icon.set_visible(not usb_unknown and chestnut_state == ChestnutState.LOADING)
self._chestnut_loading_icon.set_opacity(0.35 + 0.65 * (0.5 - 0.5 * math.cos(rl.get_time() * 6.0)))
self._chestnut_failed_icon.set_visible(not usb_unknown and chestnut_state in (ChestnutState.UNCOMPILED, ChestnutState.FAILED))
self._mic_icon.set_visible(ui_state.recording_audio)
self._body_icon.set_visible(bool(ui_state.is_body))

Some files were not shown because too many files have changed in this diff Show More