Compare commits

..

123 Commits

Author SHA1 Message Date
discountchubbs cf96d4edbd Sync: commaai/openpilot:master -> sunnypilot/sunnypilot:master
# Conflicts:
#	.gitmodules
#	.lfsconfig
#	opendbc_repo
#	openpilot/selfdrive/modeld/SConscript
#	openpilot/selfdrive/modeld/modeld.py
#	openpilot/selfdrive/ui/mici/layouts/settings/toggles.py
#	openpilot/system/sentry.py
#	panda
#	scripts/lint/lint.sh
#	tinygrad_repo
#	tools/release/build_stripped.sh
#	tools/release/check-submodules.sh
#	uv.lock
2026-09-16 22:41:24 -07:00
Harald Schäfer 6080cc6168 Use a precompiled eGPU driving model (#38930)
* Ship precompiled eGPU model and camera warps

Compile f78ed37d-afad-4dbc-8050-40ea885eedde/12864 through xx/ml_tools/openpilot_compile using the pinned tinygrad version.

* Precompile the existing master driving model

Use the unchanged master ONNX (SHA-256 6fee5937923c74848df4a63f6239eb6331c6274dd4bdb7a5d6ec0388a8b543d5) instead of updating the trained model.

* Compile camera warps on device

* Remove obsolete ONNX chunking and big model build check

* Chunk model artifacts only during release packaging

* Require model and camera warps for Chestnut readiness

* Recompile precompiled CPU helpers for the runtime host

* Ship the eGPU model with an ARM submission helper

* Exempt model pickles from the build product size limit
2026-09-16 08:13:45 -07:00
Harald Schäfer 81ae1a2e2d Use tinygrad generic ONNX compiler artifacts (#38926)
* Use tinygrad generic ONNX compiler artifacts

* Keep compiler temporary model buffers off device tmpfs

* Keep recurrent model state updates on the GPU

* Use compiled output buffers and swap recurrent state inputs

* Note when ONNX chunk reassembly can be removed

* Bump tinygrad to rebased compilers and AMD queue fix

* Keep recurrent buffers fixed and use faster USB argument updates

* Bump tinygrad for faster model startup
2026-09-15 18:01:25 -07:00
Daniel Koepping f15544b482 rm pyserial from updater bundle (#38925)
rm pyserial dependency from updater package
2026-09-15 13:20:49 -07:00
Harald Schäfer b751b04cda Use tinygrad ONNX and warp compilers for driving and DM (#38922)
* modeld: use tinygrad ONNX and warp compilers for driving and DM

* Bump tinygrad with current openpilot model compile tests

* modeld: prioritize tinygrad compiler imports in build environment

* Use compiler branch based on openpilot's pinned tinygrad

* modeld: invoke tinygrad compilers without firmware patch
2026-09-15 11:21:47 -07:00
Harald Schäfer fa9f56ed76 Compile warp and onnx separately (#38864)
* Extract shared model warp routines

* Move DM warp compilation into compile_warp

* Configure shared warp compilation through SCons flags

* Move the driving warp wrapper into compile_warp

* Build driving warps separately from the model

* Compile model inputs directly from ONNX

* modeld: simplify split input setup

* modeld: extract input packing setup

* modeld: simplify packed views and share firmware setup
2026-09-14 23:07:26 -07:00
Harald Schäfer 64f9b47b6e modeld: move history queues into ONNX (#38916)
* modeld: move history queues into ONNX

* Move ONNX history export into xx exporter

* Rename ONNX image input to new_img

* Combine ONNX model history state

* Revert model history README addition

* Keep float32 input packing from master

* Rename model input specs to input shapes
2026-09-14 20:44:33 -07:00
Harald Schäfer 7b469af438 ci: run docs builds on Namespace (#38921) 2026-09-14 20:21:16 -07:00
Harald Schäfer 2c88d1ed64 Revert "bump msgq (#38836)" (#38920)
This reverts commit 1e0914051f.
2026-09-14 20:20:53 -07:00
stef ffef0e6d3b ui: custom alert icons (#38917)
* custom alerts and alert pill better

* add comment back

* fix
2026-09-14 20:03:25 -07:00
stef e6aa405e08 ui: fix touch validity on alerts menu (#38915)
fix touch validity on alerts menu
2026-09-14 17:14:38 -07:00
Matt Purnell 2689917757 locationd: fix critical_services spelling (#38912) 2026-09-14 10:43:43 -07:00
Trey Moen 5f50bda7ba cabana: improve signal panel layout (#38910)
* cabana: improve signal panel layout

* cabana: match row actions to selection colors

* cabana: preserve master remove action

* cabana: match remove icon to selected row
2026-09-13 17:06:52 -07:00
Trey Moen 5b34f151de cabana: balance signal action spacing (#38911) 2026-09-13 17:01:04 -07:00
Trey Moen 64e5d1e5e3 cabana: preserve selected menu color on hover (#38909)
* cabana: unify menu highlight colors

* cabana: preserve selected menu color on hover
2026-09-13 16:54:34 -07:00
Trey Moen d3106c2b4b cabana: unify video and live stream titles (#38908)
* cabana: unify video and live stream titles

* cabana: put stream before dbc in window title

* cabana: show fingerprint in route details
2026-09-13 16:43:53 -07:00
Trey Moen 885792de3f cabana: remove message table outer border (#38907) 2026-09-13 16:36:16 -07:00
Trey Moen 5319474ee5 cabana: disable tool window collapse (#38906) 2026-09-13 16:31:19 -07:00
Trey Moen 164a3a1e30 cabana: stop chart tooltips over menus (#38905)
* cabana: keep chart tooltips below menus

* cabana: exclude popup menus from plot hover
2026-09-13 16:27:40 -07:00
Trey Moen c715ef4762 cabana: flatten signal panel containers (#38904)
* cabana: flatten signal panel containers

* cabana: keep messages and logs switch border
2026-09-13 16:15:25 -07:00
Trey Moen 2ae3598b51 cabana: prevent signal panel collapse (#38903)
* cabana: keep signal scrolling inside the list

* cabana: reserve space for the signal panel
2026-09-13 15:56:21 -07:00
Trey Moen 4e60afe70d cabana: show persistent fps counter (#38898)
* cabana: show persistent UI FPS

* cabana: render whole fps in monospace

* cabana: keep fps aligned beside centered progress

* cabana: move fps to top right

* Revert "cabana: move fps to top right"

This reverts commit 51a5334aec79807f8102b9a6f1f902f22814d5c7.
2026-09-13 15:48:35 -07:00
Trey Moen 5c365d3398 cabana: remove inner video container (#38901) 2026-09-13 15:46:29 -07:00
Trey Moen e6f9b3c6c4 cabana: preserve menu bar border on highlight (#38902)
* cabana: preserve menu bar border on highlight

* cabana: remove border drawing comment
2026-09-13 15:46:14 -07:00
Trey Moen 94f71fce00 cabana: add browser sign-in for remote routes (#38894)
* cabana: add browser sign-in for remote routes

* cabana: use shared dialog styling for browser sign-in

* cabana: support spacious centered action buttons

* cabana: vertically center sign-in states

* cabana: keep sign-in layout stable between states

* cabana: center sign-in error text

* cabana: avoid hint flash when retrying sign-in
2026-09-13 15:23:22 -07:00
Trey Moen b6918cb31b cabana: fix download bar layout (#38900)
* cabana: fix download bar layout

* cabana: reduce download label font size
2026-09-13 15:22:13 -07:00
Trey Moen 11093e743e cabana: improve dark slider track contrast (#38899) 2026-09-13 14:44:13 -07:00
Trey Moen ebb202f29d tools: share cancellable browser sign-in (#38893)
* tools: share cancellable browser sign-in

* tools: invoke auth JSON mode directly from Cabana

* tools: require an explicit Python module

* tools: order module constants alphabetically

* tools: use shared unauthorized handling for devices
2026-09-13 14:35:46 -07:00
Trey Moen 9c054e285d replay: retry failed segment loads (#38897)
* replay: retry failed segment loads

* replay: rename test helper for spellcheck

* replay: include portable temporary directory declaration

* replay: name attempt limit and shorten test delays

* replay: drop timing-dependent retry tests
2026-09-13 14:19:59 -07:00
Trey Moen c5cf29cf5e cabana: contain tooltips and capture plot drags (#38896)
* cabana: contain chart tooltips within columns

* cabana: refresh tooltip bounds after chart resize

* cabana: capture plot drags to prevent window movement
2026-09-13 13:53:35 -07:00
Trey Moen 45a7747ca3 cabana: fix detached panel menus (#38890)
* cabana: keep dropdowns above detached panels and within the screen

* cabana: shorten dropdown comments
2026-09-13 09:46:43 -07:00
Harald Schäfer 064a51fe59 model replay: use a mici segment (#38878)
* model replay: use a mici segment

* process replay: wait for vision listener startup
2026-09-13 09:38:23 -07:00
Trey Moen e5a6a8b73e cabana: make the Signals pane a native dock panel, fix pad (#38889)
* cabana: align center tabs and keep signal panel docked

* cabana: prevent panels from docking into the tabless center

* cabana: use native docking for the Signals panel

* cabana: migrate dock tabs when loading the layout
2026-09-13 09:19:48 -07:00
Daniel Koepping d38264c42c log chestnut telemetry in hardwared (#38866)
log chestnut hardware state independently of modeld
2026-09-12 23:57:49 -07:00
Trey Moen 0d4a4ab910 cabana: disable collapse for dockable panels (#38885) 2026-09-12 23:48:57 -07:00
Trey Moen 44914a7cf4 cabana: remove custom focus-loss handling (#38884)
* cabana: fix focus handling across docked and floating windows

* cabana: remove custom focus-loss handling
2026-09-12 22:45:32 -07:00
Trey Moen 4c0799eb8e cabana: make Charts a native dockable panel (#38883)
* cabana: make charts a native dockable panel

* cabana: remove redundant charts docking button
2026-09-12 21:40:31 -07:00
Trey Moen 1aa97338c4 Cabana: standardize floating dropdown menus (#38874)
* Cabana: standardize floating dropdown menus

* Cabana: use uniform items in menus and combo lists

* Cabana: remove dropdown test and explanatory comments
2026-09-12 20:56:08 -07:00
Trey Moen 20064d9681 cabana: show placeholder for unresolved timestamps (#38870) 2026-09-12 20:46:51 -07:00
Trey Moen dc338c2831 cabana: standardize button spacing (#38873)
* cabana: standardize button spacing and control alignment

* cabana: center shared step controls and inset chart content

* cabana: use uniform per-chart content insets
2026-09-12 20:37:01 -07:00
Daniel Koepping 298e51010c print jenkins crash log (#38880) 2026-09-12 19:48:31 -07:00
Trey Moen 7e84a61668 cabana: use ImGui decorations for floating panes (#38871)
* cabana: redraw floating panes during native resizing

* cabana: keep rendering while native resize is held

* cabana: use ImGui decorations to avoid native resize loops

* cabana: draw docking previews only on the target viewport
2026-09-12 17:45:14 -07:00
Trey Moen f147094cb7 cabana: restore Qt palettes and contrast (#38875)
* cabana: restore Qt dark palette and contrast

* cabana: restore Qt standard light palette and selection text

* cabana: drive component colors from shared style values

* cabana: remove historical palette comments
2026-09-12 17:44:43 -07:00
Adeeb Shihadeh f235784042 Revert tinygrad update (#38879) 2026-09-12 17:26:45 -07:00
Harsh Singh 39146cb240 test_runner: collect parameterized_class tests instead of skipping them with their base (#38820) 2026-09-12 14:30:51 -07:00
commaci-public 19f0f69d85 [bot] Update Python packages (#38698)
* Update Python packages

* Fix pandad exports

---------

Co-authored-by: Vehicle Researcher <user@comma.ai>
Co-authored-by: Adeeb Shihadeh <adeebshihadeh@gmail.com>
2026-09-12 14:28:19 -07:00
Adeeb Shihadeh 64b4dbf115 add shellcheck-like static analysis (#38877) 2026-09-12 14:14:39 -07:00
Kacper Rączy ad5c0b946d tools: optimize route parsing (#38714) 2026-09-12 14:13:08 -07:00
David 5cad99bf7a fix(tools): allow spaces in workspace path for op.sh (#38839) 2026-09-12 14:06:20 -07:00
Adeeb Shihadeh f1ba169fb5 remove old profiler setups 2026-09-12 13:45:51 -07:00
Daniel Koepping dd226ac791 update chestnut fw in CI (#38876)
Update Chestnut firmware before CI build
2026-09-12 12:41:14 -07:00
Adeeb Shihadeh 0cf294d85f ui: add descriptions for settings (#38867) 2026-09-11 20:13:00 -07:00
Daniel Koepping 6965492e66 clean up after jenkins disconnect (#38865)
* fix SSH build cleanup

* simplify SSH cleanup
2026-09-11 17:25:33 -07:00
Bhavya Gada 73720bfd08 Get MetaDrive simulator working on macOS (#38830) 2026-09-11 15:29:13 -07:00
Harald Schäfer 6107da1cd6 Cinque v2 (#38850)
1a421175-db71-4e3d-9d62-e2166421b02b/12864
2026-09-10 21:24:09 -07:00
Daniel Koepping 6c410eee37 validate compiled model (#38855)
* validate serialized models after writing
2026-09-10 21:06:22 -07:00
Daniel Koepping fc4af50824 enable alpha long on nightly-chestnut (#38854)
panda debug build
2026-09-10 20:57:57 -07:00
Daniel Koepping f87e170bee reject incomplete model buffers (#38852) 2026-09-10 18:17:40 -07:00
Daniel Koepping 2a70b40cdc rebuild models when helpers change (#38853)
rebuild models when serialization helpers change
2026-09-10 18:14:23 -07:00
Daniel Koepping e6f401b5cf build nightly-chestnut (#38849)
build nightly-chestnut
2026-09-10 17:38:04 -07:00
Daniel Koepping e9711bc51d longer SSH keepalive for chestnut model load (#38851)
ci: allow longer SSH keepalive gaps
2026-09-10 17:26:33 -07:00
Harald Schäfer 9b899fdaa2 Reapply Git LFS hosting on Hugging Face (#38841) 2026-09-10 16:47:40 -07:00
Daniel Koepping 511c1f1e4e chestnut updater: close files explicitly (#38846)
chestnut: close USB sysfs files after reading
2026-09-10 14:28:12 -07:00
Daniel Koepping 443967ddad enable caching in chestnut CI (#38845)
enable caching
2026-09-10 14:17:21 -07:00
Daniel Koepping 633117b2a9 test_onroad in chestnut CI (#38844)
test chestnut with live camera frames
2026-09-10 14:02:18 -07:00
Daniel Koepping f174e39d7a add powertest for mici (#38843) 2026-09-10 13:19:44 -07:00
Zeph 68d829c7c9 selfdrived: no localizer alerts from capnp defaults (#38840)
selfdrived: don't alert on a message that was never received

posenetInvalid, locationdTemporaryError and paramsdTemporaryError are raised
off the capnp defaults of deviceMotion and vehicleParameters before either
message has ever arrived. When modeld is down, locationd and paramsd never
publish, the defaults read as a nan posenet speed, and that NoEntryAlert can
hide the process not running alert that names modeld.

Same guard as the big model alert in #38500.
2026-09-10 12:14:54 -07:00
Daniel Koepping 0cd3f0f20b remove old manifest before rebuild (#38842)
invalidate manifest before rebuilt
2026-09-10 11:18:54 -07:00
Trey Moen b97a2d8201 eSIM: require internet to delete a profile (#38838)
* Guard eSIM profile deletion with internet connectivity check

* Simplify eSIM delete connectivity guard
2026-09-09 20:21:52 -07:00
Harald Schäfer d9ecb0b678 Revert "Move Git LFS hosting to Hugging Face" (#38837) 2026-09-09 18:14:47 -07:00
Harald Schäfer ff2f78c3e8 Move Git LFS hosting to Hugging Face (#38824)
* Move LFS asset transfers to xet-core

* Limit migration to Git LFS hosting on Hugging Face

---------

Co-authored-by: Harald Schäfer <6804392+haraschax@users.noreply.github.com>
2026-09-09 14:31:06 -07:00
Adeeb Shihadeh 1e0914051f bump msgq (#38836) 2026-09-09 09:57:22 -07:00
Amy Jeanes 20bddff857 Use LF line endings
Split from https://github.com/commaai/openpilot/pull/38810.
2026-09-09 09:40:15 -07:00
Adeeb Shihadeh 14617070ab lil more comment 2026-09-09 09:33:57 -07:00
Trey Moen 802a231bd2 mici: simplify eSIM connecting status (#38833) 2026-09-08 23:34:37 -07:00
Trey Moen 179718dcb0 esim: mici profile download UI (#38813)
* esim: MICI profile download UI

* Show temporary feedback for non-LPA QR codes

* Scan eSIM QR codes every 250 ms

* Shrink eSIM error text and log displayed errors

* Activate downloaded eSIM through the profile tap flow

* Let the eSIM connectivity error wrap naturally

* Release add-profile button feedback immediately

* Check internet before opening the eSIM QR scanner

* Use UI network state and standard connectivity dialog for eSIM

* Drop LPA prime settings change from eSIM download UI

* Scope eSIM UI constants to their owning classes

* Remove unused ot codespell exception

* esim: simplify profile download operation and UI state

* esim: share activation code validation with QR scanner

* esim: match QR scanner video to cabin preview

* esim: show errors in a scrollable text dialog

* esim: require nonblank profile nicknames
2026-09-08 23:16:17 -07:00
Trey Moen d70df67366 mici: process eSIM notifications after profile deletion (#38832) 2026-09-08 21:39:24 -07:00
Trey Moen 66b4dbe2a1 qrcode: add QR decoder (#38814)
* qrcode: add QR decoder

Decodes a QR code from a grayscale image or a module matrix: adaptive
binarization, finder pattern search by run ratios, perspective sampling
with alignment pattern refinement, format info, unmasking, block
de-interleaving, Reed-Solomon correction, and numeric, alphanumeric,
byte (with ECI), and Kanji segments.

Fixtures are packed matrices from python-qrcode 8.2 covering every
version and level, plus Segno 1.6.6 matrices for the ECI cases.

* qrcode: tune decoder for the device

Measured on a comma mici with real cabin frames downsampled to 672x380,
the size the eSIM screen scans at. Per frame: 30 ms -> 8 ms with no
code in view, 42 ms -> 13 ms with a code.

- binarize: fixed two-radius fill from one integral image instead of an
  iterative outward fill (a dark cabin is almost all flat tiles), tile
  stats on a contiguous layout, flat tiles decided whole so sensor noise
  cannot become speckle, uint8 threshold compare
- finder search: scan every 4th row, build column runs only at
  candidate columns
- alignment search starts at a 2-module radius
- Reed-Solomon syndromes through table lookups, placement order cached

* qrcode: simplify decoder

Fold the function-module mask into _data_coords and the format
coordinates into _read_format, evaluate masks directly on the data
coordinates, compact the ECI, numeric, and alphanumeric parsing, and
flatten the retry loop in decode. No behavior change.
2026-09-09 04:23:55 +00:00
Trey Moen ef769c173d qrcode: generalize EC, format, and GF tables (#38828)
Replace the level-L-only Reed-Solomon parameters, the single hardcoded
format word, and the bitwise GF(256) multiply with the full EC table,
the 32 format words, and log/antilog tables. Block splitting and
interleaving move into _block_lengths and _interleaved. This is the
structure a decoder needs to read any version, level, and mask; the
encoder's module matrices are unchanged and test_encoder pins them.
2026-09-08 19:15:39 -07:00
Trey Moen a4a24d5d87 qrcode: fix alignment pattern positions for version 32 (#38827)
The closed-form step between alignment patterns rounds the wrong way
for version 32 (28 instead of 26). Use the form that is right for every
version. The encoder only emits versions 1-20, so its output is
unchanged.
2026-09-08 19:05:18 -07:00
Trey Moen 1fb3edb7dd qrcode: test alignment pattern positions (#38829) 2026-09-08 18:53:33 -07:00
Daniel Koepping dd4615850c replay chestnut in CI (#38826)
run model replay on chestnut in CI
2026-09-08 18:05:03 -07:00
Daniel Koepping f23e65768c ui: resize status icons (#38749)
* ui: resize status icons

* Update orange GPU icon to 108px height

* Trigger CI after LFS upload

* ci
2026-09-08 17:06:12 -07:00
Adeeb Shihadeh 96be8c7eb5 check dependency footprint (#38825) 2026-09-08 16:46:19 -07:00
Daniel Koepping 23d0474b95 compile big model in jenkins CI (#38822)
compile chestnut in ci
2026-09-08 15:37:12 -07:00
ZwX1616 60d8ae2d4f modeld/SConscript: fix chunk_targets estimate (#38821) 2026-09-08 15:27:11 -07:00
Trey Moen 7bfe6ba4d0 pandad: allow disabling driver camera IR offroad (#38812) 2026-09-08 15:21:03 -07:00
Pramish 9946c521f7 modeld: build DM warp and model JITs for all camera configs (#38767)
* modeld: build DM warp and model JITs for all camera configs

In #38684, camera_configs on comma_arm64 was restricted to only the host device
running SCons (selecting either _os_fisheye or _ar_ox_fisheye).

However, prebuilt release and nightly bundles are built on TICI/tizi runners
in Jenkins and distributed to both Comma 3/3X (TICI) and Comma 4 (MICI) devices.
Because the build machine was TICI, dm_warp_1344x760_tinygrad.pkl was omitted
from the packaged bundle, causing dmonitoringmodeld to fail with FileNotFoundError
on Comma 4 and leaving driver-monitoring calibration stuck at 0%.

Restore CAMERA_CONFIGS so both camera resolutions (1928x1208 and 1344x760)
are built for all platforms.

Fixes #38762

* simplify

---------

Co-authored-by: ZwX1616 <zwx1616@gmail.com>
2026-09-08 14:16:21 -07:00
Trey Moen 25d9d41c90 esim: comma four profile management UI (#37844)
* esim: MICI eSIM profile management UI

* esim: align rename button position regardless of delete button

* esim: skip profile UI when SIM is not an eUICC

Probe is_euicc() on the first profile poll and cache it; non-eUICC SIMs
avoid list_profiles/process_notifications (which hang on a plain SIM) and
the eSIM button shows ICCID/MCC-MNC metadata instead of opening the
profile management screen.

* esim: satisfy ruff E731 in action_pressed helper

* esim: get modem info from modem.py state instead of shelling out

* esim: simplify switch lifecycle, drop active flag and settle window

* esim: only show 'switching...' on the target profile

* esim: read cell strength directly from HARDWARE

* esim: hide checkmark and dim cell icon during switch; keep rename available

* esim: disable active profile button (rename still clickable as overlay)

* esim: stop rename/delete buttons and labels from flashing during operations

* esim: show 'comma prime' on network button for comma profile

* esim: move display_name and is_comma onto Profile dataclass

* esim: anchor rename button to rightmost slot to prevent shift

* esim: include iccid prefix check in Profile.is_comma

* esim: drop process_notifications from cellular manager

* esim: disable network button when SIM isn't an eUICC

* esim: rename ESim* classes to Esim*

* esim: sort imports

* esim: default to 'loading...' instead of 'no active profile'

* esim: rename PROFILE_POLL_INTERVAL to PROFILE_POLL_INTERVAL_S

* esim: slim EsimNetworkButton

* esim: use DEFAULT_TEXT_COLOR; load delete dialog texture in __init__

* esim: revert cell-icon index trick, name each NetworkStrength explicitly

* esim: tighten delete/rename spacing so 'switch' label fits on one line

* esim: shrink delete/rename buttons by 25%

* esim: keep rename button at full size, only delete shrinks

* Revert "disable modem.py for now"

This reverts commit 1eeba86ec1.

* Revert "modem.py is disabled"

This reverts commit d238a1ccc4.

* lpa: inline comma iccid prefix

* ui/cellular_manager: clear switching state when LPA returns

* ui/cellular_manager: lock callback queue, drop poll log noise

* esim: sort NetworkStrength/NetworkType imports

* esim: drop switching_iccid concept

* esim: show 'switching...' on the clicked profile button

* esim: optimistic switch, skip post-switch list_profiles to avoid flicker

* esim: only style profile button from local op flags, not global busy

* esim: remove deleting/switching state, collapse profile button branches

* esim: show GSM settings on full prime when non-comma profile is active

* esim: expose CellularManager.active_profile, dedup callers

* esim: apply comma prime defaults on switch (roaming, metered, no APN)

* esim: restore re_sort on show_event to avoid first-frame jank

* lpa: apply comma prime defaults inside TiciLPA.switch_profile

* lpa: lazy-import Params to break circular import

* lpa: treat +CME ERROR 13 (SIM failure) as non-eUICC in is_euicc

* esim: show 'obtaining IP...' on non-eUICC path while connecting

* Revert "lpa: treat +CME ERROR 13 (SIM failure) as non-eUICC in is_euicc"

This reverts commit 475ca448ea914ff8f9892b4cbf4525d961d7618b.

* esim: poll profiles every 5s

* lpa: tighten Profile.is_comma to require both Webbing provider and comma BIN

* lpa: fall back to '<unnamed>' instead of iccid prefix in display_name

* esim: re-probe is_euicc each poll for runtime SIM swaps

* esim_ui: rename EsimUIMici to EsimUI

* esim: simplify cellular manager and profile UI

- optimistic profile switch at click time so the active button no longer bounces back
- unify LPA worker threads, refresh_profiles resets the poll timer
- deterministic profile ordering on show
- reuse wifi ForgetButton for delete, drop DeleteButton
- construct CellularManager inside NetworkLayoutMici
- drop comma prime param defaults from LPA.switch_profile (moved to a separate PR)

* esim: drop eUICC state change log

* ui: gate cellular settings on prime subscription

* ui: disable eSIM management for full prime

* ui: keep eSIM management disabled for full prime

* ui: unify eSIM profile action buttons

* ui: tighten eSIM profile action spacing

* ui: place rename before delete in eSIM actions

* esim: restrict individual profiles and process switch notifications

* esim: defer poll after operations and confirm eUICC loss before clearing profiles

* ui: allow cellular settings for unregistered and unpaired devices

* ui: disable all profile switching for full prime

* ui: simplify cellular access to not full prime

* ui: make profile poll interval a CellularManager constant

* ui: read modem state on the profile poll cadence

* ui: scope eSIM profile colors to their class

* ui: inline modem state read in cellular polling

* ui: rely on hardware modem state fallback
2026-09-07 21:55:28 -07:00
Trey Moen 8f2d66d0e5 cabana: round only the outer video frame (#38808)
* cabana: remove video frame corner rounding

* cabana: round the outer video frame including letterboxing
2026-09-07 21:26:53 -07:00
Trey Moen 3eafb658bf mici: remove duplicate pairing button from main settings (#38809) 2026-09-07 17:48:33 -07:00
Trey Moen 10b9e73859 cabana: fill event bar to camera edges (#38807) 2026-09-07 17:06:01 -07:00
Trey Moen bb86bee688 cabana: clean up UI wording and formatting (#38803) 2026-09-07 14:56:19 -07:00
Trey Moen 3e54af9db8 cabana: playback controls for live streaming (#38802)
* Hide Cabana playback controls during live streaming

* cabana: retain pause and go-live controls for live streams
2026-09-07 14:47:20 -07:00
Trey Moen 20ae4ac44d cabana: standardize settings step buttons (#38801)
* cabana: align and brighten settings step buttons

* cabana: use standard settings icon button color
2026-09-07 14:08:32 -07:00
Trey Moen 7c1f224443 cabana: stack the undo/redo zoom buttons in the chart menu (#38800) 2026-09-07 13:03:12 -07:00
Trey Moen 2b4d050e9c cabana: fix video startup and desktop interactions (#38798)
* cabana: fix video startup and desktop interactions

* cabana: remove util comments
2026-09-07 12:43:41 -07:00
Trey Moen 1bb019521e cabana: fix chart ranges and control layout (#38797) 2026-09-07 12:42:41 -07:00
Trey Moen 624d5b9995 cabana: fix stream and replay lifetimes (#38796) 2026-09-07 12:33:14 -07:00
Trey Moen 0f9ec7158b cabana: validate message IDs and include recorded CAN messages (#38799) 2026-09-07 12:32:22 -07:00
Trey Moen 9c02eebb72 cabana: fix clipped control outlines and filter sizing (#38795)
* cabana: fix clipped control outlines and filter sizing

* cabana: remove added comments

* cabana: document control helper API usage
2026-09-07 12:05:26 -07:00
Trey Moen cb70ba89aa cabana: smooth startup layout (#38794)
* cabana: smooth startup layout

* cabana: remove camera tab startup comment

* cabana: initialize dock geometry on the first frame

* cabana: remove route loading placeholder
2026-09-07 11:50:13 -07:00
Trey Moen 65c411433b cabana: reduce downloader startup and dense chart rendering overhead (#38793)
* cabana: reduce downloader startup and dense chart rendering overhead

* cabana: remove downloader spawn tests
2026-09-07 01:22:23 -07:00
Adeeb Shihadeh 6bbdf8ad18 remove sentry (#38790) 2026-09-06 21:20:39 -07:00
Trey Moen 9d1f0d4171 cabana: restyle (#38789)
* cabana: connect palette theme, rounded cards, floating page switch

Move the jotpluggler port's connect palette into a shared theme module
(ui/theme.h/.cc) and apply it to all of cabana: Palette struct for dark
and light, applyTheme fills ImGui and ImPlot styles, readableCurveColor
and sectionTitle for reuse by the analysis workspace. Drop the old
DarkTheme constants and per-theme color branches in widgets.

Docked panels are borderless with content in rounded bordered cards;
custom-drawn rects use the style radii. The detail widget's bottom tab
bar is replaced by a floating Messages/Logs pill.

* cabana: scrollbar and menu contrast, page switch below the page

The scrollbar grab uses the border color on the window-colored track.
Selection colors are stronger so an open menu reads as blue in both
themes and the hovered item is visible in light mode. The Messages/Logs
pill sits in a strip below the page instead of covering its last rows.

* cabana: even spacing around the page switch, steady top menu highlight

* cabana: inner spacing before the clear button of a clearable input

* cabana: center the page switch below the page, space the signal row buttons

* cabana: logs table cells padded and bordered like the messages table

* cabana: recess the signals card in the window color

* cabana: one button vocabulary

Three button shapes, all one frame height with the style padding and
rounding: ImGui::Button for framed text, iconButton for a framed square
icon, toolButton for a flat square icon (or flat text). Adjacent buttons
are ItemInnerSpacing apart, groups ItemSpacing apart. Tool bars drop
their private padding and 1 px spacing, the signal row, chart header,
detail header, logs export and signal selector buttons all use the
shared sizes, and the page switch is built from the same style values.

* cabana: center the glyph ink in square icon buttons

* cabana: framed tool buttons and plain icon glyphs

toolButton is now iconButton with an optional text, so every button in
the app is framed. Boxed glyphs (plus-square, x-square, dash-square,
arrow-*-square, window-stack) are replaced by their plain variants
(plus-lg, trash, arrows-collapse, box-arrow-*, window-plus), the light
x/plus/dash by their -lg forms, the chart menu by three-dots-vertical
and the toolbar overflow by chevron-double-right.

* cabana: draw square button glyphs at 80% so full bleed icons keep a margin

* cabana: signal values in the mono font

* cabana: center square button icons on their atlas ink, snapped to framebuffer pixels

* cabana: draw square button glyph quads directly, AddText truncates the position

* cabana: right align the messages and signals tool bars to the table edge, full size collapse button

* cabana: detail header and tab scroll buttons flush with the right edge

* cabana: header and footer strips, gap above the panels, video splitter as a separator

The menu bar and the status bar are strips in the surface color with a
separator line towards the panels, and the dockspace sits one item
spacing below the menu bar, matching the spacing above the status bar.
The video/charts splitter draws the same 1 px separator line as the
dock splitters, centered in a gap as tall as the side padding, and a
double click snaps the video back to its natural height.

* cabana: top menus flush with the menu bar, 2 px video splitter without double click

* cabana: dropdowns continue the menu bar, splitter in the border color, row button gap

Top level menus draw no top border and square top corners, and the menu
bar leaves its separator out under the open dropdown. The video splitter
uses the border color like the dock splitters. The signal row buttons
keep one inner spacing before the scrollbar.

* cabana: dropdowns share the bar's 1 px border line, messages toolbar spacing matches the video card

* cabana: tool bar groups, framed dropdown buttons, chart content flush with the tool bar

Tool bar items are ItemSpacing apart and the items of a group (a button
pair, a separator and its neighbor) ItemInnerSpacing apart, instead of
a private 1 px spacing. The dropdown buttons are framed like the other
buttons. The video time is a plain mono readout that toggles on click.
Chart cards have no horizontal margin so their header and plot line up
with the tool bar above.

* cabana: chart list spacing: gap above the scroll area, charts clear of the scrollbar, even legend gaps

* cabana: loop and speed buttons grouped like the playback buttons

* cabana: survive narrow panels and small windows

Stress tested by dragging the column and video splitters to their
limits and shrinking the window to 640x480. Fixes: the messages tool
bar and the message detail header overflow into the >> menu instead of
clipping (the view button and the name stay); the binary view keeps a
minimum cell width and scrolls sideways; the Messages/Logs switch drops
to icons when the strip is too narrow; the camera keeps its last frame
across reconnects, so a video restored from a collapsed splitter while
paused is no longer black.

* cabana: range slider spans the charts >> menu, remove all and float as menu entries

* cabana: the charts card keeps its tool bar row or collapses, like the video at the other end

* cabana: uniform >> menus: dropdowns become submenus, the time readout an action entry

* cabana: video/charts splitter gap matches the column splitters

* cabana: drop unused lambda capture in chart type menu

Claude-Session: https://claude.ai/code/session_01XTFaJpf235LeHBmyFRgeJ1

* cabana: the menu bar and the status bar stay in full screen

* cabana: simplify theme helpers and toolbar actions

* cabana: preserve panel width for overflow edit dialog

* cabana: keep message editor usable in narrow panels

* cabana: group heatmap modes in a dropdown

* cabana: trim redundant restyle comments

* cabana: fix review findings from the restyle

- set the caret and other unset colors so the light theme has no white caret
- tolerate float error in the toolbar fit check to stop spurious overflow
- restore the full screen chrome hiding
- clear stale camera frames when the server changes, keep them across a collapse
- cache icon glyph ink bounds instead of scanning the atlas every frame
- drop dead plot_bg and ImPlot globals, add a badge palette role
- add toolbarMenu and use toolbarAction for the remove message item
- derive the signal view button size seed from the font and style

* cabana: remove remaining redundant comments
2026-09-06 19:46:02 -07:00
YassineYousfi c51e3e5a71 cinque terre model 🇮🇹 (#38771)
a4c5f1d1-1f5d-4807-9593-54afa27099d5/12864
2026-09-05 19:03:12 -07:00
Trey Moen 3bc6701f34 cabana: fill video pane and reset video height (#38786) 2026-09-05 17:02:05 -07:00
Trey Moen cb0da74b41 cabana: center speed dropdown label, add divider after loop button (#38785) 2026-09-05 15:34:00 -07:00
Trey Moen a4f7c50d2a cabana: fix sparkline edges and stroke rendering (#38784)
cabana: preserve sparkline edges and improve stroke rendering
2026-09-05 14:54:17 -07:00
Trey Moen 444be0a649 cabana: pace frames instead of relying on vsync (#38783)
On Wayland a vsynced glfwSwapBuffers blocks until the compositor sends a
frame callback, which it stops doing while the window is on another
workspace or otherwise off-screen. The main thread never gets back to
glfwPollEvents, so Hyprland reports cabana as not responding.

Run with swap interval 0 and pace the loop to the monitor refresh rate.
2026-09-05 14:49:37 -07:00
Trey Moen a989bc0b50 cabana: misc UI tweaks (#38782) 2026-09-05 14:13:15 -07:00
Trey Moen f9dacd0d6b cabana: build binary directly, drop wrapper script (#38781) 2026-09-05 13:45:12 -07:00
Trey Moen 0ec3a082c7 op: support all the linuxes! (#38776) 2026-09-04 21:10:04 -07:00
Trey Moen c8603fb3ce op: point git-lfs config at the vendored binary (#38775)
git-lfs is vendored in the venv, but `git lfs install` writes filters and hooks that invoke a bare `git-lfs`, which is only on PATH inside the venv. With filter.lfs.required set, any checkout from a plain shell fails.

git runs filters and hooks from the worktree root, so a relative path into the venv resolves from anywhere.
2026-09-04 21:09:40 -07:00
Trey Moen 69ad376fef Revert "op: support all the linuxes!" (#38774)
Revert "op: support all the linuxes! (#38773)"

This reverts commit 33790c855c.
2026-09-04 19:09:45 -07:00
Trey Moen 33790c855c op: support all the linuxes! (#38773) 2026-09-04 18:30:45 -07:00
Adeeb Shihadeh 675ff56981 tools: add op docs command (#38770) 2026-09-04 15:20:43 -07:00
YassineYousfi e3def37695 revert 38742 (#38760) 2026-09-03 09:45:51 -07:00
Trey Moen 3a13b67b6f bye bye Qt! (#38753)
* cabana: remove Qt implementation
2026-09-02 20:44:11 -07:00
Trey Moen 7bade9a67a cabana: halve dock panel min width (#38759) 2026-09-02 20:28:53 -07:00
Trey Moen c163743e32 cabana: better colors (#38758) 2026-09-02 20:20:23 -07:00
Trey Moen 06d76bdb24 cabana: imgui port (#38725)
* cabana: imgui frontend base infra (ui/)

* cabana: line-for-line imgui ports of the Qt widgets and dialogs into ui/

* cabana ui: fixes from the side by side parity test

* cabana ui: deqt.md status after the line-for-line ports

* cabana ui: fixes from the line-for-line review

* cabana ui: parity fixes from the end to end matrix, event driven frame pacing

* cabana ui: preserve F1 help formatting

* cabana ui: parity fixes from the end to end matrix, event driven frame pacing

* cabana ui: deqt.md status

* cabana: fix macOS build of imgui frontend

- link Security.framework (libusb's darwin backend needs
  SecTaskCreateFromSelf/SecTaskCopyValueForEntitlement)
- drop unused loop counter in SignalView::updateChartState
- avoid -Wshadow on Sparkline::size and SignalView::highlight

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* cabana ui: parity round 3 fixes, thread pool and one malloc arena for flat memory

* cabana ui: toolbar overflow budget, menu button carets, help overlay colors, signal editor validation

* cabana ui: deqt.md status

* cabana: let vsync pace the frame loop

The loop threw away frames with glfwWaitEventsTimeout() to hit a 30fps
(10fps idle) target while glfwSwapBuffers() was already blocking on
vsync, so two clocks beat against each other. Input events also reset
frameInterval() to 0 for a few frames, so a click swung the cadence from
~33ms to ~13ms and back -- the camera view redraws on the UI's schedule,
so that showed up as a visible hitch on every message selection.

Poll and draw every iteration and let glfwSwapInterval(1) do the pacing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* cabana: fix macOS build of thread pool / malloc arena changes

- malloc.h and mallopt(M_ARENA_MAX) are glibc only; guard on __GLIBC__.
  macOS has a single allocator zone, so there is nothing to cap.
- capturing structured bindings in a lambda is a C++20 extension and
  clang rejects it under -Werror; bind the event range to plain locals.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* cabana ui: chart axis label precision, signal list click on empty space clears the selection

* cabana ui: codespell

* cabana ui: taller signal rows, system theme detection, full dark palette

- signal rows are 1.5x a frame tall with the sparkline sized to the row,
  so the graphs are readable. The expanded sub-rows keep the Qt height.
- "Automatic" resolved to the light theme regardless of the system: Qt
  gets this from QStyle::standardPalette(), which follows the macOS
  appearance, so read AppleInterfaceStyle and match it. Other platforms
  stay light, like standardPalette() does there.
- widgets that tested settings.theme == DARK_THEME themselves went light
  under AUTO_THEME while the style went dark; they now ask isDarkTheme()
  for the theme applyTheme() actually resolved.
- the dark branch only set 10 ImGuiCol_ entries, so buttons, scrollbars,
  tabs, title bars, separators, tables and grips still came from imgui's
  stock dark theme. Derive the whole palette from DarkTheme instead.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* cabana ui: drop the dead y axis alignment chain and chart cache stub

ImPlot::BeginAlignedPlots aligns the chart y axes, so the hand-rolled
alignment protocol ported from Qt no longer feeds anything: align_to was
write-only. Remove the whole chain (y_label_width and its measuring loop
in draw(), axisYLabelWidthChanged, align_timer, alignCharts), which
reduces updatePlotArea(int, bool) to updatePlotArea().

resetChartCache() has been an empty stub since the port (imgui redraws
every frame); drop it and its four call sites. Also replace the empty
move_icon_rect branch in mousePressEvent with an early return.

No behavior change. updateAxisY's "|| y_label_width == 0" guard only
fired after a unit change, and y_precision/y_tick_count depend solely on
min_y/max_y/tick_count, so the recompute it triggered was a no-op.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* cabana ui: stop the main window from scrolling

The host window that holds the dockspace uses zero WindowPadding but the
default ItemSpacing, so the dockspace and the status bar below it summed
to ItemSpacing.y (5px) more than the viewport, leaving the whole UI
scrollable by a few pixels.

Reserve the spacing along with the status bar height, and mark the host
window NoScrollbar/NoScrollWithMouse: it is pinned to the viewport work
area and should never scroll, as QMainWindow does not. Full screen is
unaffected, it draws no status bar and reserves no height.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* cabana ui: inset the status bar so its ends are not clipped

A borderless BeginChild gets WindowPadding forced to zero, so the status
bar text sat flush against both edges and the first glyph of "For Help,
Press F1" was cut off. Inset both ends by WindowPadding.x, which lines
the text up with the content of the docked panels above it.

Right align the cached minutes / FPS label to the captured child width
rather than calling GetContentRegionAvail() again after the message
text, which only returned the full width because the text had already
wrapped the cursor to a new line.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* cabana ui: drop the leftover imgui object census log

The block printed an imgui/implot object count to stderr every 10s,
which was instrumentation for tracking down a leak, not something the
port needs. It was the only user of implot_internal.h in mainwin.cc, so
drop that include with it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* cabana ui: keep the selected row highlighted while hovered

Selectable() picks its background as HeaderActive when held, else
HeaderHovered when hovered, else Header. The hovered test comes before
the selected one, so a selected row under the cursor takes the hover
color. Both tables push a transparent HeaderHovered to suppress the hover
highlight that QTreeView/QTableView do not have, which also blanked the
selection: clicking a row walked press (HeaderActive, visible), release
(HeaderHovered, transparent, looks unselected), then mouse off the row
(Header, visible), so the selection appeared to flicker until the cursor
moved away.

Only make HeaderHovered transparent for rows that are not selected.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* cabana ui: draw the binary view hex column in bold and in the text color

The Qt delegate never sets a pen for the hex column, so it inherited the
black QPainter default and the two halves of a row did not match. Use
paletteText(is_message_active), the same color the bit columns use, so
both halves read as one row and dim together when the message goes
inactive.

JetBrains Mono ships no bold variant and imgui has no embolden option, so
give drawStaticText a bold flag that redraws the glyphs a fraction of a
pixel to the right. That keeps the monospace advance, which switching to
the proportional bold face would not.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* cabana ui: highlight the whole row in the speed menu

MenuItem spans from the cursor to the right edge, so the Indent() that
made room for the radio bullet also pushed the highlight in by one
FontSize and left the bullet column outside it. QMenu highlights the
whole item, indicator included.

Draw each row as one full width Selectable with the bullet and the label
inside it. Every row declares the same width, so the popup is as wide as
the widest entry and the highlights reach both edges; a label-less
Selectable sized zero would declare no layout width and collapse the
auto-sizing popup. Selectable auto-closes its parent popup like MenuItem
does, so clicking still works in the dropdown and in the Speed submenu of
the toolbar overflow menu, which shares this function.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* cabana ui: fix help menu overlay, Ctrl+F11, time label clipping, toolbar overflow and tool table parity

* cabana ui: click to edit signal cells, light theme selection, logs cell selection and filter fixes

* cabana ui: chart x label margin, zoom context menu items, video splitter collapse, signal editor close

* cabana ui: signal editor lifetime and close, logs cell hover, tool table headers

* cabana ui: branch indicator clicks, editor swallows shortcut and button clicks, selection colors

* cabana ui: float dock windows into real OS windows

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* cabana ui: toolbar menu arrow spacing, only open the editor on editable cells

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* op: add cabana2 to run the imgui cabana

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* cabana: cabana2 launcher script builds _cabana_ui before running it

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* cabana ui: only float the dock panels into their own OS windows

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* cabana ui: open the video pane at the camera's natural aspect ratio

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* cabana: default fps to 30

* cabana ui: fusion palette, shared fusion slider, chart grid colors

* cabana ui: smaller checkbox indicator, closer to the Fusion size

* cabana ui: video toolbar height and item centering, separator extent and menu arrow like Fusion

* cabana ui: message and signal row heights match Qt

* cabana ui: timeline slider height, groove and stacking match the Qt video layout

* cabana ui: fix crash opening a live stream, the video pane default height read the missing camera widget

* cabana: drop the FPS setting, the streams update the UI at a fixed 30 fps

* cabana ui: video and charts pane run edge to edge with a uniform toolbar margin like the Qt frames

* cabana ui: binary view M/L marker size and timeline event fills match Qt

* cabana ui: signal view keeps its sparklines while filtering, number label size matches Qt

* cabana ui: no modal dim fade, QDialog does not animate in

* cabana ui: collapse all is an auto-raise tool button with the Qt icon size

* cabana ui: dialogs and tool windows open as real OS windows like QDialog

* cabana ui: hold back a focus loss while a mouse button is down so tearing a panel off does not abort the drag and create its window twice

* cabana ui: speed menu rows pad the label on the right like QMenu

* cabana ui: raise the dark theme contrast, the Darcula grays all sat on top of the background

the chart y axis guides are opaque and 2 px in dark mode, and text, outlines,
separators, table borders, scrollbar and slider grabs move away from the window
and base grays. light theme is unchanged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* cabana ui: simplify comments, drop the Qt references and the ones that only restate the code

* cabana ui: keep the sparkline antialiased, thin peaks sparkled as the window slid

Qt dropped antialiasing above 500 points because it rasterized into a pixmap. drawing
straight into the frame, an aliased 1 px segment between two columns rounds into one of
them and the rounding flips as the window advances, so the peaks of the dense signals
popped between columns. updateState runs at STREAM_UPDATE_FPS, which at a 30s range moves
the curve well under a pixel per update, so the flip is all that is visible.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* cabana ui: anchor the sparkline to the time window, it jittered sideways as samples expired

x was measured from the first sample still inside the window, which changes whenever the
oldest one falls out, so every update slid the whole curve sideways by the sample spacing.
measuring from the start of the window pins the right edge and lets the samples flow left
with the clock. the density heuristic wanted the data span, which is no longer the x of the
last point, and the flat line started at x 0 instead of at its first sample.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* cabana ui: scroll the sparkline at the frame rate, it stepped at the message rate

the window ended at the last message of the id and updateState only runs when one arrives,
so a slow message held the sparkline still for several frames and then jumped it. the window
now ends at the playback clock, and draw() slides the rendered polyline by the time that has
passed since it was built, clipped to the cell.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* cabana ui: keep the signal value on the right of the column before there is a sparkline

it was drawn left aligned from the start of the column, so the value sat next to the name
until the first samples arrived and then jumped across the row.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* cabana ui: read a little past the sparkline window so the oldest points slide out of it

a sample used to disappear the moment it aged out, which on a fixed rate signal is a point
popping off the left edge. the query reaches back a bit further and the clip rect hides it
instead. the lead in must not move the scale, so it is left out of the min and max.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* cabana ui: use the macOS fullscreen space, taking over the monitor rendered at the old size

glfwSetWindowMonitor switched the display mode and returned a drawable at that mode, so the
ui was rendered at the windowed size and stretched over the screen. NSWindow toggleFullScreen
keeps the backing scale. the green button goes through the same space, so the state is read
back from the style mask rather than tracked.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* cabana ui: let the signal value column shrink again, it held the widest value ever seen

max_value_width only ever grew, so one long value left the sparklines ending
well short of the text for the rest of the message. Track the current widest
value instead, growing on demand and giving room back once it is a couple of
characters too wide, which keeps the width from jittering per sample.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* cabana ui: make the signal rows a quarter taller so the sparklines read better

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* cabana ui: lift the binary view heatmap alphas in dark mode, the tints washed out

The alphas were tuned against a white background; over the dark base the same
values left the cells barely colored. The floor comes up and the ramp gets a
gamma in dark mode only, which keeps the flip counts in the same order.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* cabana ui: saturate the sparkline color in light mode, the pastel lines vanished

Signal colors are low saturation and high value, which is aimed at dark fills;
a 1 px line of one of them on white is hard to see. Draw the sparkline with
twice the saturation and less value when the theme is light.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* cabana ui: give the docked panels a minimum width, the dividers ran over content

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* replay: capture the downloader's stderr so the progress lines reach the handler

file_downloader.py writes PROGRESS:<cur>:<total> to stderr while it streams a
file, but runPython() only piped stdout and left stderr attached to the parent,
so the progress handler only ever fired on failure and cabana downloaded a
segment with no progress bar. Pipe stderr as well, turn the PROGRESS lines into
handler calls, and pass everything else through to the parent's stderr.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* replay: read the downloader's stderr on a thread, the two fd select was overkill

getline() handles the line splitting, partial lines and EOF, so the manual
splitter, the second drain loop and the multiplexing bookkeeping go away and
the stdout loop goes back to what it was.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* cabana ui: one toolButton helper in imgui_util.h

* cabana ui: one toImU32/toImVec4/paletteBrightText in imgui_util.h

* cabana ui: string helpers live in utils/strings.h

* cabana ui: share the fusion slider handle from imgui_util.h

* cabana ui: style.cc declarations move to imgui_util.h

* cabana ui: validatedInput and the qt validators move to imgui_util.h

* cabana ui: viewSelectable helper for the item view rows

* cabana ui: drop the comboBox pass-through

* cabana ui: tabbar moves to its own file, ready for the charts port

* cabana ui: find signal searches on the main thread again, deferred one frame like QTimer::singleShot(0)

* cabana ui: find signal computes its button and label state from the model instead of caching it

* cabana ui: share the queued popup owner protocol between the message box and the file dialog

* cabana ui: one beginDialog helper for the three centered modals, with the reopen recovery

* cabana ui: tool dialogs share the title, begin/end and escape handling, and own their connections

* cabana ui: comboBox helper replaces the hand rolled zero separated item buffers

* cabana ui: drop the unused dialog isOpen accessors

* cabana ui: one validatedText helper for the ip, double and int line edits

* cabana ui: route info reads the route segments instead of caching a row copy

* cabana ui: settings drops the phantom fps label, the constant spin flags and the raw log path buffer

* cabana ui: find similar bits drops the no-op find button disable and the address round trip

* cabana ui: message box warning overload without the empty detailed text

* cabana ui: help overlay only collects rects while it is up, skips torn off panels, and the fingerprint compare is ordered

* cabana ui: drop the glfw callbacks that only forward to the imgui backend, which already installs them

* cabana ui: elided label renders with RenderTextEllipsis instead of a paint time cache

* cabana ui: one save loop, inline cached minutes label, and drop the dead help parser branches

* cabana ui: signal cell bands are built from the two corner notches instead of a region sweep

* cabana ui: binary view items go through itemAt and updateItem assigns unconditionally

* cabana ui: message bytes read the cell padding inline and colored bytes keep the qt default pen

* cabana ui: detail widget drops createToolBar, the welcome widget flag and the unused whatsThis

* cabana ui: messages widget owns its view, header and model directly, fixed size filter editors, from_chars range parsing

* cabana ui: colored hex bytes keep the row pen, the qt delegate does not reset it per cell

* cabana ui: closeEditor drops the queued commit, its item is about to be deleted

* cabana ui: rowsInserted carries the insert position so the selected row follows its message

* cabana ui: the log filter and the message name editors use the shared validators

* cabana ui: the log header sizing and painting are free functions, no HeaderView object

* cabana ui: the visible row range is the change detector, no mirrored scroll position

* cabana ui: the line editor takes the focus flag as an argument and lets InputText revert on escape

* cabana ui: signal view drops the unreachable column parameters, unused item values and repeated sparkline tests

* cabana ui: rows outside the tree viewport are measured but not painted

* cabana ui: one toolbar item table, playback state read in draw()

* cabana ui: draw the stored thumbnail scaled by AddImage, no cpu prescaled copy

* cabana ui: one radioMenuItem helper for the speed and series type menus

* cabana ui: drop dead camera members, hoist the jpeg colour space branch, decode qlog thumbnails on the thread pool

* cabana ui: stop the vipc thread when the splitter collapses the video pane

* cabana ui: the toolbar strings and enabled state are computed in drawToolBar

* cabana ui: chartswidget uses the shared tabbar widget

* cabana ui: the chart header layout is recomputed in resizeEvent, no updateTitle

* cabana ui: updateLayout has no force flag

* cabana ui: one condition hides the value tip

* cabana ui: signal selector uses the shared dialog buttons and row indexes

* cabana ui: the chart tile geometry is one Layout struct saved and restored by drawGhost

* cabana ui: drop the empty theme branch, the unused size hint and the charts container indirections

* cabana ui: the chart holds its tip label by value

* cabana ui: one toolbar item list for both toolbar groups

* cabana ui: the sparkline polyline is emitted through the draw list path

* cabana ui: the tab bar latches a programmatic selection and defers the close request past EndTabBar

* cabana ui: validatedText refuses an invalid edit inside the imgui buffer and the ip field keeps its char filter

* cabana ui: drop the ambiguous three argument warning overload

* cabana ui: combobox reports a real change, menu rows span the popup, one popup protocol for the dialogs

* cabana ui: the startup stream is owned by the window until the first frame opens it

* cabana ui: the slider toolbar index has an out of range sentinel

* cabana ui: the find signal search runs on the frame after the pending state is painted

* cabana ui: the log clears its selection when the rows are dropped and cells are identified by their message

* cabana ui: the open editor survives a scroll, collapse all commits it and escape in the size editor does not

* cabana ui: the header display order is filled with iota

* cabana ui: qlog thumbnails decode on their own threads, the hover thumbnail is mipmapped and the time tooltip appears on the first toggle

* cabana ui: a collapsed video dock stops the vipc thread

* cabana ui: one TOOLBAR_ITEM_SPACING, the signal view used the QCommonStyle 4 instead of Fusion's 1

* cabana ui: sparkline includes imgui_util.h for isDarkTheme

* cabana ui: imgui_util.h becomes util.h and util.cc

* cabana: delete deqt.md

* cabana ui: panel min width, matching video panel padding, centered tabs

The dock panels stop shrinking at the width where the signal view tool bar
squishes instead of at a hardcoded 440. The video/charts panel keeps the
standard window padding so it lines up with the other panels, and the
Msg/Logs tabs are centered in their bar. The cached minutes label is gone
from the status bar.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* cabana ui: persist imgui state in cabana.json and migrate the Qt frontend's layout blobs

* cabana ui: 2px sparkline stroke

a 1px antialiased line is all fringe and no solid core, so the curve
faded and shimmered as the window scrolled.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* cabana: drop the Automatic color theme

it only did anything on macOS: the Qt frontend fell through to
standardPalette() for both Automatic and Light, so the two were
identical everywhere else.

LIGHT_THEME and DARK_THEME keep their values so a persisted dark theme
still reads as dark; the old Automatic 0 falls back to light on load.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* cabana ui: window color between the binary and signal views, fixed split

the binary view sits at its size hint and no longer resizes, and the gap
below it takes the window color instead of leaving a white seam.

also border the video and chart panels like the signal view.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* cabana ui: bigger tab bar scroll buttons

imgui's built in scroll arrows are fixed at FontSize - 2 wide with the
glyph drawn in a FontSize box, so they are small and sit off center.
draw chevron buttons at the frame height instead, spaced and disabled at
the ends of the scroll range like the rest of the tool buttons.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* cabana ui: 1.5px sparkline stroke

* cabana ui: stable sparkline scrolling, one sample per column, aliased spikes

Scroll the polyline by whole physical pixels with each sample's subpixel
phase fixed to its timestamp, thin to one sample per pixel column, and
draw near-vertical segments aliased so spikes stay crisp while smooth
curves keep antialiasing.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* cabana ui: fix spelling

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* cabana ui: size the value column for the widest value a signal can produce

Sizing it to the values in the last message moved the sparklines every
time a value changed length.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* cabana ui: center the tab bar scroll chevrons and give the buttons a frame

The icon font glyph sits off center in its padded advance, so the
chevron is drawn in the button rect. The buttons take the theme's
button background and border like the tool buttons next to them.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* cabana ui: darker grid lines between table cells

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* cabana ui: the gap between the video and the charts matches the side padding

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* cabana ui: plus-square icon for the new chart button, like the remove all button

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* cabana ui: video toolbar buttons as tall as the charts toolbar buttons

The buttons carried 10 px of vertical padding, so the hover rect was
much taller than the glyph and the row looked off center.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* cabana ui: the time display in the mono font at the ui font size

With proportional digits the time changed width as it ticked and the
items after it moved.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* cabana ui: closing a floated side panel docks it back, no hide tab bar button, the charts container never scrolls

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* cabana ui: the message tabs use the TabBar widget, chevron scroll buttons for both tab bars

The chevron scroll buttons move into a scrollable tab bar widget that
both use. The wheel scrolls the tabs, both tab bars get Close Other
Tabs, and the Msg tab is labeled Messages.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* cabana ui: deliver a focus loss right away on macOS, holding it back swallowed the first click in a popup

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* cabana ui: draw the remote routes dialog at the modal's level, opened inside the tab's child window it never appeared

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* cabana ui: --no-cache turns off the local route file cache, same as replay

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* cabana: parse merged segments on replay's merge thread, the parse on the main thread dropped frames on every merge while downloading

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* cabana ui: Esc leaves full screen on all platforms

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* cabana ui: an app bundle on macOS so the menu bar and the dock show Cabana

A bare binary shows its file name there. The build wraps _cabana_ui in
a minimal Cabana.app with an Info.plist and the launcher runs that.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* Revert "cabana ui: an app bundle on macOS so the menu bar and the dock show Cabana"

This reverts commit 1a45540125847ca46fcd4d7ac0522918e315057d.

* cabana ui: the app menu on macOS says Cabana

A bare binary gets an info dictionary with its file name in it, which
glfw reads for the app menu. The name is set there before glfw brings
up cocoa.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* cabana ui: macapp moves into ui/util

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* cabana ui: setMacAppName lives in ui/util.cc

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* cabana ui: apply the code review

Bugs: an empty decode batch dereferenced vals.front(), the chart x comparator
took a float, reserve used capacity(), the bit text went black while resizing
a signal, byte cells under-sized non-multiple-of-8 payloads, the CAN speed
lookup read the data speed table, the export tooltip never showed while
disabled, and the thumbnail parser and the signal search joined ad-hoc
threads on the render thread. Both now run off the pool; the search fans out
over it from its own thread.

Shared helpers in ui/util: the tool bar with its overflow menu, the menu
button, modal dialog setup, Escape inside dialogButtons, drawText,
drawElidedText, color markers, alignRight, clearableInput,
disabledItemTooltip, inputTextMultiline, tableHeadersRow with the right
click, withAlpha, boldFont, the Cocoa full screen glue, the messages panel
id. utils gets hexByte, guarded and nonEmptyDBCFiles; threadpool gets
parallelFor.

The help overlay leaves mainwin.cc for its own file, the stream teardown is
one releaseStream(), the charts own their views through unique_ptr, the
find-similar-bits message list follows the source bus, the clipboard copies
every open DBC, and the Qt vestiges (event names, model API, protected
sections, restating comments, stale TODOs, mixed member naming) are gone.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* cabana ui: collapse the model/view/delegate split

The Qt-era model, view and delegate classes were ported one to one, but
imgui draws a table in one function. MessageView, MessageViewHeader and
MessageListModel's string API fold into MessagesWidget over a plain
MessageList; HistoryLogModel folds into LogsWidget; BinaryViewModel and
BinaryItemDelegate fold into BinaryView; SignalItemDelegate folds into
SignalView, and SignalModel keeps only the item tree with three predicates
instead of the flags bitmask; FindSignalModel becomes SignalSearch. The
byte cell painter is a set of free functions shared by the two tables.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* cabana ui: load the route behind the window

The route file listing is an HTTPS round trip to the comma API and took
the whole first second of startup before the window existed. The replay
branch of main() now hands run() a loader that a worker executes after
the first frame; the window maps in ~0.1 s instead of ~1 s. On a failed
load the app stays open in the No Stream state with the error box
instead of exiting before a window ever appeared.

* cabana ui: fix the live stream video controls layout

* cabana ui: only copy dbc to clipboard with a single file

* cabana ui: report route loading failures instead of hanging

* cabana: launch the imgui frontend by default, --legacy for Qt

---------

Co-authored-by: Trey Moen <trey@Treys-MacBook-Pro.local>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-02 08:06:41 -07:00
YassineYousfi 50fb67f718 bump tg + TC_MIN_GLOBALS (#38751)
bump op + TC min globals
2026-09-01 23:12:28 -07:00
430 changed files with 16551 additions and 35604 deletions
+3 -2
View File
@@ -1,15 +1,16 @@
* text=auto
* text=auto eol=lf
# to move existing files into LFS:
# git add --renormalize .
*.onnx filter=lfs diff=lfs merge=lfs -text
openpilot/selfdrive/modeld/models/big_driving_tinygrad.pkl filter=lfs diff=lfs merge=lfs -text
openpilot/sunnypilot/modeld_v2/models/*.pkl filter=lfs diff=lfs merge=lfs -text
*.svg filter=lfs diff=lfs merge=lfs -text
*.png filter=lfs diff=lfs merge=lfs -text
*.gif filter=lfs diff=lfs merge=lfs -text
*.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
+43 -49
View File
@@ -46,7 +46,6 @@ jobs:
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"
@@ -57,8 +56,9 @@ jobs:
ONNX_PATH="openpilot/selfdrive/modeld/models/driving_supercombo.onnx"
HF_DEFAULTS_PATH="models/defaults/small"
fi
ONNX_REF=""
[ -n "$ONNX_PATH" ] && ONNX_REF=$(git log -1 --format='%H' -- "$ONNX_PATH")
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"
@@ -78,6 +78,8 @@ jobs:
if: ${{ inputs.target == 'small' }}
runs-on: [self-hosted, tici]
env:
MODELS_DIR: openpilot/selfdrive/modeld/models
COMPILER: tinygrad_repo/examples/openpilot
SMALL_ONNX: openpilot/selfdrive/modeld/models/driving_supercombo.onnx
SMALL_PKL: openpilot/selfdrive/modeld/models/driving_tinygrad.pkl
steps:
@@ -85,9 +87,10 @@ jobs:
with:
submodules: recursive
- name: Pull ONNX via LFS
- name: Pull small ONNX via LFS
run: git lfs pull -I "${{ env.SMALL_ONNX }}"
- name: Set environment variables
run: |
source /etc/profile
@@ -103,24 +106,15 @@ jobs:
source ${UV_PROJECT_ENVIRONMENT}/bin/activate
PYTHONPATH=$PYTHONPATH:${{ github.workspace }}/ ${{ github.workspace }}/scripts/manage-powersave.py --disable
- name: Compile small model with stock compiler
- name: Compile small model from ONNX
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 }}
env ${TG_FLAGS} python3 ${{ github.workspace }}/${{ env.COMPILER }}/compile_onnx.py \
${{ github.workspace }}/${{ env.SMALL_ONNX }} \
${{ github.workspace }}/${{ env.SMALL_PKL }} \
--device-input "*" --out-of-band --benchmark-runs 1
- name: Chunk small pkl
run: |
@@ -150,11 +144,14 @@ jobs:
cp "$MODELS_DIR/${PKL_BASE}".chunk* "$OUTPUT_DIR/"
cp "$MODELS_DIR/${PKL_BASE}.chunkmanifest" "$OUTPUT_DIR/"
ONNX_SHA256=$(git cat-file blob "$(git ls-files -s "${{ github.workspace }}/${{ env.SMALL_ONNX }}" | awk '{print $2}')" | grep '^oid sha256:' | cut -d: -f2)
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 }}"
--upstream-branch "${{ needs.resolve.outputs.onnx_ref }}" \
--onnx-sha256 "$ONNX_SHA256"
echo "model-${MODEL_NAME}-${{ github.run_number }}" > "$OUTPUT_DIR/artifact_name.txt"
@@ -181,15 +178,17 @@ jobs:
if: ${{ inputs.target == 'big' }}
runs-on: [self-hosted, chestnut]
env:
BIG_ONNX: openpilot/selfdrive/modeld/models/big_driving_supercombo.onnx
MODELS_DIR: openpilot/selfdrive/modeld/models
COMPILER: tinygrad_repo/examples/openpilot
BIG_PKL: openpilot/selfdrive/modeld/models/big_driving_tinygrad.pkl
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
lfs: true
- name: Pull big ONNX via LFS
run: git lfs pull -I "${{ env.BIG_ONNX }}"
- name: Pull big PKL via LFS
run: git lfs pull -I "${{ env.BIG_PKL }}"
- name: Set environment variables
run: |
@@ -222,25 +221,6 @@ jobs:
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
@@ -269,11 +249,14 @@ jobs:
cp "$MODELS_DIR/${PKL_BASE}".chunk* "$OUTPUT_DIR/"
cp "$MODELS_DIR/${PKL_BASE}.chunkmanifest" "$OUTPUT_DIR/"
PKL_LFS_SHA256=$(sha256sum "$MODELS_DIR/${PKL_BASE}" | cut -d' ' -f1)
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 }}"
--upstream-branch "${{ needs.resolve.outputs.onnx_ref }}" \
--onnx-sha256 "$PKL_LFS_SHA256"
echo "model-${MODEL_NAME}-${{ github.run_number }}" > "$OUTPUT_DIR/artifact_name.txt"
@@ -311,7 +294,8 @@ jobs:
steps:
- uses: actions/checkout@v4
- name: Pull ONNX via LFS
- name: Pull LFS for resolved ONNX files
if: ${{ needs.resolve.outputs.onnx_path != '' }}
run: git lfs pull -I "${{ needs.resolve.outputs.onnx_path }}"
- name: Install huggingface_hub
@@ -351,8 +335,6 @@ jobs:
--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 }}"
@@ -490,15 +472,27 @@ jobs:
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"
COMPILER="${{ github.workspace }}/tinygrad_repo/examples/openpilot"
MODELS_DIR="${{ github.workspace }}/openpilot/selfdrive/modeld/models"
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} \
CAM_W=$(echo $res | cut -d x -f1)
CAM_H=$(echo $res | cut -d x -f2)
STRIDE_INFO=$(python3 -c "
import sys
from openpilot.system.camerad.cameras.nv12_info import get_nv12_info
s,y,u,sz = get_nv12_info(int(sys.argv[1]), int(sys.argv[2]))
print(f'{s},{y},{u},{sz}')
" $CAM_W $CAM_H)
WARP_PKL="${MODELS_DIR}/dm_warp_${res}_tinygrad.pkl"
taskset -c 7 env ${TG_FLAGS} python3 "${COMPILER}/compile_warp.py" \
--frame ${CAM_W},${CAM_H},${STRIDE_INFO} \
--warp-to ${DM_SIZE} \
--output ${WARP_PKL}
--layout luma \
--border-fill 16 \
--transform-device NPY \
--output "${WARP_PKL}"
done
- name: Prepare DM output
@@ -0,0 +1,73 @@
name: Compile warp for sunnypilot modeld
on:
workflow_dispatch:
schedule:
- cron: '0 0 * * 0'
jobs:
compile_warps:
runs-on: [self-hosted, chestnut]
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
- 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 Warp Kernels
run: |
source ${UV_PROJECT_ENVIRONMENT}/bin/activate
export PYTHONPATH="${PYTHONPATH}:${{ github.workspace }}/tinygrad_repo:${{ github.workspace }}"
mkdir -p openpilot/sunnypilot/modeld_v2/models/
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}')")
echo "Model size: $MODEL_SIZE"
echo "Camera resolutions: $CAMERA_RES"
for res in $CAMERA_RES; do
NV12_INFO=$(python3 -c "from openpilot.system.camerad.cameras.nv12_info import get_nv12_info; w, h = map(int, '${res}'.split('x')); print(','.join(map(str, get_nv12_info(w, h))))")
WARP_PKL="openpilot/sunnypilot/modeld_v2/models/driving_warp_${res}_tinygrad.pkl"
echo "Compiling $WARP_PKL on QCOM"
taskset -c 7 env DEV=QCOM IMAGE=1 FLOAT16=1 NOLOCALS=1 JIT_BATCH_SIZE=0 OPENPILOT_HACKS=1 python3 tinygrad_repo/examples/openpilot/compile_warp.py \
--frame ${res/x/,},${NV12_INFO} --warp-to ${MODEL_SIZE} --layout yuv420 --frames 2 --output "${WARP_PKL}"
BIG_WARP_PKL="openpilot/sunnypilot/modeld_v2/models/big_driving_warp_${res}_tinygrad.pkl"
echo "Compiling $BIG_WARP_PKL on AMD"
taskset -c 7 env DEBUG=1 DEV=USB+AMD:LLVM FRAME_DEV=CPU FLOAT16=1 JIT_BATCH_SIZE=0 GMMU=0 TC_OPT=2 TC_MIN_GLOBALS=32 python3 tinygrad_repo/examples/openpilot/compile_warp.py \
--frame ${res/x/,},${NV12_INFO} --warp-to ${MODEL_SIZE} --layout yuv420 --frames 2 --output "${BIG_WARP_PKL}"
done
- name: Re-enable powersave
if: always()
run: |
source ${UV_PROJECT_ENVIRONMENT}/bin/activate
PYTHONPATH=$PYTHONPATH:${{ github.workspace }}/ ${{ github.workspace }}/scripts/manage-powersave.py --enable
- name: Create Pull Request
uses: peter-evans/create-pull-request@9153d834b60caba6d51c9b9510b087acf9f33f83
with:
author: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
token: ${{ secrets.GITHUB_TOKEN }}
commit-message: "[bot] Update warp pkl for modeld_v2"
title: "[bot] Update modeld_v2 Warp"
branch: "auto/compile-warp-kernels"
base: "master"
delete-branch: true
labels: bot
add-paths: |
openpilot/sunnypilot/modeld_v2/models/*.pkl
+9 -8
View File
@@ -18,24 +18,25 @@ concurrency:
env:
GIT_CONFIG_COUNT: 1
GIT_CONFIG_KEY_0: lfs.fetchexclude
GIT_CONFIG_VALUE_0: openpilot/selfdrive/modeld/models/big_driving_supercombo.onnx
GIT_CONFIG_VALUE_0: openpilot/selfdrive/modeld/models/big_driving_tinygrad.pkl
jobs:
docs:
name: build docs
runs-on: ubuntu-24.04
runs-on: ${{
(github.repository == 'commaai/openpilot') &&
((github.event_name != 'pull_request') ||
(github.event.pull_request.head.repo.full_name == 'commaai/openpilot'))
&& fromJSON('["namespace-profile-amd64-8x16"]')
|| fromJSON('["ubuntu-24.04"]') }}
steps:
- uses: commaai/timeout@v1
- uses: actions/checkout@v7
with:
submodules: true
- run: ./tools/op.sh setup
# Build
- name: Build docs
run: |
git lfs pull
python docs/serve.py --build
run: ./tools/op.sh docs --build
# Push to docs.comma.ai
- uses: actions/checkout@v7
+5 -1
View File
@@ -7,7 +7,7 @@ on:
env:
GIT_CONFIG_COUNT: 1
GIT_CONFIG_KEY_0: lfs.fetchexclude
GIT_CONFIG_VALUE_0: openpilot/selfdrive/modeld/models/big_driving_supercombo.onnx
GIT_CONFIG_VALUE_0: openpilot/selfdrive/modeld/models/big_driving_tinygrad.pkl
jobs:
build___nightly:
@@ -34,3 +34,7 @@ jobs:
- run: ./tools/op.sh setup
- name: Push __nightly
run: BRANCH=__nightly tools/release/build_stripped.sh
- name: Push chestnut nightly
run: |
git lfs pull --exclude=''
INCLUDE_BIG_MODEL=1 BRANCH=__nightly-chestnut tools/release/build_stripped.sh
+1 -1
View File
@@ -11,7 +11,7 @@ env:
PYTHONPATH: ${{ github.workspace }}
GIT_CONFIG_COUNT: 1
GIT_CONFIG_KEY_0: lfs.fetchexclude
GIT_CONFIG_VALUE_0: openpilot/selfdrive/modeld/models/big_driving_supercombo.onnx
GIT_CONFIG_VALUE_0: openpilot/selfdrive/modeld/models/big_driving_tinygrad.pkl
jobs:
package_updates:
+75 -62
View File
@@ -103,21 +103,23 @@ jobs:
- run: |
cd ${{ github.workspace }}/openpilot/openpilot
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
git lfs pull -X "**/selfdrive/modeld/models/big_*.onnx,**/selfdrive/modeld/models/dmonitoring_*.onnx,**/selfdrive/modeld/models/big_*.pkl,**/selfdrive/modeld/models/dmonitoring_*.pkl"
rm -f selfdrive/modeld/models/big_*.onnx selfdrive/modeld/models/dmonitoring_*.onnx selfdrive/modeld/models/big_*.pkl selfdrive/modeld/models/dmonitoring_*.pkl
else
git lfs pull -I "**/selfdrive/modeld/models/big_*.onnx" -X ""
find selfdrive/modeld/models -name "*.onnx" ! -name "big_*.onnx" -delete
git lfs pull -I "**/selfdrive/modeld/models/big_*.onnx,**/selfdrive/modeld/models/big_*.pkl" -X ""
find selfdrive/modeld/models -type f \( -name "*.onnx" -o -name "*.pkl" \) ! -name "big_*.onnx" ! -name "big_*.pkl" -delete
fi
if grep -lIF "version https://git-lfs.github.com/spec/v1" selfdrive/modeld/models/*.onnx; then
echo "::error::the ONNX files above are still LFS pointers, not real models"
if grep -lIF "version https://git-lfs.github.com/spec/v1" selfdrive/modeld/models/*.onnx selfdrive/modeld/models/*.pkl 2>/dev/null; then
echo "::error::the ONNX or PKL files above are still LFS pointers, not real models"
exit 1
fi
- name: 'Upload Artifact'
uses: actions/upload-artifact@v4
with:
name: models-${{ env.REF }}${{ inputs.artifact_suffix }}
path: ${{ github.workspace }}/openpilot/openpilot/selfdrive/modeld/models/*.onnx
path: |
${{ github.workspace }}/openpilot/openpilot/selfdrive/modeld/models/*.onnx
${{ github.workspace }}/openpilot/openpilot/selfdrive/modeld/models/*.pkl
if-no-files-found: error
build_model:
@@ -196,64 +198,75 @@ jobs:
OUTPUT_PKL="${{ env.MODELS_DIR }}/driving_tinygrad.pkl"
fi
# Generate metadata for all ONNX files
find "${{ env.MODELS_DIR }}" -maxdepth 1 -name '*.onnx' | while IFS= read -r onnx_file; do
echo "Generating metadata: $onnx_file"
env ${TG_FLAGS_QCOM} python3 "${{ env.MODELS_DIR }}/../get_model_metadata.py" "$onnx_file" || true
done
NATIVE_PKL=$(find "${{ env.MODELS_DIR }}" -maxdepth 1 -name "*.pkl" -print -quit)
# Detect model type and build compile args
VISION_ONNX=""
for f in "${{ env.MODELS_DIR }}/driving_vision.onnx" "${{ env.MODELS_DIR }}/big_driving_vision.onnx"; do
[ -f "$f" ] && VISION_ONNX="$f" && break
done
POLICY_ONNX=""
for f in "${{ env.MODELS_DIR }}/driving_policy.onnx" "${{ env.MODELS_DIR }}/big_driving_policy.onnx"; do
[ -f "$f" ] && POLICY_ONNX="$f" && break
done
OFF_POLICY_ONNX=""
for f in "${{ env.MODELS_DIR }}/driving_off_policy.onnx" "${{ env.MODELS_DIR }}/big_driving_off_policy.onnx"; do
[ -f "$f" ] && OFF_POLICY_ONNX="$f" && break
done
ON_POLICY_ONNX=""
for f in "${{ env.MODELS_DIR }}/driving_on_policy.onnx" "${{ env.MODELS_DIR }}/big_driving_on_policy.onnx"; do
[ -f "$f" ] && ON_POLICY_ONNX="$f" && break
done
SUPERCOMBO_ONNX=""
for f in "${{ env.MODELS_DIR }}/supercombo.onnx" "${{ env.MODELS_DIR }}/driving_supercombo.onnx" "${{ env.MODELS_DIR }}/big_supercombo.onnx" "${{ env.MODELS_DIR }}/big_driving_supercombo.onnx"; do
[ -f "$f" ] && SUPERCOMBO_ONNX="$f" && break
done
MODEL_TYPE="" ONNX_ARGS="" OUTPUT_NAME=""
if [ -f "$VISION_ONNX" ]; then
ONNX_ARGS="--vision-onnx $VISION_ONNX"
if [ -f "$ON_POLICY_ONNX" ] && [ -f "$OFF_POLICY_ONNX" ]; then
MODEL_TYPE=vision_multi_policy
ONNX_ARGS="$ONNX_ARGS --off-policy-onnx $OFF_POLICY_ONNX --on-policy-onnx $ON_POLICY_ONNX"
elif [ -f "$OFF_POLICY_ONNX" ] && [ -f "$POLICY_ONNX" ]; then
MODEL_TYPE=vision_multi_policy
ONNX_ARGS="$ONNX_ARGS --policy-onnx $POLICY_ONNX --off-policy-onnx $OFF_POLICY_ONNX"
elif [ -f "$POLICY_ONNX" ]; then
MODEL_TYPE=vision_policy
ONNX_ARGS="$ONNX_ARGS --policy-onnx $POLICY_ONNX"
if [ -n "$NATIVE_PKL" ]; then
echo "Found native precompiled pkl: $NATIVE_PKL"
if [ "$NATIVE_PKL" != "$OUTPUT_PKL" ]; then
mv "$NATIVE_PKL" "$OUTPUT_PKL"
fi
elif [ -f "$SUPERCOMBO_ONNX" ]; then
MODEL_TYPE=supercombo
ONNX_ARGS="--supercombo-onnx $SUPERCOMBO_ONNX"
fi
echo "Chunking pkl"
python3 -c "from openpilot.common.file_chunker import chunk_file, get_chunk_targets; import os; p='$OUTPUT_PKL'; chunk_file(p, get_chunk_targets(p, os.path.getsize(p)))"
else
# Generate metadata for all ONNX files
find "${{ env.MODELS_DIR }}" -maxdepth 1 -name '*.onnx' | while IFS= read -r onnx_file; do
echo "Generating metadata: $onnx_file"
env ${TG_FLAGS_QCOM} python3 "${{ env.MODELS_DIR }}/../get_model_metadata.py" "$onnx_file" || true
done
if [ -n "$MODEL_TYPE" ]; then
echo "Detected: $MODEL_TYPE -> $OUTPUT_PKL"
env ${TG_FLAGS} python3 "$COMPILE_MODELD" \
--model-type $MODEL_TYPE \
--model-size $MODEL_SIZE \
--camera-resolutions $CAMERA_RES \
$ONNX_ARGS \
--output "$OUTPUT_PKL"
# Detect model type and build compile args
VISION_ONNX=""
for f in "${{ env.MODELS_DIR }}/driving_vision.onnx" "${{ env.MODELS_DIR }}/big_driving_vision.onnx"; do
[ -f "$f" ] && VISION_ONNX="$f" && break
done
POLICY_ONNX=""
for f in "${{ env.MODELS_DIR }}/driving_policy.onnx" "${{ env.MODELS_DIR }}/big_driving_policy.onnx"; do
[ -f "$f" ] && POLICY_ONNX="$f" && break
done
OFF_POLICY_ONNX=""
for f in "${{ env.MODELS_DIR }}/driving_off_policy.onnx" "${{ env.MODELS_DIR }}/big_driving_off_policy.onnx"; do
[ -f "$f" ] && OFF_POLICY_ONNX="$f" && break
done
ON_POLICY_ONNX=""
for f in "${{ env.MODELS_DIR }}/driving_on_policy.onnx" "${{ env.MODELS_DIR }}/big_driving_on_policy.onnx"; do
[ -f "$f" ] && ON_POLICY_ONNX="$f" && break
done
SUPERCOMBO_ONNX=""
for f in "${{ env.MODELS_DIR }}/supercombo.onnx" "${{ env.MODELS_DIR }}/driving_supercombo.onnx" "${{ env.MODELS_DIR }}/big_supercombo.onnx" "${{ env.MODELS_DIR }}/big_driving_supercombo.onnx"; do
[ -f "$f" ] && SUPERCOMBO_ONNX="$f" && break
done
MODEL_TYPE="" ONNX_ARGS="" OUTPUT_NAME=""
if [ -f "$VISION_ONNX" ]; then
ONNX_ARGS="--vision-onnx $VISION_ONNX"
if [ -f "$ON_POLICY_ONNX" ] && [ -f "$OFF_POLICY_ONNX" ]; then
MODEL_TYPE=vision_multi_policy
ONNX_ARGS="$ONNX_ARGS --off-policy-onnx $OFF_POLICY_ONNX --on-policy-onnx $ON_POLICY_ONNX"
elif [ -f "$OFF_POLICY_ONNX" ] && [ -f "$POLICY_ONNX" ]; then
MODEL_TYPE=vision_multi_policy
ONNX_ARGS="$ONNX_ARGS --policy-onnx $POLICY_ONNX --off-policy-onnx $OFF_POLICY_ONNX"
elif [ -f "$POLICY_ONNX" ]; then
MODEL_TYPE=vision_policy
ONNX_ARGS="$ONNX_ARGS --policy-onnx $POLICY_ONNX"
fi
elif [ -f "$SUPERCOMBO_ONNX" ]; then
MODEL_TYPE=supercombo
ONNX_ARGS="--supercombo-onnx $SUPERCOMBO_ONNX"
fi
if [ -n "$MODEL_TYPE" ]; then
echo "Detected: $MODEL_TYPE -> $OUTPUT_PKL"
env ${TG_FLAGS} python3 "$COMPILE_MODELD" \
--model-type $MODEL_TYPE \
--model-size $MODEL_SIZE \
--camera-resolutions $CAMERA_RES \
$ONNX_ARGS \
--output "$OUTPUT_PKL"
fi
fi
- name: Prepare Output
@@ -170,6 +170,9 @@ jobs:
scons -j1 cache_dir="$SCONS_CACHE" --minimal \
openpilot/selfdrive/locationd openpilot/sunnypilot/selfdrive/locationd
echo "Building rest of sunnypilot"
sudo rm -rf /tmp/* || true
mkdir -p "$BUILD_DIR/tmp"
export TMPDIR="$BUILD_DIR/tmp"
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
@@ -226,16 +229,16 @@ jobs:
HF_REPO: sunnypilot/sunnypilot_models_v1
HF_DEFAULTS_PATH: models/defaults/big
steps:
- name: Resolve ONNX hash and tinygrad ref via API
- name: Resolve 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/big_driving_supercombo.onnx?ref=${REF}" --jq '.sha')
BLOB_SHA=$(gh api "repos/${GH_REPO}/contents/openpilot/selfdrive/modeld/models/big_driving_tinygrad.pkl?ref=${REF}" --jq '.sha')
[ -n "$BLOB_SHA" ] || { echo "::error::Failed to resolve big_driving_tinygrad.pkl blob SHA"; exit 1; }
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
echo "big PKL hash: $ONNX_HASH"
[ -n "$ONNX_HASH" ] || { echo "::error::Failed to extract big PKL hash"; exit 1; }
TINYGRAD_REF=$(gh api "repos/${GH_REPO}/contents/tinygrad_repo?ref=${REF}" --jq '.sha')
echo "tinygrad ref: $TINYGRAD_REF"
@@ -251,7 +254,7 @@ jobs:
}
if check_defaults; then
echo "HF defaults match repo ONNX hash and tinygrad ref"
echo "HF defaults match repo tinygrad ref"
exit 0
fi
@@ -309,7 +312,7 @@ jobs:
HF_REPO: sunnypilot/sunnypilot_models_v1
HF_DEFAULTS_PATH: models/defaults/small
steps:
- name: Resolve ONNX hash and tinygrad ref via API
- name: Resolve tinygrad ref via API
id: resolve
run: |
REF="${{ github.head_ref || github.ref_name }}"
@@ -318,7 +321,7 @@ jobs:
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
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"
@@ -334,7 +337,7 @@ jobs:
}
if check_defaults; then
echo "HF defaults match repo ONNX hash and tinygrad ref"
echo "HF defaults match repo tinygrad ref"
exit 0
fi
@@ -392,7 +395,7 @@ jobs:
HF_REPO: sunnypilot/sunnypilot_models_v1
HF_DEFAULTS_PATH: models/defaults/dm
steps:
- name: Resolve ONNX hash and tinygrad ref via API
- name: Resolve tinygrad ref via API
id: resolve
run: |
REF="${{ github.head_ref || github.ref_name }}"
@@ -401,7 +404,7 @@ jobs:
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
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"
@@ -417,7 +420,7 @@ jobs:
}
if check_defaults; then
echo "HF defaults match DM ONNX hash and tinygrad ref"
echo "HF defaults match DM tinygrad ref"
exit 0
fi
+6 -1
View File
@@ -48,6 +48,8 @@ jobs:
submodules: true
- name: Download Model Chunks in Parallel
env:
HF_TOKEN: ${{ secrets.HF_TOKEN }}
run: |
mkdir -p /tmp/model_chunks
echo '${{ toJson(matrix.artifact.chunks) }}' > chunks.json
@@ -63,12 +65,15 @@ jobs:
with open(manifest_path, "w") as f:
f.write(str(len(chunks)))
base_dir = os.environ["BASE_DIR"]
hf_token = os.environ.get("HF_TOKEN", "")
with open("/tmp/curl_config.txt", "w") as f:
for c in chunks:
fn = c["file_name"]
f.write(f"url = \"{base_dir}/{fn}\"\noutput = \"/tmp/model_chunks/{fn}\"\n")
if hf_token:
f.write(f"header = \"Authorization: Bearer {hf_token}\"\n")
'
curl -Z --parallel-immediate --parallel-max 16 -s -S -f -L -K /tmp/curl_config.txt
curl -Z --parallel-immediate --parallel-max 12 --retry 3 --retry-all-errors -s -S -f -L -K /tmp/curl_config.txt
- name: Run Model Compatibility Test
env:
+1 -1
View File
@@ -22,7 +22,7 @@ env:
PYTHONPATH: ${{ github.workspace }}
GIT_CONFIG_COUNT: 1
GIT_CONFIG_KEY_0: lfs.fetchexclude
GIT_CONFIG_VALUE_0: openpilot/selfdrive/modeld/models/big_driving_supercombo.onnx
GIT_CONFIG_VALUE_0: openpilot/selfdrive/modeld/models/big_driving_tinygrad.pkl
jobs:
build_release:
+2
View File
@@ -50,6 +50,8 @@ st[0-9A-Za-z][0-9A-Za-z][0-9A-Za-z][0-9A-Za-z][0-9A-Za-z][0-9A-Za-z]
*.stats
*.pkl
*.pkl*
!openpilot/selfdrive/modeld/models/big_driving_tinygrad.pkl
!openpilot/sunnypilot/modeld_v2/models/*.pkl
config.json
compile_commands.json
compare_runtime*.html
+1
View File
@@ -19,3 +19,4 @@
[submodule "sunnypilot/neural_network_data"]
path = openpilot/sunnypilot/neural_network_data
url = https://github.com/sunnypilot/neural-network-data.git
+1
View File
@@ -1,4 +1,5 @@
[lfs]
url = https://gitlab.com/sunnypilot/public/sunnypilot-new-lfs.git/info/lfs
pushurl = ssh://git@gitlab.com/sunnypilot/public/sunnypilot-new-lfs.git
locksverify = false
Vendored
+20 -4
View File
@@ -12,16 +12,17 @@ def retryWithDelay(int maxRetries, int delay, Closure body) {
def device(String ip, String step_label, String cmd) {
withCredentials([file(credentialsId: 'id_rsa', variable: 'key_file')]) {
def ssh_cmd = """
ssh -o ControlMaster=auto -o ControlPath=/tmp/ssh_control_%C -o ControlPersist=yes -o ConnectTimeout=5 -o ServerAliveInterval=5 -o ServerAliveCountMax=2 -o BatchMode=yes -o StrictHostKeyChecking=no -i ${key_file} 'comma@${ip}' exec /usr/bin/bash <<'END'
ssh -o ControlMaster=no -o ControlPath=none -o ConnectTimeout=5 -o ServerAliveInterval=5 -o ServerAliveCountMax=12 -o BatchMode=yes -o StrictHostKeyChecking=no -i ${key_file} 'comma@${ip}' exec setpriv --pdeathsig HUP /usr/bin/bash <<'END'
set -e
export TERM=xterm-256color
shopt -s huponexit # kill all child processes when the shell exits
trap 'kill 0' HUP # stop this process group on SSH disconnect
export CI=1
export PYTHONWARNINGS=error
export PYTHONFAULTHANDLER=1
export COMMA_CACHE=/data/tmp/comma_download_cache
#export LOGPRINT=debug # this has gotten too spammy...
export TEST_DIR=${env.TEST_DIR}
@@ -69,7 +70,8 @@ export LD_LIBRARY_PATH="\$(python -c 'import ffmpeg; print(ffmpeg.LIB_DIR)'):/us
ln -snf ${env.TEST_DIR} /data/pythonpath
cd ${env.TEST_DIR} || true
time ${cmd}
time ( ${cmd} ) &
wait \$!
END"""
sh script: ssh_cmd, label: step_label
@@ -169,7 +171,7 @@ node {
env.GIT_BRANCH = checkout(scm).GIT_BRANCH
env.GIT_COMMIT = checkout(scm).GIT_COMMIT
def excludeBranches = ['__nightly', 'devel', 'devel-staging',
def excludeBranches = ['__nightly', '__nightly-chestnut', 'devel', 'devel-staging',
'release-tizi', 'release-tizi-staging', 'release-mici', 'release-mici-staging', 'testing-closet*', 'hotfix-*']
def excludeRegex = excludeBranches.join('|').replaceAll('\\*', '.*')
@@ -201,6 +203,12 @@ node {
)
}
if (env.BRANCH_NAME == '__nightly-chestnut') {
deviceStage("build nightly-chestnut", "mici-chestnut-ci", [], [
step("build nightly-chestnut", "SCONSFLAGS=-j4 INCLUDE_BIG_MODEL=1 PANDA_DEBUG_BUILD=1 RELEASE_BRANCH=nightly-chestnut $SOURCE_DIR/tools/release/build_release.sh TestChestnutOnroad"),
])
}
if (!env.BRANCH_NAME.matches(excludeRegex)) {
parallel (
'onroad tests': {
@@ -252,6 +260,14 @@ node {
step("test amp", "./openpilot/common/hardware/comma/tests/test_amplifier.py"),
])
},
'chestnut': {
deviceStage("chestnut", "mici-chestnut-ci", ["UNSAFE=1", "CHESTNUT=1"], [
step("build", "./openpilot/selfdrive/test/chestnut.sh"),
step("model replay", "openpilot/selfdrive/test/process_replay/model_replay.py --chestnut"),
step("onroad tests", "./openpilot/selfdrive/test/test_onroad.py TestChestnutOnroad", [timeout: 120]),
step("test power draw", "./openpilot/selfdrive/test/test_power_draw.py"),
])
},
)
}
+3 -1
View File
@@ -87,7 +87,6 @@ acados_include_dirs = [
# vendored in commaai/dependencies.
allowed_system_libs = {
"EGL", "GLESv2", "GL",
"Qt5Charts", "Qt5Core", "Qt5Gui", "Qt5Widgets",
"dl", "drm", "gbm", "m", "pthread",
}
@@ -121,6 +120,7 @@ def _libflags(target, source, env, for_signature):
env = Environment(
ENV={
"PATH": os.environ['PATH'],
"TMPDIR": os.environ.get('TMPDIR', '/tmp'),
"PYTHONPATH": os.pathsep.join(submodule_python_paths),
"ACADOS_SOURCE_DIR": acados.DIR,
"ACADOS_PYTHON_INTERFACE_PATH": acados.TEMPLATE_DIR,
@@ -346,6 +346,8 @@ AddPostAction(BUILD_TARGETS or [Dir('.')], prune_cache_dir)
def check_build_product_size(target, source, env):
limit = 50 * 1024 * 1024 # GitHub max size
for t in target:
if str(t).endswith('.pkl'): # chunked during release packaging
continue
if hasattr(t, 'isfile') and t.isfile() and (size := os.path.getsize(t.abspath)) > limit:
raise SCons.Errors.UserError(f"{t} is {size / (1024 * 1024):.1f} MiB, exceeding the {limit / (1024 * 1024):.1f} MiB limit")
if not GetOption('extras'):
+7 -7
View File
@@ -34,7 +34,7 @@ A supported vehicle is one that just works when you install a comma device. All
|Chrysler|Pacifica Hybrid 2019-25|Adaptive Cruise Control (ACC)|Stock|0 mph|39 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 FCA connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Chrysler Pacifica Hybrid 2019-25">Buy Here</a></sub></details>|||
|comma|body|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|None|<a href="https://youtu.be/VT-i3yRsX2s?t=2736" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>||
|CUPRA[<sup>12</sup>](#footnotes)|Ateca 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=CUPRA Ateca 2018-23">Buy Here</a></sub></details>|||
|CUPRA[<sup>12</sup>](#footnotes)|Born 2021-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot[<sup>16</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=CUPRA Born 2021-23">Buy Here</a></sub></details>|||
|CUPRA|Born 2021-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot[<sup>16</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW MEB connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=CUPRA Born 2021-23">Buy Here</a></sub></details>|||
|Dodge|Durango 2020-21|Adaptive Cruise Control (ACC)|Stock|0 mph|39 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 FCA connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Dodge Durango 2020-21">Buy Here</a></sub></details>|||
|Ford|Bronco Sport 2021-24|Co-Pilot360 Assist+|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Ford Q3 connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Ford Bronco Sport 2021-24">Buy Here</a></sub></details>|||
|Ford|Escape 2020-22|Co-Pilot360 Assist+|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Ford Q3 connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Ford Escape 2020-22">Buy Here</a></sub></details>|||
@@ -99,7 +99,7 @@ A supported vehicle is one that just works when you install a comma device. All
|Honda|Fit 2018-20|Honda Sensing|openpilot|26 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<details><summary>Parts</summary><sub>- 1 Honda Nidec connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Honda Fit 2018-20">Buy Here</a></sub></details>|||
|Honda|Freed 2020|Honda Sensing|openpilot|26 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<details><summary>Parts</summary><sub>- 1 Honda Nidec connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Honda Freed 2020">Buy Here</a></sub></details>|||
|Honda|HR-V 2019-22|Honda Sensing|openpilot|26 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<details><summary>Parts</summary><sub>- 1 Honda Nidec connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Honda HR-V 2019-22">Buy Here</a></sub></details>|||
|Honda|HR-V 2023-25|All|openpilot available[<sup>1,5</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch B connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Honda HR-V 2023-25">Buy Here</a></sub></details>|||
|Honda|HR-V 2023-27|All|openpilot available[<sup>1,5</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch B connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Honda HR-V 2023-27">Buy Here</a></sub></details>|||
|Honda|Insight 2019-22|All|openpilot available[<sup>1,5</sup>](#footnotes)|0 mph|3 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch A connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Honda Insight 2019-22">Buy Here</a></sub></details>|||
|Honda|Inspire 2018|All|openpilot available[<sup>1,5</sup>](#footnotes)|0 mph|3 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch A connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Honda Inspire 2018">Buy Here</a></sub></details>|||
|Honda|N-Box 2018|All|openpilot available[<sup>1,5</sup>](#footnotes)|0 mph|11 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch A connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Honda N-Box 2018">Buy Here</a></sub></details>|||
@@ -268,8 +268,8 @@ A supported vehicle is one that just works when you install a comma device. All
|Škoda[<sup>12</sup>](#footnotes)|Superb 2015-22[<sup>15</sup>](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Škoda Superb 2015-22">Buy Here</a></sub></details>|||
|Tesla[<sup>10</sup>](#footnotes)|Model 3 (with HW3) 2019-23[<sup>9</sup>](#footnotes)|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Tesla A connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Tesla Model 3 (with HW3) 2019-23">Buy Here</a></sub></details>|||
|Tesla[<sup>10</sup>](#footnotes)|Model 3 (with HW4) 2024-25[<sup>9</sup>](#footnotes)|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Tesla B connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Tesla Model 3 (with HW4) 2024-25">Buy Here</a></sub></details>|||
|Tesla[<sup>10</sup>](#footnotes)|Model Y (with HW3) 2020-23[<sup>9</sup>](#footnotes)|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Tesla A connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Tesla Model Y (with HW3) 2020-23">Buy Here</a></sub></details>|||
|Tesla[<sup>10</sup>](#footnotes)|Model Y (with HW4) 2024-25[<sup>9</sup>](#footnotes)|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Tesla B connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Tesla Model Y (with HW4) 2024-25">Buy Here</a></sub></details>|||
|Tesla[<sup>10</sup>](#footnotes)|Model Y (with HW3) 2020-24[<sup>9</sup>](#footnotes)|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Tesla A connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Tesla Model Y (with HW3) 2020-24">Buy Here</a></sub></details>|||
|Tesla[<sup>10</sup>](#footnotes)|Model Y (with HW4) 2023-25[<sup>9</sup>](#footnotes)|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Tesla B connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Tesla Model Y (with HW4) 2023-25">Buy Here</a></sub></details>|||
|Toyota|Alphard 2019-20|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota Alphard 2019-20">Buy Here</a></sub></details>|||
|Toyota|Alphard Hybrid 2021|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota Alphard Hybrid 2021">Buy Here</a></sub></details>|||
|Toyota|Avalon 2016|Toyota Safety Sense P|Stock|19 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota Avalon 2016">Buy Here</a></sub></details>|||
@@ -335,8 +335,8 @@ A supported vehicle is one that just works when you install a comma device. All
|Volkswagen[<sup>12</sup>](#footnotes)|Golf R 2015-19|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Golf R 2015-19">Buy Here</a></sub></details>|||
|Volkswagen[<sup>12</sup>](#footnotes)|Golf SportsVan 2015-20|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Golf SportsVan 2015-20">Buy Here</a></sub></details>|||
|Volkswagen[<sup>12</sup>](#footnotes)|Grand California 2019-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|31 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Grand California 2019-24">Buy Here</a></sub></details>|<a href="https://youtu.be/4100gLeabmo" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>||
|Volkswagen[<sup>12</sup>](#footnotes)|ID.4 2021-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot[<sup>16</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen ID.4 2021-23">Buy Here</a></sub></details>|||
|Volkswagen[<sup>12</sup>](#footnotes)|ID.4 2024-25|Adaptive Cruise Control (ACC) & Lane Assist|openpilot[<sup>16</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen ID.4 2024-25">Buy Here</a></sub></details>|||
|Volkswagen|ID.4 2021-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot[<sup>16</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW MEB connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen ID.4 2021-23">Buy Here</a></sub></details>|||
|Volkswagen|ID.4 2024-25|Adaptive Cruise Control (ACC) & Lane Assist|openpilot[<sup>16</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW MEB connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen ID.4 2024-25">Buy Here</a></sub></details>|||
|Volkswagen[<sup>12</sup>](#footnotes)|Jetta 2019-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Jetta 2019-23">Buy Here</a></sub></details>|||
|Volkswagen[<sup>12</sup>](#footnotes)|Jetta GLI 2021-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Jetta GLI 2021-23">Buy Here</a></sub></details>|||
|Volkswagen|Passat 2015-22[<sup>14</sup>](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Passat 2015-22">Buy Here</a></sub></details>|||
@@ -363,7 +363,7 @@ A supported vehicle is one that just works when you install a comma device. All
<sup>6</sup>See more setup details for <a href="https://github.com/commaai/openpilot/wiki/nissan" target="_blank">Nissan</a>. <br />
<sup>7</sup>In the non-US market, openpilot requires the car to come equipped with EyeSight with Lane Keep Assistance. <br />
<sup>8</sup>Enabling longitudinal control (alpha) will disable all EyeSight functionality, including AEB, LDW, and RAB. <br />
<sup>9</sup>Some 2023 model years have HW4. To check which hardware type your vehicle has, look for <b>Autopilot computer</b> under <b>Software -> Additional Vehicle Information</b> on your vehicle's touchscreen. See <a href="https://www.notateslaapp.com/news/2173/how-to-check-if-your-tesla-has-hardware-4-ai4-or-hardware-3">this page</a> for more information. <br />
<sup>9</sup>Model years 2023 and 2024 can have either hardware type, depending on build date and factory. To check which hardware type your vehicle has, look for <b>Autopilot computer</b> under <b>Software -> Additional Vehicle Information</b> on your vehicle's touchscreen. See <a href="https://www.notateslaapp.com/news/2173/how-to-check-if-your-tesla-has-hardware-4-ai4-or-hardware-3">this page</a> for more information. <br />
<sup>10</sup>See more setup details for <a href="https://github.com/commaai/openpilot/wiki/tesla" target="_blank">Tesla</a>. <br />
<sup>11</sup>openpilot operates above 28mph for Camry 4CYL L, 4CYL LE and 4CYL SE which don't have Full-Speed Range Dynamic Radar Cruise Control. <br />
<sup>12</sup>The J533 harness plugs in at the CAN gateway under the dashboard, just above the steering column. More information can be found at <a href="https://docs.howtocomma.com/docs/j533-harness-install" target="_blank">this guide</a>. <br />
+2 -2
View File
@@ -5,10 +5,10 @@ The site is updated on pushes to master by this [workflow](../.github/workflows/
**1. Build the site**
``` bash
python docs/serve.py --build
op docs --build
```
**2. Run the site locally** (rebuilds on change)
``` bash
python docs/serve.py
op docs
```
-69
View File
@@ -1,69 +0,0 @@
# Action controller: stronger proportional C0 correction
The model action remains the steering target. Increase action-mode C0 P from
0.5 to 1.0 so an existing tracking error produces twice the immediate C0
correction, before command clipping. This is a trial gain, not an identified
optimum or a claim of twice the wheel response.
C0 base mapping, distances, C1 P=0.75, C1 I=1.0, command bounds, upstream request
limits, driver arbitration and CAN cadence are unchanged. Direct-path mode
retains C0 P=0.5. Explicit gain overrides remain available for offline analysis.
The C0 correction has no integral state. It vanishes at zero error and changes
sign on overshoot; the base command and existing C1 integral still remain. A
higher P can amplify noise or oscillation through delayed vehicle response.
Command replay does not establish physical stability, faster tracking or better
unwinding.
## Selecting the trial
In Sunnylink, while offroad:
- Enable **Selected-Action Path Tracking (Experimental)**.
- Disable **Model Geometry Reference (Experimental)** to select the original
model action. Leaving it enabled keeps the unchanged direct-path controller.
- Keep **C0 one-second distance** disabled for the fixed 7 m configuration.
Restart the updated software while offroad before testing. The controller and
reference selection are latched at startup. Disengagement alone does not switch
the reference. Turning the master controller toggle off restores upstream Ford.
Action-mode diagnostics now identify `model-action-curvature-c0-feedback-v19`
and record `c0_proportional_gain=1.0`. Direct path remains
`model-path-direct-feedback-v17`, with its gain at 0.5.
## Verification
Baseline: `0020c0c41940fe1f4b33dd23297def77a0a67e92`.
The Ford controller, startup selection, controlsd publication/CAN, geometry and
Sunnylink settings suites pass: 648 tests and 2 subtests. Coverage includes
catch-up, opposite correction on overshoot, zero-reference unwind, unchanged C1
state, upstream fallback and preservation of the direct-path gain.
Native-time replay over ten older Lightning routes checks every control cycle
and performs a CAN pack/decode check every tenth cycle. The baseline receives
the same curvature conversion as the new controller. C1 commands, proportional
terms, integrals and command validity are identical; C0 proportional correction
is exactly doubled within numerical tolerance, and commands retain field bounds.
There is no C0 change when feedback is disabled or the correction is zero.
```sh
PYTHONPATH=.:opendbc_repo python tools/ford_pscm_lab/c0_feedback_validate.py \
--baseline 0020c0c41940fe1f4b33dd23297def77a0a67e92 \
--output .cache/ford_action_c0_gain_v19 --workers 4
```
Three recent routes (157, 15a, 15b) additionally compare original model action
and direct-path modes against the same baseline, preserving recorded motion.
Original action is taken from the intake on 157 and timestamp-matched telemetry
on 15a/15b. All direct-path commands and their accumulated state remain identical.
The supplementary harness and per-cycle inputs are in the local replay cache.
Combined validation totals and route results are in
`ford_action_c0_gain_v19_validation.json`.
On the recorded measurements, a clean route-157 example requesting 174 degrees
with the wheel at 74 degrees changes C0 magnitude from 2.23 to 3.42 m while C1
remains at 0.5 rad. A route-15b example requesting 206 degrees with the wheel at
239 degrees reduces the remaining into-turn C0 magnitude from 0.85 to 0.46 m.
These are different commands on frozen measurements, not predicted wheel motion.
@@ -1,621 +0,0 @@
{
"baseline": "0020c0c41940fe1f4b33dd23297def77a0a67e92",
"action_c0_gain": 1.0,
"direct_path_c0_gain": 0.5,
"scope": "Frozen recorded measurements; validates command differences and preserved direct-path behavior, not changed vehicle motion.",
"tests_passed": 648,
"subtests_passed": 2,
"cycles": 1290812,
"can_round_trips": 141158,
"older_routes": [
{
"route": "112",
"baseline": "0020c0c41940fe1f4b33dd23297def77a0a67e92",
"baseline_sha256": "02b64f40a56cadefb181afbf7c3720affa3c7fe4ac18a2902dc9c546fd6b395d",
"cycles": 108971,
"wire_round_trips": 10898,
"c1_identical": true,
"c0_gain": 1.0,
"metrics": {
"all": {
"n": 83590,
"delta_c0_m_quantiles": [
0.009999999999999787,
0.28000000000000025,
1.7200000000000006
],
"c0_old_capped": 0,
"c0_new_capped": 0
},
"straight": {
"n": 43982,
"delta_c0_m_quantiles": [
0.009999999999999787,
0.05000000000000071,
1.4600000000000009
],
"c0_old_capped": 0,
"c0_new_capped": 0
},
"turn": {
"n": 9939,
"delta_c0_m_quantiles": [
0.15999999999999925,
1.12,
1.7200000000000006
],
"c0_old_capped": 0,
"c0_new_capped": 0
},
"large_error": {
"n": 3604,
"delta_c0_m_quantiles": [
0.7299999999999995,
1.4299999999999997,
1.7200000000000006
],
"c0_old_capped": 0,
"c0_new_capped": 0
}
},
"sources": {
".cache/ford_route112/route.npz": "2b08a2fb636f7d14556d7df4035eafc1d1b97932237955528562a16db2b31d3e",
".cache/ford_route112/metadata.json": "726d78a7e7aa45307dcfe27cb00775c20ecc9d54538eb7f26a0166fc216226ce",
".cache/ford_route112/model_paths.npz": "9837afe78aab4cad288cad98a595a5777fa8a66bb235986b1272a7f7c54e559a",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/c0_feedback_validate.py": "66eca5715db49a47d30d9f4ed74b1d41b2bdb1d1e513ffdf85be6d94c9c8ac39",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/openpilot/selfdrive/controls/lib/ford_model_action.py": "d5841c1f6a4368de20ff14de89a48d7dce737c6a41d79628ccb4f8b19f28f91b"
}
},
{
"route": "113",
"baseline": "0020c0c41940fe1f4b33dd23297def77a0a67e92",
"baseline_sha256": "02b64f40a56cadefb181afbf7c3720affa3c7fe4ac18a2902dc9c546fd6b395d",
"cycles": 49614,
"wire_round_trips": 4962,
"c1_identical": true,
"c0_gain": 1.0,
"metrics": {
"all": {
"n": 24171,
"delta_c0_m_quantiles": [
0.009999999999999787,
0.9499999999999993,
2.2299999999999995
],
"c0_old_capped": 0,
"c0_new_capped": 0
},
"straight": {
"n": 16815,
"delta_c0_m_quantiles": [
0.009999999999999787,
0.0529999999999931,
1.0599999999999996
],
"c0_old_capped": 0,
"c0_new_capped": 0
},
"turn": {
"n": 2920,
"delta_c0_m_quantiles": [
0.6600000000000001,
1.8699999999999997,
2.2299999999999995
],
"c0_old_capped": 0,
"c0_new_capped": 0
},
"large_error": {
"n": 2831,
"delta_c0_m_quantiles": [
0.8600000000000003,
1.9450000000000007,
2.2299999999999995
],
"c0_old_capped": 0,
"c0_new_capped": 0
}
},
"sources": {
".cache/ford_route113/route.npz": "774ca4a21b7113c2706d6130bc180c3216ea4833155300ab01e75b3486e36327",
".cache/ford_route113/metadata.json": "1c0ca74dd48b90ab9d5444c5ca7f8aa9361700bbdf98bd5e853be50ad2895d7f",
".cache/ford_route113/model_paths.npz": "93c41761eb85263f534f5371b905482cf7c948582eb1e9149966594be1d3768f",
".cache/ford_route112/metadata.json": "726d78a7e7aa45307dcfe27cb00775c20ecc9d54538eb7f26a0166fc216226ce",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/c0_feedback_validate.py": "66eca5715db49a47d30d9f4ed74b1d41b2bdb1d1e513ffdf85be6d94c9c8ac39",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/openpilot/selfdrive/controls/lib/ford_model_action.py": "d5841c1f6a4368de20ff14de89a48d7dce737c6a41d79628ccb4f8b19f28f91b"
}
},
{
"route": "117",
"baseline": "0020c0c41940fe1f4b33dd23297def77a0a67e92",
"baseline_sha256": "02b64f40a56cadefb181afbf7c3720affa3c7fe4ac18a2902dc9c546fd6b395d",
"cycles": 27301,
"wire_round_trips": 2731,
"c1_identical": true,
"c0_gain": 1.0,
"metrics": {
"all": {
"n": 13659,
"delta_c0_m_quantiles": [
0.03000000000000025,
0.5,
1.5999999999999996
],
"c0_old_capped": 0,
"c0_new_capped": 0
},
"straight": {
"n": 5001,
"delta_c0_m_quantiles": [
0.019999999999999574,
0.2999999999999998,
1.5999999999999996
],
"c0_old_capped": 0,
"c0_new_capped": 0
},
"turn": {
"n": 2891,
"delta_c0_m_quantiles": [
0.1200000000000001,
0.7400000000000002,
1.38
],
"c0_old_capped": 0,
"c0_new_capped": 0
},
"large_error": {
"n": 1195,
"delta_c0_m_quantiles": [
0.54,
1.5600000000000005,
1.5999999999999996
],
"c0_old_capped": 0,
"c0_new_capped": 0
}
},
"sources": {
".cache/ford_route117/route.npz": "65e4ea6c76dce73018b636479b3777116f1e58bb9813e6c1a7b79018b219140e",
".cache/ford_route117/metadata.json": "348396b3059c5f54d7938cd3acf2bb268a813b47ff47561da7a94cb75c3f3114",
".cache/ford_route117/model_paths.npz": "079f6003968287530e1ed6f2243c727e80c2ef3d16fb8a7d945d7eca744d54e7",
".cache/ford_route112/metadata.json": "726d78a7e7aa45307dcfe27cb00775c20ecc9d54538eb7f26a0166fc216226ce",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/c0_feedback_validate.py": "66eca5715db49a47d30d9f4ed74b1d41b2bdb1d1e513ffdf85be6d94c9c8ac39",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/openpilot/selfdrive/controls/lib/ford_model_action.py": "d5841c1f6a4368de20ff14de89a48d7dce737c6a41d79628ccb4f8b19f28f91b"
}
},
{
"route": "11c",
"baseline": "0020c0c41940fe1f4b33dd23297def77a0a67e92",
"baseline_sha256": "02b64f40a56cadefb181afbf7c3720affa3c7fe4ac18a2902dc9c546fd6b395d",
"cycles": 54146,
"wire_round_trips": 5415,
"c1_identical": true,
"c0_gain": 1.0,
"metrics": {
"all": {
"n": 36780,
"delta_c0_m_quantiles": [
0.020000000000000462,
0.3700000000000001,
2.1900000000000004
],
"c0_old_capped": 0,
"c0_new_capped": 0
},
"straight": {
"n": 16750,
"delta_c0_m_quantiles": [
0.010000000000000675,
0.09999999999999964,
0.5099999999999998
],
"c0_old_capped": 0,
"c0_new_capped": 0
},
"turn": {
"n": 2461,
"delta_c0_m_quantiles": [
0.1899999999999995,
0.8799999999999999,
2.1900000000000004
],
"c0_old_capped": 0,
"c0_new_capped": 0
},
"large_error": {
"n": 1827,
"delta_c0_m_quantiles": [
0.5500000000000003,
0.9500000000000002,
2.1900000000000004
],
"c0_old_capped": 0,
"c0_new_capped": 0
}
},
"sources": {
".cache/ford_maneuver_11c/analysis/route.npz": "1f3c37a7c7ae85ba69e9958435a9568f8244fa862f43ad1ee2a0192d1df13966",
".cache/ford_maneuver_11c/analysis/metadata.json": "2d0051d23059145988ca905e5386704def24b8080ce92541cec5e4ab501bcd0c",
".cache/ford_maneuver_11c/analysis/model_paths.npz": "0ff433da3dde1d4d7f10596cd53558ec5de9d56fbc49aec3e8afb4c9872377d5",
".cache/ford_route112/metadata.json": "726d78a7e7aa45307dcfe27cb00775c20ecc9d54538eb7f26a0166fc216226ce",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/c0_feedback_validate.py": "66eca5715db49a47d30d9f4ed74b1d41b2bdb1d1e513ffdf85be6d94c9c8ac39",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/openpilot/selfdrive/controls/lib/ford_model_action.py": "d5841c1f6a4368de20ff14de89a48d7dce737c6a41d79628ccb4f8b19f28f91b"
}
},
{
"route": "125",
"baseline": "0020c0c41940fe1f4b33dd23297def77a0a67e92",
"baseline_sha256": "02b64f40a56cadefb181afbf7c3720affa3c7fe4ac18a2902dc9c546fd6b395d",
"cycles": 41717,
"wire_round_trips": 4172,
"c1_identical": true,
"c0_gain": 1.0,
"metrics": {
"all": {
"n": 41107,
"delta_c0_m_quantiles": [
0.0,
0.05000000000000071,
0.39000000000000057
],
"c0_old_capped": 0,
"c0_new_capped": 0
},
"straight": {
"n": 33135,
"delta_c0_m_quantiles": [
0.0,
0.010000000000000675,
0.27000000000000046
],
"c0_old_capped": 0,
"c0_new_capped": 0
},
"large_error": {
"n": 192,
"delta_c0_m_quantiles": [
0.3200000000000003,
0.3800000000000003,
0.39000000000000057
],
"c0_old_capped": 0,
"c0_new_capped": 0
}
},
"sources": {
".cache/ford_c0_response_124_125/route125/route.npz": "8dcf304871bf5c6bf1ce206c788d7034bf5f056522faded0aa7948ff1cccbb75",
".cache/ford_c0_response_124_125/route125/metadata.json": "1f2b812d3c2cc8e5a2246098756940d6f8989a2591daf4d74b1ef4c8555e2d57",
".cache/ford_c0_response_124_125/route125/model_paths.npz": "4be9c7809dabbcb52296ba93b4743146dc941e49a32aca07f008b75757703e49",
".cache/ford_route112/metadata.json": "726d78a7e7aa45307dcfe27cb00775c20ecc9d54538eb7f26a0166fc216226ce",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/c0_feedback_validate.py": "66eca5715db49a47d30d9f4ed74b1d41b2bdb1d1e513ffdf85be6d94c9c8ac39",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/openpilot/selfdrive/controls/lib/ford_model_action.py": "d5841c1f6a4368de20ff14de89a48d7dce737c6a41d79628ccb4f8b19f28f91b"
}
},
{
"route": "146",
"baseline": "0020c0c41940fe1f4b33dd23297def77a0a67e92",
"baseline_sha256": "02b64f40a56cadefb181afbf7c3720affa3c7fe4ac18a2902dc9c546fd6b395d",
"cycles": 53229,
"wire_round_trips": 5323,
"c1_identical": true,
"c0_gain": 1.0,
"metrics": {
"all": {
"n": 40717,
"delta_c0_m_quantiles": [
0.0,
0.04999999999999982,
1.13
],
"c0_old_capped": 0,
"c0_new_capped": 0
},
"straight": {
"n": 30796,
"delta_c0_m_quantiles": [
0.0,
0.02999999999999936,
0.35000000000000053
],
"c0_old_capped": 0,
"c0_new_capped": 0
},
"turn": {
"n": 585,
"delta_c0_m_quantiles": [
0.3700000000000001,
1.0300000000000002,
1.13
],
"c0_old_capped": 0,
"c0_new_capped": 0
},
"large_error": {
"n": 298,
"delta_c0_m_quantiles": [
0.4500000000000002,
1.0615,
1.13
],
"c0_old_capped": 0,
"c0_new_capped": 0
}
},
"sources": {
".cache/ford_model_replay_146/full/route.npz": "b01db5bdd1c3b9fb83418a298571c98e60b5eb0509800d86cdaf07bafcdc6d63",
".cache/ford_model_replay_146/full/metadata.json": "2be1254dd1eee422f3b2c2011faa91a21ab431751998525105381c2886df15ca",
".cache/ford_model_replay_146/full/model_paths.npz": "11e0e9c8ce2959aac72d2f8a9cc4f6a025480034d5cd8b59b68a41b2ae3cf4a8",
".cache/ford_route112/metadata.json": "726d78a7e7aa45307dcfe27cb00775c20ecc9d54538eb7f26a0166fc216226ce",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/c0_feedback_validate.py": "66eca5715db49a47d30d9f4ed74b1d41b2bdb1d1e513ffdf85be6d94c9c8ac39",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/openpilot/selfdrive/controls/lib/ford_model_action.py": "d5841c1f6a4368de20ff14de89a48d7dce737c6a41d79628ccb4f8b19f28f91b"
}
},
{
"route": "149",
"baseline": "0020c0c41940fe1f4b33dd23297def77a0a67e92",
"baseline_sha256": "02b64f40a56cadefb181afbf7c3720affa3c7fe4ac18a2902dc9c546fd6b395d",
"cycles": 132334,
"wire_round_trips": 13234,
"c1_identical": true,
"c0_gain": 1.0,
"metrics": {
"all": {
"n": 109224,
"delta_c0_m_quantiles": [
0.019999999999999574,
0.4400000000000004,
2.2800000000000002
],
"c0_old_capped": 0,
"c0_new_capped": 0
},
"straight": {
"n": 54914,
"delta_c0_m_quantiles": [
0.009999999999999787,
0.07000000000000028,
2.039999999999999
],
"c0_old_capped": 0,
"c0_new_capped": 0
},
"turn": {
"n": 15161,
"delta_c0_m_quantiles": [
0.20999999999999996,
1.0300000000000002,
2.25
],
"c0_old_capped": 0,
"c0_new_capped": 0
},
"large_error": {
"n": 6659,
"delta_c0_m_quantiles": [
0.6399999999999997,
1.4100000000000001,
2.2800000000000002
],
"c0_old_capped": 0,
"c0_new_capped": 0
}
},
"sources": {
".cache/ford_route149/full/route.npz": "aa5902877343cd033ee286b3668d91a85336ffbf740861849fb0b76b0ca24ade",
".cache/ford_route149/full/metadata.json": "624fff03c25eb298661cb7b25f3dbe6d214d863f799d93635c0f4f05fc0d2b32",
".cache/ford_route149/full/model_paths.npz": "19827800b8fb5983f3d6b72fcfaf36e35a40170bb17cd7c1e49374744fb449fb",
".cache/ford_route112/metadata.json": "726d78a7e7aa45307dcfe27cb00775c20ecc9d54538eb7f26a0166fc216226ce",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/c0_feedback_validate.py": "66eca5715db49a47d30d9f4ed74b1d41b2bdb1d1e513ffdf85be6d94c9c8ac39",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/openpilot/selfdrive/controls/lib/ford_model_action.py": "d5841c1f6a4368de20ff14de89a48d7dce737c6a41d79628ccb4f8b19f28f91b"
}
},
{
"route": "151",
"baseline": "0020c0c41940fe1f4b33dd23297def77a0a67e92",
"baseline_sha256": "02b64f40a56cadefb181afbf7c3720affa3c7fe4ac18a2902dc9c546fd6b395d",
"cycles": 325708,
"wire_round_trips": 32571,
"c1_identical": true,
"c0_gain": 1.0,
"metrics": {
"all": {
"n": 124806,
"delta_c0_m_quantiles": [
0.009999999999999787,
0.10000000000000053,
2.210000000000001
],
"c0_old_capped": 0,
"c0_new_capped": 0
},
"straight": {
"n": 85546,
"delta_c0_m_quantiles": [
0.009999999999999787,
0.040000000000000036,
0.6300000000000003
],
"c0_old_capped": 0,
"c0_new_capped": 0
},
"turn": {
"n": 3715,
"delta_c0_m_quantiles": [
0.25,
1.1229999999999976,
2.210000000000001
],
"c0_old_capped": 0,
"c0_new_capped": 0
},
"large_error": {
"n": 1839,
"delta_c0_m_quantiles": [
0.6200000000000001,
1.3499999999999996,
2.210000000000001
],
"c0_old_capped": 0,
"c0_new_capped": 0
}
},
"sources": {
".cache/ford_route151/full/route.npz": "41a5b8bd388cf5a3d553f784542376ac9355fcdc5be4f427053d0504537babe1",
".cache/ford_route151/full/metadata.json": "937825317a0edd470c54647240b922be8f79dda5b3365ffdd61281f0aca877a1",
".cache/ford_route151/full/model_paths.npz": "980b3843cec04254280d744a5801cee1bf0f8ef371052398327ff245f9eee01b",
".cache/ford_route112/metadata.json": "726d78a7e7aa45307dcfe27cb00775c20ecc9d54538eb7f26a0166fc216226ce",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/c0_feedback_validate.py": "66eca5715db49a47d30d9f4ed74b1d41b2bdb1d1e513ffdf85be6d94c9c8ac39",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/openpilot/selfdrive/controls/lib/ford_model_action.py": "d5841c1f6a4368de20ff14de89a48d7dce737c6a41d79628ccb4f8b19f28f91b"
}
},
{
"route": "a9",
"baseline": "0020c0c41940fe1f4b33dd23297def77a0a67e92",
"baseline_sha256": "02b64f40a56cadefb181afbf7c3720affa3c7fe4ac18a2902dc9c546fd6b395d",
"cycles": 286319,
"wire_round_trips": 28632,
"c1_identical": true,
"c0_gain": 1.0,
"metrics": {
"all": {
"n": 142471,
"delta_c0_m_quantiles": [
0.019999999999999574,
0.15000000000000036,
1.5700000000000003
],
"c0_old_capped": 128,
"c0_new_capped": 226
},
"straight": {
"n": 99221,
"delta_c0_m_quantiles": [
0.010000000000000675,
0.05999999999999961,
1.4799999999999995
],
"c0_old_capped": 0,
"c0_new_capped": 0
},
"turn": {
"n": 6893,
"delta_c0_m_quantiles": [
0.21999999999999975,
0.9000000000000004,
1.4699999999999998
],
"c0_old_capped": 128,
"c0_new_capped": 226
},
"large_error": {
"n": 3224,
"delta_c0_m_quantiles": [
0.6299999999999999,
1.1800000000000004,
1.5700000000000003
],
"c0_old_capped": 128,
"c0_new_capped": 226
}
},
"sources": {
".cache/ford_routea9/route.npz": "3fa8cabfb729dd42d689de38618d1d21c8965c9d60f914546f4d7cc58db8c975",
".cache/ford_routea9/metadata.json": "403d35504bf596144845ac2060ce067d571f49a2955d16223af28a79a7a66e93",
".cache/ford_routea9/model_paths.npz": "7f3646ad24aa52de5982c65600813de5bbc5f7664d92a2516bbba35da6e7b240",
".cache/ford_route112/metadata.json": "726d78a7e7aa45307dcfe27cb00775c20ecc9d54538eb7f26a0166fc216226ce",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/c0_feedback_validate.py": "66eca5715db49a47d30d9f4ed74b1d41b2bdb1d1e513ffdf85be6d94c9c8ac39",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/openpilot/selfdrive/controls/lib/ford_model_action.py": "d5841c1f6a4368de20ff14de89a48d7dce737c6a41d79628ccb4f8b19f28f91b"
}
},
{
"route": "b9",
"baseline": "0020c0c41940fe1f4b33dd23297def77a0a67e92",
"baseline_sha256": "02b64f40a56cadefb181afbf7c3720affa3c7fe4ac18a2902dc9c546fd6b395d",
"cycles": 90774,
"wire_round_trips": 9078,
"c1_identical": true,
"c0_gain": 1.0,
"metrics": {
"all": {
"n": 81066,
"delta_c0_m_quantiles": [
0.010000000000000675,
0.3100000000000005,
2.130000000000001
],
"c0_old_capped": 10,
"c0_new_capped": 34
},
"straight": {
"n": 42647,
"delta_c0_m_quantiles": [
0.009999999999999787,
0.07000000000000028,
1.63
],
"c0_old_capped": 0,
"c0_new_capped": 0
},
"turn": {
"n": 9816,
"delta_c0_m_quantiles": [
0.1200000000000001,
0.82,
2.130000000000001
],
"c0_old_capped": 10,
"c0_new_capped": 34
},
"large_error": {
"n": 3486,
"delta_c0_m_quantiles": [
0.6599999999999997,
1.5,
2.130000000000001
],
"c0_old_capped": 10,
"c0_new_capped": 34
}
},
"sources": {
".cache/ford_routeb9/route.npz": "b07c789d8155335f5d120d0262fced6e4d5803fe767b0ff49b6413dce4140b5c",
".cache/ford_routeb9/metadata.json": "9ce452220cab61b81883f32fc2fcaf5db6c78a674cb255a49cc77d5029580fee",
".cache/ford_routeb9/model_paths.npz": "6b1f87897c050273fdc05af051307a049b6fc3a93072e7cda1721195ce7c3861",
".cache/ford_route112/metadata.json": "726d78a7e7aa45307dcfe27cb00775c20ecc9d54538eb7f26a0166fc216226ce",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/c0_feedback_validate.py": "66eca5715db49a47d30d9f4ed74b1d41b2bdb1d1e513ffdf85be6d94c9c8ac39",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/openpilot/selfdrive/controls/lib/ford_model_action.py": "d5841c1f6a4368de20ff14de89a48d7dce737c6a41d79628ccb4f8b19f28f91b"
}
}
],
"recent_routes": [
{
"route": "157",
"cycles": 76554,
"wire_checks": 15312,
"action_c1_and_integral_identical": true,
"action_c0_correction_doubled": true,
"direct_path_identical": true,
"action_source": "Reconstructed geometry with recorded delay/model smoothing; speed approximated by first consuming control sample"
},
{
"route": "15a",
"cycles": 21867,
"wire_checks": 4374,
"action_c1_and_integral_identical": true,
"action_c0_correction_doubled": true,
"direct_path_identical": true,
"action_source": "Logged original action and selected geometry; exact model timestamp joins"
},
{
"route": "15b",
"cycles": 22278,
"wire_checks": 4456,
"action_c1_and_integral_identical": true,
"action_c0_correction_doubled": true,
"direct_path_identical": true,
"action_source": "Logged original action and selected geometry; exact model timestamp joins"
}
],
"recent_harness_sha256": "b129b3b5d76a30ca647043a32207031a98a53eeab4620b4ebb7880c1b209a214"
}
-109
View File
@@ -1,109 +0,0 @@
# Ford C0 proportional feedback trial
This describes the original P=0.5 trial. The subsequent
[action-mode gain trial](ford_action_c0_gain_v19.md) raises action C0 P to 1.0;
direct-path mode retains P=0.5.
The existing controller sends its tracking correction through C1 only. Offline
identification on the Lightning suggests that stronger C1 requests can stop
producing faster wheel movement while additional C0 may still help. This trial
adds proportional correction to C0. It does not change the selected curvature,
C0 base geometry, C1 PI calculation, transmission rate or existing opt-in selection.
## Command
With host-sign road curvature error `e = reference - measured`:
```
scale = VM.get_steer_from_curvature(1, speed, 0) / (CP.steerRatio * CP.wheelbase)
C0_response = 0.010717679293424373 + 0.018122981795212647 / max(speed, 1.34)^2
C0_P = 0.5 * e * scale / C0_response
C0 = clip(existing_bounded_C0_base + C0_P, -5.11, 5.11)
```
The response coefficients are an offline fit to isolated C0 commands on route
`84865544361f55cb/00000145--5d9f02fee7`. They express **geometric steering curvature
per metre of C0**, not road curvature or an instantaneous wheel response. The
existing vehicle model converts the error into those units; its road-roll and
steering-angle offsets cancel in the error. No plant identification, observer or
dynamic plant simulation runs on the device.
`0.5` is an explicit trial feedback gain, not a fitted optimum. Before field
clipping, the extra C0 has a fitted steady effect equal to half the current
wheel-angle error. This does not mean 50% of the C0 field. The 1.34 m/s term
holds the fitted conversion below the identification dataset's minimum speed;
it does not disable steering or add a maneuver state.
C0 correction is recomputed every update, including updates that have no fresh
integration interval. It has no integrator, request-change state, deadband, or
additional slew limit. It becomes zero at zero error and changes sign on
overshoot. Existing driver override and PSCM arbitration disable it together
with other feedback. PSCM limit-reached continues to inhibit outward C1
integration; it does not freeze either proportional command. C1 remains
P=0.75, I=1.0. C2/C3 remain zero.
The existing `FordModelActionController` Sunnylink toggle still selects this
controller for CAN FD. With the toggle off, startup selects upstream Ford
control. No new UI or lateral-maneuver changes are included.
## Offline validation
The native-time replay compares the candidate against production commit
`d425b3260b0785b22096d130702b54a2e0761c36`, with recorded measurements, requests,
model health, driver input, service timestamps and PSCM arbitration held fixed.
- Ten Lightning routes: a9, b9, 112, 113, 117, 11c, 125, 146, 149 and 151.
- 1,170,113 cycles; 117,016 in-memory Float32/CAN encode/decode checks.
- Identical C1 output, C1 integral and command validity on every compared cycle.
- All outputs finite, inside field bounds, with C2/C3 zero.
- C0 exactly matches the old command whenever its new correction is zero or
feedback is disabled.
- Across eligible samples with at least 30 degrees of wheel-angle error, the
median C0 change is 0.65 m and the 95th percentile is 1.47 m.
- Across eligible requests below 5 degrees, the median change is 0.01 m and
the 95th percentile is 0.05 m. This cohort includes turn exits with a still
turned wheel; its largest correction is consequently much larger.
- At route 151 time 306.89 s, the replay changes C0 from 0.75 to 1.68 m for
approximately 76 degrees of tracking error. Both replays send the same C1.
The test suite covers immediate correction, no accumulation, reversal, catch-up,
invalid inputs, real vehicle-model units across stiffness/speed/roll changes,
driver/PSCM arbitration, upstream fallback, and actual controlsd publication
through the 100 Hz CAN sender. The machine-readable route replay summary is
`ford_c0_feedback_v15_validation.json`.
```
PYTHONPATH=.:opendbc_repo python -m pytest -q \
openpilot/selfdrive/controls/tests/test_ford_model_action*.py \
openpilot/selfdrive/controls/tests/test_ford_path.py
PYTHONPATH=.:opendbc_repo python tools/ford_pscm_lab/c0_feedback_validate.py \
--output .cache/ford_c0_feedback_v15 --workers 4
```
The replay requires the existing local rlog extracts. Input hashes are recorded
in its validation JSON. It does not assume the truck follows modified commands.
## What is still experimental
The earlier fitted plant overpredicted one second of wheel movement by about
16 degrees in the route 151 example. It also misses the phase of a low-speed
oscillation in route 125. Independent C0/C1 response contributions are an
approximation; a shared PSCM limit could prevent the extra movement predicted
from C0.
An additional four-second fitted-plant simulation, with controller feedback
recomputed against simulated wheel angle, showed no regression for the tested
0.5 gain in its turn, unwind and near-straight cohorts. Route 149 had 27 eligible
turn windows; route 151 had no four-second turn windows surviving the strict
intervention/status mask. Future reference and speed were frozen, and the model
does not reproduce the route 125 failure faithfully. These results are a
sanity check, not validation of road tracking or an optimized gain.
Actual acceptance is better desired-versus-actual wheel tracking on turn entry,
without added oscillation, overshoot or delayed unwind. The software behavior
is validated; the physical improvement remains to be measured.
Diagnostics identify `model-action-curvature-c0-feedback-v15` and record
`offset_proportional`, `c0_proportional_gain`, and `curvature_scale` alongside
the existing heading/feedforward/integral signals.
-856
View File
@@ -1,856 +0,0 @@
{
"scope": "Replay C0 feedback against v14 using frozen native-time Lightning measurements.\n\nThis checks software/CAN behavior, not the physical response to new commands.\nUse cached route.npz, model_paths.npz and metadata.json from the rlog extractor.\n",
"routes": [
{
"route": "112",
"baseline": "d425b3260b0785b22096d130702b54a2e0761c36",
"baseline_sha256": "c55464ecfbe7fad9f51905cf5f226be2676440ac5afec470b05515e32b065cc8",
"cycles": 108971,
"wire_round_trips": 10898,
"c1_identical": true,
"c0_gain": 0.5,
"metrics": {
"all": {
"n": 83590,
"delta_c0_m_quantiles": [
0.009999999999999787,
0.28000000000000025,
1.7300000000000004
],
"c0_old_capped": 0,
"c0_new_capped": 0
},
"straight": {
"n": 43982,
"delta_c0_m_quantiles": [
0.009999999999999787,
0.04999999999999982,
1.4699999999999998
],
"c0_old_capped": 0,
"c0_new_capped": 0
},
"turn": {
"n": 9939,
"delta_c0_m_quantiles": [
0.15999999999999925,
1.1199999999999997,
1.7300000000000004
],
"c0_old_capped": 0,
"c0_new_capped": 0
},
"large_error": {
"n": 3604,
"delta_c0_m_quantiles": [
0.73,
1.4299999999999997,
1.7300000000000004
],
"c0_old_capped": 0,
"c0_new_capped": 0
}
},
"sources": {
".cache/ford_route112/route.npz": "2b08a2fb636f7d14556d7df4035eafc1d1b97932237955528562a16db2b31d3e",
".cache/ford_route112/metadata.json": "726d78a7e7aa45307dcfe27cb00775c20ecc9d54538eb7f26a0166fc216226ce",
".cache/ford_route112/model_paths.npz": "9837afe78aab4cad288cad98a595a5777fa8a66bb235986b1272a7f7c54e559a",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/c0_feedback_validate.py": "12b843f024b671f8b4282e1981f6d68de879a9cdff20fee74fd5228b6e4168ad",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/openpilot/selfdrive/controls/lib/ford_model_action.py": "8bc8554ed6743c5faf433deaa6de3f07fae27d183f62b04f49f9c3b9f1e8dcca"
}
},
{
"route": "113",
"baseline": "d425b3260b0785b22096d130702b54a2e0761c36",
"baseline_sha256": "c55464ecfbe7fad9f51905cf5f226be2676440ac5afec470b05515e32b065cc8",
"cycles": 49614,
"wire_round_trips": 4962,
"c1_identical": true,
"c0_gain": 0.5,
"metrics": {
"all": {
"n": 24171,
"delta_c0_m_quantiles": [
0.009999999999999787,
0.9400000000000004,
2.2200000000000006
],
"c0_old_capped": 0,
"c0_new_capped": 0
},
"straight": {
"n": 16815,
"delta_c0_m_quantiles": [
0.009999999999999787,
0.05999999999999961,
1.0499999999999998
],
"c0_old_capped": 0,
"c0_new_capped": 0
},
"turn": {
"n": 2920,
"delta_c0_m_quantiles": [
0.6600000000000001,
1.8704999999999974,
2.2200000000000006
],
"c0_old_capped": 0,
"c0_new_capped": 0
},
"large_error": {
"n": 2831,
"delta_c0_m_quantiles": [
0.8699999999999997,
1.9399999999999995,
2.2200000000000006
],
"c0_old_capped": 0,
"c0_new_capped": 0
}
},
"sources": {
".cache/ford_route113/route.npz": "774ca4a21b7113c2706d6130bc180c3216ea4833155300ab01e75b3486e36327",
".cache/ford_route113/metadata.json": "1c0ca74dd48b90ab9d5444c5ca7f8aa9361700bbdf98bd5e853be50ad2895d7f",
".cache/ford_route113/model_paths.npz": "93c41761eb85263f534f5371b905482cf7c948582eb1e9149966594be1d3768f",
".cache/ford_route112/metadata.json": "726d78a7e7aa45307dcfe27cb00775c20ecc9d54538eb7f26a0166fc216226ce",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/c0_feedback_validate.py": "12b843f024b671f8b4282e1981f6d68de879a9cdff20fee74fd5228b6e4168ad",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/openpilot/selfdrive/controls/lib/ford_model_action.py": "8bc8554ed6743c5faf433deaa6de3f07fae27d183f62b04f49f9c3b9f1e8dcca"
}
},
{
"route": "117",
"baseline": "d425b3260b0785b22096d130702b54a2e0761c36",
"baseline_sha256": "c55464ecfbe7fad9f51905cf5f226be2676440ac5afec470b05515e32b065cc8",
"cycles": 27301,
"wire_round_trips": 2731,
"c1_identical": true,
"c0_gain": 0.5,
"metrics": {
"all": {
"n": 13659,
"delta_c0_m_quantiles": [
0.03000000000000025,
0.5,
1.5900000000000007
],
"c0_old_capped": 0,
"c0_new_capped": 0
},
"straight": {
"n": 5001,
"delta_c0_m_quantiles": [
0.010000000000000675,
0.3000000000000007,
1.5899999999999999
],
"c0_old_capped": 0,
"c0_new_capped": 0
},
"turn": {
"n": 2891,
"delta_c0_m_quantiles": [
0.1200000000000001,
0.7400000000000002,
1.38
],
"c0_old_capped": 0,
"c0_new_capped": 0
},
"large_error": {
"n": 1195,
"delta_c0_m_quantiles": [
0.54,
1.5599999999999996,
1.5900000000000007
],
"c0_old_capped": 0,
"c0_new_capped": 0
}
},
"sources": {
".cache/ford_route117/route.npz": "65e4ea6c76dce73018b636479b3777116f1e58bb9813e6c1a7b79018b219140e",
".cache/ford_route117/metadata.json": "348396b3059c5f54d7938cd3acf2bb268a813b47ff47561da7a94cb75c3f3114",
".cache/ford_route117/model_paths.npz": "079f6003968287530e1ed6f2243c727e80c2ef3d16fb8a7d945d7eca744d54e7",
".cache/ford_route112/metadata.json": "726d78a7e7aa45307dcfe27cb00775c20ecc9d54538eb7f26a0166fc216226ce",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/c0_feedback_validate.py": "12b843f024b671f8b4282e1981f6d68de879a9cdff20fee74fd5228b6e4168ad",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/openpilot/selfdrive/controls/lib/ford_model_action.py": "8bc8554ed6743c5faf433deaa6de3f07fae27d183f62b04f49f9c3b9f1e8dcca"
}
},
{
"route": "11c",
"baseline": "d425b3260b0785b22096d130702b54a2e0761c36",
"baseline_sha256": "c55464ecfbe7fad9f51905cf5f226be2676440ac5afec470b05515e32b065cc8",
"cycles": 54146,
"wire_round_trips": 5415,
"c1_identical": true,
"c0_gain": 0.5,
"metrics": {
"all": {
"n": 36780,
"delta_c0_m_quantiles": [
0.020000000000000462,
0.3700000000000001,
2.1799999999999997
],
"c0_old_capped": 0,
"c0_new_capped": 0
},
"straight": {
"n": 16750,
"delta_c0_m_quantiles": [
0.009999999999999787,
0.09999999999999964,
0.5
],
"c0_old_capped": 0,
"c0_new_capped": 0
},
"turn": {
"n": 2461,
"delta_c0_m_quantiles": [
0.1899999999999995,
0.8899999999999997,
2.1799999999999997
],
"c0_old_capped": 0,
"c0_new_capped": 0
},
"large_error": {
"n": 1827,
"delta_c0_m_quantiles": [
0.5599999999999996,
0.9569999999999982,
2.1799999999999997
],
"c0_old_capped": 0,
"c0_new_capped": 0
}
},
"sources": {
".cache/ford_maneuver_11c/analysis/route.npz": "1f3c37a7c7ae85ba69e9958435a9568f8244fa862f43ad1ee2a0192d1df13966",
".cache/ford_maneuver_11c/analysis/metadata.json": "2d0051d23059145988ca905e5386704def24b8080ce92541cec5e4ab501bcd0c",
".cache/ford_maneuver_11c/analysis/model_paths.npz": "0ff433da3dde1d4d7f10596cd53558ec5de9d56fbc49aec3e8afb4c9872377d5",
".cache/ford_route112/metadata.json": "726d78a7e7aa45307dcfe27cb00775c20ecc9d54538eb7f26a0166fc216226ce",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/c0_feedback_validate.py": "12b843f024b671f8b4282e1981f6d68de879a9cdff20fee74fd5228b6e4168ad",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/openpilot/selfdrive/controls/lib/ford_model_action.py": "8bc8554ed6743c5faf433deaa6de3f07fae27d183f62b04f49f9c3b9f1e8dcca"
}
},
{
"route": "125",
"baseline": "d425b3260b0785b22096d130702b54a2e0761c36",
"baseline_sha256": "c55464ecfbe7fad9f51905cf5f226be2676440ac5afec470b05515e32b065cc8",
"cycles": 41717,
"wire_round_trips": 4172,
"c1_identical": true,
"c0_gain": 0.5,
"metrics": {
"all": {
"n": 41107,
"delta_c0_m_quantiles": [
0.0,
0.05999999999999961,
0.40000000000000036
],
"c0_old_capped": 0,
"c0_new_capped": 0
},
"straight": {
"n": 33135,
"delta_c0_m_quantiles": [
0.0,
0.010000000000000675,
0.28000000000000025
],
"c0_old_capped": 0,
"c0_new_capped": 0
},
"large_error": {
"n": 192,
"delta_c0_m_quantiles": [
0.3200000000000003,
0.3799999999999999,
0.40000000000000036
],
"c0_old_capped": 0,
"c0_new_capped": 0
}
},
"sources": {
".cache/ford_c0_response_124_125/route125/route.npz": "8dcf304871bf5c6bf1ce206c788d7034bf5f056522faded0aa7948ff1cccbb75",
".cache/ford_c0_response_124_125/route125/metadata.json": "1f2b812d3c2cc8e5a2246098756940d6f8989a2591daf4d74b1ef4c8555e2d57",
".cache/ford_c0_response_124_125/route125/model_paths.npz": "4be9c7809dabbcb52296ba93b4743146dc941e49a32aca07f008b75757703e49",
".cache/ford_route112/metadata.json": "726d78a7e7aa45307dcfe27cb00775c20ecc9d54538eb7f26a0166fc216226ce",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/c0_feedback_validate.py": "12b843f024b671f8b4282e1981f6d68de879a9cdff20fee74fd5228b6e4168ad",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/openpilot/selfdrive/controls/lib/ford_model_action.py": "8bc8554ed6743c5faf433deaa6de3f07fae27d183f62b04f49f9c3b9f1e8dcca"
}
},
{
"route": "146",
"baseline": "d425b3260b0785b22096d130702b54a2e0761c36",
"baseline_sha256": "c55464ecfbe7fad9f51905cf5f226be2676440ac5afec470b05515e32b065cc8",
"cycles": 53229,
"wire_round_trips": 5323,
"c1_identical": true,
"c0_gain": 0.5,
"metrics": {
"all": {
"n": 40717,
"delta_c0_m_quantiles": [
0.0,
0.04999999999999982,
1.1300000000000003
],
"c0_old_capped": 0,
"c0_new_capped": 0
},
"straight": {
"n": 30796,
"delta_c0_m_quantiles": [
0.0,
0.02999999999999936,
0.35000000000000053
],
"c0_old_capped": 0,
"c0_new_capped": 0
},
"turn": {
"n": 585,
"delta_c0_m_quantiles": [
0.3700000000000001,
1.0300000000000002,
1.1300000000000003
],
"c0_old_capped": 0,
"c0_new_capped": 0
},
"large_error": {
"n": 298,
"delta_c0_m_quantiles": [
0.4500000000000002,
1.0614999999999999,
1.1300000000000003
],
"c0_old_capped": 0,
"c0_new_capped": 0
}
},
"sources": {
".cache/ford_model_replay_146/full/route.npz": "b01db5bdd1c3b9fb83418a298571c98e60b5eb0509800d86cdaf07bafcdc6d63",
".cache/ford_model_replay_146/full/metadata.json": "2be1254dd1eee422f3b2c2011faa91a21ab431751998525105381c2886df15ca",
".cache/ford_model_replay_146/full/model_paths.npz": "11e0e9c8ce2959aac72d2f8a9cc4f6a025480034d5cd8b59b68a41b2ae3cf4a8",
".cache/ford_route112/metadata.json": "726d78a7e7aa45307dcfe27cb00775c20ecc9d54538eb7f26a0166fc216226ce",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/c0_feedback_validate.py": "12b843f024b671f8b4282e1981f6d68de879a9cdff20fee74fd5228b6e4168ad",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/openpilot/selfdrive/controls/lib/ford_model_action.py": "8bc8554ed6743c5faf433deaa6de3f07fae27d183f62b04f49f9c3b9f1e8dcca"
}
},
{
"route": "149",
"baseline": "d425b3260b0785b22096d130702b54a2e0761c36",
"baseline_sha256": "c55464ecfbe7fad9f51905cf5f226be2676440ac5afec470b05515e32b065cc8",
"cycles": 132334,
"wire_round_trips": 13234,
"c1_identical": true,
"c0_gain": 0.5,
"metrics": {
"all": {
"n": 109224,
"delta_c0_m_quantiles": [
0.019999999999999574,
0.43999999999999995,
2.2800000000000002
],
"c0_old_capped": 0,
"c0_new_capped": 0
},
"straight": {
"n": 54914,
"delta_c0_m_quantiles": [
0.009999999999999787,
0.07000000000000028,
2.04
],
"c0_old_capped": 0,
"c0_new_capped": 0
},
"turn": {
"n": 15161,
"delta_c0_m_quantiles": [
0.20999999999999996,
1.0300000000000002,
2.25
],
"c0_old_capped": 0,
"c0_new_capped": 0
},
"large_error": {
"n": 6659,
"delta_c0_m_quantiles": [
0.6399999999999997,
1.4100000000000001,
2.2800000000000002
],
"c0_old_capped": 0,
"c0_new_capped": 0
}
},
"sources": {
".cache/ford_route149/full/route.npz": "aa5902877343cd033ee286b3668d91a85336ffbf740861849fb0b76b0ca24ade",
".cache/ford_route149/full/metadata.json": "624fff03c25eb298661cb7b25f3dbe6d214d863f799d93635c0f4f05fc0d2b32",
".cache/ford_route149/full/model_paths.npz": "19827800b8fb5983f3d6b72fcfaf36e35a40170bb17cd7c1e49374744fb449fb",
".cache/ford_route112/metadata.json": "726d78a7e7aa45307dcfe27cb00775c20ecc9d54538eb7f26a0166fc216226ce",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/c0_feedback_validate.py": "12b843f024b671f8b4282e1981f6d68de879a9cdff20fee74fd5228b6e4168ad",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/openpilot/selfdrive/controls/lib/ford_model_action.py": "8bc8554ed6743c5faf433deaa6de3f07fae27d183f62b04f49f9c3b9f1e8dcca"
}
},
{
"route": "151",
"baseline": "d425b3260b0785b22096d130702b54a2e0761c36",
"baseline_sha256": "c55464ecfbe7fad9f51905cf5f226be2676440ac5afec470b05515e32b065cc8",
"cycles": 325708,
"wire_round_trips": 32571,
"c1_identical": true,
"c0_gain": 0.5,
"metrics": {
"all": {
"n": 124806,
"delta_c0_m_quantiles": [
0.009999999999999787,
0.10000000000000053,
2.21
],
"c0_old_capped": 0,
"c0_new_capped": 0
},
"straight": {
"n": 85546,
"delta_c0_m_quantiles": [
0.009999999999999787,
0.03999999999999915,
0.6300000000000008
],
"c0_old_capped": 0,
"c0_new_capped": 0
},
"turn": {
"n": 3715,
"delta_c0_m_quantiles": [
0.25,
1.12,
2.21
],
"c0_old_capped": 0,
"c0_new_capped": 0
},
"large_error": {
"n": 1839,
"delta_c0_m_quantiles": [
0.6200000000000001,
1.35,
2.21
],
"c0_old_capped": 0,
"c0_new_capped": 0
}
},
"sources": {
".cache/ford_route151/full/route.npz": "41a5b8bd388cf5a3d553f784542376ac9355fcdc5be4f427053d0504537babe1",
".cache/ford_route151/full/metadata.json": "937825317a0edd470c54647240b922be8f79dda5b3365ffdd61281f0aca877a1",
".cache/ford_route151/full/model_paths.npz": "980b3843cec04254280d744a5801cee1bf0f8ef371052398327ff245f9eee01b",
".cache/ford_route112/metadata.json": "726d78a7e7aa45307dcfe27cb00775c20ecc9d54538eb7f26a0166fc216226ce",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/c0_feedback_validate.py": "12b843f024b671f8b4282e1981f6d68de879a9cdff20fee74fd5228b6e4168ad",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/openpilot/selfdrive/controls/lib/ford_model_action.py": "8bc8554ed6743c5faf433deaa6de3f07fae27d183f62b04f49f9c3b9f1e8dcca"
}
},
{
"route": "a9",
"baseline": "d425b3260b0785b22096d130702b54a2e0761c36",
"baseline_sha256": "c55464ecfbe7fad9f51905cf5f226be2676440ac5afec470b05515e32b065cc8",
"cycles": 286319,
"wire_round_trips": 28632,
"c1_identical": true,
"c0_gain": 0.5,
"metrics": {
"all": {
"n": 142471,
"delta_c0_m_quantiles": [
0.019999999999999574,
0.15999999999999925,
1.5699999999999994
],
"c0_old_capped": 25,
"c0_new_capped": 128
},
"straight": {
"n": 99221,
"delta_c0_m_quantiles": [
0.009999999999999787,
0.05999999999999961,
1.4800000000000004
],
"c0_old_capped": 0,
"c0_new_capped": 0
},
"turn": {
"n": 6893,
"delta_c0_m_quantiles": [
0.23000000000000043,
0.9699999999999998,
1.5600000000000005
],
"c0_old_capped": 25,
"c0_new_capped": 128
},
"large_error": {
"n": 3224,
"delta_c0_m_quantiles": [
0.6600000000000001,
1.2599999999999998,
1.5699999999999994
],
"c0_old_capped": 25,
"c0_new_capped": 128
}
},
"sources": {
".cache/ford_routea9/route.npz": "3fa8cabfb729dd42d689de38618d1d21c8965c9d60f914546f4d7cc58db8c975",
".cache/ford_routea9/metadata.json": "403d35504bf596144845ac2060ce067d571f49a2955d16223af28a79a7a66e93",
".cache/ford_routea9/model_paths.npz": "7f3646ad24aa52de5982c65600813de5bbc5f7664d92a2516bbba35da6e7b240",
".cache/ford_route112/metadata.json": "726d78a7e7aa45307dcfe27cb00775c20ecc9d54538eb7f26a0166fc216226ce",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/c0_feedback_validate.py": "12b843f024b671f8b4282e1981f6d68de879a9cdff20fee74fd5228b6e4168ad",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/openpilot/selfdrive/controls/lib/ford_model_action.py": "8bc8554ed6743c5faf433deaa6de3f07fae27d183f62b04f49f9c3b9f1e8dcca"
}
},
{
"route": "b9",
"baseline": "d425b3260b0785b22096d130702b54a2e0761c36",
"baseline_sha256": "c55464ecfbe7fad9f51905cf5f226be2676440ac5afec470b05515e32b065cc8",
"cycles": 90774,
"wire_round_trips": 9078,
"c1_identical": true,
"c0_gain": 0.5,
"metrics": {
"all": {
"n": 81066,
"delta_c0_m_quantiles": [
0.010000000000000675,
0.3100000000000005,
2.12
],
"c0_old_capped": 0,
"c0_new_capped": 10
},
"straight": {
"n": 42647,
"delta_c0_m_quantiles": [
0.009999999999999787,
0.07000000000000028,
1.63
],
"c0_old_capped": 0,
"c0_new_capped": 0
},
"turn": {
"n": 9816,
"delta_c0_m_quantiles": [
0.1200000000000001,
0.8300000000000001,
2.12
],
"c0_old_capped": 0,
"c0_new_capped": 10
},
"large_error": {
"n": 3486,
"delta_c0_m_quantiles": [
0.6600000000000001,
1.5100000000000005,
2.12
],
"c0_old_capped": 0,
"c0_new_capped": 10
}
},
"sources": {
".cache/ford_routeb9/route.npz": "b07c789d8155335f5d120d0262fced6e4d5803fe767b0ff49b6413dce4140b5c",
".cache/ford_routeb9/metadata.json": "9ce452220cab61b81883f32fc2fcaf5db6c78a674cb255a49cc77d5029580fee",
".cache/ford_routeb9/model_paths.npz": "6b1f87897c050273fdc05af051307a049b6fc3a93072e7cda1721195ce7c3861",
".cache/ford_route112/metadata.json": "726d78a7e7aa45307dcfe27cb00775c20ecc9d54538eb7f26a0166fc216226ce",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/c0_feedback_validate.py": "12b843f024b671f8b4282e1981f6d68de879a9cdff20fee74fd5228b6e4168ad",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/openpilot/selfdrive/controls/lib/ford_model_action.py": "8bc8554ed6743c5faf433deaa6de3f07fae27d183f62b04f49f9c3b9f1e8dcca"
}
}
],
"test_summary": {
"pytest_passed": 439,
"ruff": "passed",
"git_diff_check": "passed"
},
"fit_source_sha256": "536ea20ee1bfa9366453c6d01fae6c7dd7b83388f2a71662cc12b43311767c6f",
"fitted_plant_sanity_check": {
"scope": "Approximate fitted-plant simulation with frozen future reference/speed, recomputed controller feedback, 20 Hz, four-second clean windows. Not physical tracking validation.",
"routes": {
"149": {
"0.0": {
"all": {
"windows": 515,
"tracking_rmse_deg": 1.9599623978328702,
"p95_abs_error_deg": 3.6108132362595673
},
"turn": {
"windows": 27,
"tracking_rmse_deg": 5.648897344766248,
"p95_abs_error_deg": 10.154937282895988
},
"unwind": {
"windows": 16,
"tracking_rmse_deg": 4.56296777072208,
"p95_abs_error_deg": 5.8047028901317175
},
"near_straight": {
"windows": 280,
"tracking_rmse_deg": 0.5053540034793153,
"p95_abs_error_deg": 0.9099971698933058
}
},
"0.25": {
"all": {
"windows": 515,
"tracking_rmse_deg": 1.868833791306516,
"p95_abs_error_deg": 3.4194303642888486
},
"turn": {
"windows": 27,
"tracking_rmse_deg": 5.3063969129788715,
"p95_abs_error_deg": 9.669302705935252
},
"unwind": {
"windows": 16,
"tracking_rmse_deg": 4.307554036590833,
"p95_abs_error_deg": 5.549381163721539
},
"near_straight": {
"windows": 280,
"tracking_rmse_deg": 0.4868309463403626,
"p95_abs_error_deg": 0.8696197439962221
}
},
"0.5": {
"all": {
"windows": 515,
"tracking_rmse_deg": 1.7898071883511764,
"p95_abs_error_deg": 3.260221243224674
},
"turn": {
"windows": 27,
"tracking_rmse_deg": 5.006440079078192,
"p95_abs_error_deg": 9.002861391131468
},
"unwind": {
"windows": 16,
"tracking_rmse_deg": 4.078042889798365,
"p95_abs_error_deg": 5.24331028633435
},
"near_straight": {
"windows": 280,
"tracking_rmse_deg": 0.4694670576005347,
"p95_abs_error_deg": 0.8331203606609966
}
},
"0.75": {
"all": {
"windows": 515,
"tracking_rmse_deg": 1.7203138728888623,
"p95_abs_error_deg": 3.1149598999491044
},
"turn": {
"windows": 27,
"tracking_rmse_deg": 4.743912352245749,
"p95_abs_error_deg": 8.664577551257613
},
"unwind": {
"windows": 16,
"tracking_rmse_deg": 3.8750067743533827,
"p95_abs_error_deg": 4.9966209407120425
},
"near_straight": {
"windows": 280,
"tracking_rmse_deg": 0.45412470534078503,
"p95_abs_error_deg": 0.7964604020331516
}
},
"1.0": {
"all": {
"windows": 515,
"tracking_rmse_deg": 1.6590272653809652,
"p95_abs_error_deg": 2.990976062659628
},
"turn": {
"windows": 27,
"tracking_rmse_deg": 4.507245023760021,
"p95_abs_error_deg": 8.107112731535002
},
"unwind": {
"windows": 16,
"tracking_rmse_deg": 3.693644190831072,
"p95_abs_error_deg": 4.749308802498215
},
"near_straight": {
"windows": 280,
"tracking_rmse_deg": 0.4404620597734029,
"p95_abs_error_deg": 0.7694584025732562
}
}
},
"151": {
"0.0": {
"all": {
"windows": 794,
"tracking_rmse_deg": 0.9049924838460381,
"p95_abs_error_deg": 1.496850675938314
},
"unwind": {
"windows": 7,
"tracking_rmse_deg": 1.3568942395552432,
"p95_abs_error_deg": 2.4717442023707914
},
"near_straight": {
"windows": 525,
"tracking_rmse_deg": 0.47746381422226297,
"p95_abs_error_deg": 0.9045192063209317
}
},
"0.25": {
"all": {
"windows": 794,
"tracking_rmse_deg": 0.8744021810097441,
"p95_abs_error_deg": 1.4250449905919713
},
"unwind": {
"windows": 7,
"tracking_rmse_deg": 1.2734874215389322,
"p95_abs_error_deg": 2.3213325000763567
},
"near_straight": {
"windows": 525,
"tracking_rmse_deg": 0.45648429088650827,
"p95_abs_error_deg": 0.8575351438117865
}
},
"0.5": {
"all": {
"windows": 794,
"tracking_rmse_deg": 0.8487165771252436,
"p95_abs_error_deg": 1.3633464420801624
},
"unwind": {
"windows": 7,
"tracking_rmse_deg": 1.1986282107421773,
"p95_abs_error_deg": 2.163514655425466
},
"near_straight": {
"windows": 525,
"tracking_rmse_deg": 0.4395196212749708,
"p95_abs_error_deg": 0.82723861668119
}
},
"0.75": {
"all": {
"windows": 794,
"tracking_rmse_deg": 0.8256116631456035,
"p95_abs_error_deg": 1.309241916169811
},
"unwind": {
"windows": 7,
"tracking_rmse_deg": 1.1341202040172427,
"p95_abs_error_deg": 2.0707107084230465
},
"near_straight": {
"windows": 525,
"tracking_rmse_deg": 0.42406706004679057,
"p95_abs_error_deg": 0.7946947170401512
}
},
"1.0": {
"all": {
"windows": 794,
"tracking_rmse_deg": 0.8052189413919172,
"p95_abs_error_deg": 1.2575495536727697
},
"unwind": {
"windows": 7,
"tracking_rmse_deg": 1.0739985410343649,
"p95_abs_error_deg": 1.9599553503928666
},
"near_straight": {
"windows": 525,
"tracking_rmse_deg": 0.41043500769646296,
"p95_abs_error_deg": 0.7683540262306987
}
}
},
"125": {
"0.0": {
"all": {
"windows": 346,
"tracking_rmse_deg": 1.5101438591368561,
"p95_abs_error_deg": 1.3433430124202965
},
"near_straight": {
"windows": 246,
"tracking_rmse_deg": 0.3997814518183382,
"p95_abs_error_deg": 0.8420670524669589
}
},
"0.25": {
"all": {
"windows": 346,
"tracking_rmse_deg": 1.460719401448126,
"p95_abs_error_deg": 1.2614591679946765
},
"near_straight": {
"windows": 246,
"tracking_rmse_deg": 0.377498378600397,
"p95_abs_error_deg": 0.7868969957812859
}
},
"0.5": {
"all": {
"windows": 346,
"tracking_rmse_deg": 1.4162750202728391,
"p95_abs_error_deg": 1.188472341120946
},
"near_straight": {
"windows": 246,
"tracking_rmse_deg": 0.35896062148345687,
"p95_abs_error_deg": 0.7523514432810818
}
},
"0.75": {
"all": {
"windows": 346,
"tracking_rmse_deg": 1.3766402449201502,
"p95_abs_error_deg": 1.1248966870109076
},
"near_straight": {
"windows": 246,
"tracking_rmse_deg": 0.34130621657460447,
"p95_abs_error_deg": 0.7141102879383627
}
},
"1.0": {
"all": {
"windows": 346,
"tracking_rmse_deg": 1.3414563262275077,
"p95_abs_error_deg": 1.0712299506410858
},
"near_straight": {
"windows": 246,
"tracking_rmse_deg": 0.3255065971687205,
"p95_abs_error_deg": 0.6807731116817476
}
}
}
}
}
}
-85
View File
@@ -1,85 +0,0 @@
# Action-mode C0 feedback softening
Route 166 used v21 and improved normal driving, but the first two user bookmarks
showed roughly 2 Hz wheel oscillations around a smoother requested angle at
1731 mph. The first occurred with uninterrupted feedback, small I and no
PSCM limitReached. Both C0 P and C1 P followed the error, so these recordings
do not isolate either channel's contribution to physical oscillation.
This trial changes C0 P only. With the curvature error converted into `x` metres
of C0 before applying the existing gain, the new correction is:
```text
C0 P = gain * (x - 0.125 * tanh(x / 0.25))
```
Near zero, the slope is half the old gain. It increases smoothly toward the old
gain without exceeding it. The response stays symmetric, monotonic and nonzero
for nonzero error; it introduces no time filter, deadband, cutoff or additional
state in the control law. Zero error removes P immediately, and opposite error
commands opposite P in the same cycle.
Large corrections lose at most 0.125 m times the existing C0 gain. This is a
bound on the difference from the old correction, **not a C0 command cap**.
The field bounds remain C0 ±5.11 m and C1 ±0.5 rad. The scale and minimum slope
are explicit drive-trial choices, not an identified stable PSCM calibration.
Scales 0.15, 0.25, 0.4 and 0.5 m were compared on route 166; 0.25 m preserves
about 96% of the good left's peak entry command while reducing the repeated
C0 command component in both wobbles.
The base action mapping, selected desired curvature, C1 P/I, delayed reference,
integral cadence and anti-windup, driver/PSCM arbitration, CAN cadence and
upstream limits are unchanged. The softening applies at all speeds in action
mode: the bookmarked wobbles were above the proposed 11.2 mph freeze cutoff.
Direct-path feedback retains its linear law; when it falls back to an action
reference, the action softening applies. The master toggle still selects
upstream Ford control when disabled. No new toggle is added.
Diagnostics identify v22 and include `offset_proportional_linear` alongside
the actual `offset_proportional`, so a new log can show exactly what softening
removed. The extra stored scalar is diagnostic only and resets with P.
## Validation
Five native-time route replays cover 646,178 cycles and 64,620 CAN encode/decode
checks: routes 149, 151, 157, 162 and 166. The v21 baseline matches its archived
commands and integral exactly. C1 commands, C1 P/I, reference, validity,
arbitration and base mapping remain identical on every cycle. C2/C3 remain
zero. The largest raw C0 P change is 0.125 m; after CAN quantization the
command difference is at most 0.13 m.
| Route-166 event | Repeating total C0 component, before → after | Reduction |
| --- | --- | --- |
| First wobble | 0.146 → 0.076 m peak-to-peak | 48% |
| Second wobble | 0.222 → 0.150 m peak-to-peak | 32% |
| Second wobble exit | 0.335 → 0.199 m peak-to-peak | 41% |
These use a same-frequency sine fit with quadratic trend removal. They measure
the **command**, not a predicted reduction in wheel oscillation. The good left's
peak entry C0 changes from -2.93 to -2.81 m; its mid-turn unwind peak changes
from +1.44 to +1.31 m. C1 is identical. Entry and unwind are both softened;
neither physical response is proven better by a frozen-motion replay.
Regression tests first failed against v21, then passed with v22. They exercise
small nonzero corrections, preserved large-error authority, monotonic bounded
incremental gain, immediate reversal/release, unaffected C1 and base mapping,
and the direct-path exception. Existing controls-to-CAN integration, model
selection, driver override, request-history timing and default-upstream tests
also pass. Exact counts and provenance are in
`ford_c0_softening_v22_validation.json`.
Reproduce a route comparison with the built Python dependencies:
```sh
PYTHONPATH=.:opendbc_repo:.cache/ford_geometry_deps python \
tools/ford_pscm_lab/c0_softening_replay.py \
--source .cache/ford_route166/full \
--delay-intake .cache/ford_route166/intake.npz \
--archive .cache/ford_feedback_delay_v21/166/commands.npz \
--output .cache/ford_c0_softening_v22/166
```
For the next drive, the discriminating observations are whether small
left-right corrections settle sooner, whether centering becomes too loose,
and whether strong entry and prompt release remain. Do not interpret this
offline validation as a demonstrated physical stability fix.
-415
View File
@@ -1,415 +0,0 @@
{
"baseline_commit": "ed9c44f57584b5f033f79db6331e21b9b3e0ad55",
"method": "Native-time production-controller replay using recorded wheel motion. Tests command behavior, not changed physical tracking.",
"cycles": 646178,
"wire_checks": 64620,
"tests": {
"ford_controller_and_integration": 708,
"subtests": 2,
"ford_can": 11,
"ruff": "passed",
"diff_check": "passed",
"ford_can_subtests": 23
},
"selection": {
"0.15": {
"wobble1": {
"P_periodic_reduction_pct": 43.471957098342315,
"P_before": 0.1502831327602382,
"P_after": 0.08495211376066261
},
"wobble2": {
"P_periodic_reduction_pct": 20.901758921591217,
"P_before": 0.22856483271408398,
"P_after": 0.1807907624006479
},
"wobble2_exit": {
"P_periodic_reduction_pct": 31.59900962734852,
"P_before": 0.3407619177930598,
"P_after": 0.2330845265832934
},
"left_entry_peak": {
"t": 557.084304569,
"before": -2.9300000000000006,
"after_unpacked": -2.8568021983086886,
"retained_percent": 97.50178151224192
}
},
"0.25": {
"wobble1": {
"P_periodic_reduction_pct": 47.14718986588794,
"P_before": 0.1502831327602382,
"P_after": 0.07942885882136426
},
"wobble2": {
"P_periodic_reduction_pct": 31.648695347911747,
"P_before": 0.22856483271408398,
"P_after": 0.1562270451359394
},
"wobble2_exit": {
"P_periodic_reduction_pct": 40.10137585312036,
"P_before": 0.3407619177930598,
"P_after": 0.2041117003745639
},
"left_entry_peak": {
"t": 557.084304569,
"before": -2.9300000000000006,
"after_unpacked": -2.8068022480277577,
"retained_percent": 95.79529856750024
}
},
"0.4": {
"wobble1": {
"P_periodic_reduction_pct": 48.78814118850091,
"P_before": 0.1502831327602382,
"P_after": 0.07696278576667091
},
"wobble2": {
"P_periodic_reduction_pct": 40.16673505205207,
"P_before": 0.22856483271408398,
"P_after": 0.13675780193565182
},
"wobble2_exit": {
"P_periodic_reduction_pct": 45.26729702228269,
"P_before": 0.3407619177930598,
"P_after": 0.18650820832684864
},
"left_entry_peak": {
"t": 557.084304569,
"before": -2.9300000000000006,
"after_unpacked": -2.7318281193111016,
"retained_percent": 93.23645458399662
}
},
"0.5": {
"wobble1": {
"P_periodic_reduction_pct": 49.20782335256252,
"P_before": 0.1502831327602382,
"P_after": 0.07633207426288317
},
"wobble2": {
"P_periodic_reduction_pct": 43.09149572817541,
"P_before": 0.22856483271408398,
"P_after": 0.1300728275889832
},
"wobble2_exit": {
"P_periodic_reduction_pct": 46.79902581397264,
"P_before": 0.3407619177930598,
"P_after": 0.18128865992089754
},
"left_entry_peak": {
"t": 557.084304569,
"before": -2.9300000000000006,
"after_unpacked": -2.682025078944252,
"retained_percent": 91.53669211413828
}
}
},
"bookmarks": {
"wobble1": {
"window": [
420.8,
424.0
],
"p": {
"old_peak_to_peak": 0.1502831327602382,
"new_peak_to_peak": 0.07942885882136426,
"reduction_percent": 47.14718986588794
},
"c0": {
"old_peak_to_peak": 0.1463643509329174,
"new_peak_to_peak": 0.07619232987207852,
"reduction_percent": 47.943382807061084
}
},
"wobble2": {
"window": [
514.3,
521.4
],
"p": {
"old_peak_to_peak": 0.22856483271408398,
"new_peak_to_peak": 0.15622704513593932,
"reduction_percent": 31.648695347911794
},
"c0": {
"old_peak_to_peak": 0.22189262078364064,
"new_peak_to_peak": 0.15013115100291197,
"reduction_percent": 32.3406292319656
}
},
"wobble2_exit": {
"window": [
523.7,
526.8
],
"p": {
"old_peak_to_peak": 0.3407619177930598,
"new_peak_to_peak": 0.2041117003745639,
"reduction_percent": 40.10137585312036
},
"c0": {
"old_peak_to_peak": 0.3347422828817533,
"new_peak_to_peak": 0.19869356211391034,
"reduction_percent": 40.642825159886286
}
}
},
"good_left_points": [
{
"meaning": "entry",
"t": 557.084304569,
"target_angle": 167.83670043945312,
"actual_angle": 76.5999984741211,
"old_c0": -2.9300000000000006,
"new_c0": -2.8100000000000005,
"old_p": -1.9288202876424623,
"new_p": -1.8038203373625472,
"c1": -0.4565,
"i": -0.058477505092137665
},
{
"meaning": "midturn_unwind",
"t": 558.284113233,
"target_angle": 109.08927154541016,
"actual_angle": 183.6999969482422,
"old_c0": 1.44,
"new_c0": 1.31,
"old_p": 2.0905105735349747,
"new_p": 1.9655105871733405,
"c1": -0.11499999999999999,
"i": -0.05363273646512535
}
],
"routes": {
"166": {
"cycles": 62579,
"wire_checks": 6258,
"baseline_matches_archived_v21_exactly": true,
"c1_integral_reference_arbitration_equal_every_cycle": true,
"P_reduction_p50_p95_p99_max_m": [
0.003636227080371235,
0.10752626130792256,
0.12490297403763004,
0.12499999961974328
],
"C0_change_p50_p95_p99_max_m": [
0.0,
0.10999999999999943,
0.1299999999999999,
0.13000000000000078
],
"variants": {
"old": {
"c0_step_p95_p99_max": [
0.020000000000000018,
0.050000000000000266,
1.8900000000000006
],
"c0_bound_seconds": 0.0
},
"new": {
"c0_step_p95_p99_max": [
0.010000000000000675,
0.040000000000000036,
1.7700000000000005
],
"c0_bound_seconds": 0.0
}
},
"baseline_commit": "ed9c44f57584b5f033f79db6331e21b9b3e0ad55",
"method": "Replay C0 softening against v21 on frozen recorded motion, not a PSCM model.",
"controller_sha256": "b0aff6d70ad8ae7693c69068dbe646767e4d9fc20a23af70cbddcf6ebf849f48",
"source_sha256": {
".cache/ford_route166/full/route.npz": "0ce273e4a12725c4481b11dab38b304950d364455b89f216b929a9c61b7feb1c",
".cache/ford_route166/full/model_paths.npz": "720fe9ba9865a05937989449b0b0bd3304baf5f1730042da6b61adc2941c0f5a",
".cache/ford_route166/intake.npz": "660b5e7f6789d931660ad71b10f7d8ce2b63436e7573b615ccc8e5da1f2a14d0",
".cache/ford_feedback_delay_v21/166/commands.npz": "bfe51abd9e914edea505efc4fb42fe66950cfa5835e4f929d922992e90c88b5b"
}
},
"162": {
"cycles": 49003,
"wire_checks": 4901,
"baseline_matches_archived_v21_exactly": true,
"c1_integral_reference_arbitration_equal_every_cycle": true,
"P_reduction_p50_p95_p99_max_m": [
0.01864462197101791,
0.12428797325001033,
0.12499916992638829,
0.12499999992682431
],
"C0_change_p50_p95_p99_max_m": [
0.019999999999999574,
0.1200000000000001,
0.1299999999999999,
0.13000000000000078
],
"variants": {
"old": {
"c0_step_p95_p99_max": [
0.03000000000000025,
0.09999999999999964,
2.7299999999999995
],
"c0_bound_seconds": 0.0
},
"new": {
"c0_step_p95_p99_max": [
0.03000000000000025,
0.08999999999999986,
2.5999999999999996
],
"c0_bound_seconds": 0.0
}
},
"baseline_commit": "ed9c44f57584b5f033f79db6331e21b9b3e0ad55",
"method": "Replay C0 softening against v21 on frozen recorded motion, not a PSCM model.",
"controller_sha256": "b0aff6d70ad8ae7693c69068dbe646767e4d9fc20a23af70cbddcf6ebf849f48",
"source_sha256": {
".cache/ford_route162/full/route.npz": "a725f9ca309b90a2d750d51a9b9d5b459d99119a54ab048631bc0fce07348af8",
".cache/ford_route162/full/model_paths.npz": "cffe310b122d9ae26a53ee504608156b4242a6bba5f681898d5b661951ac5ce5",
".cache/ford_route162/intake.npz": "9455b01e1d1d3dcba08276b382a8c6cd54ff6fb19cb417b72d0ac89cf193034b",
".cache/ford_feedback_delay_v21/162/commands.npz": "6c70518fb9b7232b689740662c4bd5c0c24cabee4e743a2b4f3b2313bcce1912"
}
},
"157": {
"cycles": 76554,
"wire_checks": 7656,
"baseline_matches_archived_v21_exactly": true,
"c1_integral_reference_arbitration_equal_every_cycle": true,
"P_reduction_p50_p95_p99_max_m": [
0.00530841320802854,
0.12371750128642126,
0.12499999318487974,
0.125
],
"C0_change_p50_p95_p99_max_m": [
0.009999999999999787,
0.1200000000000001,
0.1299999999999999,
0.13000000000000078
],
"variants": {
"old": {
"c0_step_p95_p99_max": [
0.03000000000000025,
0.09000000000000014,
5.6000000000000005
],
"c0_bound_seconds": 0.2999506119999751
},
"new": {
"c0_step_p95_p99_max": [
0.02999999999999936,
0.08000000000000007,
5.6000000000000005
],
"c0_bound_seconds": 0.15561390599987135
}
},
"baseline_commit": "ed9c44f57584b5f033f79db6331e21b9b3e0ad55",
"method": "Replay C0 softening against v21 on frozen recorded motion, not a PSCM model.",
"controller_sha256": "b0aff6d70ad8ae7693c69068dbe646767e4d9fc20a23af70cbddcf6ebf849f48",
"source_sha256": {
".cache/ford_route157/full/route.npz": "e2e2573f904aa11ee9e10450e7f5b965d475657b61127e827a67eadcd6b857fb",
".cache/ford_route157/full/model_paths.npz": "25fc2526e092fde5ceb5aab63a5aa4f73a2c989a3263cedb0a26faf8da61c47b",
".cache/ford_route157/intake.npz": "5dffd86fde68197727d6c6b9c5eae82320a456fa1206c4cf18e5e9c69419dde4",
".cache/ford_feedback_delay_v21/157/commands.npz": "7d7659c54662e7f56015313e634b959be29dae524522b425f4c2adccc391fc9d"
}
},
"151": {
"cycles": 325708,
"wire_checks": 32571,
"baseline_matches_archived_v21_exactly": true,
"c1_integral_reference_arbitration_equal_every_cycle": true,
"P_reduction_p50_p95_p99_max_m": [
0.004569097471431923,
0.07115894622455567,
0.12458615764509068,
0.12499999999990274
],
"C0_change_p50_p95_p99_max_m": [
0.009999999999999787,
0.07000000000000028,
0.1200000000000001,
0.13000000000000078
],
"variants": {
"old": {
"c0_step_p95_p99_max": [
0.010000000000000675,
0.040000000000000036,
3.45
],
"c0_bound_seconds": 0.0
},
"new": {
"c0_step_p95_p99_max": [
0.010000000000000231,
0.03000000000000025,
3.33
],
"c0_bound_seconds": 0.0
}
},
"baseline_commit": "ed9c44f57584b5f033f79db6331e21b9b3e0ad55",
"method": "Replay C0 softening against v21 on frozen recorded motion, not a PSCM model.",
"controller_sha256": "b0aff6d70ad8ae7693c69068dbe646767e4d9fc20a23af70cbddcf6ebf849f48",
"source_sha256": {
".cache/ford_route151/full/route.npz": "41a5b8bd388cf5a3d553f784542376ac9355fcdc5be4f427053d0504537babe1",
".cache/ford_route151/full/model_paths.npz": "980b3843cec04254280d744a5801cee1bf0f8ef371052398327ff245f9eee01b",
".cache/ford_route151/intake.npz": "18bbefb7738cebd0071dc987e90f74469a0c8ed456f9412324030592cafd68c9",
".cache/ford_feedback_delay_v21/151/commands.npz": "4ab25e4c31fcddf08184c396efc3db78953d5ce60da7c653964bc2fa7e3184c9"
}
},
"149": {
"cycles": 132334,
"wire_checks": 13234,
"baseline_matches_archived_v21_exactly": true,
"c1_integral_reference_arbitration_equal_every_cycle": true,
"P_reduction_p50_p95_p99_max_m": [
0.010345416404636773,
0.12405821661525897,
0.12499991437968122,
0.12499999999999556
],
"C0_change_p50_p95_p99_max_m": [
0.009999999999999787,
0.1200000000000001,
0.1299999999999999,
0.13000000000000078
],
"variants": {
"old": {
"c0_step_p95_p99_max": [
0.03000000000000025,
0.08999999999999986,
3.87
],
"c0_bound_seconds": 0.0
},
"new": {
"c0_step_p95_p99_max": [
0.020000000000000462,
0.08000000000000007,
3.750000000000001
],
"c0_bound_seconds": 0.0
}
},
"baseline_commit": "ed9c44f57584b5f033f79db6331e21b9b3e0ad55",
"method": "Replay C0 softening against v21 on frozen recorded motion, not a PSCM model.",
"controller_sha256": "b0aff6d70ad8ae7693c69068dbe646767e4d9fc20a23af70cbddcf6ebf849f48",
"source_sha256": {
".cache/ford_route149/full/route.npz": "aa5902877343cd033ee286b3668d91a85336ffbf740861849fb0b76b0ca24ade",
".cache/ford_route149/full/model_paths.npz": "19827800b8fb5983f3d6b72fcfaf36e35a40170bb17cd7c1e49374744fb449fb",
".cache/ford_route149/intake.npz": "503919c4f1566ef850007c59f16ce074697ba57732f21beb7b158c1ca7be3a9d",
".cache/ford_feedback_delay_v21/149/commands.npz": "15c4ad68338a921cd6b2cd22d0b3d6b5b8637614dacbe61f954a3a6d7cb8d8d9"
}
}
},
"code_sha256": {
"openpilot/selfdrive/controls/lib/ford_model_action.py": "b0aff6d70ad8ae7693c69068dbe646767e4d9fc20a23af70cbddcf6ebf849f48",
"tools/ford_pscm_lab/c0_softening_replay.py": "43e185871e5b9875cee5288160d344fe4d606c4bdf63dd719fb6a6cd9404307e"
}
}
-102
View File
@@ -1,102 +0,0 @@
# Ford C1 correction carryover experiment
The feedback controller at `5fbb583e5` can retain a correction from an earlier
turn that outweighs the new base C1. The measured curvature can already be
opposite the desired curvature, yet total C1 continues to request the old
direction while the integral works back toward zero.
This experiment keeps the existing 1:1 feedback strength and adds a conditional
reset of that correction. It is a command-policy experiment, not a demonstrated
improvement in physical steering response.
## Release rule
All of the following must be true on a valid, active cycle:
- Feedback is enabled and a fresh steering publication advances measurement time.
- Base C1 is nonzero by at least one DBC step (0.0005 rad).
- Both target C0 and the slewed C0 request agree with base C1's direction,
by at least one DBC step (0.01 m).
- Measured steering-derived curvature points opposite the desired curvature.
- The accumulated correction prevents total C1 from requesting the base direction:
the sum of base C1 and correction is zero or opposite base C1.
The stored correction is then set to zero before the usual feedback increment.
The final C1 command still passes through its existing ±0.5 rad amplitude and
0.5 rad/s slew limits. The reset cannot directly jump the transmitted command.
The DBC steps reject requests smaller than one representable step; they are
not new strength multipliers. This reset policy is itself an engineering choice.
There is no reset simply because steering error crosses zero, or because C1
and its correction have opposite signs. Matched curvature, neutral/conflicting
C0, a correction that does not outweigh base C1, and repeated measurements all
preserve normal integration. The condition can apply to small steering
corrections as well as large turns; it has no turn-size or speed threshold.
No previous-turn direction or timer is stored. Agreement between current path
requests and disagreement with measured curvature are the confirmation. This
does not establish which part of the combined C0/C1 request a PSCM physically
needs. In particular, when C0 still points into the previous turn, this rule
deliberately leaves the integral alone.
## Preserved behavior and diagnostics
C0's 7 m mapping, its limits, the base C1 mapping, upstream curvature limiting,
the original integral strength, driver/PSCM arbitration, C2=C3=0 and the 100 Hz
sender are unchanged. No fitted PSCM model, proportional term or gain schedule
is added. There are still three values used by the command law: C0, C1 and
the correction. A diagnostic-only `carryover_release_count` is added and resets
with the controller. It is included in the existing periodic diagnostic event.
The same default-off Sunnylink toggle selects this version. Its diagnostic
identity is `model-action-c1-feedback-v2`. See the [drive-test instructions](ford_model_action_drive_test.md).
## Offline evidence
The two mirrored command-regression tests failed before the change. After
building correction through actual feedback, the old controller still requested
the old C1 direction 0.4 s into a reversal. Both tests now pass with the original
output slew. Additional tests cover holding a steady curve, small error
crossings, neutral and conflicting C0, representable command boundaries,
freshness, driver override and PSCM limits. Integration tests execute the actual
controlsd selection and upstream limiter, Float32 publication and Ford CAN
builder, using both model and maneuver-plan requests and both turn directions.
The combined suite passes **567 tests and 9,146 subtests**, with the same 178
inherited/unsupported safety-test skips as the original feedback validation.
The randomized checks include mirrored inputs, zero-error compatibility, and
comparison against the exact previous controller from cloned pre-update states.
Frozen b8 replay triggers 11 releases; b9 triggers 14. Activation and C0 match
the previous controller exactly on every reconstructed cycle. In b9, most
releases concern small corrections; one follows the large turn around 13:28.
The C0/C1 disagreement at 14:36 is preserved. Numerical details and source
hashes are in `ford_c1_carryover_validation.json`.
At the release around 13:28, the candidate C1 crosses into the requested
direction 0.255 s earlier than the previous controller on identical frozen
inputs. This is a command zero-crossing comparison, not a measured improvement
in the truck's steering response. The lab checks total 669,343 Float32/CAN
round trips, in addition to the integration tests.
Replay preserves recorded model requests and measured motion. A difference
between candidate and baseline commands can persist because the recorded
steering does not respond to the changed command. Replay cannot predict wheel
angles, centering, oscillation, or how much earlier the vehicle would unwind.
No device build, boot, installation or physical validation was performed.
## Reproduction
Use the branch's native dependencies and pinned opendbc revision
`c21a9013700734dd20b09e05aa68329ad8cc20f9`. The route commands require the existing
full-rlog b8/b9 extracts and the baseline Git revision. Run:
```sh
export PYTHONDONTWRITEBYTECODE=1
export PYTHONPATH=.:opendbc_repo
python -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 openpilot/sunnypilot/sunnylink/tests openpilot/common/tests/test_params.py opendbc_repo/opendbc/car/ford/tests/test_ford.py opendbc_repo/opendbc/safety/tests/test_ford.py
python -m tools.ford_pscm_lab.feedback_replay stress --cycles 200000 --output .cache/ford_c1_carryover/stress.json
python -m tools.ford_pscm_lab.stress_model_action --cycles 200000 --seed 20260907 --opendbc-revision c21a9013700734dd20b09e05aa68329ad8cc20f9 --output .cache/ford_c1_carryover/zero_error_stress.json
python -m tools.ford_pscm_lab.feedback_replay route .cache/ford_routeb8 --baseline 5fbb583e592d30de266f8160a5d6b9c620c97f56 --output .cache/ford_c1_carryover/routeb8
python -m tools.ford_pscm_lab.feedback_replay route .cache/ford_routeb9 --baseline 5fbb583e592d30de266f8160a5d6b9c620c97f56 --output .cache/ford_c1_carryover/routeb9
```
-177
View File
@@ -1,177 +0,0 @@
{
"created_at_utc": "2026-09-10T14:00:37.853762+00:00",
"scope": "Conditional release of accumulated C1 correction; offline command behavior only, no predicted vehicle response.",
"baseline_commit": "5fbb583e592d30de266f8160a5d6b9c620c97f56",
"baseline_source_sha256": "4499defbb7fc5ddf5029ca42c549f0935b0758b08818c5bf0490fb52221f9a34",
"deployment_target": {
"repository": "sunnypilot/sunnypilot",
"branch": "hiimisaac-dev"
},
"hypothesis": "model-action-c1-feedback-v2",
"calibration_approved": false,
"toggle": {
"key": "FordModelActionController",
"default_enabled": false,
"activation": "Existing controlsd startup selection"
},
"release_rule": "Fresh enabled feedback; target and slewed C0 agree with base C1 by >= one DBC step; measured curvature is opposite; stored correction makes total C1 zero or opposite base. Clear correction, then apply original integration and output slew.",
"engineering_choices": "Conditional reset policy, using existing DBC steps (0.01 m, 0.0005 rad) to confirm nonzero commands. Original 1:1 integral strength is unchanged.",
"preserved": [
"C0 mapping and limits",
"Base C1 mapping",
"Original integral strength",
"Final C1 amplitude and slew limits",
"Driver and PSCM arbitration",
"Upstream selection and limiting",
"100 Hz sender",
"C2=C3=0"
],
"panda_safety_changed": false,
"opendbc_submodule_changed": false,
"opendbc_head": "c21a9013700734dd20b09e05aa68329ad8cc20f9",
"controller_size": {
"total_lines": 194,
"code_lines_excluding_blanks_comments_docstrings": 131,
"core_command_state_values": 3,
"core_diagnostic_counters": 1
},
"tests": {
"combined_suite": "567 passed, 178 skipped, 9146 subtests passed in 6.45s",
"safety_skips": "Same 178 inherited or unsupported variants recorded in ford_c1_feedback_validation.json.",
"regression": "Two mirrored carryover command tests fail on the exact baseline class and pass in the candidate suite.",
"ruff_changed_python": "pass",
"ty_controller": "pass",
"settings_compiler_check": "pass",
"carryover_controlsd_to_can_frames": 1120,
"existing_feedback_controlsd_to_can_frames": 1010,
"integration_scope": "Actual source selection, upstream limiting, controller, Float32 publication, Ford sender, both plan sources and signs, all counters and checksums."
},
"routes": {
"b8": {
"cycles": 160431,
"active_cycles": 68217,
"validity_and_c0_match_baseline_exactly": true,
"c1_changed_cycles": 6065,
"max_abs_c1_change_rad": 0.09250000000000003,
"can_round_trips": 160431,
"timing_limit": "Controls publication time proxies the computation clock; full SubMaster checks are unavailable.",
"reference_limit": "Uses exact consumed model publication as reference; b8 and b9 have no maneuver-plan messages.",
"carryover_release_count": 11,
"input_sha256": {
"route.npz": "6f5dd369b70eaed4b95b28c8b25c9f2e9b830fa07a334881a185505481667c8b",
"model_paths.npz": "939af6cf7e74251d8842581cc078d26d9fbfd22a0d7817cb0e368697d419b615",
"metadata.json": "73b439132d1de37ec187b544c04d2b05c80965065515a4b7dec29ba57ae37e7c"
}
},
"b9": {
"cycles": 90774,
"active_cycles": 86474,
"validity_and_c0_match_baseline_exactly": true,
"c1_changed_cycles": 15208,
"max_abs_c1_change_rad": 0.10400000000000004,
"can_round_trips": 90774,
"timing_limit": "Controls publication time proxies the computation clock; full SubMaster checks are unavailable.",
"reference_limit": "Uses exact consumed model publication as reference; b8 and b9 have no maneuver-plan messages.",
"carryover_release_count": 14,
"input_sha256": {
"route.npz": "b07c789d8155335f5d120d0262fced6e4d5803fe767b0ff49b6413dce4140b5c",
"model_paths.npz": "6b1f87897c050273fdc05af051307a049b6fc3a93072e7cda1721195ce7c3861",
"metadata.json": "9ce452220cab61b81883f32fc2fcaf5db6c78a674cb255a49cc77d5029580fee"
}
}
},
"command_timing_example": {
"event": {
"time_s": 808.286646083,
"correction_before_rad": -0.13089810321135922,
"correction_after_rad": 0.0,
"base_c1_rad": 0.06412824021622576,
"desired_angle_deg": -24.17155647277832,
"actual_angle_deg": -0.30000001192092896,
"speed_m_s": 11.804088592529297,
"baseline_c0_c1": [
0.15000000000000036,
-0.06600000000000006
],
"candidate_c0_c1": [
0.15000000000000036,
-0.062000000000000055
]
},
"scope": "Command zero crossing on identical frozen recorded inputs; not wheel response.",
"baseline_c1_rightward_at_s": 808.67241324,
"candidate_c1_rightward_at_s": 808.4169884780001,
"command_crossing_advance_s": 0.25542476199984776
},
"feedback_stress": {
"cycles": 200000,
"mirrored_updates": 200000,
"can_round_trips": 200000,
"carryover_release_count": 946,
"baseline_revision": "5fbb583e592d30de266f8160a5d6b9c620c97f56",
"baseline_source_sha256": "4499defbb7fc5ddf5029ca42c549f0935b0758b08818c5bf0490fb52221f9a34",
"exact_unchanged_state_and_commands_without_release": 199054,
"checks": "Mirror symmetry, reset/override, amplitude, slew, correction bounds, carryover direction/confirmation, integration, PSCM limits, CAN.",
"scope": "Numerical software invariants only; no model of vehicle motion.",
"calibration_approved": false,
"controller_sha256": "6f40a05977253987a2c96e74c8c18d912367ed1e55630558ed7b28d52576e552"
},
"zero_error_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": "Zero-error numerical construction: measured equals requested curvature. No PSCM response claims.",
"opendbc_import_head": "c21a9013700734dd20b09e05aa68329ad8cc20f9"
},
"total_lab_float32_can_round_trips": 669343,
"source_sha256": {
"openpilot/selfdrive/controls/lib/ford_model_action.py": "6f40a05977253987a2c96e74c8c18d912367ed1e55630558ed7b28d52576e552",
"openpilot/selfdrive/controls/tests/test_ford_model_action_feedback.py": "04935fb941a795cb243870a4c03f7073c68147b01da3cedf2872476aa5fb798e",
"openpilot/selfdrive/controls/tests/test_ford_model_action_adapter.py": "e2e98d3a0a531235abd032fc4d3564796613ad51ff6ba22a230c48e36f6f6848",
"openpilot/selfdrive/controls/tests/test_ford_controlsd_logging.py": "90fb42ce580f0085e349467086a2eef28c2512c671755d0771f6331d30b19035",
"tools/ford_pscm_lab/feedback_replay.py": "9bf145fbff6ed685aec2c0e5d0584e2dc7ff831021f15110f939e8a94c93280b",
"tools/ford_pscm_lab/stress_model_action.py": "0b25188edf2b248ebe741173ce02ce75bd59f1f39fd5bd909d41a3dca2294aa8",
"tools/ford_pscm_lab/model_action_replay.py": "af97c665f342c66b1be2502e188c63e6f3ee106d0a0d5e80997bc3040373ff9f",
"docs/ford_c1_carryover.md": "e8d08375963efc6d1ae6ce503bb580ba00cfa90adb71c941d56fe6e3701d5cbb",
"docs/ford_c1_feedback.md": "1b440a03082e5cec264a1d6693833ed0a7e07b6c6e2122f8e4455c5971121b57",
"docs/ford_model_action_drive_test.md": "7ac5ca0faf9690a23e7058d09b55f23e21e2ba74666001cacb56b67e8d8b4376",
"openpilot/selfdrive/controls/controlsd.py": "2b7e246f00bccce3a2bb9f6f44009ca77690cadb8527cd2bdfe855e9ad72ad1e",
"opendbc_repo/opendbc/car/ford/carcontroller.py": "b2d327a1833fb1f0d09ee17f54c9c8d45517fa29beb04a4543cfbf1b43f1a65e",
"opendbc_repo/opendbc/safety/modes/ford.h": "1d9d996292d6697ab4f02d55fae348d6aca1df94a07f7bdae48b68971b91afe7",
"openpilot/sunnypilot/sunnylink/settings_ui.json": "7d38f315a7c5ce6d46d01a06f7eaddd4933f85639e5325ff71fdce22866ef401"
},
"artifact_sha256": {
".cache/ford_c1_carryover/tests.txt": "cd65146df93632e4a2c1e086781e1da4673db7d038c7e673124f227efefbb567",
".cache/ford_c1_carryover/baseline_regression.txt": "4469873f95ccf45a376a27fed05be3bdad5f808af7ceca472c6e2cb9d973eb7f",
".cache/ford_c1_carryover/stress.json": "7a8d20d7abd7cf444f55b7316e8bedbed0fbe8e9587e09f90d5b1946c3c2e98c",
".cache/ford_c1_carryover/zero_error_stress.json": "09e64eaac35df4ec324b41fabdc8baf91931106ac98b89c1ca71f4c8bf8796a4",
".cache/ford_c1_carryover/timing.json": "0d18ed4164803adedbbd660fe024f4c28de8eb7921caa60659346ff386d3847f",
".cache/ford_c1_carryover/routeb8/report.json": "d2c6f767cef29a74e292b6a16263d2da13b8c302e4653e419b0e232e1aaf762e",
".cache/ford_c1_carryover/routeb8/commands.npz": "89b5c3474940b61afce060111c27fd9bad9e24d703c59fca61adf4ce10473df3",
".cache/ford_c1_carryover/routeb9/report.json": "e3ff7bfa70e770eca763b125c283fd8a1d509ef1b6e7f26a81c398aca89a87da",
".cache/ford_c1_carryover/routeb9/commands.npz": "e4f5f341146e2897a479baf222d678fd16352c8da931876a2471c3719faf9edf"
},
"test_environment": {
"python": "/Users/ibpersonal/dev/sunnypilot/.venv/bin/python",
"PYTHONPATH": ".:opendbc_repo:.cache/ford_v6/test_deps",
"PYTHONDONTWRITEBYTECODE": "1",
"LOG_ROOT": "/private/tmp/ford-carryover-logs",
"PARAMS_ROOT": "/private/tmp/ford-carryover-params"
},
"limitations": [
"Frozen replay preserves recorded requests and measured motion; changed commands do not establish changed wheel angles, centering or stability.",
"C0/C1 agreement is a reset-policy choice, not an identified relationship between PSCM input and wheel angle.",
"The rule can release small corrections and does not promise unchanged centering during transients.",
"No device build, boot, installation or physical validation was performed."
]
}
-129
View File
@@ -1,129 +0,0 @@
# Ford C1 feedback experiment
This document records the original feedback change at `5fbb583e5`. The current
version retains its feedback law and adds [conditional carryover release](ford_c1_carryover.md).
The validation counts below describe the original change; current results are
recorded in `ford_c1_carryover_validation.json`.
The restored original v1 can leave a steering error while C0 and C1 still have
room. Its command law does not directly correct measured steering error. This
experiment keeps that mapping and adds one accumulated C1 correction:
```text
error = selected_limited_desired_curvature - measured_curvature
correction += error * speed * elapsed_measurement_time
C1_target = original_model_C1 + correction
```
Curvature (1/m) multiplied by traveled distance (m) gives heading mismatch in
radians. Applying that mismatch to C1 at **1:1 is an explicit feedback-strength
choice**. Dimensional consistency does not prove that every PSCM responds
correctly to that strength. There is no fitted PSCM response model or new
tunable multiplier.
For example, at 20 m/s, a constant curvature shortfall of 0.001/m adds 0.02 rad
to C1 over one second when the output can accept it. When measured curvature
matches the request, the correction holds. If the vehicle turns more than
requested, the correction moves in the unwind direction. Changing the model
request still changes the base immediately, subject to the existing slew.
## Preserved mapping and limits
- C0 is the current model path's lateral offset at 7 m of arc distance, holding
the available endpoint for shorter paths; its limits remain ±5.11 m and 4 m/s.
- Base C1 is `max(7 m, speed × 1 s) × selected_limited_desired_curvature`, clipped
to ±0.5 rad. Final C1 uses the same ±0.5 rad and 0.5 rad/s limits as v1.
- C2 and C3 are zero. Sign conversion, Float32/CAN rounding, upstream curvature
limiting and the 100 Hz sender retain their existing behavior.
The core holds three values: unquantized C0, unquantized C1 and the correction.
Zero error from a reset leaves the correction at zero and preserves the old
command arithmetic exactly. There is no separate percentage or distance cap
on the correction.
## Feedback measurement, timing and limits
The measurement is `controlsd.curvature`, computed from measured steering
angle with the existing live vehicle parameters. It matches the curvature
used for the desired-versus-actual steering comparison. It is not an independent
measurement of tire slip or the vehicle's actual ground path. CAN yaw remains
an input-health gate and does not drive this feedback.
The adapter integrates only elapsed time between fresh `carState` publications.
The first publication after reset integrates zero time. Duplicate timestamps
integrate zero; a fresh timestamp accounts for the elapsed measurement interval.
Output slew continues on valid control cycles. Existing service-age, speed,
model-geometry and clock-order gates remain, with the same finite/range check
also applied to measured curvature. Disengagement or invalid input clears all
three core states.
The correction cannot accumulate farther into an unavailable C1 amplitude or
slew request. Increments that move back toward the available output remain
allowed. Moving the base request does not itself rewrite the correction.
Fresh PSCM status means a valid message whose original CAN receipt timestamp
is within the existing 5 to +150 ms age allowance. Reached-limit status (2)
prevents extra accumulation in the measured turn direction. An old correction
opposing that direction can return to zero; it cannot be trapped below the
base request by the limit flag. Unwind and base model changes remain available.
Close-to-limit status (1) does not block feedback. Missing or stale status
does not gate it; local amplitude and slew anti-windup still apply.
Driver steering-pressed, torque above the existing 1 Nm allowance, nonfinite
torque, or fresh driver-limit status (3) clears the correction. Fresh denied
or inactive PSCM status also clears it. The base model request continues
through existing engagement and driver arbitration; clearing the correction
does not bypass the final output slew.
## Offline evidence and reproduction
`ford_c1_feedback_validation.json` records the source hashes and completed
checks. Tests exercise build, hold, unwind, saturation, limit flags, immediate
driver input, stale and repeated measurements, invalid inputs and both signs.
Integration tests execute actual controlsd selection and limiting, Float32
publication, CarControlSP conversion and the Ford CarController CAN builder.
Randomized runs check feedback invariants separately from zero-error
compatibility with the original independent scalar oracle.
The combined suite passes **511 tests and 9,146 subtests**. Its 178 skips are
in inherited safety base classes or unsupported safety-test variants. Ruff,
the controller's Ty check and settings compilation pass. Feedback stress,
zero-error stress and the b8 replay total **578,569 Float32/CAN round trips**;
the integration test separately verifies 1,010 transmitted packet constructions,
including every counter and checksum. No packets are sent to hardware.
The b8 replay retains recorded desired/measured curvature, model publications,
driver input and PSCM flags. It compares candidate commands with the restored
v1 at `a7d70e2b0890184636827351e4789d866f2a7c97`. All 160,431 reconstructed
activation decisions and C0 commands match. C1 changes on 58,106 cycles.
At 4:12.493, for example, reconstructed host C1 changes from 0.1625 to
0.2035 rad; at 3:56.250 it changes from 0.1280 to 0.1080 rad. These are
changes to commands on frozen measurements, not predicted wheel angles.
Controls publication time proxies the unlogged computation clock, and the
full SubMaster health state cannot be reconstructed. This route uses the
consumed model publication as its reference and has no maneuver-plan messages.
Replay cannot show whether this feedback fixes weak turns, hanging turns or
oscillation. A new drive is needed to measure those outcomes.
Use the branch's native dependencies and pinned opendbc revision
`c21a9013700734dd20b09e05aa68329ad8cc20f9`:
```sh
export PYTHONDONTWRITEBYTECODE=1
export PYTHONPATH=.:opendbc_repo
python -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 openpilot/sunnypilot/sunnylink/tests openpilot/common/tests/test_params.py opendbc_repo/opendbc/car/ford/tests/test_ford.py opendbc_repo/opendbc/safety/tests/test_ford.py
python openpilot/sunnypilot/sunnylink/tools/compile_settings_ui.py --check
python -m tools.ford_pscm_lab.feedback_replay stress --cycles 200000 --output .cache/ford_c1_feedback/feedback_stress.json
python -m tools.ford_pscm_lab.stress_model_action --cycles 200000 --seed 20260907 --opendbc-revision c21a9013700734dd20b09e05aa68329ad8cc20f9 --output .cache/ford_c1_feedback/zero_error_stress.json
python -m tools.ford_pscm_lab.feedback_replay route .cache/ford_routeb8 --output .cache/ford_c1_feedback/routeb8
```
The last command requires the existing full-rlog b8 extract (`route.npz`,
`model_paths.npz`, `metadata.json`), identified by hashes in the validation
record. The historical route90/95 replay deliberately sets measured curvature
equal to requested curvature to check zero-error compatibility; it does not
exercise recorded steering feedback.
Enable using the [existing Sunnylink toggle](ford_model_action_drive_test.md).
The diagnostic identity is `model-action-c1-feedback-v1`.
-258
View File
@@ -1,258 +0,0 @@
{
"created_at_utc": "2026-09-09T14:45:33.853345+00:00",
"baseline_commit": "a7d70e2b0890184636827351e4789d866f2a7c97",
"deployment_target": {
"repository": "sunnypilot/sunnypilot",
"branch": "hiimisaac-dev"
},
"scope": "C1 measured-curvature feedback on restored original v1. Offline software validation only; no predicted or measured physical improvement.",
"calibration_approved": false,
"toggle": {
"key": "FordModelActionController",
"default_enabled": false,
"activation": "Existing startup selection after offroad-to-onroad cycle"
},
"feedback_law": "correction += (desired_curvature - measured_curvature) * speed * elapsed_measurement_time, subject to output and PSCM anti-windup",
"feedback_strength": "Explicit 1:1 heading-error-to-C1 choice; no fitted PSCM plant or new tunable multiplier",
"preserved": [
"C0 mapping and limits",
"C2=C3=0",
"C1 final amplitude and slew limits",
"100 Hz sender",
"upstream selection and limiting"
],
"panda_safety_changed": false,
"opendbc_submodule_changed": false,
"opendbc_head": "c21a9013700734dd20b09e05aa68329ad8cc20f9",
"controller_size": {
"total_lines": 181,
"code_lines_excluding_blanks_comments_docstrings": 123,
"core_persistent_values": 3,
"adapter_timestamps": 3
},
"tests": {
"combined_suite": "511 passed, 178 skipped, 9146 subtests passed in 5.14s",
"suite_log_sha256": "001ef6633b22513317593dd8debc160a0ca8aaf78ea53418c5f7a50c370cc818",
"ruff_changed_python": "pass",
"ty_controller": "pass",
"settings_compiler_check": "pass",
"safety_skip_reasons": [
"SKIPPED [145] ../../../../dev/sunnypilot/.venv/lib/python3.12/site-packages/_pytest/unittest.py:523: Skipped",
"SKIPPED [9] opendbc_repo/opendbc/safety/tests/common.py:64: Safety mode implements no _user_regen_msg",
"SKIPPED [3] opendbc_repo/opendbc/safety/tests/mads_common.py:51: Skipping test because MADS button is not supported",
"SKIPPED [3] opendbc_repo/opendbc/safety/tests/mads_common.py:254: Skipping test because MADS button is not supported",
"SKIPPED [3] opendbc_repo/opendbc/safety/tests/mads_common.py:67: Skipping test because _acc_state_msg is not implemented for this car",
"SKIPPED [3] opendbc_repo/opendbc/safety/tests/mads_common.py:165: Skipping test because MADS button is not supported",
"SKIPPED [3] opendbc_repo/opendbc/safety/tests/mads_common.py:165: Skipping test because ACC main is not supported",
"SKIPPED [3] opendbc_repo/opendbc/safety/tests/mads_common.py:411: MADS button not supported",
"SKIPPED [1] opendbc_repo/opendbc/safety/tests/test_ford.py:378: CAN FD only",
"SKIPPED [1] opendbc_repo/opendbc/safety/tests/test_ford.py:361: CAN FD only",
"SKIPPED [1] opendbc_repo/opendbc/safety/tests/test_ford.py:351: CAN FD only",
"SKIPPED [1] opendbc_repo/opendbc/safety/tests/test_ford.py:327: CAN FD only",
"SKIPPED [1] opendbc_repo/opendbc/safety/tests/test_ford.py:341: CAN FD only",
"SKIPPED [1] opendbc_repo/opendbc/safety/tests/test_ford.py:320: CAN FD only"
],
"safety_native_build": "Pinned safety C source is compiled locally by libsafety_py before testing.",
"controlsd_to_can_feedback_integration_frames": 1010,
"integration_checks": "Both signs: build, hold, unwind, rebuild, immediate driver override; actual 100 Hz sender, counter, checksum, fields and publication. Separate integration tests validate PSCM service forwarding.",
"regression_test_evidence": [
"Nonzero-error integration failed with zero correction before implementing feedback.",
"Both sign tests failed when a reached limit trapped an old opposing correction; they pass after allowing return to zero."
]
},
"route_b8": {
"baseline_revision": "a7d70e2b0890184636827351e4789d866f2a7c97",
"baseline_source_sha256": "8f3bc5d68e0051776f614a2ccffae84a88f7898dc95bdc12c23dcfe10dfe676a",
"cycles": 160431,
"active_cycles": 68217,
"validity_and_c0_match_original_v1_exactly": true,
"status_counts": {
"inactive": 92214,
"active": 68217
},
"feedback_enabled_seconds": 598.256888772994,
"pscm_limit_2_seconds": 12.139079590997426,
"c1_changed_cycles": 58106,
"max_abs_c1_change_rad": 0.29800000000000004,
"max_abs_correction_rad": 0.29816844327770786,
"can_round_trips": 160431,
"timing_limit": "Controls publication time proxies the computation clock; full SubMaster checks are unavailable.",
"reference_limit": "Uses exact consumed model publication as reference; the b8 route has no maneuver-plan messages.",
"example_points": [
{
"time_s": 130.9368894940053,
"old_c0_c1": [
-0.7400000000000002,
-0.18700000000000006
],
"candidate_c0_c1": [
-0.7400000000000002,
-0.22899999999999998
],
"correction_rad": -0.042171663052515254,
"feedback_enabled": true,
"pscm_limited": false
},
{
"time_s": 235.3960996990063,
"old_c0_c1": [
-2.04,
-0.40449999999999997
],
"candidate_c0_c1": [
-2.04,
-0.4145
],
"correction_rad": -0.00989648519895422,
"feedback_enabled": true,
"pscm_limited": true
},
{
"time_s": 236.25034470800165,
"old_c0_c1": [
-1.46,
-0.128
],
"candidate_c0_c1": [
-1.46,
-0.10799999999999998
],
"correction_rad": 0.01990758350705991,
"feedback_enabled": true,
"pscm_limited": false
},
{
"time_s": 252.49320156300382,
"old_c0_c1": [
-0.6699999999999999,
-0.16249999999999998
],
"candidate_c0_c1": [
-0.6699999999999999,
-0.20350000000000001
],
"correction_rad": -0.040907632902654506,
"feedback_enabled": true,
"pscm_limited": false
},
{
"time_s": 674.430371745002,
"old_c0_c1": [
0.4299999999999997,
0.128
],
"candidate_c0_c1": [
0.4299999999999997,
0.1345
],
"correction_rad": 0.00639271291315417,
"feedback_enabled": true,
"pscm_limited": false
},
{
"time_s": 1534.5190040400048,
"old_c0_c1": [
2.62,
0.5
],
"candidate_c0_c1": [
2.62,
0.5
],
"correction_rad": 0.0,
"feedback_enabled": true,
"pscm_limited": true
},
{
"time_s": 1562.5074677500015,
"old_c0_c1": [
-0.1200000000000001,
-0.051000000000000045
],
"candidate_c0_c1": [
-0.1200000000000001,
-0.046499999999999986
],
"correction_rad": 0.004453988923883501,
"feedback_enabled": true,
"pscm_limited": false
}
]
},
"route_input_sha256": {
"route.npz": "6f5dd369b70eaed4b95b28c8b25c9f2e9b830fa07a334881a185505481667c8b",
"model_paths.npz": "939af6cf7e74251d8842581cc078d26d9fbfd22a0d7817cb0e368697d419b615",
"metadata.json": "73b439132d1de37ec187b544c04d2b05c80965065515a4b7dec29ba57ae37e7c"
},
"feedback_stress": {
"cycles": 200000,
"mirrored_updates": 200000,
"can_round_trips": 200000,
"checks": "Mirror symmetry, reset/override, amplitude, slew, correction bounds, integration direction/size, PSCM anti-windup, CAN fields.",
"scope": "Numerical software invariants only; no model of vehicle motion.",
"calibration_approved": false,
"controller_sha256": "4499defbb7fc5ddf5029ca42c549f0935b0758b08818c5bf0490fb52221f9a34"
},
"zero_error_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": "Zero-error numerical construction: measured equals requested curvature. No PSCM response claims.",
"opendbc_import_head": "c21a9013700734dd20b09e05aa68329ad8cc20f9"
},
"total_lab_float32_can_round_trips": 578569,
"artifact_sha256": {
".cache/ford_c1_feedback/routeb8/report.json": "648887eebb76260a60f7c0f0d9aaac83d0f1c06b443e28bc6fdf296bad4526c0",
".cache/ford_c1_feedback/routeb8/commands.npz": "1aef98365572b3fb3cb8ba2a93cf30041ff3be133718d469145b710f2c94dc32",
".cache/ford_c1_feedback/feedback_stress.json": "abf4e7bccc1e460008cc7450fcd92e9b2a6108bd71e53a01bdc24e31e5b5ad32",
".cache/ford_c1_feedback/zero_error_stress.json": "2a3f284e10e5054205a788cce59bcf57bd837e13afff327457244141ba5522f0",
".cache/ford_c1_feedback/safety_skip_reasons.txt": "5384c82b07b7cc20c6b22b8e94246cb104d53f8866af02b28fda7d4138cf377f"
},
"native_params": {
"library_sha256": "270bf43241cf7c02cc432cf78ec9411a62d7653ca445695efe785ae82241aa09",
"sources_match_original_rebuild_record": true,
"provenance": "Same locally rebuilt native library and source hashes recorded in ford_model_action_drive_test_validation.json; verified for this run."
},
"test_environment": {
"python": "/Users/ibpersonal/dev/sunnypilot/.venv/bin/python",
"PYTHONPATH": ".:opendbc_repo:.cache/ford_v6/test_deps",
"LOG_ROOT": "/private/tmp/ford-feedback-logs",
"PARAMS_ROOT": "/private/tmp/ford-feedback-params",
"PYTHONDONTWRITEBYTECODE": "1"
},
"source_sha256": {
"docs/ford_c1_feedback.md": "c1bc7f24c5ebe28679b4a04d09085d7b937926e63a38d43a3abfac93dfcfa0f9",
"docs/ford_model_action_candidate.md": "20cd8d10008cd796cc8719f5795ee80f50d8133d6e7684fb057a78fb05323fbe",
"docs/ford_model_action_drive_test.md": "7860ae26a61682aff86743ba302eb23c8f271d5700a2e616da1b6b38d438b57d",
"opendbc_repo/opendbc/car/vehicle_model.py": "ddc2a93d9c2b2ef6c9a913a5aef4c51e2bc387db1f7640473657e5ade4e50fac",
"openpilot/selfdrive/controls/controlsd.py": "2b7e246f00bccce3a2bb9f6f44009ca77690cadb8527cd2bdfe855e9ad72ad1e",
"openpilot/selfdrive/controls/lib/drive_helpers.py": "916bcd83c2a909a89795da58c7c43d7b168c9b82e1a6d281484bae45c667c01e",
"openpilot/selfdrive/controls/lib/ford_model_action.py": "4499defbb7fc5ddf5029ca42c549f0935b0758b08818c5bf0490fb52221f9a34",
"openpilot/selfdrive/controls/lib/ford_path.py": "383538fc7cdae3bc28dffb71fe12ac5f3f9866ffbe6adfb7457f3593e9fc903a",
"openpilot/selfdrive/controls/tests/test_ford_controlsd_logging.py": "84113b1b7800c868117af0034278f53a1a6153c7bb5fadc1ea45958e62c4f0d0",
"openpilot/selfdrive/controls/tests/test_ford_model_action.py": "cbe1b2aa1961deba3a42e1d82f5f75ae0c3d7a219428dea5f50cb70e1b27fd11",
"openpilot/selfdrive/controls/tests/test_ford_model_action_adapter.py": "c7f5ffd650e804e6e02fa12d435e0867b56b13a49c3d9fa511993188d5cb625a",
"openpilot/selfdrive/controls/tests/test_ford_model_action_feedback.py": "f7a956c082a246d9506e21adbf348cbdc7f94d5342d832841058c71f7e264eeb",
"openpilot/sunnypilot/selfdrive/controls/controlsd_ext.py": "7a13dc5ce49b40e27e05e62cdb9ef1bb764de8ed8167f7e982d54a4dffe97ed4",
"openpilot/sunnypilot/sunnylink/settings_ui.json": "7d38f315a7c5ce6d46d01a06f7eaddd4933f85639e5325ff71fdce22866ef401",
"openpilot/sunnypilot/sunnylink/settings_ui_src/pages/vehicle.yaml": "410e306958ece12e49fc114707741c57a2dd927c6ba3410e160834e52a759ea9",
"tools/ford_pscm_lab/feedback_replay.py": "ca552217953f3cce35da0b1666252fd43b6f8ab6c067e5c9102da8ea8d97c2f3",
"tools/ford_pscm_lab/model_action_replay.py": "af97c665f342c66b1be2502e188c63e6f3ee106d0a0d5e80997bc3040373ff9f",
"tools/ford_pscm_lab/stress_model_action.py": "0b25188edf2b248ebe741173ce02ce75bd59f1f39fd5bd909d41a3dca2294aa8"
},
"limitations": [
"Frozen route replay changes commands only; it cannot establish tracking, unwind response or closed-loop stability.",
"Measured curvature uses the existing steering-angle vehicle model; it is not an independent ground-path measurement.",
"No full device build, device boot, installation or road validation was performed."
]
}
-102
View File
@@ -1,102 +0,0 @@
# Ford C1 integral trial: I=1.0
Route 151 had large entry shortfalls before driver input while C1 still had
command range available. At the segment-5 right-turn shortfall, the selected
request was 125 degrees right and the wheel was at 49 degrees right. The stored
correction was only 0.015 rad and was growing at about 0.0245 rad/s. Neither the
C1 field bound nor the PSCM reached-limit flag explained that point.
This trial raises I from 0.25 to **1.0**, keeping P at **0.75**. The fresh-error
increment is four times larger for the same curvature error, speed and elapsed
measurement time. This also retires existing correction four times faster for
the same opposing error, before the existing accumulation/headroom rules apply.
It does not introduce a new state, rate limit, threshold or release heuristic.
Runtime changes are one gain constant and the diagnostic identifier
`model-action-curvature-c0-distance-pi-v14`.
C0's desired-curvature formula and distance toggle, base C1, +0.40 s low-speed
model preview, feedback measurement, driver override, PSCM arbitration, field
bounds and 100 Hz sender remain unchanged. Zero error holds I. Driver override
clears P and I. Fresh limitReached blocks outward accumulation while allowing
retirement. Accumulation cannot charge beyond the combined command's available
range. C2/C3 stay zero, and toggle-off still selects upstream Ford control.
## Why this coefficient
Compared I=0.25, 0.50, 1.0 and 2.0 with P=0.75 on routes 149 and 151, using
the production adapter and Float32/CAN path for every sample. I=1.0 gives a
substantial increase in retained correction. I=2.0 adds considerably more
opposite-direction correction on reviewed exits and approaches the C1 bound
in the route-151 right turn. I=1.0 is a fourfold experimental step, not an
offline-fitted optimum or a validated physical calibration.
Paired fixed-motion examples with I=0.25 / I=1.0:
| Route / time | Recorded situation | C1 before | C1 candidate |
| --- | --- | ---: | ---: |
| 151 / 306.891 s | 125-degree right request, 49-degree wheel | +0.3270 | +0.3725 |
| 151 / 307.999 s | Wheel remains behind on the same right turn | +0.2675 | +0.3625 |
| 151 / 2363.807 s | 137-degree left request, 64-degree wheel | -0.3410 | -0.3785 |
| 149 / 319.549 s | Right-turn entry shortfall | +0.4190 | +0.4840 |
| 149 / 850.497 s | Right-turn release | +0.0670 | +0.0010 |
| 149 / 851.331 s | Near center on that release | +0.0105 | -0.0460 |
These are same-cycle controller requests, not necessarily the preceding CAN
message at that timestamp. Positive C1 requests right steering; positive logged
wheel angle means left. Both replays use P=0.75; route 149 originally drove
P=0.50, so the old-I replay is not its historical command trace.
The exit examples show why faster retirement does not guarantee a smoother
unwind: the candidate can accumulate more correction in the opposite direction
and retain it when error reaches zero. Higher I also affects centering. On
route 151's clean requests under 10 degrees, mean absolute C1 rises from about
0.0083 to 0.0122 rad on the fixed recording; route 149 rises from 0.0077 to
0.0154 rad. The vehicle would generate different errors under the candidate.
These are command observations, not predictions of wheel motion, stability,
overshoot, or future tracking accuracy.
## Broader validation
The paired production replay covers 22 extracts: 112117, 119, 11a, 120, 124,
125, 146, 149, 151, a0, a2, a5, a9, b8, b9, ca and Raptor 02. It processes
2,600,956 source cycles and 5,201,912 Float32/CAN round trips. Both settings have
identical eligibility, C0, P, base heading, overflow, and driver/PSCM feedback
gates on every cycle. Output remains finite and bounded; inactive commands and
C2/C3 are zero. The decoder checks fields, mode and counter on every command.
Scoring excludes driver override, inactive/invalid control, disabled feedback,
the following second, and speeds below 3 mph. There are 12,213.62 scored seconds.
C1-bound time rises from 22.67 to 25.13 seconds. Route 151 has no C1-bound samples
in that cohort under either setting. Its I=0.25 baseline matches the recorded
path output to Float32 precision: maximum C0 error 5.8e-8 m, C1 error 1.5e-8 rad.
Older routes intentionally retain their original model requests and physical
measurements, including any historical tracking errors. Their baseline command
traces need not match older controller implementations.
The existing controlsd-to-publication-to-CAN feedback test was updated before
the gain change. It failed on the old default (0.005 rad accumulated versus
0.020 rad required over its one-second error interval), then passed with the
new default. **468 tests and 25 subtests pass**, covering selection, current
references, accumulation/hold/retirement, reversals, duplicate measurements,
PSCM limits, driver overrides, downstream checksums and upstream fallback.
Ruff and `git diff --check` pass. No device build or physical evaluation is
claimed by these offline checks.
Evidence: [ford_c1_i1_validation.json](ford_c1_i1_validation.json). Local arrays
are in `.cache/ford_i1_trial`, with the four-setting comparison in
`.cache/ford_i_trial_sweep`. Reproduce a comparison:
```sh
PYTHONPATH=.:opendbc_repo PYTHONDONTWRITEBYTECODE=1 python \
tools/ford_pscm_lab/proportional_replay.py \
--routes 151=.cache/ford_route151/full 149=.cache/ford_route149/full \
--settings .75:.25 .75:1.0 \
--output .cache/ford_i1_recheck --workers 2
```
The replay tool's default P=0.50 / P=0.75 comparison with I=0.25 is preserved.
Explicit `--settings` accepts P:I pairs; the first is the baseline. Publication
timestamps approximate execution time because full process scheduling and
SubMaster state are not logged. The trial still needs physical measurement;
route 151 also changed the big model from CTMV2 to Tee Time, so its comparison
with route 149 cannot isolate the controller's physical effect.
File diff suppressed because it is too large Load Diff
-82
View File
@@ -1,82 +0,0 @@
# Ford continuous PI drive trial (v7)
The selected controller follows the current, upstream-limited desired curvature
with P=0.50 and I=0.25. It replaces v6's request-change, C0-confirmation and
unwind-catchup release rules. C0 mapping, heading overflow allocation, input
health gates, driver/PSCM arbitration and final output limits remain unchanged.
Only C0, C1 and I carry control history. C2/C3 stay zero.
```text
D = max(7 m, speed × 1 s)
error = selected desired curvature measured steering-derived curvature
base = clip(D × desired curvature, 0.5, +0.5)
P = 0.50 × D × error
increment = 0.25 × speed × error × fresh steering measurement interval
C1 target = clip(base + P + I, 0.5, +0.5)
```
After PSCM outward-accumulation arbitration, the increment first cancels up to
its own magnitude of opposing I. It cannot cross zero in that step. Any remainder
is bounded by the combined command's amplitude/slew headroom. C1 output still
slews at 0.5 rad/s. This allows old I to unwind even when the output is already
slewing. It adds no timer, request history, turn detection or position threshold.
P doubles relative to v6, and I builds at one quarter the previous rate for an
identical error and fresh measurement interval. Lower I also unwinds more slowly
for an identical existing state and error; its replay benefit comes mainly from
storing less correction. Zero error removes P and holds I. Persistent tracking
bias may need that holding correction. There is no claim that all I is unwanted.
## Evidence and limitations
Six offline settings were compared across fourteen routes and 1,578,250 source
cycles before selecting this candidate. The production selector and adapter now
exactly reproduce the selected P=0.50/I=0.25 archived commands, validity, P, I,
feedforward, C0 overflow and feedback/PSCM gates on every cycle of all fourteen
routes. These comprise Lightning 112117, a0, a2, a5, a9, b8, b9, ca and Raptor 02.
The integration replay performs 1,578,250 actual Float32/CAN round trips.
A further 20,000 randomized cycles with mirrored and independent C0 paths perform
60,000 CAN checks on production, checking independent scalar arithmetic,
C0-independent feedback, symmetry, reset, amplitude and slew behavior.
The Ford, Sunnylink, params, sender and safety suite passes 679 tests and
9,145 subtests; 178 are skipped by the platform test suite. Removed maneuver
heuristic tests are replaced with continuous-error, cancellation, freshness,
three-state reproduction and actual controlsd-to-CAN entry/exit checks.
Historical untracked offline experiment tests are outside this deployment suite.
At a previously reviewed route-115 exit (133.595 s), the original small PI
controller requested +0.1090 rad C1; this trial requests +0.0175. That sample
includes driver context. Reviewed large entries remain similar, but commands
are not identical everywhere. In a previously well-tracked route-116 bend
(112 s), C1 falls from +0.1530 to +0.1355 rad. Lower I could weaken a persistent
bend, while higher P can increase response to measurement fluctuations.
Recorded wheel motion remains fixed in replay. These checks establish software
behavior and exact integration of the candidate; they do not establish improved
physical tracking or stability. Gains remain experimental, not an identified
universal PSCM calibration. No device build, boot or new drive is claimed.
## Selection and reproduction
Use the existing default-off Sunnylink **Selected-Action Path Tracking
(Experimental)** toggle on any Ford CAN FD, followed by a real offroad-to-onroad
cycle. Logs identify `model-action-c1-pi-v7`, `proportional_gain=0.5`,
`integral_gain=0.25`. Toggle-off selects upstream Ford control. See the
[drive instructions](ford_model_action_drive_test.md).
With the built cereal/opendbc environment and archived local extracts:
```sh
export PYTHONPATH=.:opendbc_repo:.cache/ford_v6/test_deps
export PYTHONDONTWRITEBYTECODE=1
export PARAMS_ROOT=/tmp/ford-v7-params
export LOG_ROOT=/tmp/ford-v7-logs
python -m tools.ford_pscm_lab.minimal_pi_validate --output .cache/ford_minimal_tuning/production --workers 4
python -m tools.ford_pscm_lab.minimal_pi_production_stress --cycles 20000 --output .cache/ford_minimal_tuning/production_stress.json
```
The validation JSON records route and source hashes. The archived six-setting
sweep is local evidence, not a checked-in dataset. Historical v5/v6 lab tools
load their pinned controller revisions so their baseline comparisons retain
their original meaning after production changes.
-332
View File
@@ -1,332 +0,0 @@
{
"scope": "Production integration of the selected continuous PI drive trial; fixed recorded motion, not a physical tracking prediction.",
"parent_revision": "5e6993aab",
"previous_controller_revision": "bf00bc691def830e1beb15363d05416714c1dc42",
"hypothesis": "model-action-c1-pi-v7",
"kp": 0.5,
"ki": 0.25,
"control_history": [
"c0",
"c1",
"correction"
],
"physical_lines": {
"module": 201,
"core_class": 58,
"core_update": 42
},
"production_source_sha256": "a899b8595e903d6fc4401392a8d123ef77725f6d6d7fa10392a7b3fe72e732b8",
"route_cycles": 1578250,
"production_can_round_trips": 1638250,
"tests": {
"passed": 679,
"skipped": 178,
"subtests_passed": 9145,
"log_sha256": "2bc3dfd7ba08d4985d3dea98f235eb1fc99b23e5bbb3927e823a2439c8e9a481",
"scope": "Ford controls, tracked PSCM lab tests, PSCM status, Sunnylink, params, Ford car and safety suites; historical untracked experiments excluded."
},
"ruff": "pass",
"ty_production": "pass",
"routes": [
{
"scope": "Verify production commands against the archived P=.50/I=.25 offline candidate.\n\nFixed recorded motion verifies integration parity, not physical tracking.\n",
"route": "112",
"cycles": 108971,
"can_round_trips": 108971,
"production_matches_archived_trial_exactly": true,
"opendbc_revision": "64aa61b9b3fd26e70a7caa915acab207ff3cd64a",
"hypothesis": "model-action-c1-pi-v7",
"kp": 0.5,
"ki": 0.25,
"source_sha256": {
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route112/route.npz": "2b08a2fb636f7d14556d7df4035eafc1d1b97932237955528562a16db2b31d3e",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route112/model_paths.npz": "9837afe78aab4cad288cad98a595a5777fa8a66bb235986b1272a7f7c54e559a",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route112/metadata.json": "726d78a7e7aa45307dcfe27cb00775c20ecc9d54538eb7f26a0166fc216226ce",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_minimal_tuning/sweep/112/report.json": "d8cbcf8b676a67a90310390611da31783b4e025a0a965239ef4672caa47aae56",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_minimal_tuning/sweep/112/commands.npz": "8d91f449c7f8741ce465ec4326949a9d9527ed6b5de7a526ad7d30212f16c469",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/minimal_pi_validate.py": "35b5334f52eb6069b79fc3ed3be71ceaa81a3db8e1f76e8b1ab6e363cd4f18ae",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/openpilot/selfdrive/controls/lib/ford_model_action.py": "a899b8595e903d6fc4401392a8d123ef77725f6d6d7fa10392a7b3fe72e732b8"
}
},
{
"scope": "Verify production commands against the archived P=.50/I=.25 offline candidate.\n\nFixed recorded motion verifies integration parity, not physical tracking.\n",
"route": "113",
"cycles": 49614,
"can_round_trips": 49614,
"production_matches_archived_trial_exactly": true,
"opendbc_revision": "64aa61b9b3fd26e70a7caa915acab207ff3cd64a",
"hypothesis": "model-action-c1-pi-v7",
"kp": 0.5,
"ki": 0.25,
"source_sha256": {
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route113/route.npz": "774ca4a21b7113c2706d6130bc180c3216ea4833155300ab01e75b3486e36327",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route113/model_paths.npz": "93c41761eb85263f534f5371b905482cf7c948582eb1e9149966594be1d3768f",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route113/metadata.json": "1c0ca74dd48b90ab9d5444c5ca7f8aa9361700bbdf98bd5e853be50ad2895d7f",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_minimal_tuning/sweep/113/report.json": "e2dfeba5d3fe8a1147f82bce9ddf2ad3f14271bda35fac1639d383f2a0189228",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_minimal_tuning/sweep/113/commands.npz": "3890c802072dd2d38b4cded5f9f80310d1f86eb8a8f3598138235a0e2cc16460",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/minimal_pi_validate.py": "35b5334f52eb6069b79fc3ed3be71ceaa81a3db8e1f76e8b1ab6e363cd4f18ae",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/openpilot/selfdrive/controls/lib/ford_model_action.py": "a899b8595e903d6fc4401392a8d123ef77725f6d6d7fa10392a7b3fe72e732b8"
}
},
{
"scope": "Verify production commands against the archived P=.50/I=.25 offline candidate.\n\nFixed recorded motion verifies integration parity, not physical tracking.\n",
"route": "114",
"cycles": 61027,
"can_round_trips": 61027,
"production_matches_archived_trial_exactly": true,
"opendbc_revision": "64aa61b9b3fd26e70a7caa915acab207ff3cd64a",
"hypothesis": "model-action-c1-pi-v7",
"kp": 0.5,
"ki": 0.25,
"source_sha256": {
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route114/route.npz": "83524f07d61104b84b004ddc46bb751ca7f61da67798aa47db56d307765f5b3e",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route114/model_paths.npz": "14d14362d1a3e46398edd8e22b7cc4e36277a7596a73f546192d0e14c6642b07",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route114/metadata.json": "ec677b9275c1707e477ebd0ffa235d49b5ec63a1ee717b16040c3acdbcd3bdc0",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_minimal_tuning/sweep/114/report.json": "eb630bb5f35a43c8902e15a97b53d83db3dc3cfa843a14d09cb8a1e6dab89289",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_minimal_tuning/sweep/114/commands.npz": "899033d0c9eeffe7929aa1289bfbc0e7da5608833b03989ce10a87d975344d3d",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/minimal_pi_validate.py": "35b5334f52eb6069b79fc3ed3be71ceaa81a3db8e1f76e8b1ab6e363cd4f18ae",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/openpilot/selfdrive/controls/lib/ford_model_action.py": "a899b8595e903d6fc4401392a8d123ef77725f6d6d7fa10392a7b3fe72e732b8"
}
},
{
"scope": "Verify production commands against the archived P=.50/I=.25 offline candidate.\n\nFixed recorded motion verifies integration parity, not physical tracking.\n",
"route": "115",
"cycles": 40037,
"can_round_trips": 40037,
"production_matches_archived_trial_exactly": true,
"opendbc_revision": "64aa61b9b3fd26e70a7caa915acab207ff3cd64a",
"hypothesis": "model-action-c1-pi-v7",
"kp": 0.5,
"ki": 0.25,
"source_sha256": {
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route115/route.npz": "e0df32d9e80f1c6b7d56327b37cf07af60070ffc212b8d111e343306148bff23",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route115/model_paths.npz": "52f3ed9e188e4947618a57a3ed872fd1966a887fe1014999df741553b6c13bc0",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route115/metadata.json": "d3c73035bad8eb5059e07962fd274c19cb70c8952b6f1514835d312d08b87a16",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_minimal_tuning/sweep/115/report.json": "58cee499da3191808aedbd79a9497d9c1553f49007f24543c44bc2b58ff971fa",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_minimal_tuning/sweep/115/commands.npz": "09c2da33dabbc0072c61578224cd5ab27ed44690394315350c1c33708b0a67bc",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/minimal_pi_validate.py": "35b5334f52eb6069b79fc3ed3be71ceaa81a3db8e1f76e8b1ab6e363cd4f18ae",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/openpilot/selfdrive/controls/lib/ford_model_action.py": "a899b8595e903d6fc4401392a8d123ef77725f6d6d7fa10392a7b3fe72e732b8"
}
},
{
"scope": "Verify production commands against the archived P=.50/I=.25 offline candidate.\n\nFixed recorded motion verifies integration parity, not physical tracking.\n",
"route": "116",
"cycles": 68793,
"can_round_trips": 68793,
"production_matches_archived_trial_exactly": true,
"opendbc_revision": "64aa61b9b3fd26e70a7caa915acab207ff3cd64a",
"hypothesis": "model-action-c1-pi-v7",
"kp": 0.5,
"ki": 0.25,
"source_sha256": {
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route116/route.npz": "13df1d49edd0c92bd4a4236c766ee76baa6dad477824b9de4b79927c98410394",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route116/model_paths.npz": "85d1112b5e89340e8a955b7fe4040b3135cf1c91a54ff756826c6119cc9cc01d",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route116/metadata.json": "d52fe63d013fa546524ad9522a26943d9372730d397d92d75dfd9d75c63ce766",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_minimal_tuning/sweep/116/report.json": "01d2e608a53f9fd805baee853d18acc63b5506319e0f0ceeeb3e59a92bae0385",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_minimal_tuning/sweep/116/commands.npz": "2186a1a1fea2ea98390fb90c9df1288d321cd165f70e3aef0c268cbbd2fd8c95",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/minimal_pi_validate.py": "35b5334f52eb6069b79fc3ed3be71ceaa81a3db8e1f76e8b1ab6e363cd4f18ae",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/openpilot/selfdrive/controls/lib/ford_model_action.py": "a899b8595e903d6fc4401392a8d123ef77725f6d6d7fa10392a7b3fe72e732b8"
}
},
{
"scope": "Verify production commands against the archived P=.50/I=.25 offline candidate.\n\nFixed recorded motion verifies integration parity, not physical tracking.\n",
"route": "117",
"cycles": 27301,
"can_round_trips": 27301,
"production_matches_archived_trial_exactly": true,
"opendbc_revision": "64aa61b9b3fd26e70a7caa915acab207ff3cd64a",
"hypothesis": "model-action-c1-pi-v7",
"kp": 0.5,
"ki": 0.25,
"source_sha256": {
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route117/route.npz": "65e4ea6c76dce73018b636479b3777116f1e58bb9813e6c1a7b79018b219140e",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route117/model_paths.npz": "079f6003968287530e1ed6f2243c727e80c2ef3d16fb8a7d945d7eca744d54e7",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route117/metadata.json": "348396b3059c5f54d7938cd3acf2bb268a813b47ff47561da7a94cb75c3f3114",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_minimal_tuning/sweep/117/report.json": "80efcf48f018d3a887e8b9fdb36f67d9c5d9a57b09f117c8fdecd4d5556ff80b",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_minimal_tuning/sweep/117/commands.npz": "c2056b5d9faf765cb4713f73e9c4937bb24c9eeb1d4dac0d21e141322d52609e",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/minimal_pi_validate.py": "35b5334f52eb6069b79fc3ed3be71ceaa81a3db8e1f76e8b1ab6e363cd4f18ae",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/openpilot/selfdrive/controls/lib/ford_model_action.py": "a899b8595e903d6fc4401392a8d123ef77725f6d6d7fa10392a7b3fe72e732b8"
}
},
{
"scope": "Verify production commands against the archived P=.50/I=.25 offline candidate.\n\nFixed recorded motion verifies integration parity, not physical tracking.\n",
"route": "a0",
"cycles": 114582,
"can_round_trips": 114582,
"production_matches_archived_trial_exactly": true,
"opendbc_revision": "64aa61b9b3fd26e70a7caa915acab207ff3cd64a",
"hypothesis": "model-action-c1-pi-v7",
"kp": 0.5,
"ki": 0.25,
"source_sha256": {
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_routea0/route.npz": "01ce8b0361c3154d07d5449525a650baa2cc261675095cb1f04ec9e37a9174d3",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_routea0/model_paths.npz": "5d0c69d10a6d13882b47d950ae64c4d98f7ca2b59eab27e4e9557eef3bc4917b",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_routea0/metadata.json": "00496a11afbee475e9b5d66838a4d1663a3c0063cd562b4d1408cce6d71e586e",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_minimal_tuning/sweep/a0/report.json": "5f3ca84b700e019cc72201d9e4ff85c42fbd15e9a3850434caf1bac688b332d8",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_minimal_tuning/sweep/a0/commands.npz": "5c52d587ba45446117677686aabc6af81d5d2eacab5c04b02b53fe6fb95e36d0",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/minimal_pi_validate.py": "35b5334f52eb6069b79fc3ed3be71ceaa81a3db8e1f76e8b1ab6e363cd4f18ae",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/openpilot/selfdrive/controls/lib/ford_model_action.py": "a899b8595e903d6fc4401392a8d123ef77725f6d6d7fa10392a7b3fe72e732b8"
}
},
{
"scope": "Verify production commands against the archived P=.50/I=.25 offline candidate.\n\nFixed recorded motion verifies integration parity, not physical tracking.\n",
"route": "a2",
"cycles": 71111,
"can_round_trips": 71111,
"production_matches_archived_trial_exactly": true,
"opendbc_revision": "64aa61b9b3fd26e70a7caa915acab207ff3cd64a",
"hypothesis": "model-action-c1-pi-v7",
"kp": 0.5,
"ki": 0.25,
"source_sha256": {
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_routea2/route.npz": "af4128d278be6be0042f72a02255ddbec96a8a5aa435b9852c8202e4a5cb39e5",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_routea2/model_paths.npz": "5a5a7c4fc30c7b3bc7f2413daa8b11fe86b69dd3249e8df2d35b0a0636a49717",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_routea2/metadata.json": "cfecd240216e409a1e2591c6c2e6f0bad002ecd2a50c8f6dbf22a8f9174708da",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_minimal_tuning/sweep/a2/report.json": "de61021f0c13af16e98688b17aadae9d61c99d20be82329b7cc235836defa41a",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_minimal_tuning/sweep/a2/commands.npz": "fd263ab667b97905d840751577179ad75472421104cf20663e3ee51bf32dd2ee",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/minimal_pi_validate.py": "35b5334f52eb6069b79fc3ed3be71ceaa81a3db8e1f76e8b1ab6e363cd4f18ae",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/openpilot/selfdrive/controls/lib/ford_model_action.py": "a899b8595e903d6fc4401392a8d123ef77725f6d6d7fa10392a7b3fe72e732b8"
}
},
{
"scope": "Verify production commands against the archived P=.50/I=.25 offline candidate.\n\nFixed recorded motion verifies integration parity, not physical tracking.\n",
"route": "a5",
"cycles": 38961,
"can_round_trips": 38961,
"production_matches_archived_trial_exactly": true,
"opendbc_revision": "64aa61b9b3fd26e70a7caa915acab207ff3cd64a",
"hypothesis": "model-action-c1-pi-v7",
"kp": 0.5,
"ki": 0.25,
"source_sha256": {
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_routea5/route.npz": "815d1e248ff5c3e5e5cfc11dfbd0690d0d13ddafe1436890e239975a72dae9b8",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_routea5/model_paths.npz": "72e014b844322df6318f6af588c27175ce68d9a37bda62b70eafca5bdfe6dfe4",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_routea5/metadata.json": "c533b06d1b7c598330b36b117bdb3624221bc4193ded8354246d5c86f36b2628",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_minimal_tuning/sweep/a5/report.json": "dc6c05043c5caef6bf12b12eff69ef53d213fe8606f59865859d2c475e0aa3ee",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_minimal_tuning/sweep/a5/commands.npz": "45f551b32a354a65bff585b81a89091abc5edad406b6ca100a8dcabc50c5dbe3",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/minimal_pi_validate.py": "35b5334f52eb6069b79fc3ed3be71ceaa81a3db8e1f76e8b1ab6e363cd4f18ae",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/openpilot/selfdrive/controls/lib/ford_model_action.py": "a899b8595e903d6fc4401392a8d123ef77725f6d6d7fa10392a7b3fe72e732b8"
}
},
{
"scope": "Verify production commands against the archived P=.50/I=.25 offline candidate.\n\nFixed recorded motion verifies integration parity, not physical tracking.\n",
"route": "a9",
"cycles": 286319,
"can_round_trips": 286319,
"production_matches_archived_trial_exactly": true,
"opendbc_revision": "64aa61b9b3fd26e70a7caa915acab207ff3cd64a",
"hypothesis": "model-action-c1-pi-v7",
"kp": 0.5,
"ki": 0.25,
"source_sha256": {
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_routea9/route.npz": "3fa8cabfb729dd42d689de38618d1d21c8965c9d60f914546f4d7cc58db8c975",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_routea9/model_paths.npz": "7f3646ad24aa52de5982c65600813de5bbc5f7664d92a2516bbba35da6e7b240",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_routea9/metadata.json": "403d35504bf596144845ac2060ce067d571f49a2955d16223af28a79a7a66e93",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_minimal_tuning/sweep/a9/report.json": "17c41415b9fb9fcd8d72835347a205113c32d32d955c0de68850a9764968b3e9",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_minimal_tuning/sweep/a9/commands.npz": "27af5f6fb8f6e08f31d54e24c90430d30c72d31b1448bc1fbcb31f35804afe8a",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/minimal_pi_validate.py": "35b5334f52eb6069b79fc3ed3be71ceaa81a3db8e1f76e8b1ab6e363cd4f18ae",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/openpilot/selfdrive/controls/lib/ford_model_action.py": "a899b8595e903d6fc4401392a8d123ef77725f6d6d7fa10392a7b3fe72e732b8"
}
},
{
"scope": "Verify production commands against the archived P=.50/I=.25 offline candidate.\n\nFixed recorded motion verifies integration parity, not physical tracking.\n",
"route": "b8",
"cycles": 160431,
"can_round_trips": 160431,
"production_matches_archived_trial_exactly": true,
"opendbc_revision": "64aa61b9b3fd26e70a7caa915acab207ff3cd64a",
"hypothesis": "model-action-c1-pi-v7",
"kp": 0.5,
"ki": 0.25,
"source_sha256": {
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_routeb8/route.npz": "6f5dd369b70eaed4b95b28c8b25c9f2e9b830fa07a334881a185505481667c8b",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_routeb8/model_paths.npz": "939af6cf7e74251d8842581cc078d26d9fbfd22a0d7817cb0e368697d419b615",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_routeb8/metadata.json": "73b439132d1de37ec187b544c04d2b05c80965065515a4b7dec29ba57ae37e7c",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_minimal_tuning/sweep/b8/report.json": "5c6b52222abbb95147dc756a34e62ada97c42aa550b952fd6ae7b26d8d9639fa",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_minimal_tuning/sweep/b8/commands.npz": "9c158eff98009bcb039a4e7c29b97b0d3a9c67d61160a4e4766a176174de52d7",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/minimal_pi_validate.py": "35b5334f52eb6069b79fc3ed3be71ceaa81a3db8e1f76e8b1ab6e363cd4f18ae",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/openpilot/selfdrive/controls/lib/ford_model_action.py": "a899b8595e903d6fc4401392a8d123ef77725f6d6d7fa10392a7b3fe72e732b8"
}
},
{
"scope": "Verify production commands against the archived P=.50/I=.25 offline candidate.\n\nFixed recorded motion verifies integration parity, not physical tracking.\n",
"route": "b9",
"cycles": 90774,
"can_round_trips": 90774,
"production_matches_archived_trial_exactly": true,
"opendbc_revision": "64aa61b9b3fd26e70a7caa915acab207ff3cd64a",
"hypothesis": "model-action-c1-pi-v7",
"kp": 0.5,
"ki": 0.25,
"source_sha256": {
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_routeb9/route.npz": "b07c789d8155335f5d120d0262fced6e4d5803fe767b0ff49b6413dce4140b5c",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_routeb9/model_paths.npz": "6b1f87897c050273fdc05af051307a049b6fc3a93072e7cda1721195ce7c3861",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_routeb9/metadata.json": "9ce452220cab61b81883f32fc2fcaf5db6c78a674cb255a49cc77d5029580fee",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_minimal_tuning/sweep/b9/report.json": "9eeda0c24ede7a55ef8a924e0ebe0bcb0dea7a9612457af0c5aff898a828c195",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_minimal_tuning/sweep/b9/commands.npz": "2dcc0865591c988d8a08671311a9631f3c4aa3ded4e90ee6f76d28b1654809ad",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/minimal_pi_validate.py": "35b5334f52eb6069b79fc3ed3be71ceaa81a3db8e1f76e8b1ab6e363cd4f18ae",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/openpilot/selfdrive/controls/lib/ford_model_action.py": "a899b8595e903d6fc4401392a8d123ef77725f6d6d7fa10392a7b3fe72e732b8"
}
},
{
"scope": "Verify production commands against the archived P=.50/I=.25 offline candidate.\n\nFixed recorded motion verifies integration parity, not physical tracking.\n",
"route": "ca",
"cycles": 327448,
"can_round_trips": 327448,
"production_matches_archived_trial_exactly": true,
"opendbc_revision": "64aa61b9b3fd26e70a7caa915acab207ff3cd64a",
"hypothesis": "model-action-c1-pi-v7",
"kp": 0.5,
"ki": 0.25,
"source_sha256": {
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_routeca/route.npz": "ae9d46770eaf0dbbac6af86aebc926320eed0cf114eb43d5f78b0676e8e0dbf9",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_routeca/model_paths.npz": "bf17deb442383aaa79432566cd382df24a1bbbbd0521d0cafab956618f5bdd96",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_routeca/metadata.json": "a759d5cdf878df8b05d91db637b1935b6b4bdd87af96f0f256b67e7d809b3525",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_minimal_tuning/sweep/ca/report.json": "025f2f49f68ec2164a10474a4c6ec0eb1b6022990e135fc38134a052a8710dff",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_minimal_tuning/sweep/ca/commands.npz": "cbe9f63955a409495f57466dc5811d3eed65af2cf096ee060d924437488c3f76",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/minimal_pi_validate.py": "35b5334f52eb6069b79fc3ed3be71ceaa81a3db8e1f76e8b1ab6e363cd4f18ae",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/openpilot/selfdrive/controls/lib/ford_model_action.py": "a899b8595e903d6fc4401392a8d123ef77725f6d6d7fa10392a7b3fe72e732b8"
}
},
{
"scope": "Verify production commands against the archived P=.50/I=.25 offline candidate.\n\nFixed recorded motion verifies integration parity, not physical tracking.\n",
"route": "raptor02",
"cycles": 132881,
"can_round_trips": 132881,
"production_matches_archived_trial_exactly": true,
"opendbc_revision": "64aa61b9b3fd26e70a7caa915acab207ff3cd64a",
"hypothesis": "model-action-c1-pi-v7",
"kp": 0.5,
"ki": 0.25,
"source_sha256": {
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_raptor_route02/route.npz": "8a4cfe53994988d053b47d9caadf21ebd064fd20e598fa05ddc2da245bd0e980",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_raptor_route02/model_paths.npz": "d5dc6f72d91472b8f2fb1add1680cdaea1e122774ddd6d8519fd7673daf71bb5",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_raptor_route02/metadata.json": "9602e08efc2bd784133837c4156c075bb89bad3bedd6b309f15e884ed20e338a",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_minimal_tuning/sweep/raptor02/report.json": "2c2dda2a5e4e8a764a10852cca3e5caf319b62e3d1d841ef9e1b8c11be77ebf6",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_minimal_tuning/sweep/raptor02/commands.npz": "e0440d0a4035b975a9c4a47d85335b5b0f0903fcd00dd7e7c858856deffd1249",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/minimal_pi_validate.py": "35b5334f52eb6069b79fc3ed3be71ceaa81a3db8e1f76e8b1ab6e363cd4f18ae",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/openpilot/selfdrive/controls/lib/ford_model_action.py": "a899b8595e903d6fc4401392a8d123ef77725f6d6d7fa10392a7b3fe72e732b8"
}
}
],
"production_stress": {
"cycles": 20000,
"kp": 0.5,
"ki": 0.25,
"controller_updates": 60000,
"can_round_trips": 60000,
"seed": 20260913,
"independent_c0_comparisons": 20000,
"checks": "Independent scalar PI/unwind-first/anti-windup arithmetic, mirror symmetry, exact C0 independence, limits, slew, resets, CAN.",
"source_sha256": {
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/minimal_pi_production_stress.py": "fa9058826512498be1938797efba25666df832080fb7ad6ff18788ee4bda333d",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/openpilot/selfdrive/controls/lib/ford_model_action.py": "a899b8595e903d6fc4401392a8d123ef77725f6d6d7fa10392a7b3fe72e732b8"
}
},
"limits": [
"No hardware build, device boot or new physical drive performed.",
"Matched offline commands do not establish improved tracking or closed-loop stability.",
"Higher P can amplify measurement fluctuations; lower I can take longer to correct persistent error.",
"Toggle defaults off and off selects upstream Ford control; on applies to any Ford CAN FD."
]
}
-131
View File
@@ -1,131 +0,0 @@
# Ford base-heading overflow experiment
This records the overflow implementation and validation at `b81c00f5b`.
The later [toggle-off restoration](ford_upstream_fallback.md) updates selection
and the opendbc sender while preserving the enabled experiment's command law.
The Lightning ca route recorded controller `959ae3d6e`. Its large turns included
flat C1 requests at ±0.5 rad while C0 still had available range. Those were
nonzero, active commands, but increasing base heading above the C1 limit was
discarded. Other apparent pauses followed reductions in the selected model
request; this change continues to follow those reductions.
The new experiment allocates clipped-away **base heading** to C0 using the
existing 7 m reference. It does not allocate the accumulated feedback correction.
This is a hypothesis about command allocation, not a measured improvement in
PSCM response or a claim that C0 and C1 are physically interchangeable.
## Command rule
Using the selected, upstream-limited desired curvature:
```text
raw_base_c1 = max(7 m, speed × 1 s) × desired_curvature
base_c1 = clip(raw_base_c1, -0.5 rad, +0.5 rad)
extra_c0 = 7 m × (raw_base_c1 - base_c1)
c0_target = clip(model_y_at_7m + extra_c0, -5.11 m, +5.11 m)
```
The combined C0 target still passes through the existing 4 m/s slew limit.
C1 retains its existing feedback, ±0.5 rad amplitude and 0.5 rad/s slew limits.
C2 and C3 remain zero. Short model paths retain their existing endpoint hold.
Before amplitude/slew limits, the allocation preserves the linear reference
`C0 + 7*C1` for the base request. This is a single-reference identity; it does
not preserve the entire path or predict steering torque. The 7 m reference is
an existing engineering choice. No fitted plant, new tunable strength multiplier,
timer or stored overflow is added. The existing 1:1 feedback strength remains.
Extra C0 falls with raw base heading and its target becomes zero at the C1 cap.
The output can take longer to return because of its existing slew state. There
is no guarantee that increasing C0 makes every PSCM turn better or release sooner.
## Preserved integration
The conditional correction release still requires **original model C0** and
applied C0 to confirm base C1's direction. Added overflow cannot itself substitute
for model confirmation. Changed applied C0 can nevertheless affect release
timing in some histories. Driver/PSCM arbitration, service freshness, resets,
upstream curvature limiting, Float32 publication and the 100 Hz sender remain.
No opendbc dependency or Panda safety change is made.
The existing default-off Sunnylink toggle selects this version on any Ford
CAN FD vehicle. Diagnostic identity is `model-action-c1-feedback-v3`.
`offset_overflow` records extra target meters before C0 amplitude and slew;
`offset_request` continues to record the actual continuous C0 state.
See the [drive-test instructions](ford_model_action_drive_test.md).
## Offline evidence
The focused overflow regressions initially produced 28 failures and 18 passes
against the prior controller. They now pass. They cover both signs, several
speeds, the heading threshold, combined C0 clipping, short paths, release,
feedback-only saturation, driver/PSCM feedback gates and independent model
confirmation. Twelve integration cases send 4,800 frames through actual
controlsd selection/limiting, Float32 publication and Ford CAN packing on all
six listed Ford CAN FD platforms, checking counters and checksums.
The combined suite passes **670 tests and 9,146 subtests**, with 178 inherited
or unsupported safety-test skips. Both 200,000-cycle randomized runs pass,
including mirrored inputs, independent scalar target/slew checks, feedback
invariants, comparison with the exact prior controller from cloned states,
and 18,138 exhaustive field/Float32 boundary cases. These checks and the ca
replay total **745,586 Float32/CAN round trips**, in addition to integration tests.
Frozen ca replay covers 327,448 control cycles across all 55 extracted segments.
Activation is identical; C1, C2 and C3 are identical on every cycle. C0 differs
for 855 cycles (8.607 s), concentrated in the large turns and their slew tails.
The extra target is present for 7.359 s. Before the first overflow, every command
matches the prior controller. All disabled cycles have zero commands.
At the same recorded peak-request timestamps, absolute packed C0 changes as follows:
| Segment | Previous C0 | Candidate C0 | C1 magnitude, both |
| --- | ---: | ---: | ---: |
| 10 | 3.90 m | 5.11 m | 0.50 rad |
| 31 | 2.85 m | 3.08 m | 0.50 rad |
| 35 | 3.67 m | 5.11 m | 0.50 rad |
| 52 | 3.11 m | 3.42 m | 0.50 rad |
The candidate reaches the existing C0 cap for 2.054 s. These are reconstructed
commands on original inputs, not newly transmitted commands or predicted wheel
angles. Segment 52's output is still slewing at the selected timestamp.
Every overflow episode returns to the previous C0 output without a reset or
another overflow interrupting the comparison. After overflow first becomes
zero, the longest output tails are **0.475 s in segment 10** and **0.712 s in
segment 35**. This is the added slew tail relative to the prior command, not
the truck's physical release delay. It is a material behavior to inspect during
controlled evaluation: more pull through capped turns may also add hanging
on exit. Ordinary requests below the cap retain the original target mapping.
The recorded model, vehicle motion, driver input and PSCM flags remain fixed.
Replay cannot establish resulting tracking, centering, torque or stability.
The route has no maneuver-plan messages; the replay uses the consumed model
reference and recorded selected curvature. Computation time is approximated
by control publication time, and full SubMaster health checks are unavailable.
No device build, boot or installation is performed offline.
## Reproduction
Numeric results and source hashes are in `ford_c1_overflow_validation.json`.
Use native project dependencies and pinned opendbc
`c21a9013700734dd20b09e05aa68329ad8cc20f9`. The ca replay requires the existing
full-rlog extract (`route.npz`, `model_paths.npz`, `metadata.json`).
The following commands apply to this version; earlier validation documents
record their named historical controllers.
```sh
export PYTHONDONTWRITEBYTECODE=1
export PYTHONPATH=.:opendbc_repo
python -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 openpilot/sunnypilot/sunnylink/tests openpilot/common/tests/test_params.py opendbc_repo/opendbc/car/ford/tests/test_ford.py opendbc_repo/opendbc/safety/tests/test_ford.py
python -m tools.ford_pscm_lab.feedback_replay stress --cycles 200000 --output .cache/ford_c1_overflow/stress.json
python -m tools.ford_pscm_lab.stress_model_action --cycles 200000 --seed 20260910 --opendbc-revision c21a9013700734dd20b09e05aa68329ad8cc20f9 --output .cache/ford_c1_overflow/zero_error_stress.json
python -m tools.ford_pscm_lab.feedback_replay route .cache/ford_routeca --baseline 959ae3d6e76c479f48e081c060b0f3569a6f15f4 --output .cache/ford_c1_overflow/routeca
python openpilot/sunnypilot/sunnylink/tools/compile_settings_ui.py --check
```
For slew-tail analysis, in the replay's `commands.npz` find each nonzero run of
`offset_overflow`. From its first zero sample, measure until packed candidate
and baseline C0 agree within 1e-8 m, stopping separately at another overflow or
inactive cycle. Sum sample durations capped at 30 ms for weighted time totals.
-307
View File
@@ -1,307 +0,0 @@
{
"created_at_utc": "2026-09-10T22:04:22.589047+00:00",
"scope": "Base heading overflow allocated to C0; frozen-input command verification only, no vehicle response prediction.",
"baseline_commit": "959ae3d6e76c479f48e081c060b0f3569a6f15f4",
"baseline_source_sha256": "47fff1fd1bd7e65ca6d6b437fa9d2e8a864d421622d9efdfe0fe04b927c6d972",
"deployment_target": {
"repository": "sunnypilot/sunnypilot",
"branch": "hiimisaac-dev"
},
"hypothesis": "model-action-c1-feedback-v3",
"calibration_approved": false,
"toggle": {
"key": "FordModelActionController",
"default_enabled": false,
"eligibility": "Any Ford CAN FD",
"activation": "Existing controlsd startup selection"
},
"command_rule": "extra C0 = 7 m * (raw base C1 - clip(raw base C1, -0.5, 0.5)); add to original model C0, then existing C0 amplitude/slew limits.",
"engineering_choices": "Single-reference linear allocation at existing 7 m. No new tuning parameter or stored overflow. Does not establish physical C0/C1 interchangeability. Existing 1:1 integral feedback strength remains.",
"output_limits": {
"c0_m": [
-5.11,
5.11
],
"c1_rad": [
-0.5,
0.5
],
"c0_slew_m_s": 4.0,
"c1_slew_rad_s": 0.5,
"c2": 0.0,
"c3": 0.0
},
"preserved": [
"Upstream reference selection/limiting",
"Driver and PSCM arbitration",
"Freshness/reset gates",
"C1 feedback law",
"Original model C0 required for carryover confirmation",
"100 Hz CAN sender",
"Float32 publication"
],
"controller_command_state_values": 3,
"controller_diagnostic_counters": 1,
"controller_total_lines": 200,
"opendbc_submodule_changed": false,
"panda_safety_changed": false,
"opendbc_head": "c21a9013700734dd20b09e05aa68329ad8cc20f9",
"tests": {
"combined_suite": "670 passed, 178 skipped, 9146 subtests passed in 5.77s",
"safety_skips": "Inherited or unsupported variants; unchanged from prior feedback validations.",
"new_core_regressions": "Before implementation: 28 failed, 18 passed; after: all 46 pass.",
"overflow_controlsd_to_can_cases": 12,
"overflow_controlsd_to_can_frames": 4800,
"integration_scope": "All six listed CAN FD platforms; both signs; selected upstream-limited request, release, limits, Float32 publication, decoded commands, zero C2/C3, active mode, counters, checksums.",
"ruff_changed_python": "pass",
"ty_controller": "pass",
"git_diff_check": "pass",
"settings_compiler_check": "pass"
},
"stress": {
"cycles": 200000,
"mirrored_updates": 200000,
"can_round_trips": 200000,
"carryover_release_count": 864,
"baseline_revision": "959ae3d6e76c479f48e081c060b0f3569a6f15f4",
"baseline_source_sha256": "47fff1fd1bd7e65ca6d6b437fa9d2e8a864d421622d9efdfe0fe04b927c6d972",
"exact_unchanged_state_and_commands_without_overflow": 56061,
"checks": "Mirror symmetry, reset/override, amplitude, slew, correction bounds, carryover direction/confirmation, integration, PSCM limits, CAN.",
"scope": "Numerical software invariants only; no model of vehicle motion.",
"calibration_approved": false
},
"zero_error_stress": {
"seed": 20260910,
"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.4000000000000019,
0.05000000000000002
],
"calibration_approved": false,
"scope": "Zero-error numerical construction: measured equals requested curvature. No PSCM response claims.",
"opendbc_import_head": "c21a9013700734dd20b09e05aa68329ad8cc20f9"
},
"route_ca": {
"baseline_revision": "959ae3d6e76c479f48e081c060b0f3569a6f15f4",
"baseline_source_sha256": "47fff1fd1bd7e65ca6d6b437fa9d2e8a864d421622d9efdfe0fe04b927c6d972",
"calibration_approved": false,
"cycles": 327448,
"active_cycles": 118756,
"validity_matches_baseline_exactly": true,
"status_counts": {
"inactive": 208692,
"active": 118756
},
"c0_matches_baseline_exactly": false,
"c0_changed_cycles": 855,
"max_abs_c0_change_m": 2.2,
"offset_overflow_seconds": 7.359375754000212,
"max_abs_offset_overflow_target_m": 2.867466852068901,
"feedback_enabled_seconds": 1128.448330694985,
"pscm_limit_2_seconds": 1.5229074359986043,
"c1_changed_cycles": 0,
"max_abs_c1_change_rad": 0.0,
"max_abs_correction_rad": 0.13238253764709199,
"can_round_trips": 327448,
"timing_limit": "Controls publication time proxies the computation clock; full SubMaster checks are unavailable.",
"reference_limit": "Uses exact consumed model publication as reference; selected maneuver-plan messages are not reconstructed.",
"carryover_release_count": 32
},
"route_ca_release": {
"scope": "Describe candidate command tails on frozen ca measurements, not wheel response.",
"changed_c0_seconds": 8.606610047996583,
"overflow_target_seconds": 7.359375754000212,
"changed_c0_without_current_overflow_seconds": 1.4386431469993113,
"candidate_c0_at_cap_seconds": 2.054423716999736,
"baseline_c0_at_cap_seconds": 0.0,
"all_c1_c2_c3_match_baseline_exactly": true,
"all_pre_overflow_commands_match_baseline_exactly": true,
"all_inactive_commands_zero": true,
"windows": [
{
"start_s": 645.4515506280004,
"last_overflow_s": 647.9591778859995,
"duration_s": 2.5175176129996544,
"extra_target_peak_m": 2.867466852068901,
"c0_change_peak_m": 2.2,
"post_overflow_tail_s": 0.4753179760009516,
"tail_end_s": 648.444386217001,
"tail_ended_by": "matches baseline"
},
{
"start_s": 1896.7594062080007,
"last_overflow_s": 1897.0382758169999,
"duration_s": 0.29005605599923,
"extra_target_peak_m": 0.16229432076215744,
"c0_change_peak_m": 0.16999999999999993,
"post_overflow_tail_s": 0.0,
"tail_end_s": 1897.0494622639999,
"tail_ended_by": "matches baseline"
},
{
"start_s": 1897.3739526980007,
"last_overflow_s": 1897.4413651220002,
"duration_s": 0.0802515539999149,
"extra_target_peak_m": 0.049945808947086334,
"c0_change_peak_m": 0.04999999999999982,
"post_overflow_tail_s": 0.0,
"tail_end_s": 1897.4542042520006,
"tail_ended_by": "matches baseline"
},
{
"start_s": 1897.492552009,
"last_overflow_s": 1897.681751143,
"duration_s": 0.19981637000091723,
"extra_target_peak_m": 0.10685679316520691,
"c0_change_peak_m": 0.11000000000000032,
"post_overflow_tail_s": 0.01193945599879953,
"tail_end_s": 1897.7043078349998,
"tail_ended_by": "matches baseline"
},
{
"start_s": 1897.743499225,
"last_overflow_s": 1897.7842587169998,
"duration_s": 0.05174380599964934,
"extra_target_peak_m": 0.04502199590206146,
"c0_change_peak_m": 0.040000000000000036,
"post_overflow_tail_s": 0.011911696001334349,
"tail_end_s": 1897.807154727001,
"tail_ended_by": "matches baseline"
},
{
"start_s": 1897.9437203819998,
"last_overflow_s": 1899.2943476260007,
"duration_s": 1.361525778000214,
"extra_target_peak_m": 0.22848158329725266,
"c0_change_peak_m": 0.22999999999999954,
"post_overflow_tail_s": 0.021877274999496876,
"tail_end_s": 1899.3271234349995,
"tail_ended_by": "matches baseline"
},
{
"start_s": 2146.154050938001,
"last_overflow_s": 2146.185627021001,
"duration_s": 0.04039318899958744,
"extra_target_peak_m": 0.045987628400325775,
"c0_change_peak_m": 0.050000000000000266,
"post_overflow_tail_s": 0.034061254000334884,
"tail_end_s": 2146.228505381001,
"tail_ended_by": "matches baseline"
},
{
"start_s": 2146.2950925490004,
"last_overflow_s": 2146.5387542179997,
"duration_s": 0.25293986199903884,
"extra_target_peak_m": 0.21167446672916412,
"c0_change_peak_m": 0.17999999999999972,
"post_overflow_tail_s": 0.040063559001282556,
"tail_end_s": 2146.5880959700007,
"tail_ended_by": "matches baseline"
},
{
"start_s": 2146.6577708509994,
"last_overflow_s": 2148.3543101739997,
"duration_s": 1.7074812410010054,
"extra_target_peak_m": 2.0555079206824303,
"c0_change_peak_m": 1.58,
"post_overflow_tail_s": 0.7118495689992415,
"tail_end_s": 2149.0771016609997,
"tail_ended_by": "matches baseline"
},
{
"start_s": 3121.067818462001,
"last_overflow_s": 3121.8665919270006,
"duration_s": 0.8071899679998751,
"extra_target_peak_m": 0.5380096957087517,
"c0_change_peak_m": 0.5099999999999998,
"post_overflow_tail_s": 0.06879738699899463,
"tail_end_s": 3121.943805817,
"tail_ended_by": "matches baseline"
},
{
"start_s": 3122.0664172689994,
"last_overflow_s": 3122.105900957,
"duration_s": 0.05046031700112508,
"extra_target_peak_m": 0.008371405303478241,
"c0_change_peak_m": 0.010000000000000231,
"post_overflow_tail_s": 0.02107719199921121,
"tail_end_s": 3122.1379547779998,
"tail_ended_by": "matches baseline"
}
],
"example_points": [
{
"segment": 10,
"time_s": 646.7503591629993,
"baseline_c0_m": 3.9000000000000004,
"candidate_c0_m": 5.11,
"extra_c0_target_m": 2.867466852068901,
"baseline_c1_rad": 0.5,
"candidate_c1_rad": 0.5
},
{
"segment": 31,
"time_s": 1898.0837712450011,
"baseline_c0_m": -2.8499999999999996,
"candidate_c0_m": -3.079999999999999,
"extra_c0_target_m": -0.22848158329725266,
"baseline_c1_rad": -0.5,
"candidate_c1_rad": -0.5
},
{
"segment": 35,
"time_s": 2147.4098699660008,
"baseline_c0_m": 3.67,
"candidate_c0_m": 5.11,
"extra_c0_target_m": 2.0555079206824303,
"baseline_c1_rad": 0.5,
"candidate_c1_rad": 0.5
},
{
"segment": 52,
"time_s": 3121.239466367,
"baseline_c0_m": 3.11,
"candidate_c0_m": 3.42,
"extra_c0_target_m": 0.5380096957087517,
"baseline_c1_rad": 0.5,
"candidate_c1_rad": 0.5
}
]
},
"route_ca_input_sha256": {
"route.npz": "ae9d46770eaf0dbbac6af86aebc926320eed0cf114eb43d5f78b0676e8e0dbf9",
"model_paths.npz": "bf17deb442383aaa79432566cd382df24a1bbbbd0521d0cafab956618f5bdd96",
"metadata.json": "a759d5cdf878df8b05d91db637b1935b6b4bdd87af96f0f256b67e7d809b3525"
},
"lab_can_round_trips_excluding_integration": 745586,
"validation_environment": {
"python": "/Users/ibpersonal/dev/sunnypilot/.venv/bin/python",
"PYTHONPATH": ".:opendbc_repo:.cache/ford_v6/test_deps",
"PYTHONDONTWRITEBYTECODE": "1",
"LOG_ROOT": "/private/tmp/ford-overflow-logs",
"PARAMS_ROOT": "/private/tmp/ford-overflow-params"
},
"source_sha256": {
"openpilot/selfdrive/controls/lib/ford_model_action.py": "90269d748d55558bf2495d3b4afcfd7429f373108df1f9259e154d1b92184262",
"openpilot/selfdrive/controls/tests/test_ford_model_action_overflow.py": "9086434d2b76ab51f69ef08c4f0033c4eaa1950083cc9cce4b34279eb17c5b1b",
"openpilot/selfdrive/controls/tests/test_ford_model_action_adapter.py": "dc552f3088b7e437a928f349989b74c4e7772d2a88b14a93e933ea8b8344d23b",
"openpilot/selfdrive/controls/tests/test_ford_model_action.py": "4e82101463c5d83f6b1f72ba4918c2b37731bc2db6ec7e29e6b402ea67276db2",
"openpilot/selfdrive/controls/tests/test_ford_controlsd_logging.py": "6b73e70b120527275a9e4e3dff07dd7d19648da517402187d57a90ba09b017eb",
"tools/ford_pscm_lab/feedback_replay.py": "7c65515d37aac900eaaef8451a7d643b72c566a2366651f843de2ffca5f24b8d",
"tools/ford_pscm_lab/stress_model_action.py": "cec2619285dd41274562ac035ee8ea0a389269a0c4ef1b62efa6252ad1a714aa",
"openpilot/sunnypilot/sunnylink/settings_ui_src/pages/vehicle.yaml": "3ae6b16a8c267ab181e00f4b59be8b65134480bec26c8c039bcbae88cb745feb",
"openpilot/sunnypilot/sunnylink/settings_ui.json": "ba803819751b304e82936a95902afae63ff88e16e64988622715a7b713f3b982"
},
"limitations": [
"Recorded vehicle motion does not react to changed commands.",
"Computation time proxies and service-check reconstruction limits apply.",
"C0 slew can leave extra command after overflow stops; maximum observed tail 0.712 s.",
"No device build, boot, installation, road tracking or physical stability validation."
]
}
-98
View File
@@ -1,98 +0,0 @@
# Ford C1 proportional trial: P=0.75
Route 149 contains large steering shortfalls before driver intervention while
the selected curvature matches the model and neither C1's bound nor the PSCM
reached-limit flag explains the shortfall. This trial raises the immediate C1
error correction from P=0.50 to P=0.75. I remains 0.25. Runtime changes are the
gain constant and the diagnostic version, `model-action-curvature-c0-distance-pi-v13`.
The intended effect is more correction while behind and more release correction
when measured steering exceeds the request. Increasing P does not establish a
faster physical response: it can also amplify measurement fluctuations and
produce oscillation. This is a trial coefficient, not a learned calibration.
The selected model action, +0.40 s low-speed preview, C0 distance setting and
formula, integral arithmetic, PSCM arbitration, field bounds, and 100 Hz sender
remain unchanged. C2/C3 stay zero. The existing default-off Sunnylink toggle
still selects the experiment on Ford CAN FD; toggle-off selects upstream Ford.
## Paired production replay
Compared explicit P=0.50 and P=0.75 production adapters with I=0.25 and fixed-7 m
C0 across 21 route extracts: 112117, 119, 11a, 120, 124, 125, 146, 149, a0, a2,
a5, a9, b8, b9, ca, and Raptor 02. The passes cover 2,275,248 source cycles and
4,550,496 real Float32-to-CAN encode/decode round trips.
Both passes use the same recorded selected curvature, measured motion, model
geometry, input timestamps, and driver/PSCM flags. Older routes retain their
original model requests; their neural inference is not rerun with the new delay.
This compares commands, not predicted wheel motion or tracking accuracy.
Checks passed on every cycle: identical C0, eligibility, feedforward, overflow,
and feedback/driver/PSCM gates; finite and bounded output; inactive zero output;
zero C2/C3; exact decoded fields, mode and counter; and the expected 1.5 ratio
between proportional terms. Integration tests separately check the selected
defaults, downstream checksums, reference selection, reversals, driver override,
reached-limit behavior, duplicate measurements, and toggle-off upstream behavior.
On route 149, the P=0.50 replay agrees with the recorded path commands over the
clean scoring cohort to Float32 precision: maximum C0 difference 5.8e-8 m and C1
difference 1.5e-8 rad. Full SubMaster health and exact control execution clocks
are not in the extract; publication timestamps approximate them. Historical
versions used different command laws, so their recorded commands are not
expected to match this baseline.
Clean scoring excludes driver steering, unavailable feedback, inactive/invalid
control, the following second, and speed below 3 mph. It contains 11,128.80 s.
Durations use original timestamps, clipping gaps to 30 ms. Percentiles are
sample-based. Request-angle categories do not identify road geometry.
| Recorded request magnitude | Scored seconds | Mean absolute C1 change | P95 C1 change |
| --- | ---: | ---: | ---: |
| Under 10 degrees | 9,051.37 | 0.00088 rad | 0.00250 rad |
| 1045 degrees | 1,631.68 | 0.00250 rad | 0.00900 rad |
| At least 45 degrees | 445.75 | 0.00829 rad | 0.03100 rad |
C1 bound exposure increases from 21.30 to 22.67 s over the clean cohort. Mean
absolute stored I changes from 0.005987 to 0.005984 rad; a larger P term changes
the remaining accumulation headroom even though I's gain is unchanged.
The largest small-request C1 difference is 0.093 rad on route 117, where the
recorded request is +4.6 degrees and the wheel is still at -210 degrees. This is
a large release error, not ordinary centering. Restricting both requested and
actual wheel angle to within 10 degrees leaves 8,788.38 s: mean absolute C1
change 0.00073 rad, P95 0.00200 rad, maximum 0.01250 rad. These measurements do not
establish preserved centering or closed-loop stability.
In route 149, the candidate increases the C1 request at the reviewed entry
misses and reduces the remaining turn command during the clean segment-14
release. Its clean C1 bound exposure rises from 0.23 to 0.61 s. The paired I
traces remain nearly identical. The local report includes five entry, reversal,
and exit comparisons with the recorded wheel trace clearly distinguished from
replayed command traces.
## Validation and reproduction
455 tests and 25 subtests pass, including Ford controller/adapter/selection,
C0 distance settings, diagnostic logging, delay helpers, and Ford CAN tests.
The actual controlsd-to-CAN integration test failed at the old proportional
output before changing the default, then passed at the new setting. Ruff and
`git diff --check` pass. A device build, installation, and physical evaluation
are not part of these offline checks.
The compact evidence record is [ford_c1_p75_validation.json](ford_c1_p75_validation.json).
Full command arrays and per-route reports are in `.cache/ford_p75_trial` locally.
Reproduce one route using the built cereal/opendbc environment:
```sh
PYTHONPATH=.:opendbc_repo PYTHONDONTWRITEBYTECODE=1 python \
tools/ford_pscm_lab/proportional_replay.py \
--routes 149=.cache/ford_route149/full \
--output .cache/ford_p75_recheck --workers 1
```
Additional `label=extract-directory` pairs replay independently. Each directory
must contain `route.npz`, `model_paths.npz`, and `metadata.json`; injection routes
are rejected. The input hashes are recorded in each result. The new test is
worth evaluating as a bounded change, but improved entry and preserved smooth
release still require measured vehicle response.
-707
View File
@@ -1,707 +0,0 @@
{
"routes": 21,
"cycles": 2275248,
"can_round_trips": 4550496,
"scope": "Fixed-motion production controller and CAN replay. No predicted wheel motion or stability claim.",
"cohorts": {
"all": {
"seconds": 11128.798286258982,
"mean_abs_c1_change": 0.0014142689534451223,
"p95_abs_c1_change": 0.004500000000000004,
"max_abs_c1_change": 0.09299999999999997,
"c1_bound_seconds": [
21.296853938993763,
22.66821589999995
],
"mean_abs_integral": [
0.00598675347437431,
0.005984439210041702
]
},
"small": {
"seconds": 9051.37332302302,
"mean_abs_c1_change": 0.0008801267467942443,
"p95_abs_c1_change": 0.0025000000000000022,
"max_abs_c1_change": 0.09299999999999997,
"c1_bound_seconds": [
0.0,
0.0
],
"mean_abs_integral": [
0.005708567029833155,
0.005708567029833155
]
},
"bend": {
"seconds": 1631.677856559975,
"mean_abs_c1_change": 0.0024991973101179256,
"p95_abs_c1_change": 0.008999999999999897,
"max_abs_c1_change": 0.08899999999999997,
"c1_bound_seconds": [
0.0,
0.0
],
"mean_abs_integral": [
0.006396022651557497,
0.006396128425459091
]
},
"turn": {
"seconds": 445.74710667598833,
"mean_abs_c1_change": 0.008289169314090508,
"p95_abs_c1_change": 0.031000000000000028,
"max_abs_c1_change": 0.08000000000000002,
"c1_bound_seconds": [
21.296853938993763,
22.66821589999995
],
"mean_abs_integral": [
0.01013747903479613,
0.010079312488152544
]
},
"releasing": {
"seconds": 106.96494071107061,
"mean_abs_c1_change": 0.005058356779798081,
"p95_abs_c1_change": 0.025500000000000023,
"max_abs_c1_change": 0.08899999999999997,
"c1_bound_seconds": [
0.009879735000026812,
0.0
],
"mean_abs_integral": [
0.006354435594257913,
0.006350676023352573
]
}
},
"baseline_commit": "b720e9f1bb805c69e5560f13dd86034d4bf50260",
"gains": [
0.5,
0.75
],
"integral_gain": 0.25,
"c0_time_based": false,
"hypothesis": "model-action-curvature-c0-distance-pi-v13",
"tests": {
"passed": 455,
"subtests_passed": 25,
"ruff": "passed",
"diff_check": "passed"
},
"replay_command_invariants_passed": true,
"physical_tracking_or_stability_validated": false,
"route_results": [
{
"route": "112",
"cycles": 108971,
"can_round_trips": 217942,
"baseline_difference_from_recorded_path": {
"c0": {
"mean": 0.0679107408662434,
"p95": 0.26000000476837126,
"max": 3.119999904632568
},
"c1": {
"mean": 0.005686226432883118,
"p95": 0.014999999850988377,
"max": 0.18949999547004703
}
},
"clean_seconds": 732.0411244650002,
"c1_bound_seconds": [
0.0,
0.021201738000058867
],
"source_sha256": {
".cache/ford_route112/route.npz": "2b08a2fb636f7d14556d7df4035eafc1d1b97932237955528562a16db2b31d3e",
".cache/ford_route112/model_paths.npz": "9837afe78aab4cad288cad98a595a5777fa8a66bb235986b1272a7f7c54e559a",
".cache/ford_route112/metadata.json": "726d78a7e7aa45307dcfe27cb00775c20ecc9d54538eb7f26a0166fc216226ce",
"tools/ford_pscm_lab/proportional_replay.py": "0296fedbc134c3e11022c51e0b94cc16e1135b620fd278bf381ce040e953a7ed",
"openpilot/selfdrive/controls/lib/ford_model_action.py": "0c07b85db9c5cb0f42433116aea5d270fb244d8840119b4d7e8a526ec5b090d1"
}
},
{
"route": "113",
"cycles": 49614,
"can_round_trips": 99228,
"baseline_difference_from_recorded_path": {
"c0": {
"mean": 0.1281940820409913,
"p95": 0.897999979019158,
"max": 3.260000095367432
},
"c1": {
"mean": 0.006052000019791209,
"p95": 0.033500000965595265,
"max": 0.13200000381469723
}
},
"clean_seconds": 212.64164745499875,
"c1_bound_seconds": [
1.2653679959998954,
1.2653679959998954
],
"source_sha256": {
".cache/ford_route113/route.npz": "774ca4a21b7113c2706d6130bc180c3216ea4833155300ab01e75b3486e36327",
".cache/ford_route113/model_paths.npz": "93c41761eb85263f534f5371b905482cf7c948582eb1e9149966594be1d3768f",
".cache/ford_route113/metadata.json": "1c0ca74dd48b90ab9d5444c5ca7f8aa9361700bbdf98bd5e853be50ad2895d7f",
"tools/ford_pscm_lab/proportional_replay.py": "0296fedbc134c3e11022c51e0b94cc16e1135b620fd278bf381ce040e953a7ed",
"openpilot/selfdrive/controls/lib/ford_model_action.py": "0c07b85db9c5cb0f42433116aea5d270fb244d8840119b4d7e8a526ec5b090d1"
}
},
{
"route": "114",
"cycles": 61027,
"can_round_trips": 122054,
"baseline_difference_from_recorded_path": {
"c0": {
"mean": 0.11075714097529091,
"p95": 0.3999999904632565,
"max": 3.500000133514404
},
"c1": {
"mean": 0.006464089004288734,
"p95": 0.01899999927878381,
"max": 0.17200000238418578
}
},
"clean_seconds": 336.37317285400025,
"c1_bound_seconds": [
0.26486993000000325,
0.26486993000000325
],
"source_sha256": {
".cache/ford_route114/route.npz": "83524f07d61104b84b004ddc46bb751ca7f61da67798aa47db56d307765f5b3e",
".cache/ford_route114/model_paths.npz": "14d14362d1a3e46398edd8e22b7cc4e36277a7596a73f546192d0e14c6642b07",
".cache/ford_route114/metadata.json": "ec677b9275c1707e477ebd0ffa235d49b5ec63a1ee717b16040c3acdbcd3bdc0",
"tools/ford_pscm_lab/proportional_replay.py": "0296fedbc134c3e11022c51e0b94cc16e1135b620fd278bf381ce040e953a7ed",
"openpilot/selfdrive/controls/lib/ford_model_action.py": "0c07b85db9c5cb0f42433116aea5d270fb244d8840119b4d7e8a526ec5b090d1"
}
},
{
"route": "115",
"cycles": 40037,
"can_round_trips": 80074,
"baseline_difference_from_recorded_path": {
"c0": {
"mean": 0.08961574629129629,
"p95": 0.3500000071525573,
"max": 2.7699999141693112
},
"c1": {
"mean": 0.006259665157786696,
"p95": 0.0225000038146973,
"max": 0.10550000000000004
}
},
"clean_seconds": 213.9659020539998,
"c1_bound_seconds": [
0.0,
0.0
],
"source_sha256": {
".cache/ford_route115/route.npz": "e0df32d9e80f1c6b7d56327b37cf07af60070ffc212b8d111e343306148bff23",
".cache/ford_route115/model_paths.npz": "52f3ed9e188e4947618a57a3ed872fd1966a887fe1014999df741553b6c13bc0",
".cache/ford_route115/metadata.json": "d3c73035bad8eb5059e07962fd274c19cb70c8952b6f1514835d312d08b87a16",
"tools/ford_pscm_lab/proportional_replay.py": "0296fedbc134c3e11022c51e0b94cc16e1135b620fd278bf381ce040e953a7ed",
"openpilot/selfdrive/controls/lib/ford_model_action.py": "0c07b85db9c5cb0f42433116aea5d270fb244d8840119b4d7e8a526ec5b090d1"
}
},
{
"route": "116",
"cycles": 68793,
"can_round_trips": 137586,
"baseline_difference_from_recorded_path": {
"c0": {
"mean": 0.1229211861673407,
"p95": 0.40999998092651335,
"max": 3.539999895095825
},
"c1": {
"mean": 0.00492455679344663,
"p95": 0.01600000357627862,
"max": 0.12550000327825545
}
},
"clean_seconds": 286.72210779700015,
"c1_bound_seconds": [
0.0,
0.0
],
"source_sha256": {
".cache/ford_route116/route.npz": "13df1d49edd0c92bd4a4236c766ee76baa6dad477824b9de4b79927c98410394",
".cache/ford_route116/model_paths.npz": "85d1112b5e89340e8a955b7fe4040b3135cf1c91a54ff756826c6119cc9cc01d",
".cache/ford_route116/metadata.json": "d52fe63d013fa546524ad9522a26943d9372730d397d92d75dfd9d75c63ce766",
"tools/ford_pscm_lab/proportional_replay.py": "0296fedbc134c3e11022c51e0b94cc16e1135b620fd278bf381ce040e953a7ed",
"openpilot/selfdrive/controls/lib/ford_model_action.py": "0c07b85db9c5cb0f42433116aea5d270fb244d8840119b4d7e8a526ec5b090d1"
}
},
{
"route": "117",
"cycles": 27301,
"can_round_trips": 54602,
"baseline_difference_from_recorded_path": {
"c0": {
"mean": 0.1466742382755,
"p95": 0.6379999847412033,
"max": 4.270000057220459
},
"c1": {
"mean": 0.00582792986837905,
"p95": 0.024499997889995594,
"max": 0.05100000405311583
}
},
"clean_seconds": 97.48647941900026,
"c1_bound_seconds": [
0.9928339700004472,
1.080442829000276
],
"source_sha256": {
".cache/ford_route117/route.npz": "65e4ea6c76dce73018b636479b3777116f1e58bb9813e6c1a7b79018b219140e",
".cache/ford_route117/model_paths.npz": "079f6003968287530e1ed6f2243c727e80c2ef3d16fb8a7d945d7eca744d54e7",
".cache/ford_route117/metadata.json": "348396b3059c5f54d7938cd3acf2bb268a813b47ff47561da7a94cb75c3f3114",
"tools/ford_pscm_lab/proportional_replay.py": "0296fedbc134c3e11022c51e0b94cc16e1135b620fd278bf381ce040e953a7ed",
"openpilot/selfdrive/controls/lib/ford_model_action.py": "0c07b85db9c5cb0f42433116aea5d270fb244d8840119b4d7e8a526ec5b090d1"
}
},
{
"route": "119",
"cycles": 171423,
"can_round_trips": 342846,
"baseline_difference_from_recorded_path": {
"c0": {
"mean": 0.06276063585086782,
"p95": 0.22999999165535012,
"max": 4.129999866485596
},
"c1": {
"mean": 0.00042448693421451755,
"p95": 0.0024999996423721305,
"max": 0.035999999776482605
}
},
"clean_seconds": 616.4493080259994,
"c1_bound_seconds": [
0.0,
0.0
],
"source_sha256": {
".cache/ford_route119/route.npz": "415a099935fef152123b7422870442d6cfe6302bab9b8983ce3b3ffc71a7702b",
".cache/ford_route119/model_paths.npz": "dfd41383bea486ccd0a476e94e612921a44a099a0c1213ff132ab81df3aa94d7",
".cache/ford_route119/metadata.json": "d61bd3351912f2f1a3c51f6061beffe89b430ebc50178c35abff8d1ecc1fc04d",
"tools/ford_pscm_lab/proportional_replay.py": "0296fedbc134c3e11022c51e0b94cc16e1135b620fd278bf381ce040e953a7ed",
"openpilot/selfdrive/controls/lib/ford_model_action.py": "0c07b85db9c5cb0f42433116aea5d270fb244d8840119b4d7e8a526ec5b090d1"
}
},
{
"route": "11a",
"cycles": 235771,
"can_round_trips": 471542,
"baseline_difference_from_recorded_path": {
"c0": {
"mean": 0.060649023224496834,
"p95": 0.17000000953674288,
"max": 3.7
},
"c1": {
"mean": 0.00018421321567653392,
"p95": 0.0005000000949949013,
"max": 0.045000003814697265
}
},
"clean_seconds": 1085.65972405699,
"c1_bound_seconds": [
0.0811529149996204,
0.0811529149996204
],
"source_sha256": {
".cache/ford_route11a/route.npz": "428c203d090731386f06bf9fdeefe608f1999c43c52ff39cab7ee86ec0ac804f",
".cache/ford_route11a/model_paths.npz": "e2b827fd7c3dfbf66d7872a56b57eaec13c600a9a48e12dae59be81c31b0978f",
".cache/ford_route11a/metadata.json": "b7b10ccb1c0fa740779a1cf977faa4ae6714c9f9d7a0ffc39f3684680c16ae2a",
"tools/ford_pscm_lab/proportional_replay.py": "0296fedbc134c3e11022c51e0b94cc16e1135b620fd278bf381ce040e953a7ed",
"openpilot/selfdrive/controls/lib/ford_model_action.py": "0c07b85db9c5cb0f42433116aea5d270fb244d8840119b4d7e8a526ec5b090d1"
}
},
{
"route": "120",
"cycles": 56704,
"can_round_trips": 113408,
"baseline_difference_from_recorded_path": {
"c0": {
"mean": 0.07882976760905562,
"p95": 0.3099999999999996,
"max": 3.3499999046325684
},
"c1": {
"mean": 6.576444779324904e-10,
"p95": 3.218650845848714e-09,
"max": 1.478195188475695e-08
}
},
"clean_seconds": 325.62925950599976,
"c1_bound_seconds": [
0.0,
0.0
],
"source_sha256": {
".cache/ford_route120/route.npz": "ca40b52257af313ed0abd1109a33115a0a3227bd3438367faf282467e807a34a",
".cache/ford_route120/model_paths.npz": "9b4a21357de4076c555e8776ed36b1d5728da1307d739268a3f84e84593e12b4",
".cache/ford_route120/metadata.json": "110fe2016561722743064f2619e32f1b69164cc86015ef01c0a9fd2dd7f24bba",
"tools/ford_pscm_lab/proportional_replay.py": "0296fedbc134c3e11022c51e0b94cc16e1135b620fd278bf381ce040e953a7ed",
"openpilot/selfdrive/controls/lib/ford_model_action.py": "0c07b85db9c5cb0f42433116aea5d270fb244d8840119b4d7e8a526ec5b090d1"
}
},
{
"route": "124",
"cycles": 5820,
"can_round_trips": 11640,
"baseline_difference_from_recorded_path": {
"c0": {
"mean": 2.909556870453723e-10,
"p95": 6.70552502413102e-10,
"max": 4.76837147544984e-09
},
"c1": {
"mean": 2.2964119221745972e-10,
"p95": 8.344650526126429e-10,
"max": 3.3974647539736225e-09
}
},
"clean_seconds": 22.732139372000063,
"c1_bound_seconds": [
0.0,
0.0
],
"source_sha256": {
".cache/ford_c0_response_124_125/route124/route.npz": "26f8cf284e54c5f80bc7414c1302c429d4c10d54147e137fc5b8a348249e4fe5",
".cache/ford_c0_response_124_125/route124/model_paths.npz": "898d75cc80f29fc50a818c29614961ca8fdf56d1e8908993662c6e92e54ebf6f",
".cache/ford_c0_response_124_125/route124/metadata.json": "8f93b186178c89a95e56177dda7f0aba740bc57dfb832cde6d5c3fd3f43a8126",
"tools/ford_pscm_lab/proportional_replay.py": "0296fedbc134c3e11022c51e0b94cc16e1135b620fd278bf381ce040e953a7ed",
"openpilot/selfdrive/controls/lib/ford_model_action.py": "0c07b85db9c5cb0f42433116aea5d270fb244d8840119b4d7e8a526ec5b090d1"
}
},
{
"route": "125",
"cycles": 41717,
"can_round_trips": 83434,
"baseline_difference_from_recorded_path": {
"c0": {
"mean": 3.143796501787139e-10,
"p95": 1.3411041166477844e-09,
"max": 4.76837147544984e-09
},
"c1": {
"mean": 0.001590841778388488,
"p95": 0.004000000216066835,
"max": 0.0040000033974647575
}
},
"clean_seconds": 400.6387703520005,
"c1_bound_seconds": [
0.0,
0.0
],
"source_sha256": {
".cache/ford_c0_response_124_125/route125/route.npz": "8dcf304871bf5c6bf1ce206c788d7034bf5f056522faded0aa7948ff1cccbb75",
".cache/ford_c0_response_124_125/route125/model_paths.npz": "4be9c7809dabbcb52296ba93b4743146dc941e49a32aca07f008b75757703e49",
".cache/ford_c0_response_124_125/route125/metadata.json": "1f2b812d3c2cc8e5a2246098756940d6f8989a2591daf4d74b1ef4c8555e2d57",
"tools/ford_pscm_lab/proportional_replay.py": "0296fedbc134c3e11022c51e0b94cc16e1135b620fd278bf381ce040e953a7ed",
"openpilot/selfdrive/controls/lib/ford_model_action.py": "0c07b85db9c5cb0f42433116aea5d270fb244d8840119b4d7e8a526ec5b090d1"
}
},
{
"route": "146",
"cycles": 53229,
"can_round_trips": 106458,
"baseline_difference_from_recorded_path": {
"c0": {
"mean": 3.47655752720815e-10,
"p95": 1.3411041166477844e-09,
"max": 2.8610229740877458e-08
},
"c1": {
"mean": 2.782228297198912e-10,
"p95": 1.19209286886246e-09,
"max": 1.3351440397713077e-08
}
},
"clean_seconds": 388.4780471659999,
"c1_bound_seconds": [
0.0,
0.0
],
"source_sha256": {
".cache/ford_model_replay_146/full/route.npz": "b01db5bdd1c3b9fb83418a298571c98e60b5eb0509800d86cdaf07bafcdc6d63",
".cache/ford_model_replay_146/full/model_paths.npz": "11e0e9c8ce2959aac72d2f8a9cc4f6a025480034d5cd8b59b68a41b2ae3cf4a8",
".cache/ford_model_replay_146/full/metadata.json": "2be1254dd1eee422f3b2c2011faa91a21ab431751998525105381c2886df15ca",
"tools/ford_pscm_lab/proportional_replay.py": "0296fedbc134c3e11022c51e0b94cc16e1135b620fd278bf381ce040e953a7ed",
"openpilot/selfdrive/controls/lib/ford_model_action.py": "0c07b85db9c5cb0f42433116aea5d270fb244d8840119b4d7e8a526ec5b090d1"
}
},
{
"route": "149",
"cycles": 132334,
"can_round_trips": 264668,
"baseline_difference_from_recorded_path": {
"c0": {
"mean": 1.604709350643841e-09,
"p95": 7.152557657263969e-09,
"max": 5.7220459037665705e-08
},
"c1": {
"mean": 6.774543859790247e-10,
"p95": 3.0994415922691587e-09,
"max": 1.478195188475695e-08
}
},
"clean_seconds": 868.9707798709996,
"c1_bound_seconds": [
0.2315784860001031,
0.6131428530001415
],
"source_sha256": {
".cache/ford_route149/full/route.npz": "aa5902877343cd033ee286b3668d91a85336ffbf740861849fb0b76b0ca24ade",
".cache/ford_route149/full/model_paths.npz": "19827800b8fb5983f3d6b72fcfaf36e35a40170bb17cd7c1e49374744fb449fb",
".cache/ford_route149/full/metadata.json": "624fff03c25eb298661cb7b25f3dbe6d214d863f799d93635c0f4f05fc0d2b32",
"tools/ford_pscm_lab/proportional_replay.py": "0296fedbc134c3e11022c51e0b94cc16e1135b620fd278bf381ce040e953a7ed",
"openpilot/selfdrive/controls/lib/ford_model_action.py": "0c07b85db9c5cb0f42433116aea5d270fb244d8840119b4d7e8a526ec5b090d1"
}
},
{
"route": "a0",
"cycles": 114582,
"can_round_trips": 229164,
"baseline_difference_from_recorded_path": {
"c0": {
"mean": 0.02570244338167474,
"p95": 0.08000000059604684,
"max": 1.4500001144409183
},
"c1": {
"mean": 0.01759150615156611,
"p95": 0.0444999985605478,
"max": 0.1095000026226044
}
},
"clean_seconds": 926.827608547,
"c1_bound_seconds": [
7.083908395000037,
7.184738287000073
],
"source_sha256": {
".cache/ford_routea0/route.npz": "01ce8b0361c3154d07d5449525a650baa2cc261675095cb1f04ec9e37a9174d3",
".cache/ford_routea0/model_paths.npz": "5d0c69d10a6d13882b47d950ae64c4d98f7ca2b59eab27e4e9557eef3bc4917b",
".cache/ford_routea0/metadata.json": "00496a11afbee475e9b5d66838a4d1663a3c0063cd562b4d1408cce6d71e586e",
"tools/ford_pscm_lab/proportional_replay.py": "0296fedbc134c3e11022c51e0b94cc16e1135b620fd278bf381ce040e953a7ed",
"openpilot/selfdrive/controls/lib/ford_model_action.py": "0c07b85db9c5cb0f42433116aea5d270fb244d8840119b4d7e8a526ec5b090d1"
}
},
{
"route": "a2",
"cycles": 71111,
"can_round_trips": 142222,
"baseline_difference_from_recorded_path": {
"c0": {
"mean": 0.04191847719972587,
"p95": 0.11000001072883592,
"max": 2.5599999904632567
},
"c1": {
"mean": 0.018344418527189366,
"p95": 0.06350000252127648,
"max": 0.10950000333786014
}
},
"clean_seconds": 362.6477764930001,
"c1_bound_seconds": [
4.459983808000175,
4.459983808000175
],
"source_sha256": {
".cache/ford_routea2/route.npz": "af4128d278be6be0042f72a02255ddbec96a8a5aa435b9852c8202e4a5cb39e5",
".cache/ford_routea2/model_paths.npz": "5a5a7c4fc30c7b3bc7f2413daa8b11fe86b69dd3249e8df2d35b0a0636a49717",
".cache/ford_routea2/metadata.json": "cfecd240216e409a1e2591c6c2e6f0bad002ecd2a50c8f6dbf22a8f9174708da",
"tools/ford_pscm_lab/proportional_replay.py": "0296fedbc134c3e11022c51e0b94cc16e1135b620fd278bf381ce040e953a7ed",
"openpilot/selfdrive/controls/lib/ford_model_action.py": "0c07b85db9c5cb0f42433116aea5d270fb244d8840119b4d7e8a526ec5b090d1"
}
},
{
"route": "a5",
"cycles": 38961,
"can_round_trips": 77922,
"baseline_difference_from_recorded_path": {
"c0": {
"mean": 0.0678814615486646,
"p95": 0.23999999046325726,
"max": 2.549999809265137
},
"c1": {
"mean": 0.010050927104249109,
"p95": 0.02549999953061341,
"max": 0.11449999988079068
}
},
"clean_seconds": 281.78537111799994,
"c1_bound_seconds": [
3.117821645999925,
3.1221818889999327
],
"source_sha256": {
".cache/ford_routea5/route.npz": "815d1e248ff5c3e5e5cfc11dfbd0690d0d13ddafe1436890e239975a72dae9b8",
".cache/ford_routea5/model_paths.npz": "72e014b844322df6318f6af588c27175ce68d9a37bda62b70eafca5bdfe6dfe4",
".cache/ford_routea5/metadata.json": "c533b06d1b7c598330b36b117bdb3624221bc4193ded8354246d5c86f36b2628",
"tools/ford_pscm_lab/proportional_replay.py": "0296fedbc134c3e11022c51e0b94cc16e1135b620fd278bf381ce040e953a7ed",
"openpilot/selfdrive/controls/lib/ford_model_action.py": "0c07b85db9c5cb0f42433116aea5d270fb244d8840119b4d7e8a526ec5b090d1"
}
},
{
"route": "a9",
"cycles": 286319,
"can_round_trips": 572638,
"baseline_difference_from_recorded_path": {
"c0": {
"mean": 0.0574873679806263,
"p95": 0.24000002861022907,
"max": 1.8600000095367433
},
"c1": {
"mean": 0.012787646152435335,
"p95": 0.04100000113248825,
"max": 0.20099999380111694
}
},
"clean_seconds": 1231.1594245750002,
"c1_bound_seconds": [
0.945385510000051,
1.1985475469998619
],
"source_sha256": {
".cache/ford_routea9/route.npz": "3fa8cabfb729dd42d689de38618d1d21c8965c9d60f914546f4d7cc58db8c975",
".cache/ford_routea9/model_paths.npz": "7f3646ad24aa52de5982c65600813de5bbc5f7664d92a2516bbba35da6e7b240",
".cache/ford_routea9/metadata.json": "403d35504bf596144845ac2060ce067d571f49a2955d16223af28a79a7a66e93",
"tools/ford_pscm_lab/proportional_replay.py": "0296fedbc134c3e11022c51e0b94cc16e1135b620fd278bf381ce040e953a7ed",
"openpilot/selfdrive/controls/lib/ford_model_action.py": "0c07b85db9c5cb0f42433116aea5d270fb244d8840119b4d7e8a526ec5b090d1"
}
},
{
"route": "b8",
"cycles": 160431,
"can_round_trips": 320862,
"baseline_difference_from_recorded_path": {
"c0": {
"mean": 0.09075655656311847,
"p95": 0.3199999809265135,
"max": 3.8600001049041746
},
"c1": {
"mean": 0.014587774875948546,
"p95": 0.047500000029802314,
"max": 0.2005000000223518
}
},
"clean_seconds": 465.3759713629988,
"c1_bound_seconds": [
1.233373995994043,
1.536421163000341
],
"source_sha256": {
".cache/ford_routeb8/route.npz": "6f5dd369b70eaed4b95b28c8b25c9f2e9b830fa07a334881a185505481667c8b",
".cache/ford_routeb8/model_paths.npz": "939af6cf7e74251d8842581cc078d26d9fbfd22a0d7817cb0e368697d419b615",
".cache/ford_routeb8/metadata.json": "73b439132d1de37ec187b544c04d2b05c80965065515a4b7dec29ba57ae37e7c",
"tools/ford_pscm_lab/proportional_replay.py": "0296fedbc134c3e11022c51e0b94cc16e1135b620fd278bf381ce040e953a7ed",
"openpilot/selfdrive/controls/lib/ford_model_action.py": "0c07b85db9c5cb0f42433116aea5d270fb244d8840119b4d7e8a526ec5b090d1"
}
},
{
"route": "b9",
"cycles": 90774,
"can_round_trips": 181548,
"baseline_difference_from_recorded_path": {
"c0": {
"mean": 0.0779309579021297,
"p95": 0.3100000107288361,
"max": 4.5400001907348635
},
"c1": {
"mean": 0.007145982184135228,
"p95": 0.020500003129243882,
"max": 0.14649999999999996
}
},
"clean_seconds": 680.5921192759997,
"c1_bound_seconds": [
0.7494592899999759,
0.9690469479999706
],
"source_sha256": {
".cache/ford_routeb9/route.npz": "b07c789d8155335f5d120d0262fced6e4d5803fe767b0ff49b6413dce4140b5c",
".cache/ford_routeb9/model_paths.npz": "6b1f87897c050273fdc05af051307a049b6fc3a93072e7cda1721195ce7c3861",
".cache/ford_routeb9/metadata.json": "9ce452220cab61b81883f32fc2fcaf5db6c78a674cb255a49cc77d5029580fee",
"tools/ford_pscm_lab/proportional_replay.py": "0296fedbc134c3e11022c51e0b94cc16e1135b620fd278bf381ce040e953a7ed",
"openpilot/selfdrive/controls/lib/ford_model_action.py": "0c07b85db9c5cb0f42433116aea5d270fb244d8840119b4d7e8a526ec5b090d1"
}
},
{
"route": "ca",
"cycles": 327448,
"can_round_trips": 654896,
"baseline_difference_from_recorded_path": {
"c0": {
"mean": 0.015992715429034462,
"p95": 0.05000000029802365,
"max": 1.0699999809265135
},
"c1": {
"mean": 0.0030197859214003767,
"p95": 0.00799999958276748,
"max": 0.10800000715255731
}
},
"clean_seconds": 999.6971771089911,
"c1_bound_seconds": [
0.871117996999601,
0.871117996999601
],
"source_sha256": {
".cache/ford_routeca/route.npz": "ae9d46770eaf0dbbac6af86aebc926320eed0cf114eb43d5f78b0676e8e0dbf9",
".cache/ford_routeca/model_paths.npz": "bf17deb442383aaa79432566cd382df24a1bbbbd0521d0cafab956618f5bdd96",
".cache/ford_routeca/metadata.json": "a759d5cdf878df8b05d91db637b1935b6b4bdd87af96f0f256b67e7d809b3525",
"tools/ford_pscm_lab/proportional_replay.py": "0296fedbc134c3e11022c51e0b94cc16e1135b620fd278bf381ce040e953a7ed",
"openpilot/selfdrive/controls/lib/ford_model_action.py": "0c07b85db9c5cb0f42433116aea5d270fb244d8840119b4d7e8a526ec5b090d1"
}
},
{
"route": "raptor02",
"cycles": 132881,
"can_round_trips": 265762,
"baseline_difference_from_recorded_path": {
"c0": {
"mean": 0.01159572992270899,
"p95": 0.06918421261012586,
"max": 0.39572428941726656
},
"c1": {
"mean": 0.017327589689457763,
"p95": 0.04501086750242392,
"max": 0.10240508272871374
}
},
"clean_seconds": 592.9243753840037,
"c1_bound_seconds": [
0.0,
0.0
],
"source_sha256": {
".cache/ford_raptor_route02/route.npz": "8a4cfe53994988d053b47d9caadf21ebd064fd20e598fa05ddc2da245bd0e980",
".cache/ford_raptor_route02/model_paths.npz": "d5dc6f72d91472b8f2fb1add1680cdaea1e122774ddd6d8519fd7673daf71bb5",
".cache/ford_raptor_route02/metadata.json": "9602e08efc2bd784133837c4156c075bb89bad3bedd6b309f15e884ed20e338a",
"tools/ford_pscm_lab/proportional_replay.py": "0296fedbc134c3e11022c51e0b94cc16e1135b620fd278bf381ce040e953a7ed",
"openpilot/selfdrive/controls/lib/ford_model_action.py": "0c07b85db9c5cb0f42433116aea5d270fb244d8840119b4d7e8a526ec5b090d1"
}
}
],
"selected_runtime_source_sha256": "a653b9c16ff8e5ac91e7c1751eaeb7a77b50701d185ce426fe1074d1a58a07b4",
"opendbc_commit": "64aa61b9b3fd26e70a7caa915acab207ff3cd64a"
}
-151
View File
@@ -1,151 +0,0 @@
# Ford C1 proportional feedback trial
V6 adds **P = 0.25** to the selected-action controller. It responds to a steering
shortfall immediately and subtracts demand immediately when the wheel exceeds
the selected request. V5 accumulated correction over traveled distance. P has
no stored correction to release when its error disappears.
This is an initial drive-trial gain, not an identified PSCM calibration or a
claim of improved physical tracking. Six Lightning routes establish command
behavior across recorded scenarios. They cannot identify the best stable gain
without observing the vehicle responding to the changed commands.
## Command law
With curvature in inverse meters, speed in meters per second and heading in radians:
```text
D = max(7 m, speed × 1 s)
error = selected_limited_curvature - measured_steering_curvature
base_C1 = clip(D × selected_limited_curvature, -0.5, +0.5)
P = 0.25 × D × error
I_increment = speed × error × fresh_measurement_elapsed_time
C1 = amplitude_and_slew_limit(base_C1 + P + I)
```
P is 25% of the heading-equivalent tracking error, not a 25% multiplier on the
model request. At matched curvature it is zero. It is stateless and can change
with a new request even if a steering publication repeats; repeated steering
publications still cannot integrate I twice. Driver override and fresh PSCM
denied/inactive states clear both feedback terms. Fresh `limit=2` inhibits
outward I accumulation while permitting unwind; P remains available inside the
existing combined output envelope.
The existing C1 amplitude limit (±0.5 rad) and slew (0.5 rad/s) apply to the sum.
Anti-windup includes P when calculating I's available headroom. P can consume
a slew interval that previously allowed I accumulation. The conditional I
release rules, including [completed-unwind release](ford_unwind_catchup.md),
remain. C0 retains the same 7 m mapping, base-heading overflow, cap and slew;
neither P nor I spills into C0. C2/C3 stay zero. No plant, gain schedule or
automatic gain learning is introduced.
Onroad selection explicitly supplies `C1_PROPORTIONAL_GAIN = 0.25`. Direct
`FordModelActionController()` and `ModelActionController()` construction defaults
to zero P for v5 reference/replay compatibility. The existing default-off
Sunnylink toggle selects v6 on any Ford CAN FD. Toggle off still selects
upstream Ford control. See [installation and selection](ford_model_action_drive_test.md).
## Lightning replay findings
Routes `112`, `113`, `114`, `115`, `b9` and `ca` supplied 677,871 control cycles.
Each was replayed with P gains 0, 0.1, 0.25 and 0.5, paired with diagnostic
feedback delays 0, 0.2 and 0.4 s: 12 combinations and 8,134,452 candidate updates.
Recorded model, driver, steering and PSCM inputs stayed fixed.
Both command columns are replayed C1 in radians with left positive. The angle
pair is the single recorded desired/actual wheel measurement, not a predicted
outcome for either candidate.
| Example | Desired / actual angle | V5 C1 | P=0.25 C1 |
| --- | ---: | ---: | ---: |
| 115, 207.908 s: late left entry | 94.2° / 51.2° | +0.1685 | +0.1825 |
| 115, 208.099 s: entry continues | 111.0° / 72.8° | +0.2105 | +0.2245 |
| 114, 473.086 s: well-tracked bend | 57.3° / 56.3° | +0.1855 | +0.1850 |
| 113, 481.567 s: hanging right exit | 7.6° / 94.4° | +0.0645 | +0.0875 |
| 115, 133.595 s: completed unwind | 6.0° / 29.9° | +0.0105 | 0.0000 |
The completed-unwind example is excluded by the original quality/driver clean
mask. It is useful for checking command release, not autonomous tracking
attribution. Large-turn windows often contain interventions and require review
of driver input before assigning a tracking result to the controller.
Across 2,740.44 seconds of valid, feedback-enabled, clean samples with desired
wheel angle below 30°, the duration-weighted mean absolute C1 change is
0.001001 rad at P=0.25, versus 0.001961 at P=0.5. Per-route 95th-percentile
changes at P=0.25 are 0.00250.0070 rad; the largest ordinary-cohort change is
0.0350 rad. Small average command changes do not establish unchanged centering
or stability.
P=0.25 is an engineering choice between the tested smaller and larger responses,
not an optimization result. At the late-entry example, adding a fixed 0.4 s
feedback delay instead gives C1 +0.1420 rad. Across the ordinary cohort, that
delayed P=0.25 variant changes C1 by 0.011255 rad on average. V6 therefore
retains v5's feedback timing to isolate P. This does not identify or disprove
the vehicle's physical delay. The diagnostic delay variants change only P and
I integration targets; request-release decisions still use the current request.
## Tuning and next-drive evidence
Comma's [torque controller](https://github.com/commaai/openpilot/blob/master/openpilot/selfdrive/controls/lib/latcontrol_torque.py)
separates feedforward, P and I and aligns its torque feedback reference with
steering delay. Its [angle PID controller](https://github.com/commaai/openpilot/blob/master/openpilot/selfdrive/controls/lib/latcontrol_pid.py)
uses desired-minus-measured steering angle directly. These different paths do
not imply one delay setting should be copied into Ford C1.
Use the same discipline: explicit parameters, separate term logging, fixed
request conditions and measured response. Comma's
[lateral maneuver report](https://blog.comma.ai/0111release/#lateral-maneuver-report)
uses repeatable step/sine maneuvers to assess response. C1 is a path-heading
request to another controller, not normalized steering torque; numerical torque
gains and torque calibration cannot be copied across.
For the next controlled evaluation, compare similar speeds and model requests:
entry delay/shortfall, overshoot as the request relaxes, correction after catch-up,
ordinary-bend centering and oscillation. Keep desired/actual tracking on original
timestamps. Check C0/C1 caps, slew, driver input and fresh PSCM flags separately.
More gain cannot remove hardware limits and can introduce oscillation. These
logs all come from a Lightning; the gain is not yet validated across other
PSCMs. No scripted maneuver mode is enabled by this change.
Periodic `Ford C2-free path tracking` events identify
`hypothesis=model-action-c1-pi-v6` and expose `heading_proportional`,
`proportional_gain`, `feedback_curvature` and `feedback_error` alongside
`heading_feedforward`, `heading_correction`, command and release diagnostics.
`calibration_approved=false` remains.
## Validation and reproduction
The final selected path exactly matches the sweep's P=0.25, zero-delay variant
on all six routes, including C0/C1, P, I and activation. Zero-P/zero-delay matches
v5 exactly on every cycle. All variants preserve C0 and activation. Another
200,000 seeded stress cycles check PI arithmetic, anti-windup, mirror symmetry,
driver/PSCM arbitration, resets, amplitude/slew and zero-P parity. Sweep,
selected replay and stress total **9,012,323 Float32/CAN round trips**.
Encoding checks do not test vehicle motion.
**776 tests and 9,145 subtests passed; 178 were skipped.** Coverage includes
actual startup selection, controlsd request source/limiting, Float32 publication,
100 Hz CAN encoding, checksums, both turn signs, integral release, Sunnylink
persistence, toggle-off upstream behavior and Ford safety tests. Ruff,
controller Ty and settings compilation passed. A hardware build/device boot
and physical response tests have not been performed.
Use the project's Python environment and built cereal/opendbc dependencies:
```sh
export PYTHONPATH=.:opendbc_repo:.cache/ford_v6/test_deps
export PYTHONDONTWRITEBYTECODE=1
export PARAMS_ROOT=/tmp/ford-pi-test-params
export LOG_ROOT=/tmp/ford-pi-test-logs
python -m tools.ford_pscm_lab.pi_replay .cache/ford_route115 --output .cache/ford_pi_sweep/route115
python -m tools.ford_pscm_lab.feedback_replay route .cache/ford_route115 --baseline 22d188776cb557acea459a1fca70812bdb2df46c --output .cache/ford_pi_sweep/selected115
python -m tools.ford_pscm_lab.pi_stress --cycles 200000 --gain .25 --output .cache/ford_pi_sweep/stress.json
python -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 openpilot/sunnypilot/sunnylink/tests openpilot/common/tests/test_params.py opendbc_repo/opendbc/car/ford/tests/test_ford.py opendbc_repo/opendbc/safety/tests/test_ford.py
```
Repeat both replay commands for the other five extracts. The machine-readable
[validation record](ford_c1_pi_validation.json) records counts, source hashes,
cohort definitions and sampled command changes. Publication time proxies the
computation clock; full SubMaster state and selected maneuver-plan publications
are not reconstructed. Real maneuver source selection is exercised in integration
tests. Historical controller reports retain their original version scope.
-373
View File
@@ -1,373 +0,0 @@
{
"hypothesis": "model-action-c1-pi-v6",
"baseline_revision": "22d188776cb557acea459a1fca70812bdb2df46c",
"opendbc_revision": "64aa61b9b3fd26e70a7caa915acab207ff3cd64a",
"selected_gain": 0.25,
"selected_feedback_delay_s": 0.0,
"calibration_approved": false,
"scope": "Frozen recorded steering, model, driver and PSCM inputs. Command checks only; no predicted wheel response or physical tracking improvement score.",
"controller_sha256": "8eaf242a8627c398b732e9c185527640b2ae39b4726cbb138c0a10490a4b890f",
"routes": {
"112": {
"route": "84865544361f55cb_00000112--ec2edd4afc",
"fingerprint": "FORD_F_150_LIGHTNING_MK1",
"cycles": 108971,
"candidate_updates": 1307652,
"sweep_can_round_trips": 1307652,
"selected_can_round_trips": 108971,
"selected_matches_sweep_commands_p_i_valid_exactly": true,
"zero_gain_zero_delay_matches_v5_exactly": true,
"all_c0_and_activation_match_exactly": true,
"ordinary_clean_seconds": 655.2228206790003,
"ordinary_mean_abs_c1_change_rad": 0.0008885702173989647,
"ordinary_p95_abs_c1_change_rad": 0.0025000000000000022,
"ordinary_max_abs_c1_change_rad": 0.02400000000000002,
"all_valid_max_abs_c1_change_rad": 0.054500000000000104,
"source_sha256": {
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route112/route.npz": "2b08a2fb636f7d14556d7df4035eafc1d1b97932237955528562a16db2b31d3e",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route112/model_paths.npz": "9837afe78aab4cad288cad98a595a5777fa8a66bb235986b1272a7f7c54e559a",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route112/metadata.json": "726d78a7e7aa45307dcfe27cb00775c20ecc9d54538eb7f26a0166fc216226ce",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/feedback_replay.py": "c7de9d9d7aefb27221ad6d964df723fbf9f2b0f820cdd7de4799c83984d05485",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/openpilot/selfdrive/controls/lib/ford_model_action.py": "8eaf242a8627c398b732e9c185527640b2ae39b4726cbb138c0a10490a4b890f",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route112/angles.npz": "9429813237b4988dd99a1461d1a9bd6f6d3056693a93a5dd1ec63567d19f2aa1"
},
"sweep_report_sha256": "413e844d20d7591d32d95b1d48a14db11fa089800315cb899e9f5a32a3e630f5",
"selected_report_sha256": "ab4ced7ec6b628ac4a941fb968e6e18d06f9c616f8704f1a6abd0081a4fb0e65"
},
"113": {
"route": "84865544361f55cb_00000113--3947b0487c",
"fingerprint": "FORD_F_150_LIGHTNING_MK1",
"cycles": 49614,
"candidate_updates": 595368,
"sweep_can_round_trips": 595368,
"selected_can_round_trips": 49614,
"selected_matches_sweep_commands_p_i_valid_exactly": true,
"zero_gain_zero_delay_matches_v5_exactly": true,
"all_c0_and_activation_match_exactly": true,
"ordinary_clean_seconds": 187.8263240149995,
"ordinary_mean_abs_c1_change_rad": 0.0015004664599222238,
"ordinary_p95_abs_c1_change_rad": 0.007000000000000006,
"ordinary_max_abs_c1_change_rad": 0.025000000000000022,
"all_valid_max_abs_c1_change_rad": 0.08250000000000002,
"source_sha256": {
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route113/route.npz": "774ca4a21b7113c2706d6130bc180c3216ea4833155300ab01e75b3486e36327",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route113/model_paths.npz": "93c41761eb85263f534f5371b905482cf7c948582eb1e9149966594be1d3768f",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route113/metadata.json": "1c0ca74dd48b90ab9d5444c5ca7f8aa9361700bbdf98bd5e853be50ad2895d7f",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/feedback_replay.py": "c7de9d9d7aefb27221ad6d964df723fbf9f2b0f820cdd7de4799c83984d05485",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/openpilot/selfdrive/controls/lib/ford_model_action.py": "8eaf242a8627c398b732e9c185527640b2ae39b4726cbb138c0a10490a4b890f",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route113/angles.npz": "2f280a1b5973901d878f1e36564d6e7ae6d817cac176149f2cb898ab7591d65b"
},
"sweep_report_sha256": "a712fdbe0602404ade075b0ccb540d5f571e5a55d37723fc2674c79b5f62252f",
"selected_report_sha256": "0098ef5b1dd0b8b1f0b742cfb5f8e80211eb60718a08e471927c78b9a7c740b3"
},
"114": {
"route": "84865544361f55cb_00000114--03902c6e04",
"fingerprint": "FORD_F_150_LIGHTNING_MK1",
"cycles": 61027,
"candidate_updates": 732324,
"sweep_can_round_trips": 732324,
"selected_can_round_trips": 61027,
"selected_matches_sweep_commands_p_i_valid_exactly": true,
"zero_gain_zero_delay_matches_v5_exactly": true,
"all_c0_and_activation_match_exactly": true,
"ordinary_clean_seconds": 263.004525574,
"ordinary_mean_abs_c1_change_rad": 0.0010481495632417845,
"ordinary_p95_abs_c1_change_rad": 0.003500000000000003,
"ordinary_max_abs_c1_change_rad": 0.010999999999999954,
"all_valid_max_abs_c1_change_rad": 0.0655,
"source_sha256": {
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route114/route.npz": "83524f07d61104b84b004ddc46bb751ca7f61da67798aa47db56d307765f5b3e",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route114/model_paths.npz": "14d14362d1a3e46398edd8e22b7cc4e36277a7596a73f546192d0e14c6642b07",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route114/metadata.json": "ec677b9275c1707e477ebd0ffa235d49b5ec63a1ee717b16040c3acdbcd3bdc0",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/feedback_replay.py": "c7de9d9d7aefb27221ad6d964df723fbf9f2b0f820cdd7de4799c83984d05485",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/openpilot/selfdrive/controls/lib/ford_model_action.py": "8eaf242a8627c398b732e9c185527640b2ae39b4726cbb138c0a10490a4b890f",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route114/angles.npz": "63aa2ff6a21e8613519bda476690e2eda6f42cb5dcf108606bc6072c9e29408d"
},
"sweep_report_sha256": "138c522c4278d6827eb6192acc2052ba4832f652c3194e2ca330d3a4458a2f85",
"selected_report_sha256": "52cdc595248505301501d815d7f6219c47e8cdcac2a059dc345498b3ef707875"
},
"115": {
"route": "84865544361f55cb_00000115--899b9bf91d",
"fingerprint": "FORD_F_150_LIGHTNING_MK1",
"cycles": 40037,
"candidate_updates": 480444,
"sweep_can_round_trips": 480444,
"selected_can_round_trips": 40037,
"selected_matches_sweep_commands_p_i_valid_exactly": true,
"zero_gain_zero_delay_matches_v5_exactly": true,
"all_c0_and_activation_match_exactly": true,
"ordinary_clean_seconds": 165.0304544679998,
"ordinary_mean_abs_c1_change_rad": 0.0009869321832509364,
"ordinary_p95_abs_c1_change_rad": 0.0030000000000000027,
"ordinary_max_abs_c1_change_rad": 0.034999999999999976,
"all_valid_max_abs_c1_change_rad": 0.09900000000000003,
"source_sha256": {
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route115/route.npz": "e0df32d9e80f1c6b7d56327b37cf07af60070ffc212b8d111e343306148bff23",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route115/model_paths.npz": "52f3ed9e188e4947618a57a3ed872fd1966a887fe1014999df741553b6c13bc0",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route115/metadata.json": "d3c73035bad8eb5059e07962fd274c19cb70c8952b6f1514835d312d08b87a16",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/feedback_replay.py": "c7de9d9d7aefb27221ad6d964df723fbf9f2b0f820cdd7de4799c83984d05485",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/openpilot/selfdrive/controls/lib/ford_model_action.py": "8eaf242a8627c398b732e9c185527640b2ae39b4726cbb138c0a10490a4b890f",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route115/angles.npz": "71415b0f49efbfeda495ae52bde4beb7e16f31a1f5570af515a365e756056d3b"
},
"sweep_report_sha256": "cdb08fede1f86427174c1291e40f63a3ac18044dfb6276e98e0bc184bbff7dc5",
"selected_report_sha256": "92d3e720d12018599a5883ecfb3c2dd1330e22235e07b3cd0dcc9bbc0aa7c3dd"
},
"b9": {
"route": "84865544361f55cb_000000b9--5da7fe66ad",
"fingerprint": "FORD_F_150_LIGHTNING_MK1",
"cycles": 90774,
"candidate_updates": 1089288,
"sweep_can_round_trips": 1089288,
"selected_can_round_trips": 90774,
"selected_matches_sweep_commands_p_i_valid_exactly": true,
"zero_gain_zero_delay_matches_v5_exactly": true,
"all_c0_and_activation_match_exactly": true,
"ordinary_clean_seconds": 577.3471252719997,
"ordinary_mean_abs_c1_change_rad": 0.001220959786299961,
"ordinary_p95_abs_c1_change_rad": 0.0050000000000000044,
"ordinary_max_abs_c1_change_rad": 0.02350000000000002,
"all_valid_max_abs_c1_change_rad": 0.10250000000000004,
"source_sha256": {
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_routeb9/route.npz": "b07c789d8155335f5d120d0262fced6e4d5803fe767b0ff49b6413dce4140b5c",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_routeb9/model_paths.npz": "6b1f87897c050273fdc05af051307a049b6fc3a93072e7cda1721195ce7c3861",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_routeb9/metadata.json": "9ce452220cab61b81883f32fc2fcaf5db6c78a674cb255a49cc77d5029580fee",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/feedback_replay.py": "c7de9d9d7aefb27221ad6d964df723fbf9f2b0f820cdd7de4799c83984d05485",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/openpilot/selfdrive/controls/lib/ford_model_action.py": "8eaf242a8627c398b732e9c185527640b2ae39b4726cbb138c0a10490a4b890f",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_routeb9/angles.npz": "ce16212d9b13e41993d2c8df3246ddfe21324ad0cb213310e62eee1d685b197b"
},
"sweep_report_sha256": "13c9b2c4c0f89ec580e9c7a70c7556b4e40873d4d98561c9edfa1c12a0855669",
"selected_report_sha256": "733c3eb657cbae29fcb809ca42b85ed6fc03ab44665c5fe31020e266717ce53c"
},
"ca": {
"route": "84865544361f55cb_000000ca--1f70b49ec6",
"fingerprint": "FORD_F_150_LIGHTNING_MK1",
"cycles": 327448,
"candidate_updates": 3929376,
"sweep_can_round_trips": 3929376,
"selected_can_round_trips": 327448,
"selected_matches_sweep_commands_p_i_valid_exactly": true,
"zero_gain_zero_delay_matches_v5_exactly": true,
"all_c0_and_activation_match_exactly": true,
"ordinary_clean_seconds": 892.0115341569954,
"ordinary_mean_abs_c1_change_rad": 0.0008244438245156879,
"ordinary_p95_abs_c1_change_rad": 0.0025000000000000022,
"ordinary_max_abs_c1_change_rad": 0.01200000000000001,
"all_valid_max_abs_c1_change_rad": 0.05149999999999999,
"source_sha256": {
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_routeca/route.npz": "ae9d46770eaf0dbbac6af86aebc926320eed0cf114eb43d5f78b0676e8e0dbf9",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_routeca/model_paths.npz": "bf17deb442383aaa79432566cd382df24a1bbbbd0521d0cafab956618f5bdd96",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_routeca/metadata.json": "a759d5cdf878df8b05d91db637b1935b6b4bdd87af96f0f256b67e7d809b3525",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/feedback_replay.py": "c7de9d9d7aefb27221ad6d964df723fbf9f2b0f820cdd7de4799c83984d05485",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/openpilot/selfdrive/controls/lib/ford_model_action.py": "8eaf242a8627c398b732e9c185527640b2ae39b4726cbb138c0a10490a4b890f",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_routeca/angles.npz": "1681bda6db7dac332abf627d97e7452acd7e0faef0c196b11e55e22291cad073"
},
"sweep_report_sha256": "4ced4be50569d4025c9de7fb8085714deebfb6098238ab63e5a2e4c88460a94b",
"selected_report_sha256": "db0edb05105600f211e7ff833a324887f4402dc64ddceb44ee8ddec911de7533"
}
},
"examples": [
{
"description": "Late left entry",
"route": "115",
"time_s": 207.908066963,
"desired_angle_deg": 94.1796646118164,
"actual_angle_deg": 51.20000076293945,
"speed_mph": 12.099885821086458,
"strict_clean": true,
"c0_m_left_positive": 0.96,
"c1_rad_left_positive": {
"v5": 0.16849999999999998,
"p_025": 0.1825,
"p_025_delay_04": 0.14200000000000002
},
"p_rad_left_positive": 0.018101743422448635,
"i_rad_left_positive": 0.015372994845796784
},
{
"description": "Entry continues",
"route": "115",
"time_s": 208.09921410900006,
"desired_angle_deg": 111.01296997070312,
"actual_angle_deg": 72.80000305175781,
"speed_mph": 12.297558204224886,
"strict_clean": true,
"c0_m_left_positive": 1.13,
"c1_rad_left_positive": {
"v5": 0.21050000000000002,
"p_025": 0.22450000000000003,
"p_025_delay_04": 0.18100000000000005
},
"p_rad_left_positive": 0.016087211202830076,
"i_rad_left_positive": 0.02176835685651445
},
{
"description": "Completed right-turn unwind",
"route": "115",
"time_s": 133.59461515599992,
"desired_angle_deg": 5.989600658416748,
"actual_angle_deg": 29.899999618530273,
"speed_mph": 5.890273288393662,
"strict_clean": false,
"c0_m_left_positive": 0.1900000000000004,
"c1_rad_left_positive": {
"v5": 0.010500000000000065,
"p_025": 0.0,
"p_025_delay_04": -0.0030000000000000027
},
"p_rad_left_positive": -0.010158478980883956,
"i_rad_left_positive": -0.0015653052344988395
},
{
"description": "Large left overshoot; nearby driver input",
"route": "114",
"time_s": 168.957705716,
"desired_angle_deg": 278.3920593261719,
"actual_angle_deg": 450.20001220703125,
"speed_mph": 7.027778014508665,
"strict_clean": true,
"c0_m_left_positive": 5.11,
"c1_rad_left_positive": {
"v5": 0.34099999999999997,
"p_025": 0.2875,
"p_025_delay_04": 0.3125
},
"p_rad_left_positive": -0.07201755233108997,
"i_rad_left_positive": -0.13527950258838367
},
{
"description": "Well-tracked left bend",
"route": "114",
"time_s": 473.085523785,
"desired_angle_deg": 57.32655334472656,
"actual_angle_deg": 56.29999923706055,
"speed_mph": 27.33261651794824,
"strict_clean": true,
"c0_m_left_positive": 0.6699999999999999,
"c1_rad_left_positive": {
"v5": 0.1855,
"p_025": 0.18500000000000005,
"p_025_delay_04": 0.15900000000000003
},
"p_rad_left_positive": 0.0007143015310955292,
"i_rad_left_positive": 0.022011912629614844
},
{
"description": "Hanging right exit",
"route": "113",
"time_s": 481.56693996600006,
"desired_angle_deg": -7.566320896148682,
"actual_angle_deg": -94.4000015258789,
"speed_mph": 11.959057581279636,
"strict_clean": true,
"c0_m_left_positive": -0.5800000000000001,
"c1_rad_left_positive": {
"v5": 0.0645,
"p_025": 0.08750000000000002,
"p_025_delay_04": 0.034499999999999975
},
"p_rad_left_positive": 0.03597008844371885,
"i_rad_left_positive": 0.06765882642510383
}
],
"ordinary_cohort": "Existing interval-clean angle mask, valid replay and feedback enabled, absolute desired wheel angle <30 degrees.",
"weighting": "Extracted interval duration weights for seconds and mean absolute command changes; percentiles are cycle-weighted.",
"ordinary_clean_seconds": 2740.4427841649945,
"ordinary_mean_abs_c1_change_by_setting_rad": [
{
"kp": 0.0,
"delay_s": 0.0,
"mean_abs_delta_rad": 0.0
},
{
"kp": 0.1,
"delay_s": 0.0,
"mean_abs_delta_rad": 0.0003946011107110312
},
{
"kp": 0.25,
"delay_s": 0.0,
"mean_abs_delta_rad": 0.0010009008647461168
},
{
"kp": 0.5,
"delay_s": 0.0,
"mean_abs_delta_rad": 0.001961192031401653
},
{
"kp": 0.0,
"delay_s": 0.2,
"mean_abs_delta_rad": 0.006131012841938232
},
{
"kp": 0.1,
"delay_s": 0.2,
"mean_abs_delta_rad": 0.006179941022213607
},
{
"kp": 0.25,
"delay_s": 0.2,
"mean_abs_delta_rad": 0.006305419932002006
},
{
"kp": 0.5,
"delay_s": 0.2,
"mean_abs_delta_rad": 0.006643212063186747
},
{
"kp": 0.0,
"delay_s": 0.4,
"mean_abs_delta_rad": 0.010964264260175235
},
{
"kp": 0.1,
"delay_s": 0.4,
"mean_abs_delta_rad": 0.011070256870936306
},
{
"kp": 0.25,
"delay_s": 0.4,
"mean_abs_delta_rad": 0.011255438766715855
},
{
"kp": 0.5,
"delay_s": 0.4,
"mean_abs_delta_rad": 0.01162257561022028
}
],
"totals": {
"cycles": 677871,
"candidate_updates": 8134452,
"sweep_can_round_trips": 8134452,
"selected_can_round_trips": 677871,
"can_round_trips_including_stress": 9012323
},
"stress": {
"cycles": 200000,
"gain": 0.25,
"seed": 20260913,
"mirrored_updates": 200000,
"zero_gain_exact_v5_comparisons": 200000,
"can_round_trips": 200000,
"baseline_revision": "22d188776cb557acea459a1fca70812bdb2df46c",
"baseline_source_sha256": "2ceb4cd8717bb3325f9b22189c78605ad5d42a1061dec9ca2e4dbb90fd256d1e",
"release_cycles": 91,
"calibration_approved": false,
"checks": "Independent scalar PI arithmetic, combined anti-windup, mirror symmetry, slew/amplitude, driver/PSCM gates, resets, zero-P v5 parity and CAN.",
"scope": "Check PI arithmetic and CAN invariants without a model of vehicle response.",
"source_sha256": {
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/pi_stress.py": "07f3bd99c56e2184ba3402d7b2f324506ac0b786c7a15e422e9e2392c934544a",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/openpilot/selfdrive/controls/lib/ford_model_action.py": "8eaf242a8627c398b732e9c185527640b2ae39b4726cbb138c0a10490a4b890f"
},
"formatting_only_final_source_ast_identical": true,
"final_script_sha256": "cff014beda81b0bfbaabd7219b8e2848464c5c45ac779669f80084d5be347a06"
},
"tests": {
"passed": 776,
"skipped": 178,
"subtests_passed": 9145,
"log_sha256": "5b9274e2557f41b630ad285b0426c916835c2997fc7f5c4fcad91aa4c6f64243",
"ruff": "passed",
"controller_ty": "passed",
"settings_compiler": "passed"
}
}
-128
View File
@@ -1,128 +0,0 @@
# Ford changed-request correction release
The Chestnut/Tee Time routes 112 and 113 ran `17a86842f` (C1 feedback v3).
In route 113, an increasing turn request remained below its base C1 because
negative correction from an earlier oversteer episode took time to return to
zero. The existing reversal release did not apply: requested and measured
curvature were already in the same turn direction.
Version `model-action-c1-feedback-v4` retires a bounded amount of correction
when a changed request and measured error both oppose that correction. This
addresses software command delay. It does not establish improved wheel tracking
or fix all the recorded hanging exits.
## Rule
On a fresh steering measurement, evaluate the previous selected curvature and
the current selected curvature using today's existing heading reference:
```text
distance = max(7 m, speed * 1 s)
change = clip(distance * desired, -0.5, 0.5)
- clip(distance * previous_desired, -0.5, 0.5)
error = desired - measured
```
Retirement requires all of:
- Feedback enabled and a previous feedback request available.
- Heading change at least one existing C1 DBC step (0.0005 rad).
- Change and current error agree in direction.
- Stored correction opposes that direction.
- The magnitude of `distance * error` is at least the correction magnitude.
Move the correction toward zero by at most the heading change, without crossing
zero. Then run the existing reversal release, elapsed-distance integration,
PSCM arbitration and final output slew. The mismatch requirement is an
engineering guard using the existing reference distance; it is not a fitted
PSCM response threshold or proof of stability. It protects a larger learned
correction from small target/measurement noise. There is no new tunable strength
multiplier, and the existing 1:1 feedback-strength choice remains.
The last selected curvature adds one control state. Duplicate steering samples
do not advance this history or retire correction; the next fresh sample uses
the net request change. A speed change alone cannot cause retirement because
both requests are evaluated at the same current speed. Invalid input and
disengagement reset the history. Driver override clears correction and prevents
a pending request change from being applied later.
C0 mapping and overflow, C2/C3 zeroing, amplitude/slew limits, sender cadence and
all input/driver/PSCM gates remain unchanged. Toggle off still selects upstream
Ford control on every platform. The existing default-off toggle selects v4 on
Ford CAN FD vehicles. `request_release` logs signed radians retired on that
cycle; periodic diagnostics do not capture every individual retirement.
## Evidence and limits
The regression command `python -m pytest -q -p no:cacheprovider
openpilot/selfdrive/controls/tests/test_ford_model_action_request_release.py`
initially returned **4 failed** on v3. It checks that an obsolete correction no
longer delays a changed same-direction turn or unwind after the output slew
has time to respond. Expanded cases cover small noise, matched tracking,
insufficient error, speed-only changes, clipped base requests, duplicate
measurements, override and reset. Integration tests exercise actual controlsd
selection/limiting, both model and maneuver references, Float32 publication
and Ford CAN packing.
Frozen replay compares v4 with the deployed v3 on the same recorded model,
measurement, driver and PSCM inputs:
| Route | Control cycles | Retirement cycles | Largest C1 difference |
| --- | ---: | ---: | ---: |
| 112 | 108,971 | 414 | 0.0235 rad |
| 113 | 49,614 | 119 | 0.0275 rad |
| Historical b9 | 90,774 | 440 | 0.0350 rad |
Activation and C0 are identical on every replay cycle. C2/C3 remain zero.
Retirement changes subsequent correction history, so command differences can
persist after a retirement cycle. In frozen measurements the vehicle cannot
react to those differences. Command differences occur in ordinary bends too;
these tests do not establish unchanged real-world centering or stability.
In route 113, segment 3, old opposing correction reaches zero at **3:18.630**
instead of **3:19.253**: **0.623 s earlier**. At 3:18.649, C1 magnitude is
0.319 rad instead of 0.2925 rad. The PSCM limit flag still inhibits additional
outward integration; retiring opposing correction cannot create new stored
outward demand through that gate.
The route 113 exit at 8:01.567 has **identical C1** in this replay. Route 112's
11:40.555 overshoot changes C1 by only 0.0015 rad, slightly later in the unwind
direction on the frozen history. These are material limits: the change does
not solve those exits. C0's contribution and physical PSCM response remain
unresolved. No counterfactual wheel-angle or tracking-error score is reported.
The combined suite passes **717 tests and 9,145 subtests**, with 178 inherited
or unsupported safety-test skips. Feedback stress and zero-error stress cover
200,000 cycles each; the latter also covers 18,138 field-boundary cases.
Together with the three route replays, these verify **667,497 Float32/CAN round
trips**, separately from the integration suite. Stress compares each step to
v3 after only the declared retirement and checks sign symmetry, bounds, slew,
resets, arbitration and correction direction. Ruff, the controller Ty check
and settings compilation pass. Numerical records are in
`ford_c1_request_release_validation.json`.
## Reproduction
Use the project's native Python dependencies and unchanged opendbc revision
`64aa61b9b3fd26e70a7caa915acab207ff3cd64a`. Route commands require the full-rlog
extracts (`route.npz`, `model_paths.npz`, `metadata.json`) identified by the
validation hashes. No original logs are modified.
```sh
export PYTHONDONTWRITEBYTECODE=1
export PYTHONPATH=.:opendbc_repo
# Optional writable roots for the tests' temporary parameter stores and logs:
export PARAMS_ROOT=/tmp/ford-v4-test-params
export LOG_ROOT=/tmp/ford-v4-test-logs
python -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 openpilot/sunnypilot/sunnylink/tests openpilot/common/tests/test_params.py opendbc_repo/opendbc/car/ford/tests/test_ford.py opendbc_repo/opendbc/safety/tests/test_ford.py
python -m tools.ford_pscm_lab.feedback_replay stress --cycles 200000 --output .cache/ford_v4/stress.json
python -m tools.ford_pscm_lab.stress_model_action --cycles 200000 --seed 20260912 --opendbc-revision 64aa61b9b3fd26e70a7caa915acab207ff3cd64a --output .cache/ford_v4/zero_error.json
python -m tools.ford_pscm_lab.feedback_replay route .cache/ford_route112 --baseline 17a86842f --output .cache/ford_v4/route112
python -m tools.ford_pscm_lab.feedback_replay route .cache/ford_route113 --baseline 17a86842f --output .cache/ford_v4/route113
python -m tools.ford_pscm_lab.feedback_replay route .cache/ford_routeb9 --baseline 17a86842f --output .cache/ford_v4/routeb9
```
Publication time approximates the computation clock; full SubMaster health is
not reconstructable. These routes have no selected maneuver-plan publications;
that source is covered by integration tests. No device build, boot, installation
or physical steering test is performed offline.
@@ -1,214 +0,0 @@
{
"hypothesis": "model-action-c1-feedback-v4",
"baseline_revision": "17a86842f97f65216443a5d89648c8ace8518758",
"scope": "Software command delay and invariants only; no counterfactual wheel motion or proven physical tracking improvement. Hanging exits remain unresolved.",
"calibration_approved": false,
"tests": {
"passed": 717,
"subtests_passed": 9145,
"skipped": 178,
"log_sha256": "1864f8618b9d788f67e57766cf7b9ab9eda8e98a2ad0caf1c668bb9936b6eb7d",
"initial_regression": "4 failures on v3; same-turn command delay tests pass on v4",
"environment": "PARAMS_ROOT and LOG_ROOT point to dedicated temporary directories; initial sandbox path failures resolved without changing tests."
},
"feedback_stress": {
"cycles": 200000,
"mirrored_updates": 200000,
"can_round_trips": 200000,
"carryover_release_count": 165,
"baseline_revision": "17a86842f97f65216443a5d89648c8ace8518758",
"baseline_source_sha256": "167ae5a01fdd7ea014e6ad3fe9d0b6e31c67de8ba057ec5ecf18ab38fc16353f",
"request_release_cycles": 20454,
"exact_unchanged_state_and_commands_without_request_release": 179546,
"exact_v3_match_after_only_declared_retirement": 200000,
"checks": "Mirror symmetry, reset/override, amplitude, slew, correction bounds, bounded request retirement, carryover direction/confirmation, integration, PSCM limits, CAN.",
"scope": "Numerical software invariants only; no model of vehicle motion.",
"calibration_approved": false,
"controller_sha256": "5673630d31910fcfa5a3cc9a8d533b6b8fe9a76e2627bf1f67b52b3550ec7442"
},
"zero_error_stress": {
"seed": 20260912,
"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.4000000000000019,
0.05000000000000002
],
"calibration_approved": false,
"scope": "Zero-error numerical construction: measured equals requested curvature. No PSCM response claims.",
"opendbc_import_head": "64aa61b9b3fd26e70a7caa915acab207ff3cd64a",
"source_sha256": {
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/stress_model_action.py": "cec2619285dd41274562ac035ee8ea0a389269a0c4ef1b62efa6252ad1a714aa",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/openpilot/selfdrive/controls/lib/ford_model_action.py": "1a4ce5f5f63b4d2f1f6e0537c9b2ca7c463ca44b427349d71d28fb8a1138b00f",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/model_action_replay.py": "af97c665f342c66b1be2502e188c63e6f3ee106d0a0d5e80997bc3040373ff9f"
}
},
"replays": {
"112": {
"baseline_revision": "17a86842f",
"baseline_source_sha256": "167ae5a01fdd7ea014e6ad3fe9d0b6e31c67de8ba057ec5ecf18ab38fc16353f",
"calibration_approved": false,
"cycles": 108971,
"active_cycles": 91414,
"validity_matches_baseline_exactly": true,
"status_counts": {
"inactive": 17557,
"active": 91414
},
"c0_matches_baseline_exactly": true,
"c0_changed_cycles": 0,
"max_abs_c0_change_m": 0.0,
"offset_overflow_seconds": 1.43945091800002,
"max_abs_offset_overflow_target_m": 0.5727187991142273,
"request_release_cycles": 414,
"request_release_seconds": 4.192443282002046,
"max_abs_request_release_rad": 0.008723706007003784,
"feedback_enabled_seconds": 840.7664582650004,
"pscm_limit_2_seconds": 14.441855805999936,
"c1_changed_cycles": 58152,
"max_abs_c1_change_rad": 0.023500000000000076,
"max_abs_correction_rad": 0.205313389369823,
"can_round_trips": 108971,
"source_sha256": {
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route112/route.npz": "2b08a2fb636f7d14556d7df4035eafc1d1b97932237955528562a16db2b31d3e",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route112/model_paths.npz": "9837afe78aab4cad288cad98a595a5777fa8a66bb235986b1272a7f7c54e559a",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route112/metadata.json": "726d78a7e7aa45307dcfe27cb00775c20ecc9d54538eb7f26a0166fc216226ce",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/feedback_replay.py": "2e9ea03d4947c7bb3a02c864e0dbc7c9bf031af51dba5e44a8aaa137c3aac6b2",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/openpilot/selfdrive/controls/lib/ford_model_action.py": "5673630d31910fcfa5a3cc9a8d533b6b8fe9a76e2627bf1f67b52b3550ec7442"
},
"timing_limit": "Controls publication time proxies the computation clock; full SubMaster checks are unavailable.",
"reference_limit": "Uses exact consumed model publication as reference; selected maneuver-plan messages are not reconstructed.",
"targeted_points": [
{
"time_s": 700.5553145380001,
"baseline_c0_c1": [
-0.34999999999999964,
0.010000000000000009
],
"candidate_c0_c1": [
-0.34999999999999964,
0.008500000000000008
]
}
]
},
"113": {
"baseline_revision": "17a86842f",
"baseline_source_sha256": "167ae5a01fdd7ea014e6ad3fe9d0b6e31c67de8ba057ec5ecf18ab38fc16353f",
"calibration_approved": false,
"cycles": 49614,
"active_cycles": 27207,
"validity_matches_baseline_exactly": true,
"status_counts": {
"inactive": 22407,
"active": 27207
},
"c0_matches_baseline_exactly": true,
"c0_changed_cycles": 0,
"max_abs_c0_change_m": 0.0,
"offset_overflow_seconds": 2.7607263189997866,
"max_abs_offset_overflow_target_m": 0.6065444126725197,
"request_release_cycles": 119,
"request_release_seconds": 1.235422420998475,
"max_abs_request_release_rad": 0.009946223348379135,
"feedback_enabled_seconds": 243.3033761190004,
"pscm_limit_2_seconds": 14.003248144999816,
"c1_changed_cycles": 17825,
"max_abs_c1_change_rad": 0.027500000000000024,
"max_abs_correction_rad": 0.16966817302181283,
"can_round_trips": 49614,
"source_sha256": {
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route113/route.npz": "774ca4a21b7113c2706d6130bc180c3216ea4833155300ab01e75b3486e36327",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route113/model_paths.npz": "93c41761eb85263f534f5371b905482cf7c948582eb1e9149966594be1d3768f",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route113/metadata.json": "1c0ca74dd48b90ab9d5444c5ca7f8aa9361700bbdf98bd5e853be50ad2895d7f",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/feedback_replay.py": "2e9ea03d4947c7bb3a02c864e0dbc7c9bf031af51dba5e44a8aaa137c3aac6b2",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/openpilot/selfdrive/controls/lib/ford_model_action.py": "5673630d31910fcfa5a3cc9a8d533b6b8fe9a76e2627bf1f67b52b3550ec7442"
},
"timing_limit": "Controls publication time proxies the computation clock; full SubMaster checks are unavailable.",
"reference_limit": "Uses exact consumed model publication as reference; selected maneuver-plan messages are not reconstructed.",
"old_correction_zero_s": 199.2531750590001,
"new_correction_zero_s": 198.62974550599984,
"earlier_correction_zero_s": 0.6234295530002782,
"targeted_points": [
{
"time_s": 198.64943081699994,
"baseline_c0_c1": [
2.16,
0.2925
],
"candidate_c0_c1": [
2.16,
0.319
]
},
{
"time_s": 481.56693996600006,
"baseline_c0_c1": [
0.5800000000000001,
-0.0645
],
"candidate_c0_c1": [
0.5800000000000001,
-0.0645
]
}
]
},
"b9": {
"baseline_revision": "17a86842f",
"baseline_source_sha256": "167ae5a01fdd7ea014e6ad3fe9d0b6e31c67de8ba057ec5ecf18ab38fc16353f",
"calibration_approved": false,
"cycles": 90774,
"active_cycles": 86474,
"validity_matches_baseline_exactly": true,
"status_counts": {
"inactive": 4300,
"active": 86474
},
"c0_matches_baseline_exactly": true,
"c0_changed_cycles": 0,
"max_abs_c0_change_m": 0.0,
"offset_overflow_seconds": 5.703841178999909,
"max_abs_offset_overflow_target_m": 4.280821338295937,
"request_release_cycles": 440,
"request_release_seconds": 4.520361682000512,
"max_abs_request_release_rad": 0.017186015844345093,
"feedback_enabled_seconds": 816.0284774219999,
"pscm_limit_2_seconds": 9.308613716000167,
"c1_changed_cycles": 48629,
"max_abs_c1_change_rad": 0.03500000000000003,
"max_abs_correction_rad": 0.18457476562660308,
"can_round_trips": 90774,
"source_sha256": {
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_routeb9/route.npz": "b07c789d8155335f5d120d0262fced6e4d5803fe767b0ff49b6413dce4140b5c",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_routeb9/model_paths.npz": "6b1f87897c050273fdc05af051307a049b6fc3a93072e7cda1721195ce7c3861",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_routeb9/metadata.json": "9ce452220cab61b81883f32fc2fcaf5db6c78a674cb255a49cc77d5029580fee",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/feedback_replay.py": "cd76c6106b2e41e905b752f1638d5b3e0feaa10ec21e038268df33183a91c640",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/openpilot/selfdrive/controls/lib/ford_model_action.py": "1a4ce5f5f63b4d2f1f6e0537c9b2ca7c463ca44b427349d71d28fb8a1138b00f"
},
"timing_limit": "Controls publication time proxies the computation clock; full SubMaster checks are unavailable.",
"reference_limit": "Uses exact consumed model publication as reference; selected maneuver-plan messages are not reconstructed."
}
},
"float32_can_round_trips_excluding_integration_tests": 667497,
"checks": {
"ruff": true,
"controller_ty": true,
"settings_compilation": true,
"toggle_off_upstream_integration": true
},
"final_source_sha256": {
"openpilot/selfdrive/controls/lib/ford_model_action.py": "1a4ce5f5f63b4d2f1f6e0537c9b2ca7c463ca44b427349d71d28fb8a1138b00f",
"openpilot/selfdrive/controls/tests/test_ford_model_action_request_release.py": "4b4a69d67eba0ff9d5db0a50a4f868c5eb5f7c8c79d70832500554febc7d28fe",
"openpilot/selfdrive/controls/tests/test_ford_model_action.py": "7b2429a5c40e5067b8edea4c11e9cdd4c6271d09f7982e30126eb42b93425a50",
"openpilot/selfdrive/controls/tests/test_ford_model_action_adapter.py": "5943a37f6e3865297ac543fb922a3c8b6e016c5af3eff589f518bd9615bbdb4f",
"openpilot/selfdrive/controls/tests/test_ford_controlsd_logging.py": "df883702a847465815feb6c4fbf88130c27e69d7e3e256c9962d99a9738ee353",
"tools/ford_pscm_lab/feedback_replay.py": "cd76c6106b2e41e905b752f1638d5b3e0feaa10ec21e038268df33183a91c640"
},
"source_note": "Controller comment/docstring cleanup followed feedback stress and routes 112/113. The recorded tested hashes are retained; b9 and zero-error stress record the final controller source. No executable controller change followed those runs."
}
-99
View File
@@ -1,99 +0,0 @@
# Desired-curvature C0 on continuous PI
This local candidate changes C0's reference from the live model path's lateral
position at 7 m to a circular arc of the selected, upstream-limited desired
curvature. It is based on v7 (`08b3a14ad`), with the same P=0.50/I=0.25 controller.
No new gain, state, release condition, reference delay or output limit is added.
The candidate is on `codex/ford-curvature-c0-trial`; this evaluation does not
publish it over the v7 onroad branch.
For selected curvature k and the existing 7 m reference distance:
```text
arc C0 = (1 cos(7 × k)) / k, or 0 when k = 0
≈ 24.5 × k for small curvature
C0 target = clip(arc C0 + 7 × clipped-away base C1, 5.11, +5.11)
```
The implementation uses the equivalent squared-sinc expression to avoid
subtracting nearly equal floating-point numbers near zero. C0 keeps its 4 m/s
output slew and Float32/CAN quantization. C1 keeps the existing mapping and PI
law, ±0.5 rad bound and 0.5 rad/s slew. C2 and C3 stay zero.
This is a geometric reference choice, not a model of the PSCM. The arc starts
at zero lateral position and heading. Independent live model-path position and
heading are omitted, while selected curvature can still include the model's
centering decision. Valid live model geometry remains a health gate. Short valid
paths do not shorten the synthetic 7 m arc. Both C0 and C1 use the selected
request, including the maneuver source when selected by controlsd.
## What the recorded routes show
All fourteen previous routes were replayed with v7 and this C0 replacement:
Lightning 112117, a0, a2, a5, a9, b8, b9, ca and Raptor 02. On all 1,578,250
control cycles, C1, P, I, activation, feedforward, overflow and feedback/PSCM
gates match exactly. The v7 baseline also reproduces its archived commands
exactly. C0 matches the earlier isolated geometry experiment, but C1 no longer
has the release rules that coupled it to C0 in that experiment.
Duration-weighted clean samples, grouped by requested steering-wheel angle:
| Absolute requested wheel angle | v7 mean absolute C0 | Curvature C0 | Reduction |
| --- | ---: | ---: | ---: |
| Under 5° | 0.0136 m | 0.0072 m | 46.7% |
| 530° | 0.0965 m | 0.0608 m | 37.0% |
| 3090° | 0.5064 m | 0.2916 m | 42.4% |
| At least 90° | 2.6314 m | 1.5282 m | 41.9% |
The clean cohort is 6,654.52 s with the existing quality/driver mask and margins,
speed at least 2 m/s and replay feedback enabled. Only 36.20 s have a requested
wheel angle of at least 90°. These are command magnitudes, not torque or tracking
scores, and do not classify driver interventions as controller failures.
At route 117, 128.272 s (right entry), C0 changes from 2.51 m to 1.35 m while
C1 remains 0.427 rad. At 142.670 s (request relaxing/reversing), C0 changes from
+0.18 m to 0.02 m while C1 remains 0.0425 rad. Both comparisons are replayed
on the same recorded vehicle motion. The new C0 follows the selected request
more directly, but it supplies less C0 during the entry as well as the exit.
## Validation and interpretation
- 694 Ford/controlsd, tracked PSCM lab, Sunnylink, params, sender and safety
tests pass; 9,145 subtests pass and 178 platform tests skip. Historical
untracked offline experiment tests are outside this deployment suite.
- The isolated two-controller replay performs 3,156,500 Float32/CAN checks.
- Production selection/adapter replay exactly reproduces the isolated candidate
on every route cycle, adding 1,578,250 Float32/CAN checks.
- A 20,000-cycle scalar geometry/PI stress with mirrored and unrelated model
paths adds 60,000 checks. Total: 4,794,750 CAN round trips.
- Tests cover zero/tiny curvature, signs, circular geometry, short/malformed
paths, selected maneuver requests, overflow, unwind, duplicate measurements,
model/driver/PSCM gates, caps/slew and toggle-off upstream Ford fallback.
- Ruff, production Ty and diff whitespace checks pass.
Software C1 parity does not guarantee identical physical unwind: changing C0
changes the PSCM's input and therefore the vehicle response and future feedback.
Smaller C0 is not established as better or worse tracking. No device build,
boot or drive of this candidate is claimed. Collecting the promising v7 drive's
logs before replacing it would preserve a useful comparison.
## Reproduction
Use the built cereal/opendbc environment and the same local route extracts:
```sh
export PYTHONPATH=.:opendbc_repo:.cache/ford_v6/test_deps
export PYTHONDONTWRITEBYTECODE=1
export PARAMS_ROOT=/tmp/ford-c0-params
export LOG_ROOT=/tmp/ford-c0-logs
python -m tools.ford_pscm_lab.curvature_c0_v7_replay .cache/ford_route117 --output .cache/ford_curvature_c0_v7/117
python -m tools.ford_pscm_lab.curvature_c0_validate --output .cache/ford_curvature_c0_v7/production --workers 4
python -m tools.ford_pscm_lab.curvature_c0_production_stress --cycles 20000 --output .cache/ford_curvature_c0_v7/production_stress.json
```
Repeat the first command for each label before validating all routes. Raptor
uses input `.cache/ford_raptor_route02` and output label `raptor02`. The first
replay loads isolated copies of pinned v7; the second tests this checkout's
actual selector and adapter. The validation JSON records source and extract
hashes, settings, example points and both sets of route reports. Older v7-only
production validation commands should run from the v7 commit.
-890
View File
@@ -1,890 +0,0 @@
{
"scope": "Curvature-derived C0 candidate on the continuous PI controller; fixed recorded motion, no physical tracking prediction.",
"baseline_revision": "08b3a14ad46260fda0f8d3a1c2cee2d272504153",
"branch": "codex/ford-curvature-c0-trial",
"deployment_at_evaluation": "local candidate; hiimisaac-dev remains v7",
"hypothesis": "model-action-curvature-c0-pi-v8",
"source_sha256": "6f8c4a6f7fa54fa2e23f32f1e83ace83da5f0b6f45011941f1902a1dae778ffc",
"kp": 0.5,
"ki": 0.25,
"station_m": 7.0,
"cycles": 1578250,
"route_count": 14,
"same_c1_and_integral_on_every_recorded_cycle": true,
"baseline_exactly_matches_archived_v7": true,
"c0_commands_match_prior_geometry_experiment": true,
"can_round_trips": 4794750,
"tests": {
"passed": 694,
"skipped": 178,
"subtests_passed": 9145,
"scope": "Same Ford controls, tracked PSCM lab, car status, Sunnylink, params, Ford car and safety suite as v7; historical untracked experiments excluded."
},
"ruff": "pass",
"ty_production": "pass",
"bins": [
{
"requested_wheel_angle_degrees": "0-5",
"seconds": 4863.242382185041,
"mean_abs_c0_m": [
0.013572257514406704,
0.007239040142159917
],
"reduction_pct": 46.662962042417725
},
{
"requested_wheel_angle_degrees": "5-30",
"seconds": 1576.952442509967,
"mean_abs_c0_m": [
0.09652124113032315,
0.06077627429873566
],
"reduction_pct": 37.03326481610879
},
{
"requested_wheel_angle_degrees": "30-90",
"seconds": 178.12595563801986,
"mean_abs_c0_m": [
0.5064290157685561,
0.2916269430470148
],
"reduction_pct": 42.4150406144399
},
{
"requested_wheel_angle_degrees": "90-inf",
"seconds": 36.19569558700505,
"mean_abs_c0_m": [
2.6314148997254545,
1.528200796263899
],
"reduction_pct": 41.92474944094366
}
],
"examples": [
{
"route": "116",
"time": 112.00019806100002,
"desired_angle": 59.83296203613281,
"actual_angle": 60.900001525878906,
"c0_left_positive": [
0.7599999999999998,
0.33999999999999986
],
"c1_both_left_positive": 0.13550000000000006,
"clean": true
},
{
"route": "117",
"time": 128.2721161039999,
"desired_angle": -226.72189331054688,
"actual_angle": -185.1999969482422,
"c0_left_positive": [
-2.5100000000000002,
-1.35
],
"c1_both_left_positive": -0.427,
"clean": true
},
{
"route": "116",
"time": 673.6933639450001,
"desired_angle": -152.83456420898438,
"actual_angle": -272.1000061035156,
"c0_left_positive": [
-2.21,
-0.9199999999999999
],
"c1_both_left_positive": -0.183,
"clean": true
},
{
"route": "117",
"time": 142.52000677500018,
"desired_angle": 6.6218461990356445,
"actual_angle": 40.599998474121094,
"c0_left_positive": [
0.2599999999999998,
0.040000000000000036
],
"c1_both_left_positive": -0.024499999999999966,
"clean": true
},
{
"route": "117",
"time": 142.66993146999994,
"desired_angle": -2.186957597732544,
"actual_angle": 28.5,
"c0_left_positive": [
0.17999999999999972,
-0.019999999999999574
],
"c1_both_left_positive": -0.04249999999999998,
"clean": true
}
],
"replay_reports": [
{
"scope": "Circular-arc C0 experiment against the pinned continuous PI v7 controller.\n\nBoth controllers run on identical recorded motion. Replace only the encoder's\nC0 target, retaining path validity, C1 overflow, continuous PI feedback and\noutput limits. No onroad selector or production module is modified.\n",
"baseline_revision": "08b3a14ad46260fda0f8d3a1c2cee2d272504153",
"baseline_source_sha256": "a899b8595e903d6fc4401392a8d123ef77725f6d6d7fa10392a7b3fe72e732b8",
"opendbc_revision": "64aa61b9b3fd26e70a7caa915acab207ff3cd64a",
"proportional_gain": 0.5,
"integral_gain": 0.25,
"station_m": 7.0,
"cycles": 108971,
"controller_updates": 217942,
"active_cycles_per_controller": 91414,
"can_round_trips": 217942,
"same_activation_p_feedforward_overflow_and_feedback_gates": true,
"changed_c1_cycles": 0,
"max_abs_c1_change_rad": 0.0,
"changed_integral_cycles": 0,
"pscm_status_fresh_active_seconds": 919.4963447830002,
"clean_seconds": 678.5721400560004,
"source_sha256": {
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route112/route.npz": "2b08a2fb636f7d14556d7df4035eafc1d1b97932237955528562a16db2b31d3e",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route112/model_paths.npz": "9837afe78aab4cad288cad98a595a5777fa8a66bb235986b1272a7f7c54e559a",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route112/metadata.json": "726d78a7e7aa45307dcfe27cb00775c20ecc9d54538eb7f26a0166fc216226ce",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route112/angles.npz": "9429813237b4988dd99a1461d1a9bd6f6d3056693a93a5dd1ec63567d19f2aa1",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/curvature_c0_v7_replay.py": "fcd8a10db3d68024dbc836dce1b4c9a459912837b1a4234f61b2dd508c6d5e9f"
},
"limitations": [
"No new steering trace or physical tracking score: recorded motion stays fixed.",
"The baseline is v7 on every route, regardless of its recorded controller version.",
"C1 and integral state match exactly on fixed recorded motion; physical feedback may differ.",
"Publication time proxies computation time; full SubMaster state is unavailable.",
"The synthetic arc keeps the original model-health gates for a controlled comparison."
]
},
{
"scope": "Circular-arc C0 experiment against the pinned continuous PI v7 controller.\n\nBoth controllers run on identical recorded motion. Replace only the encoder's\nC0 target, retaining path validity, C1 overflow, continuous PI feedback and\noutput limits. No onroad selector or production module is modified.\n",
"baseline_revision": "08b3a14ad46260fda0f8d3a1c2cee2d272504153",
"baseline_source_sha256": "a899b8595e903d6fc4401392a8d123ef77725f6d6d7fa10392a7b3fe72e732b8",
"opendbc_revision": "64aa61b9b3fd26e70a7caa915acab207ff3cd64a",
"proportional_gain": 0.5,
"integral_gain": 0.25,
"station_m": 7.0,
"cycles": 49614,
"controller_updates": 99228,
"active_cycles_per_controller": 27207,
"can_round_trips": 99228,
"same_activation_p_feedforward_overflow_and_feedback_gates": true,
"changed_c1_cycles": 0,
"max_abs_c1_change_rad": 0.0,
"changed_integral_cycles": 0,
"pscm_status_fresh_active_seconds": 273.884368861,
"clean_seconds": 195.66672679699968,
"source_sha256": {
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route113/route.npz": "774ca4a21b7113c2706d6130bc180c3216ea4833155300ab01e75b3486e36327",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route113/model_paths.npz": "93c41761eb85263f534f5371b905482cf7c948582eb1e9149966594be1d3768f",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route113/metadata.json": "1c0ca74dd48b90ab9d5444c5ca7f8aa9361700bbdf98bd5e853be50ad2895d7f",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route113/angles.npz": "2f280a1b5973901d878f1e36564d6e7ae6d817cac176149f2cb898ab7591d65b",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/curvature_c0_v7_replay.py": "fcd8a10db3d68024dbc836dce1b4c9a459912837b1a4234f61b2dd508c6d5e9f"
},
"limitations": [
"No new steering trace or physical tracking score: recorded motion stays fixed.",
"The baseline is v7 on every route, regardless of its recorded controller version.",
"C1 and integral state match exactly on fixed recorded motion; physical feedback may differ.",
"Publication time proxies computation time; full SubMaster state is unavailable.",
"The synthetic arc keeps the original model-health gates for a controlled comparison."
]
},
{
"scope": "Circular-arc C0 experiment against the pinned continuous PI v7 controller.\n\nBoth controllers run on identical recorded motion. Replace only the encoder's\nC0 target, retaining path validity, C1 overflow, continuous PI feedback and\noutput limits. No onroad selector or production module is modified.\n",
"baseline_revision": "08b3a14ad46260fda0f8d3a1c2cee2d272504153",
"baseline_source_sha256": "a899b8595e903d6fc4401392a8d123ef77725f6d6d7fa10392a7b3fe72e732b8",
"opendbc_revision": "64aa61b9b3fd26e70a7caa915acab207ff3cd64a",
"proportional_gain": 0.5,
"integral_gain": 0.25,
"station_m": 7.0,
"cycles": 61027,
"controller_updates": 122054,
"active_cycles_per_controller": 48419,
"can_round_trips": 122054,
"same_activation_p_feedforward_overflow_and_feedback_gates": true,
"changed_c1_cycles": 0,
"max_abs_c1_change_rad": 0.0,
"changed_integral_cycles": 0,
"pscm_status_fresh_active_seconds": 487.557504183,
"clean_seconds": 290.3177928490001,
"source_sha256": {
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route114/route.npz": "83524f07d61104b84b004ddc46bb751ca7f61da67798aa47db56d307765f5b3e",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route114/model_paths.npz": "14d14362d1a3e46398edd8e22b7cc4e36277a7596a73f546192d0e14c6642b07",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route114/metadata.json": "ec677b9275c1707e477ebd0ffa235d49b5ec63a1ee717b16040c3acdbcd3bdc0",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route114/angles.npz": "63aa2ff6a21e8613519bda476690e2eda6f42cb5dcf108606bc6072c9e29408d",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/curvature_c0_v7_replay.py": "fcd8a10db3d68024dbc836dce1b4c9a459912837b1a4234f61b2dd508c6d5e9f"
},
"limitations": [
"No new steering trace or physical tracking score: recorded motion stays fixed.",
"The baseline is v7 on every route, regardless of its recorded controller version.",
"C1 and integral state match exactly on fixed recorded motion; physical feedback may differ.",
"Publication time proxies computation time; full SubMaster state is unavailable.",
"The synthetic arc keeps the original model-health gates for a controlled comparison."
]
},
{
"scope": "Circular-arc C0 experiment against the pinned continuous PI v7 controller.\n\nBoth controllers run on identical recorded motion. Replace only the encoder's\nC0 target, retaining path validity, C1 overflow, continuous PI feedback and\noutput limits. No onroad selector or production module is modified.\n",
"baseline_revision": "08b3a14ad46260fda0f8d3a1c2cee2d272504153",
"baseline_source_sha256": "a899b8595e903d6fc4401392a8d123ef77725f6d6d7fa10392a7b3fe72e732b8",
"opendbc_revision": "64aa61b9b3fd26e70a7caa915acab207ff3cd64a",
"proportional_gain": 0.5,
"integral_gain": 0.25,
"station_m": 7.0,
"cycles": 40037,
"controller_updates": 80074,
"active_cycles_per_controller": 33976,
"can_round_trips": 80074,
"same_activation_p_feedforward_overflow_and_feedback_gates": true,
"changed_c1_cycles": 0,
"max_abs_c1_change_rad": 0.0,
"changed_integral_cycles": 0,
"pscm_status_fresh_active_seconds": 341.89711117800005,
"clean_seconds": 176.48340830000006,
"source_sha256": {
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route115/route.npz": "e0df32d9e80f1c6b7d56327b37cf07af60070ffc212b8d111e343306148bff23",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route115/model_paths.npz": "52f3ed9e188e4947618a57a3ed872fd1966a887fe1014999df741553b6c13bc0",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route115/metadata.json": "d3c73035bad8eb5059e07962fd274c19cb70c8952b6f1514835d312d08b87a16",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route115/angles.npz": "71415b0f49efbfeda495ae52bde4beb7e16f31a1f5570af515a365e756056d3b",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/curvature_c0_v7_replay.py": "fcd8a10db3d68024dbc836dce1b4c9a459912837b1a4234f61b2dd508c6d5e9f"
},
"limitations": [
"No new steering trace or physical tracking score: recorded motion stays fixed.",
"The baseline is v7 on every route, regardless of its recorded controller version.",
"C1 and integral state match exactly on fixed recorded motion; physical feedback may differ.",
"Publication time proxies computation time; full SubMaster state is unavailable.",
"The synthetic arc keeps the original model-health gates for a controlled comparison."
]
},
{
"scope": "Circular-arc C0 experiment against the pinned continuous PI v7 controller.\n\nBoth controllers run on identical recorded motion. Replace only the encoder's\nC0 target, retaining path validity, C1 overflow, continuous PI feedback and\noutput limits. No onroad selector or production module is modified.\n",
"baseline_revision": "08b3a14ad46260fda0f8d3a1c2cee2d272504153",
"baseline_source_sha256": "a899b8595e903d6fc4401392a8d123ef77725f6d6d7fa10392a7b3fe72e732b8",
"opendbc_revision": "64aa61b9b3fd26e70a7caa915acab207ff3cd64a",
"proportional_gain": 0.5,
"integral_gain": 0.25,
"station_m": 7.0,
"cycles": 68793,
"controller_updates": 137586,
"active_cycles_per_controller": 52456,
"can_round_trips": 137586,
"same_activation_p_feedforward_overflow_and_feedback_gates": true,
"changed_c1_cycles": 0,
"max_abs_c1_change_rad": 0.0,
"changed_integral_cycles": 0,
"pscm_status_fresh_active_seconds": 527.966444073,
"clean_seconds": 218.019997628,
"source_sha256": {
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route116/route.npz": "13df1d49edd0c92bd4a4236c766ee76baa6dad477824b9de4b79927c98410394",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route116/model_paths.npz": "85d1112b5e89340e8a955b7fe4040b3135cf1c91a54ff756826c6119cc9cc01d",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route116/metadata.json": "d52fe63d013fa546524ad9522a26943d9372730d397d92d75dfd9d75c63ce766",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route116/angles.npz": "68e699e01c3ea291e6602ee53edf9cee27c0b461e16c57c3acc0ad12bf29b2aa",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/curvature_c0_v7_replay.py": "fcd8a10db3d68024dbc836dce1b4c9a459912837b1a4234f61b2dd508c6d5e9f"
},
"limitations": [
"No new steering trace or physical tracking score: recorded motion stays fixed.",
"The baseline is v7 on every route, regardless of its recorded controller version.",
"C1 and integral state match exactly on fixed recorded motion; physical feedback may differ.",
"Publication time proxies computation time; full SubMaster state is unavailable.",
"The synthetic arc keeps the original model-health gates for a controlled comparison."
]
},
{
"scope": "Circular-arc C0 experiment against the pinned continuous PI v7 controller.\n\nBoth controllers run on identical recorded motion. Replace only the encoder's\nC0 target, retaining path validity, C1 overflow, continuous PI feedback and\noutput limits. No onroad selector or production module is modified.\n",
"baseline_revision": "08b3a14ad46260fda0f8d3a1c2cee2d272504153",
"baseline_source_sha256": "a899b8595e903d6fc4401392a8d123ef77725f6d6d7fa10392a7b3fe72e732b8",
"opendbc_revision": "64aa61b9b3fd26e70a7caa915acab207ff3cd64a",
"proportional_gain": 0.5,
"integral_gain": 0.25,
"station_m": 7.0,
"cycles": 27301,
"controller_updates": 54602,
"active_cycles_per_controller": 15841,
"can_round_trips": 54602,
"same_activation_p_feedforward_overflow_and_feedback_gates": true,
"changed_c1_cycles": 0,
"max_abs_c1_change_rad": 0.0,
"changed_integral_cycles": 0,
"pscm_status_fresh_active_seconds": 159.4681704720001,
"clean_seconds": 74.58932843500065,
"source_sha256": {
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route117/route.npz": "65e4ea6c76dce73018b636479b3777116f1e58bb9813e6c1a7b79018b219140e",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route117/model_paths.npz": "079f6003968287530e1ed6f2243c727e80c2ef3d16fb8a7d945d7eca744d54e7",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route117/metadata.json": "348396b3059c5f54d7938cd3acf2bb268a813b47ff47561da7a94cb75c3f3114",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route117/angles.npz": "0575b2d888a6848649b9fb1f7df7c72140f0abbceaa1c451b6eecd034729ef7b",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/curvature_c0_v7_replay.py": "fcd8a10db3d68024dbc836dce1b4c9a459912837b1a4234f61b2dd508c6d5e9f"
},
"limitations": [
"No new steering trace or physical tracking score: recorded motion stays fixed.",
"The baseline is v7 on every route, regardless of its recorded controller version.",
"C1 and integral state match exactly on fixed recorded motion; physical feedback may differ.",
"Publication time proxies computation time; full SubMaster state is unavailable.",
"The synthetic arc keeps the original model-health gates for a controlled comparison."
]
},
{
"scope": "Circular-arc C0 experiment against the pinned continuous PI v7 controller.\n\nBoth controllers run on identical recorded motion. Replace only the encoder's\nC0 target, retaining path validity, C1 overflow, continuous PI feedback and\noutput limits. No onroad selector or production module is modified.\n",
"baseline_revision": "08b3a14ad46260fda0f8d3a1c2cee2d272504153",
"baseline_source_sha256": "a899b8595e903d6fc4401392a8d123ef77725f6d6d7fa10392a7b3fe72e732b8",
"opendbc_revision": "64aa61b9b3fd26e70a7caa915acab207ff3cd64a",
"proportional_gain": 0.5,
"integral_gain": 0.25,
"station_m": 7.0,
"cycles": 114582,
"controller_updates": 229164,
"active_cycles_per_controller": 105382,
"can_round_trips": 229164,
"same_activation_p_feedforward_overflow_and_feedback_gates": true,
"changed_c1_cycles": 0,
"max_abs_c1_change_rad": 0.0,
"changed_integral_cycles": 0,
"pscm_status_fresh_active_seconds": 1061.1945591,
"clean_seconds": 884.2159145080002,
"source_sha256": {
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_routea0/route.npz": "01ce8b0361c3154d07d5449525a650baa2cc261675095cb1f04ec9e37a9174d3",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_routea0/model_paths.npz": "5d0c69d10a6d13882b47d950ae64c4d98f7ca2b59eab27e4e9557eef3bc4917b",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_routea0/metadata.json": "00496a11afbee475e9b5d66838a4d1663a3c0063cd562b4d1408cce6d71e586e",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_routea0/angles.npz": "feb7b558d75292ab69ce8657540f3295bbc1b0c915d27f39d831bdece3fac534",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/curvature_c0_v7_replay.py": "fcd8a10db3d68024dbc836dce1b4c9a459912837b1a4234f61b2dd508c6d5e9f"
},
"limitations": [
"No new steering trace or physical tracking score: recorded motion stays fixed.",
"The baseline is v7 on every route, regardless of its recorded controller version.",
"C1 and integral state match exactly on fixed recorded motion; physical feedback may differ.",
"Publication time proxies computation time; full SubMaster state is unavailable.",
"The synthetic arc keeps the original model-health gates for a controlled comparison."
]
},
{
"scope": "Circular-arc C0 experiment against the pinned continuous PI v7 controller.\n\nBoth controllers run on identical recorded motion. Replace only the encoder's\nC0 target, retaining path validity, C1 overflow, continuous PI feedback and\noutput limits. No onroad selector or production module is modified.\n",
"baseline_revision": "08b3a14ad46260fda0f8d3a1c2cee2d272504153",
"baseline_source_sha256": "a899b8595e903d6fc4401392a8d123ef77725f6d6d7fa10392a7b3fe72e732b8",
"opendbc_revision": "64aa61b9b3fd26e70a7caa915acab207ff3cd64a",
"proportional_gain": 0.5,
"integral_gain": 0.25,
"station_m": 7.0,
"cycles": 71111,
"controller_updates": 142222,
"active_cycles_per_controller": 43430,
"can_round_trips": 142222,
"same_activation_p_feedforward_overflow_and_feedback_gates": true,
"changed_c1_cycles": 0,
"max_abs_c1_change_rad": 0.0,
"changed_integral_cycles": 0,
"pscm_status_fresh_active_seconds": 437.0644468180001,
"clean_seconds": 332.6767952400001,
"source_sha256": {
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_routea2/route.npz": "af4128d278be6be0042f72a02255ddbec96a8a5aa435b9852c8202e4a5cb39e5",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_routea2/model_paths.npz": "5a5a7c4fc30c7b3bc7f2413daa8b11fe86b69dd3249e8df2d35b0a0636a49717",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_routea2/metadata.json": "cfecd240216e409a1e2591c6c2e6f0bad002ecd2a50c8f6dbf22a8f9174708da",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_routea2/angles.npz": "2424b32f02f7e5079f8ae6ffdd30a811820d1d1bfa5b17636c5457eb67d64ecb",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/curvature_c0_v7_replay.py": "fcd8a10db3d68024dbc836dce1b4c9a459912837b1a4234f61b2dd508c6d5e9f"
},
"limitations": [
"No new steering trace or physical tracking score: recorded motion stays fixed.",
"The baseline is v7 on every route, regardless of its recorded controller version.",
"C1 and integral state match exactly on fixed recorded motion; physical feedback may differ.",
"Publication time proxies computation time; full SubMaster state is unavailable.",
"The synthetic arc keeps the original model-health gates for a controlled comparison."
]
},
{
"scope": "Circular-arc C0 experiment against the pinned continuous PI v7 controller.\n\nBoth controllers run on identical recorded motion. Replace only the encoder's\nC0 target, retaining path validity, C1 overflow, continuous PI feedback and\noutput limits. No onroad selector or production module is modified.\n",
"baseline_revision": "08b3a14ad46260fda0f8d3a1c2cee2d272504153",
"baseline_source_sha256": "a899b8595e903d6fc4401392a8d123ef77725f6d6d7fa10392a7b3fe72e732b8",
"opendbc_revision": "64aa61b9b3fd26e70a7caa915acab207ff3cd64a",
"proportional_gain": 0.5,
"integral_gain": 0.25,
"station_m": 7.0,
"cycles": 38961,
"controller_updates": 77922,
"active_cycles_per_controller": 35677,
"can_round_trips": 77922,
"same_activation_p_feedforward_overflow_and_feedback_gates": true,
"changed_c1_cycles": 0,
"max_abs_c1_change_rad": 0.0,
"changed_integral_cycles": 0,
"pscm_status_fresh_active_seconds": 359.186785903,
"clean_seconds": 249.91415584299997,
"source_sha256": {
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_routea5/route.npz": "815d1e248ff5c3e5e5cfc11dfbd0690d0d13ddafe1436890e239975a72dae9b8",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_routea5/model_paths.npz": "72e014b844322df6318f6af588c27175ce68d9a37bda62b70eafca5bdfe6dfe4",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_routea5/metadata.json": "c533b06d1b7c598330b36b117bdb3624221bc4193ded8354246d5c86f36b2628",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_routea5/angles.npz": "5466a674cae3f63eca8758f9f370a8c6cdee3dbb6bcf115f85e3b2e95cf5c1fb",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/curvature_c0_v7_replay.py": "fcd8a10db3d68024dbc836dce1b4c9a459912837b1a4234f61b2dd508c6d5e9f"
},
"limitations": [
"No new steering trace or physical tracking score: recorded motion stays fixed.",
"The baseline is v7 on every route, regardless of its recorded controller version.",
"C1 and integral state match exactly on fixed recorded motion; physical feedback may differ.",
"Publication time proxies computation time; full SubMaster state is unavailable.",
"The synthetic arc keeps the original model-health gates for a controlled comparison."
]
},
{
"scope": "Circular-arc C0 experiment against the pinned continuous PI v7 controller.\n\nBoth controllers run on identical recorded motion. Replace only the encoder's\nC0 target, retaining path validity, C1 overflow, continuous PI feedback and\noutput limits. No onroad selector or production module is modified.\n",
"baseline_revision": "08b3a14ad46260fda0f8d3a1c2cee2d272504153",
"baseline_source_sha256": "a899b8595e903d6fc4401392a8d123ef77725f6d6d7fa10392a7b3fe72e732b8",
"opendbc_revision": "64aa61b9b3fd26e70a7caa915acab207ff3cd64a",
"proportional_gain": 0.5,
"integral_gain": 0.25,
"station_m": 7.0,
"cycles": 286319,
"controller_updates": 572638,
"active_cycles_per_controller": 152043,
"can_round_trips": 572638,
"same_activation_p_feedforward_overflow_and_feedback_gates": true,
"changed_c1_cycles": 0,
"max_abs_c1_change_rad": 0.0,
"changed_integral_cycles": 0,
"pscm_status_fresh_active_seconds": 1530.3877326270015,
"clean_seconds": 1093.2748892979992,
"source_sha256": {
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_routea9/route.npz": "3fa8cabfb729dd42d689de38618d1d21c8965c9d60f914546f4d7cc58db8c975",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_routea9/model_paths.npz": "7f3646ad24aa52de5982c65600813de5bbc5f7664d92a2516bbba35da6e7b240",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_routea9/metadata.json": "403d35504bf596144845ac2060ce067d571f49a2955d16223af28a79a7a66e93",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_routea9/angles.npz": "cfcc9743aacfadc7be97e9ca130453c2ee6f7870edc3e98ccf9a873dac6d4897",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/curvature_c0_v7_replay.py": "fcd8a10db3d68024dbc836dce1b4c9a459912837b1a4234f61b2dd508c6d5e9f"
},
"limitations": [
"No new steering trace or physical tracking score: recorded motion stays fixed.",
"The baseline is v7 on every route, regardless of its recorded controller version.",
"C1 and integral state match exactly on fixed recorded motion; physical feedback may differ.",
"Publication time proxies computation time; full SubMaster state is unavailable.",
"The synthetic arc keeps the original model-health gates for a controlled comparison."
]
},
{
"scope": "Circular-arc C0 experiment against the pinned continuous PI v7 controller.\n\nBoth controllers run on identical recorded motion. Replace only the encoder's\nC0 target, retaining path validity, C1 overflow, continuous PI feedback and\noutput limits. No onroad selector or production module is modified.\n",
"baseline_revision": "08b3a14ad46260fda0f8d3a1c2cee2d272504153",
"baseline_source_sha256": "a899b8595e903d6fc4401392a8d123ef77725f6d6d7fa10392a7b3fe72e732b8",
"opendbc_revision": "64aa61b9b3fd26e70a7caa915acab207ff3cd64a",
"proportional_gain": 0.5,
"integral_gain": 0.25,
"station_m": 7.0,
"cycles": 160431,
"controller_updates": 320862,
"active_cycles_per_controller": 68217,
"can_round_trips": 320862,
"same_activation_p_feedforward_overflow_and_feedback_gates": true,
"changed_c1_cycles": 0,
"max_abs_c1_change_rad": 0.0,
"changed_integral_cycles": 0,
"pscm_status_fresh_active_seconds": 686.4382411050101,
"clean_seconds": 390.78219015704235,
"source_sha256": {
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_routeb8/route.npz": "6f5dd369b70eaed4b95b28c8b25c9f2e9b830fa07a334881a185505481667c8b",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_routeb8/model_paths.npz": "939af6cf7e74251d8842581cc078d26d9fbfd22a0d7817cb0e368697d419b615",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_routeb8/metadata.json": "73b439132d1de37ec187b544c04d2b05c80965065515a4b7dec29ba57ae37e7c",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_routeb8/angles.npz": "6f4ea54d101595a48d301a0914c940d6ad932249b14d4eaa4fd7e43a11dc6f92",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/curvature_c0_v7_replay.py": "fcd8a10db3d68024dbc836dce1b4c9a459912837b1a4234f61b2dd508c6d5e9f"
},
"limitations": [
"No new steering trace or physical tracking score: recorded motion stays fixed.",
"The baseline is v7 on every route, regardless of its recorded controller version.",
"C1 and integral state match exactly on fixed recorded motion; physical feedback may differ.",
"Publication time proxies computation time; full SubMaster state is unavailable.",
"The synthetic arc keeps the original model-health gates for a controlled comparison."
]
},
{
"scope": "Circular-arc C0 experiment against the pinned continuous PI v7 controller.\n\nBoth controllers run on identical recorded motion. Replace only the encoder's\nC0 target, retaining path validity, C1 overflow, continuous PI feedback and\noutput limits. No onroad selector or production module is modified.\n",
"baseline_revision": "08b3a14ad46260fda0f8d3a1c2cee2d272504153",
"baseline_source_sha256": "a899b8595e903d6fc4401392a8d123ef77725f6d6d7fa10392a7b3fe72e732b8",
"opendbc_revision": "64aa61b9b3fd26e70a7caa915acab207ff3cd64a",
"proportional_gain": 0.5,
"integral_gain": 0.25,
"station_m": 7.0,
"cycles": 90774,
"controller_updates": 181548,
"active_cycles_per_controller": 86474,
"can_round_trips": 181548,
"same_activation_p_feedforward_overflow_and_feedback_gates": true,
"changed_c1_cycles": 0,
"max_abs_c1_change_rad": 0.0,
"changed_integral_cycles": 0,
"pscm_status_fresh_active_seconds": 870.449417766,
"clean_seconds": 604.9269013949997,
"source_sha256": {
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_routeb9/route.npz": "b07c789d8155335f5d120d0262fced6e4d5803fe767b0ff49b6413dce4140b5c",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_routeb9/model_paths.npz": "6b1f87897c050273fdc05af051307a049b6fc3a93072e7cda1721195ce7c3861",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_routeb9/metadata.json": "9ce452220cab61b81883f32fc2fcaf5db6c78a674cb255a49cc77d5029580fee",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_routeb9/angles.npz": "ce16212d9b13e41993d2c8df3246ddfe21324ad0cb213310e62eee1d685b197b",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/curvature_c0_v7_replay.py": "fcd8a10db3d68024dbc836dce1b4c9a459912837b1a4234f61b2dd508c6d5e9f"
},
"limitations": [
"No new steering trace or physical tracking score: recorded motion stays fixed.",
"The baseline is v7 on every route, regardless of its recorded controller version.",
"C1 and integral state match exactly on fixed recorded motion; physical feedback may differ.",
"Publication time proxies computation time; full SubMaster state is unavailable.",
"The synthetic arc keeps the original model-health gates for a controlled comparison."
]
},
{
"scope": "Circular-arc C0 experiment against the pinned continuous PI v7 controller.\n\nBoth controllers run on identical recorded motion. Replace only the encoder's\nC0 target, retaining path validity, C1 overflow, continuous PI feedback and\noutput limits. No onroad selector or production module is modified.\n",
"baseline_revision": "08b3a14ad46260fda0f8d3a1c2cee2d272504153",
"baseline_source_sha256": "a899b8595e903d6fc4401392a8d123ef77725f6d6d7fa10392a7b3fe72e732b8",
"opendbc_revision": "64aa61b9b3fd26e70a7caa915acab207ff3cd64a",
"proportional_gain": 0.5,
"integral_gain": 0.25,
"station_m": 7.0,
"cycles": 327448,
"controller_updates": 654896,
"active_cycles_per_controller": 118756,
"can_round_trips": 654896,
"same_activation_p_feedforward_overflow_and_feedback_gates": true,
"changed_c1_cycles": 0,
"max_abs_c1_change_rad": 0.0,
"changed_integral_cycles": 0,
"pscm_status_fresh_active_seconds": 1194.5832199259985,
"clean_seconds": 894.0687052669919,
"source_sha256": {
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_routeca/route.npz": "ae9d46770eaf0dbbac6af86aebc926320eed0cf114eb43d5f78b0676e8e0dbf9",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_routeca/model_paths.npz": "bf17deb442383aaa79432566cd382df24a1bbbbd0521d0cafab956618f5bdd96",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_routeca/metadata.json": "a759d5cdf878df8b05d91db637b1935b6b4bdd87af96f0f256b67e7d809b3525",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_routeca/angles.npz": "1681bda6db7dac332abf627d97e7452acd7e0faef0c196b11e55e22291cad073",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/curvature_c0_v7_replay.py": "fcd8a10db3d68024dbc836dce1b4c9a459912837b1a4234f61b2dd508c6d5e9f"
},
"limitations": [
"No new steering trace or physical tracking score: recorded motion stays fixed.",
"The baseline is v7 on every route, regardless of its recorded controller version.",
"C1 and integral state match exactly on fixed recorded motion; physical feedback may differ.",
"Publication time proxies computation time; full SubMaster state is unavailable.",
"The synthetic arc keeps the original model-health gates for a controlled comparison."
]
},
{
"scope": "Circular-arc C0 experiment against the pinned continuous PI v7 controller.\n\nBoth controllers run on identical recorded motion. Replace only the encoder's\nC0 target, retaining path validity, C1 overflow, continuous PI feedback and\noutput limits. No onroad selector or production module is modified.\n",
"baseline_revision": "08b3a14ad46260fda0f8d3a1c2cee2d272504153",
"baseline_source_sha256": "a899b8595e903d6fc4401392a8d123ef77725f6d6d7fa10392a7b3fe72e732b8",
"opendbc_revision": "64aa61b9b3fd26e70a7caa915acab207ff3cd64a",
"proportional_gain": 0.5,
"integral_gain": 0.25,
"station_m": 7.0,
"cycles": 132881,
"controller_updates": 265762,
"active_cycles_per_controller": 64776,
"can_round_trips": 265762,
"same_activation_p_feedforward_overflow_and_feedback_gates": true,
"changed_c1_cycles": 0,
"max_abs_c1_change_rad": 0.0,
"changed_integral_cycles": 0,
"pscm_status_fresh_active_seconds": 651.3446633269996,
"clean_seconds": 571.0075301469988,
"source_sha256": {
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_raptor_route02/route.npz": "8a4cfe53994988d053b47d9caadf21ebd064fd20e598fa05ddc2da245bd0e980",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_raptor_route02/model_paths.npz": "d5dc6f72d91472b8f2fb1add1680cdaea1e122774ddd6d8519fd7673daf71bb5",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_raptor_route02/metadata.json": "9602e08efc2bd784133837c4156c075bb89bad3bedd6b309f15e884ed20e338a",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_raptor_route02/angles.npz": "82af2973ad40c1df392e11bdc5dd5895bfc6c170fc894735123af7704d95cd57",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/curvature_c0_v7_replay.py": "fcd8a10db3d68024dbc836dce1b4c9a459912837b1a4234f61b2dd508c6d5e9f"
},
"limitations": [
"No new steering trace or physical tracking score: recorded motion stays fixed.",
"The baseline is v7 on every route, regardless of its recorded controller version.",
"C1 and integral state match exactly on fixed recorded motion; physical feedback may differ.",
"Publication time proxies computation time; full SubMaster state is unavailable.",
"The synthetic arc keeps the original model-health gates for a controlled comparison."
]
}
],
"production_parity": [
{
"scope": "Verify production commands against the archived curvature-C0 offline candidate.\n\nFixed recorded motion verifies integration parity, not physical tracking.\n",
"route": "112",
"cycles": 108971,
"can_round_trips": 108971,
"production_matches_archived_trial_exactly": true,
"opendbc_revision": "64aa61b9b3fd26e70a7caa915acab207ff3cd64a",
"hypothesis": "model-action-curvature-c0-pi-v8",
"kp": 0.5,
"ki": 0.25,
"source_sha256": {
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route112/route.npz": "2b08a2fb636f7d14556d7df4035eafc1d1b97932237955528562a16db2b31d3e",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route112/model_paths.npz": "9837afe78aab4cad288cad98a595a5777fa8a66bb235986b1272a7f7c54e559a",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route112/metadata.json": "726d78a7e7aa45307dcfe27cb00775c20ecc9d54538eb7f26a0166fc216226ce",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_curvature_c0_v7/112/report.json": "1be0b52e00e8f67043ce35e19cc6612f788d6e3853b40d1bc2dd372b2ff4e70d",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_curvature_c0_v7/112/commands.npz": "3951aa6160295ee6c3835517b39c7c835074b93b1bda5fb013d4ef56f88b8f1e",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/curvature_c0_validate.py": "c91ead82b0320725276141b1bc9b84b132cd6345a144ea826ba2b7f069870932",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/openpilot/selfdrive/controls/lib/ford_model_action.py": "6f8c4a6f7fa54fa2e23f32f1e83ace83da5f0b6f45011941f1902a1dae778ffc"
}
},
{
"scope": "Verify production commands against the archived curvature-C0 offline candidate.\n\nFixed recorded motion verifies integration parity, not physical tracking.\n",
"route": "113",
"cycles": 49614,
"can_round_trips": 49614,
"production_matches_archived_trial_exactly": true,
"opendbc_revision": "64aa61b9b3fd26e70a7caa915acab207ff3cd64a",
"hypothesis": "model-action-curvature-c0-pi-v8",
"kp": 0.5,
"ki": 0.25,
"source_sha256": {
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route113/route.npz": "774ca4a21b7113c2706d6130bc180c3216ea4833155300ab01e75b3486e36327",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route113/model_paths.npz": "93c41761eb85263f534f5371b905482cf7c948582eb1e9149966594be1d3768f",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route113/metadata.json": "1c0ca74dd48b90ab9d5444c5ca7f8aa9361700bbdf98bd5e853be50ad2895d7f",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_curvature_c0_v7/113/report.json": "c6e98c0f2e6f43f1ede8d8104b77606a91fb3e95efa8348f7f089e01a4f6c823",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_curvature_c0_v7/113/commands.npz": "0a7aa93e8e3e64659022dac254b476e06fb95649c4b97e1de66e4808fae73d6f",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/curvature_c0_validate.py": "c91ead82b0320725276141b1bc9b84b132cd6345a144ea826ba2b7f069870932",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/openpilot/selfdrive/controls/lib/ford_model_action.py": "6f8c4a6f7fa54fa2e23f32f1e83ace83da5f0b6f45011941f1902a1dae778ffc"
}
},
{
"scope": "Verify production commands against the archived curvature-C0 offline candidate.\n\nFixed recorded motion verifies integration parity, not physical tracking.\n",
"route": "114",
"cycles": 61027,
"can_round_trips": 61027,
"production_matches_archived_trial_exactly": true,
"opendbc_revision": "64aa61b9b3fd26e70a7caa915acab207ff3cd64a",
"hypothesis": "model-action-curvature-c0-pi-v8",
"kp": 0.5,
"ki": 0.25,
"source_sha256": {
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route114/route.npz": "83524f07d61104b84b004ddc46bb751ca7f61da67798aa47db56d307765f5b3e",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route114/model_paths.npz": "14d14362d1a3e46398edd8e22b7cc4e36277a7596a73f546192d0e14c6642b07",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route114/metadata.json": "ec677b9275c1707e477ebd0ffa235d49b5ec63a1ee717b16040c3acdbcd3bdc0",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_curvature_c0_v7/114/report.json": "b7f1d341f4f33d2c16dcfa2694b36cafe0bf76405d0e7b61bc4e469caef6b1dc",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_curvature_c0_v7/114/commands.npz": "3b79d094ecb89d3f3554c492d83826ec60662c236d6854891feba95fffcd7bc7",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/curvature_c0_validate.py": "c91ead82b0320725276141b1bc9b84b132cd6345a144ea826ba2b7f069870932",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/openpilot/selfdrive/controls/lib/ford_model_action.py": "6f8c4a6f7fa54fa2e23f32f1e83ace83da5f0b6f45011941f1902a1dae778ffc"
}
},
{
"scope": "Verify production commands against the archived curvature-C0 offline candidate.\n\nFixed recorded motion verifies integration parity, not physical tracking.\n",
"route": "115",
"cycles": 40037,
"can_round_trips": 40037,
"production_matches_archived_trial_exactly": true,
"opendbc_revision": "64aa61b9b3fd26e70a7caa915acab207ff3cd64a",
"hypothesis": "model-action-curvature-c0-pi-v8",
"kp": 0.5,
"ki": 0.25,
"source_sha256": {
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route115/route.npz": "e0df32d9e80f1c6b7d56327b37cf07af60070ffc212b8d111e343306148bff23",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route115/model_paths.npz": "52f3ed9e188e4947618a57a3ed872fd1966a887fe1014999df741553b6c13bc0",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route115/metadata.json": "d3c73035bad8eb5059e07962fd274c19cb70c8952b6f1514835d312d08b87a16",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_curvature_c0_v7/115/report.json": "e66d2be86136b0f6fdbcc2998673b2d1e8487f0543f99e4618e46a2f429538e4",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_curvature_c0_v7/115/commands.npz": "9a72eb6bab9116cc83210c92dfb12c5d63a2e4b3103ccdd3f5e70214f3f0e142",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/curvature_c0_validate.py": "c91ead82b0320725276141b1bc9b84b132cd6345a144ea826ba2b7f069870932",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/openpilot/selfdrive/controls/lib/ford_model_action.py": "6f8c4a6f7fa54fa2e23f32f1e83ace83da5f0b6f45011941f1902a1dae778ffc"
}
},
{
"scope": "Verify production commands against the archived curvature-C0 offline candidate.\n\nFixed recorded motion verifies integration parity, not physical tracking.\n",
"route": "116",
"cycles": 68793,
"can_round_trips": 68793,
"production_matches_archived_trial_exactly": true,
"opendbc_revision": "64aa61b9b3fd26e70a7caa915acab207ff3cd64a",
"hypothesis": "model-action-curvature-c0-pi-v8",
"kp": 0.5,
"ki": 0.25,
"source_sha256": {
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route116/route.npz": "13df1d49edd0c92bd4a4236c766ee76baa6dad477824b9de4b79927c98410394",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route116/model_paths.npz": "85d1112b5e89340e8a955b7fe4040b3135cf1c91a54ff756826c6119cc9cc01d",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route116/metadata.json": "d52fe63d013fa546524ad9522a26943d9372730d397d92d75dfd9d75c63ce766",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_curvature_c0_v7/116/report.json": "501b944c65f41f6c1e05fccdc6c38f3a80450db6c4696adc35bf85640a04df94",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_curvature_c0_v7/116/commands.npz": "2469c545d0ce62894d59e07799942ebf40bec583693391aaa1dd9e75c218a396",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/curvature_c0_validate.py": "c91ead82b0320725276141b1bc9b84b132cd6345a144ea826ba2b7f069870932",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/openpilot/selfdrive/controls/lib/ford_model_action.py": "6f8c4a6f7fa54fa2e23f32f1e83ace83da5f0b6f45011941f1902a1dae778ffc"
}
},
{
"scope": "Verify production commands against the archived curvature-C0 offline candidate.\n\nFixed recorded motion verifies integration parity, not physical tracking.\n",
"route": "117",
"cycles": 27301,
"can_round_trips": 27301,
"production_matches_archived_trial_exactly": true,
"opendbc_revision": "64aa61b9b3fd26e70a7caa915acab207ff3cd64a",
"hypothesis": "model-action-curvature-c0-pi-v8",
"kp": 0.5,
"ki": 0.25,
"source_sha256": {
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route117/route.npz": "65e4ea6c76dce73018b636479b3777116f1e58bb9813e6c1a7b79018b219140e",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route117/model_paths.npz": "079f6003968287530e1ed6f2243c727e80c2ef3d16fb8a7d945d7eca744d54e7",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route117/metadata.json": "348396b3059c5f54d7938cd3acf2bb268a813b47ff47561da7a94cb75c3f3114",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_curvature_c0_v7/117/report.json": "896b30e7ded613f1b05b693067fc2f19155016b6a33f26f881bbfff2b193019d",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_curvature_c0_v7/117/commands.npz": "6d88bfdb54fde00240954db9dbbf1f3e5059bfac9b6161c138f0069213ac35a8",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/curvature_c0_validate.py": "c91ead82b0320725276141b1bc9b84b132cd6345a144ea826ba2b7f069870932",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/openpilot/selfdrive/controls/lib/ford_model_action.py": "6f8c4a6f7fa54fa2e23f32f1e83ace83da5f0b6f45011941f1902a1dae778ffc"
}
},
{
"scope": "Verify production commands against the archived curvature-C0 offline candidate.\n\nFixed recorded motion verifies integration parity, not physical tracking.\n",
"route": "a0",
"cycles": 114582,
"can_round_trips": 114582,
"production_matches_archived_trial_exactly": true,
"opendbc_revision": "64aa61b9b3fd26e70a7caa915acab207ff3cd64a",
"hypothesis": "model-action-curvature-c0-pi-v8",
"kp": 0.5,
"ki": 0.25,
"source_sha256": {
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_routea0/route.npz": "01ce8b0361c3154d07d5449525a650baa2cc261675095cb1f04ec9e37a9174d3",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_routea0/model_paths.npz": "5d0c69d10a6d13882b47d950ae64c4d98f7ca2b59eab27e4e9557eef3bc4917b",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_routea0/metadata.json": "00496a11afbee475e9b5d66838a4d1663a3c0063cd562b4d1408cce6d71e586e",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_curvature_c0_v7/a0/report.json": "52cd87929fbdc721450c735bd02ac049260cafa439af7dcdcd405e977dc9ea22",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_curvature_c0_v7/a0/commands.npz": "0f718892b0802cbf754ba33c9c9c6da8a3e915e88fa7a4e008b9b33d63a429e0",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/curvature_c0_validate.py": "c91ead82b0320725276141b1bc9b84b132cd6345a144ea826ba2b7f069870932",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/openpilot/selfdrive/controls/lib/ford_model_action.py": "6f8c4a6f7fa54fa2e23f32f1e83ace83da5f0b6f45011941f1902a1dae778ffc"
}
},
{
"scope": "Verify production commands against the archived curvature-C0 offline candidate.\n\nFixed recorded motion verifies integration parity, not physical tracking.\n",
"route": "a2",
"cycles": 71111,
"can_round_trips": 71111,
"production_matches_archived_trial_exactly": true,
"opendbc_revision": "64aa61b9b3fd26e70a7caa915acab207ff3cd64a",
"hypothesis": "model-action-curvature-c0-pi-v8",
"kp": 0.5,
"ki": 0.25,
"source_sha256": {
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_routea2/route.npz": "af4128d278be6be0042f72a02255ddbec96a8a5aa435b9852c8202e4a5cb39e5",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_routea2/model_paths.npz": "5a5a7c4fc30c7b3bc7f2413daa8b11fe86b69dd3249e8df2d35b0a0636a49717",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_routea2/metadata.json": "cfecd240216e409a1e2591c6c2e6f0bad002ecd2a50c8f6dbf22a8f9174708da",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_curvature_c0_v7/a2/report.json": "fbaf59a1499df5dba0f790f76f211efefb3b68f327e9e49c763fb175f6f8e7a9",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_curvature_c0_v7/a2/commands.npz": "159f76d2d511af7f423941cc9d168c7112a8aec7a34e05bbd043f65a2324b325",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/curvature_c0_validate.py": "c91ead82b0320725276141b1bc9b84b132cd6345a144ea826ba2b7f069870932",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/openpilot/selfdrive/controls/lib/ford_model_action.py": "6f8c4a6f7fa54fa2e23f32f1e83ace83da5f0b6f45011941f1902a1dae778ffc"
}
},
{
"scope": "Verify production commands against the archived curvature-C0 offline candidate.\n\nFixed recorded motion verifies integration parity, not physical tracking.\n",
"route": "a5",
"cycles": 38961,
"can_round_trips": 38961,
"production_matches_archived_trial_exactly": true,
"opendbc_revision": "64aa61b9b3fd26e70a7caa915acab207ff3cd64a",
"hypothesis": "model-action-curvature-c0-pi-v8",
"kp": 0.5,
"ki": 0.25,
"source_sha256": {
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_routea5/route.npz": "815d1e248ff5c3e5e5cfc11dfbd0690d0d13ddafe1436890e239975a72dae9b8",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_routea5/model_paths.npz": "72e014b844322df6318f6af588c27175ce68d9a37bda62b70eafca5bdfe6dfe4",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_routea5/metadata.json": "c533b06d1b7c598330b36b117bdb3624221bc4193ded8354246d5c86f36b2628",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_curvature_c0_v7/a5/report.json": "ba9cedd6d9f2bbe2caa3ad0eeb298fa3334750ad60086422d7d48f3b690480f7",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_curvature_c0_v7/a5/commands.npz": "8ac5c77a29824b8f621c3071309990d320a1a61cc968ad55f76408a530680913",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/curvature_c0_validate.py": "c91ead82b0320725276141b1bc9b84b132cd6345a144ea826ba2b7f069870932",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/openpilot/selfdrive/controls/lib/ford_model_action.py": "6f8c4a6f7fa54fa2e23f32f1e83ace83da5f0b6f45011941f1902a1dae778ffc"
}
},
{
"scope": "Verify production commands against the archived curvature-C0 offline candidate.\n\nFixed recorded motion verifies integration parity, not physical tracking.\n",
"route": "a9",
"cycles": 286319,
"can_round_trips": 286319,
"production_matches_archived_trial_exactly": true,
"opendbc_revision": "64aa61b9b3fd26e70a7caa915acab207ff3cd64a",
"hypothesis": "model-action-curvature-c0-pi-v8",
"kp": 0.5,
"ki": 0.25,
"source_sha256": {
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_routea9/route.npz": "3fa8cabfb729dd42d689de38618d1d21c8965c9d60f914546f4d7cc58db8c975",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_routea9/model_paths.npz": "7f3646ad24aa52de5982c65600813de5bbc5f7664d92a2516bbba35da6e7b240",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_routea9/metadata.json": "403d35504bf596144845ac2060ce067d571f49a2955d16223af28a79a7a66e93",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_curvature_c0_v7/a9/report.json": "677a2f66ecc5696fa44190c5e3ee27cd3dc45f1deda9ffe3a2f330b4e1d0d20d",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_curvature_c0_v7/a9/commands.npz": "e109e77124dc2296a00f65563c1b1ec3c960b414473a3cc7c6caaafbe7e65037",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/curvature_c0_validate.py": "c91ead82b0320725276141b1bc9b84b132cd6345a144ea826ba2b7f069870932",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/openpilot/selfdrive/controls/lib/ford_model_action.py": "6f8c4a6f7fa54fa2e23f32f1e83ace83da5f0b6f45011941f1902a1dae778ffc"
}
},
{
"scope": "Verify production commands against the archived curvature-C0 offline candidate.\n\nFixed recorded motion verifies integration parity, not physical tracking.\n",
"route": "b8",
"cycles": 160431,
"can_round_trips": 160431,
"production_matches_archived_trial_exactly": true,
"opendbc_revision": "64aa61b9b3fd26e70a7caa915acab207ff3cd64a",
"hypothesis": "model-action-curvature-c0-pi-v8",
"kp": 0.5,
"ki": 0.25,
"source_sha256": {
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_routeb8/route.npz": "6f5dd369b70eaed4b95b28c8b25c9f2e9b830fa07a334881a185505481667c8b",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_routeb8/model_paths.npz": "939af6cf7e74251d8842581cc078d26d9fbfd22a0d7817cb0e368697d419b615",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_routeb8/metadata.json": "73b439132d1de37ec187b544c04d2b05c80965065515a4b7dec29ba57ae37e7c",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_curvature_c0_v7/b8/report.json": "f48796acdfad87a0caaaddac2a41945c97311e85fa57f9fc73a1730d264ddba4",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_curvature_c0_v7/b8/commands.npz": "554891332a496498ff64e08e4eba4210125fd5f2da8307231dd6d9377becfe0a",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/curvature_c0_validate.py": "c91ead82b0320725276141b1bc9b84b132cd6345a144ea826ba2b7f069870932",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/openpilot/selfdrive/controls/lib/ford_model_action.py": "6f8c4a6f7fa54fa2e23f32f1e83ace83da5f0b6f45011941f1902a1dae778ffc"
}
},
{
"scope": "Verify production commands against the archived curvature-C0 offline candidate.\n\nFixed recorded motion verifies integration parity, not physical tracking.\n",
"route": "b9",
"cycles": 90774,
"can_round_trips": 90774,
"production_matches_archived_trial_exactly": true,
"opendbc_revision": "64aa61b9b3fd26e70a7caa915acab207ff3cd64a",
"hypothesis": "model-action-curvature-c0-pi-v8",
"kp": 0.5,
"ki": 0.25,
"source_sha256": {
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_routeb9/route.npz": "b07c789d8155335f5d120d0262fced6e4d5803fe767b0ff49b6413dce4140b5c",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_routeb9/model_paths.npz": "6b1f87897c050273fdc05af051307a049b6fc3a93072e7cda1721195ce7c3861",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_routeb9/metadata.json": "9ce452220cab61b81883f32fc2fcaf5db6c78a674cb255a49cc77d5029580fee",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_curvature_c0_v7/b9/report.json": "3812983a690e5eafca93ea6af05620fe82e4e0cc8efc07e24505a63664444d66",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_curvature_c0_v7/b9/commands.npz": "2ffc685e29463a7b89d39bfb3583a37eb10a57d7201a24394550fcf9a89e3c4c",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/curvature_c0_validate.py": "c91ead82b0320725276141b1bc9b84b132cd6345a144ea826ba2b7f069870932",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/openpilot/selfdrive/controls/lib/ford_model_action.py": "6f8c4a6f7fa54fa2e23f32f1e83ace83da5f0b6f45011941f1902a1dae778ffc"
}
},
{
"scope": "Verify production commands against the archived curvature-C0 offline candidate.\n\nFixed recorded motion verifies integration parity, not physical tracking.\n",
"route": "ca",
"cycles": 327448,
"can_round_trips": 327448,
"production_matches_archived_trial_exactly": true,
"opendbc_revision": "64aa61b9b3fd26e70a7caa915acab207ff3cd64a",
"hypothesis": "model-action-curvature-c0-pi-v8",
"kp": 0.5,
"ki": 0.25,
"source_sha256": {
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_routeca/route.npz": "ae9d46770eaf0dbbac6af86aebc926320eed0cf114eb43d5f78b0676e8e0dbf9",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_routeca/model_paths.npz": "bf17deb442383aaa79432566cd382df24a1bbbbd0521d0cafab956618f5bdd96",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_routeca/metadata.json": "a759d5cdf878df8b05d91db637b1935b6b4bdd87af96f0f256b67e7d809b3525",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_curvature_c0_v7/ca/report.json": "753760c726ef1b85daf83b7108f758f9304dab408b49018f303616b99c96b81f",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_curvature_c0_v7/ca/commands.npz": "3fa932ac86e14440d325a6cc4677b47cd75c5c6019b929d3a5d078a62c5590df",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/curvature_c0_validate.py": "c91ead82b0320725276141b1bc9b84b132cd6345a144ea826ba2b7f069870932",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/openpilot/selfdrive/controls/lib/ford_model_action.py": "6f8c4a6f7fa54fa2e23f32f1e83ace83da5f0b6f45011941f1902a1dae778ffc"
}
},
{
"scope": "Verify production commands against the archived curvature-C0 offline candidate.\n\nFixed recorded motion verifies integration parity, not physical tracking.\n",
"route": "raptor02",
"cycles": 132881,
"can_round_trips": 132881,
"production_matches_archived_trial_exactly": true,
"opendbc_revision": "64aa61b9b3fd26e70a7caa915acab207ff3cd64a",
"hypothesis": "model-action-curvature-c0-pi-v8",
"kp": 0.5,
"ki": 0.25,
"source_sha256": {
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_raptor_route02/route.npz": "8a4cfe53994988d053b47d9caadf21ebd064fd20e598fa05ddc2da245bd0e980",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_raptor_route02/model_paths.npz": "d5dc6f72d91472b8f2fb1add1680cdaea1e122774ddd6d8519fd7673daf71bb5",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_raptor_route02/metadata.json": "9602e08efc2bd784133837c4156c075bb89bad3bedd6b309f15e884ed20e338a",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_curvature_c0_v7/raptor02/report.json": "af6b8fa4e4bfe3d5543794cdde4fbe4fd7560addf02717c2b6144c3987effafa",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_curvature_c0_v7/raptor02/commands.npz": "99ee65b866eda85e5c5d54f8896a7f952504826753eca31a01a29c7cf36c6482",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/curvature_c0_validate.py": "c91ead82b0320725276141b1bc9b84b132cd6345a144ea826ba2b7f069870932",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/openpilot/selfdrive/controls/lib/ford_model_action.py": "6f8c4a6f7fa54fa2e23f32f1e83ace83da5f0b6f45011941f1902a1dae778ffc"
}
}
],
"production_stress": {
"cycles": 20000,
"kp": 0.5,
"ki": 0.25,
"controller_updates": 60000,
"can_round_trips": 60000,
"seed": 20260913,
"independent_c0_comparisons": 20000,
"checks": "Independent scalar PI/unwind-first/anti-windup arithmetic, mirror symmetry, exact C0 independence, limits, slew, resets, CAN.",
"source_sha256": {
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/curvature_c0_production_stress.py": "4bf031a793efe9ebc066e5b6db6897ca8e844d5e34c6faecd4701a4d8f04132a",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/openpilot/selfdrive/controls/lib/ford_model_action.py": "6f8c4a6f7fa54fa2e23f32f1e83ace83da5f0b6f45011941f1902a1dae778ffc"
}
},
"limitations": [
"C1 parity applies only to fixed recorded inputs; changing C0 changes physical response and subsequent feedback.",
"Curvature arc omits independent live model position/heading; seven meters remains an engineering reference choice.",
"Smaller C0 is not proof of better or worse tracking; no candidate hardware drive has been performed."
]
}
-95
View File
@@ -1,95 +0,0 @@
# Direct model-path C0/C1 trial
This trial takes priority over the untested filtered-driver change. Driver
arbitration is restored to the last driven baseline, `18ded0380`: a raw torque
crossing above 1 Nm, filtered driver input, or fresh PSCM driver override still
clears feedback. The filtered-driver experiment remains available in history at
`4f7d2b8d2` and is not included in this trial.
Keep **Selected-Action Path Tracking** and **Model Geometry Reference** enabled
in Sunnylink. The existing geometry toggle now selects direct-path mapping at
controlsd startup. Keep **C0 one-second distance** off for fixed 7 m C0. Apply an
offroad-to-onroad cycle after updating. Geometry off restores the original
model-action controller; the master controller toggle off restores upstream
Ford control. Joystick and lateral-maneuver overrides retain their priority.
## Mapping
The path is parameterized by accumulated arc length in the model frame. C0
samples model lateral position at 7 m, or max(7 m, speed × 1 s) when the existing
C0 distance setting is enabled. C1 samples model orientation at max(7 m,
speed × 1 s). A shorter path holds its endpoint; invalid paths disable the
experimental command. Both distances are 7 m below about 15.7 mph. These are
engineering choices, not identified Ford reference points.
C0 and C1 are independent base requests. C0 is no longer reconstructed as a
circular arc from the curvature used for C1. C2/C3 remain zero, so this still
does not transmit the model's entire curved path to the PSCM.
For the existing request limits and feedback, heading is divided by its sample
distance to form a heading-equivalent curvature. controlsd applies its normal
curvature limits to that value, logs it, and uses it for desired steering angle
and feedback. Multiplication by the same distance recovers the model heading
when unconstrained. C0 independently uses 2 × offset / distance² to apply the
same request-limit function, then reverses that normalization. This does not
infer C0 from C1. These envelopes constrain base requests; they are not proof of
physical lateral acceleration or jerk under the unknown PSCM response.
P/I gains, measured-steering feedback, C1 anti-windup, C1-overflow allocation to
C0, field bounds, driver arbitration, and CAN cadence retain the driven
baseline behavior. There is no new C0/C1 actuator slew limiter. The independent
C0 envelope adds one previous-reference state, reset with the controller.
This changes preview semantics: direct commands use the distance stations
above, rather than the prior modeld curvature preview (about 0.744 s at low
speed on these drives). The raw model points do not pass through the old
curvature reference's 0.1 s smoother. Existing request limits still apply.
## Logging
Controller diagnostics identify `model-path-direct-feedback-v17`, with
`direct_path=true` only when the model path is in control. A valid maneuver
reference uses the original curvature-to-arc mapping and logs
`direct_path=false`. `offset_reference` records the independently limited C0
normalization. Desired curvature and desired steering angle describe the C1
feedback target, not an independent measure of lateral path position.
modeld's geometry-reference telemetry is retained for comparison. Its
`selectedCurvature` and published model action are not the direct controller's
selected reference; use controlsState and the Ford controller diagnostics.
## Offline results
The replay compares the driven baseline against the direct-path candidate on
identical recorded model frames and frozen vehicle measurements from full
rlogs 15a and 15b. It covers 44,145 control cycles and 4,415 candidate CAN
serialization checks. All commands remain finite and within field bounds;
C2/C3 remain zero and driver/PSCM override behavior matches the baseline.
The baseline reproduces recorded C1 to Float32 precision; C0 differs by at
most one 0.01 m command quantum on 15b.
| Metric | 15a baseline → direct | 15b baseline → direct |
|---|---:|---:|
| C1 field-bound time | 2.57 → 6.77 s | 6.70 → 7.54 s |
| Low-speed C0 steps > 0.25 m | 52 → 52 | 85 → 129 |
| Low-speed C1 steps > 0.05 rad | 36 → 46 | 32 → 57 |
Low-speed counts use valid consecutive commands below 15 mph, normally 10 ms
apart. They are descriptive command changes, not physical wheel jerk. The
direct mapping changes requests substantially and does **not** establish a
smoothness improvement: abrupt command changes and C1 field-bound time can
increase. Fixed recorded measurements cannot predict the resulting wheel
response, centering, or unwind. No on-road improvement is claimed.
**593 tests pass** (547 controller/path/geometry tests and 46 settings tests).
The tests cover independent position/heading commands, distances, endpoint
handling, invalid input resets, request limits through reversals, feedback,
baseline driver overrides, toggle selection, maneuver priority, publication,
and CAN encoding. Reproduce replay with:
```sh
PYTHONPATH=.:opendbc_repo python tools/ford_pscm_lab/direct_model_path_replay.py \
--source .cache/ford_route15a/rlog_full --output .cache/ford_direct_path/15a
```
Detailed measurements and source hashes: `ford_direct_path_validation.json`.
-70
View File
@@ -1,70 +0,0 @@
# Ford direct C0/C1 requests — v9
The v8 maneuver route `84865544361f55cb/0000011c--99f4537696` showed
commands being delayed by our extra 4 m/s C0 and 0.5 rad/s C1 slews.
Those values were controller choices, not measurements of the PSCM's limits.
For example, a C1 target reversal from +0.10 to -0.10 rad required at least
0.4 seconds in our output stage alone.
V9 sends the current bounded C0 and C1 requests on each update. There is no
additional actuator ramp, zero hold, reversal mode, or initial engagement ramp.
C0 remains the selected-curvature arc at 7 m plus base-C1 overflow. C1 remains
base heading plus proportional and integral feedback, with P=0.50 and I=0.25.
These are calculated commands, not a known inverse of the PSCM's response.
Integral cancellation happens first. New integral accumulation fits the
combined command's magnitude headroom, rather than the old slew headroom.
This removes a second dependence on the old ramp. Fresh-measurement cadence,
driver override, PSCM arbitration, stale/invalid input reset, and C2/C3=0 remain.
The default-off Sunnylink `FordModelActionController` selector still applies
to any Ford CAN FD vehicle. Toggle off restores upstream Ford control.
Logs identify `model-action-direct-c0-c1-pi-v9`.
## Remaining bounds
- Selected desired curvature still passes through upstream `clip_curvature`:
3 m/s² lateral acceleration adjusted for roll, 5 m/s³ lateral jerk, and
absolute curvature 0.2 m⁻¹. These bound the reference; they are not proof of
a physical acceleration or jerk bound under custom C0/C1 feedback.
- C0 remains within ±5.11 m and C1 within ±0.50 rad. The downstream packer
retains the actual wire ranges, preventing out-of-range values wrapping.
- Driver, CAN, timing, service health, and fault gates remain intact.
- Panda safety, the 100 Hz sender, and ramp-type selection are unchanged.
No assumption is made that extended path mode independently enforces ISO
limits. The absence of `LimitReached` is not evidence of unrestricted authority.
## Offline validation
The same-cycle reversal regression failed on v8 in both directions at 2, 10,
and 100 ms timesteps, then passed after the change. Zero-error release reaches
zero immediately. Full controlsd/publication/CAN tests check current-request
output, upstream reference selection, field packing, driver and PSCM gates,
integral cancellation and anti-windup, invalid input, and toggle-off fallback.
- 358 tests and 23 subtests passed across controller and Ford sender suites.
- 20,000 seeded randomized cases produced 60,000 controller updates and CAN
round trips, checking independent arithmetic, symmetry, bounds and resets.
- Routes 11c, 119 and 11a supplied 461,340 input cycles: 922,680 baseline/v9
controller updates and CAN round trips. V8 and v9 activation, feedforward,
proportional feedback and feedback gates matched. Every active v9 output
matched its current bounded request within wire quantization.
- Ruff passed for changed production/tests and the new replay/stress tools.
| Route | Input cycles | Maximum C0 difference | Maximum C1 difference |
|---|---:|---:|---:|
| 11c maneuver suite | 54,146 | 0.09 m | 0.1605 rad |
| 119 | 171,423 | 2.42 m | 0.4950 rad |
| 11a | 235,771 | 3.36 m | 0.3710 rad |
The maximum differences include engagement and other transitions. Removing
slews permits abrupt changes; these are not predictions of wheel motion.
Recorded motion, driver input and PSCM feedback stay fixed in replay.
Publication timestamps approximate computation time; replay is not a claim of
exact onroad command parity. Synthetic reference freshness is approximated
from valid maneuver publications. Physical tracking and stability are unvalidated.
The reproducible tools are `tools/ford_pscm_lab/direct_path_replay.py` and
`tools/ford_pscm_lab/direct_path_production_stress.py`. Machine-readable checks
are collected in `ford_direct_path_v9_validation.json`.
-125
View File
@@ -1,125 +0,0 @@
{
"version": "model-action-direct-c0-c1-pi-v9",
"baseline": "57ae29f257498f58170e2beec140c0f8103c1b43",
"tests": {
"passed": 358,
"subtests_passed": 23
},
"stress": {
"cycles": 20000,
"kp": 0.5,
"ki": 0.25,
"controller_updates": 60000,
"can_round_trips": 60000,
"seed": 20260913,
"independent_c0_comparisons": 20000,
"checks": "Independent scalar PI/unwind-first/anti-windup arithmetic, mirror symmetry, exact C0 independence, amplitude bounds, current commands, resets, CAN.",
"source_sha256": {
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/direct_path_production_stress.py": "ae0d0e3a32cddefc4072a58664f5afca84019067c8c5d9fa24e89dd1cbef2b99",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/openpilot/selfdrive/controls/lib/ford_model_action.py": "fbc2309c15289db184aa54bda697feb7cfc26a218998b5b21f063381f47aad45"
}
},
"routes": {
"11c": {
"scope": "Compare v8 and direct C0/C1 on fixed logged inputs, without predicting motion.",
"baseline": "57ae29f257498f58170e2beec140c0f8103c1b43",
"baseline_source_sha256": "6f8c4a6f7fa54fa2e23f32f1e83ace83da5f0b6f45011941f1902a1dae778ffc",
"cycles": 54146,
"controller_updates": 108292,
"can_round_trips": 108292,
"status_counts": {
"inactive": 5066,
"active": 49080
},
"identical_validity_feedforward_p_and_feedback_gates": true,
"all_candidate_outputs_match_current_bounded_request": true,
"changed_c0_cycles": 73,
"changed_c1_cycles": 17133,
"max_abs_c0_change_m": 0.08999999999999986,
"max_abs_c1_change_rad": 0.16049999999999998,
"max_abs_integral_rad": 0.02639216769448115,
"limitations": [
"Recorded motion remains fixed; this cannot establish improved tracking or stability.",
"Publication time proxies computation time; reconstructed baseline is not exact onroad parity.",
"Synthetic reference freshness is approximated from valid publications; upstream selection itself is unchanged."
],
"source_sha256": {
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_maneuver_11c/analysis/route.npz": "1f3c37a7c7ae85ba69e9958435a9568f8244fa862f43ad1ee2a0192d1df13966",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_maneuver_11c/analysis/model_paths.npz": "0ff433da3dde1d4d7f10596cd53558ec5de9d56fbc49aec3e8afb4c9872377d5",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/direct_path_replay.py": "2a1baff08152230929d56b23b8f7fb03e1ae9c2b30c5e152a2be431188fc20e7",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/openpilot/selfdrive/controls/lib/ford_model_action.py": "fbc2309c15289db184aa54bda697feb7cfc26a218998b5b21f063381f47aad45"
}
},
"119": {
"scope": "Compare v8 and direct C0/C1 on fixed logged inputs, without predicting motion.",
"baseline": "57ae29f257498f58170e2beec140c0f8103c1b43",
"baseline_source_sha256": "6f8c4a6f7fa54fa2e23f32f1e83ace83da5f0b6f45011941f1902a1dae778ffc",
"cycles": 171423,
"controller_updates": 342846,
"can_round_trips": 342846,
"status_counts": {
"inactive": 87482,
"active": 83941
},
"identical_validity_feedforward_p_and_feedback_gates": true,
"all_candidate_outputs_match_current_bounded_request": true,
"changed_c0_cycles": 199,
"changed_c1_cycles": 16426,
"max_abs_c0_change_m": 2.42,
"max_abs_c1_change_rad": 0.495,
"max_abs_integral_rad": 0.05010140673563736,
"limitations": [
"Recorded motion remains fixed; this cannot establish improved tracking or stability.",
"Publication time proxies computation time; reconstructed baseline is not exact onroad parity.",
"Synthetic reference freshness is approximated from valid publications; upstream selection itself is unchanged."
],
"source_sha256": {
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route119/route.npz": "415a099935fef152123b7422870442d6cfe6302bab9b8983ce3b3ffc71a7702b",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route119/model_paths.npz": "dfd41383bea486ccd0a476e94e612921a44a099a0c1213ff132ab81df3aa94d7",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/direct_path_replay.py": "2a1baff08152230929d56b23b8f7fb03e1ae9c2b30c5e152a2be431188fc20e7",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/openpilot/selfdrive/controls/lib/ford_model_action.py": "fbc2309c15289db184aa54bda697feb7cfc26a218998b5b21f063381f47aad45"
}
},
"11a": {
"scope": "Compare v8 and direct C0/C1 on fixed logged inputs, without predicting motion.",
"baseline": "57ae29f257498f58170e2beec140c0f8103c1b43",
"baseline_source_sha256": "6f8c4a6f7fa54fa2e23f32f1e83ace83da5f0b6f45011941f1902a1dae778ffc",
"cycles": 235771,
"controller_updates": 471542,
"can_round_trips": 471542,
"status_counts": {
"inactive": 103039,
"active": 132732
},
"identical_validity_feedforward_p_and_feedback_gates": true,
"all_candidate_outputs_match_current_bounded_request": true,
"changed_c0_cycles": 689,
"changed_c1_cycles": 16106,
"max_abs_c0_change_m": 3.3600000000000003,
"max_abs_c1_change_rad": 0.371,
"max_abs_integral_rad": 0.05163860736233724,
"limitations": [
"Recorded motion remains fixed; this cannot establish improved tracking or stability.",
"Publication time proxies computation time; reconstructed baseline is not exact onroad parity.",
"Synthetic reference freshness is approximated from valid publications; upstream selection itself is unchanged."
],
"source_sha256": {
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route11a/route.npz": "428c203d090731386f06bf9fdeefe608f1999c43c52ff39cab7ee86ec0ac804f",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route11a/model_paths.npz": "e2b827fd7c3dfbf66d7872a56b57eaec13c600a9a48e12dae59be81c31b0978f",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/direct_path_replay.py": "2a1baff08152230929d56b23b8f7fb03e1ae9c2b30c5e152a2be431188fc20e7",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/openpilot/selfdrive/controls/lib/ford_model_action.py": "fbc2309c15289db184aa54bda697feb7cfc26a218998b5b21f063381f47aad45"
}
}
},
"limits_removed": [
"C0 4 m/s extra slew",
"C1 0.5 rad/s extra slew and associated integral slew headroom"
],
"limits_preserved": [
"upstream selected-curvature acceleration/jerk/curvature bounds",
"C0/C1 magnitude and wire packing bounds",
"driver/fault/validity/freshness/PSCM feedback gates"
],
"physical_tracking_validated": false,
"note": "Replay/stress script hashes identify their execution versions before formatting-only lint cleanup; production controller is unchanged after validation."
}
-154
View File
@@ -1,154 +0,0 @@
{
"15a": {
"cycles": 21867,
"wire_checks": 2187,
"baseline_recorded_error_p50_p95_p99_max": {
"c0": [
4.172324707951702e-09,
5.2452087118126656e-08,
8.583068833445395e-08,
1.1444091807533141e-07
],
"c1": [
8.791685157660822e-10,
1.1682510403510094e-08,
1.4305114759416426e-08,
1.478195188475695e-08
]
},
"latched_angle_match_fraction": 1.0,
"command_changes": {
"c0": [
0.2799999999999998,
1.3800000000000003,
1.8000000000000007,
2.51
],
"c1": [
0.01749999999999996,
0.124,
0.2940450000000001,
0.45200000000000007
]
},
"reference_change_p50_p95_max": [
0.0015834828704371431,
0.012526763883651669,
0.03905121179470369
],
"variants": {
"old": {
"feedback_switches_active": 112,
"feedback_switches_low_speed": 61,
"low_speed_c0_steps_over_025m": 52,
"low_speed_c1_steps_over_005rad": 36,
"low_speed_step_p99": {
"c0": 0.4996000000000004,
"c1": 0.05668000000000003
},
"c0_bound_active_s": 0.0,
"c1_bound_active_s": 2.5702606670000137
},
"new": {
"feedback_switches_active": 112,
"feedback_switches_low_speed": 61,
"low_speed_c0_steps_over_025m": 52,
"low_speed_c1_steps_over_005rad": 46,
"low_speed_step_p99": {
"c0": 0.8499999999999996,
"c1": 0.08540000000000011
},
"c0_bound_active_s": 0.0,
"c1_bound_active_s": 6.774772933000349
}
},
"method": "Compare direct model-path commands against the driven geometry-curvature controller.\n\nInput: extract.py route.npz/model_paths.npz/metadata.json directories. This checks\nreference selection, command continuity and overrides; it does not predict a changed wheel response.\n",
"provenance": {
"baseline_commit": "18ded0380045c29520cf1f690688e48ebad043d8",
"baseline_controller_sha256": "8bc8554ed6743c5faf433deaa6de3f07fae27d183f62b04f49f9c3b9f1e8dcca",
"candidate_controller_sha256": "02b64f40a56cadefb181afbf7c3720affa3c7fe4ac18a2902dc9c546fd6b395d",
"fixed_c0_distance_m": 7.0
},
"sources_sha256": {
".cache/ford_route15a/rlog_full/route.npz": "057ce69ce028b2c9c1b42ab8110f35c888a6c398ddcc4156e74afc98c83d119d",
".cache/ford_route15a/rlog_full/model_paths.npz": "ef820d9df14afa308bf46f95463b636a599ff24fd08bffaff1ae671b3932c24a",
".cache/ford_route15a/rlog_full/metadata.json": "bea47c7f0c0b583964ced3484fe814ec9a7a24abbfe6272e70f9dde12580a756"
}
},
"15b": {
"cycles": 22278,
"wire_checks": 2228,
"baseline_recorded_error_p50_p95_p99_max": {
"c0": [
6.70552502413102e-10,
3.814697269177714e-08,
1.0490417512443173e-07,
0.009999947547912669
],
"c1": [
1.9371515502797365e-10,
5.4836273299940785e-09,
1.2159347528850617e-08,
1.478195188475695e-08
]
},
"latched_angle_match_fraction": 0.9999321895978843,
"command_changes": {
"c0": [
0.040000000000000036,
1.08,
1.7699999999999996,
2.46
],
"c1": [
0.003500000000000003,
0.12449999999999994,
0.22634999999999764,
0.406
]
},
"reference_change_p50_p95_max": [
0.00021085212911765025,
0.013382994089416386,
0.03945627628640698
],
"variants": {
"old": {
"feedback_switches_active": 238,
"feedback_switches_low_speed": 107,
"low_speed_c0_steps_over_025m": 85,
"low_speed_c1_steps_over_005rad": 32,
"low_speed_step_p99": {
"c0": 0.36999999999999944,
"c1": 0.03771999999999984
},
"c0_bound_active_s": 0.0,
"c1_bound_active_s": 6.701652654999748
},
"new": {
"feedback_switches_active": 238,
"feedback_switches_low_speed": 107,
"low_speed_c0_steps_over_025m": 129,
"low_speed_c1_steps_over_005rad": 57,
"low_speed_step_p99": {
"c0": 0.6187999999999921,
"c1": 0.05121999999999982
},
"c0_bound_active_s": 0.0,
"c1_bound_active_s": 7.542629173000023
}
},
"method": "Compare direct model-path commands against the driven geometry-curvature controller.\n\nInput: extract.py route.npz/model_paths.npz/metadata.json directories. This checks\nreference selection, command continuity and overrides; it does not predict a changed wheel response.\n",
"provenance": {
"baseline_commit": "18ded0380045c29520cf1f690688e48ebad043d8",
"baseline_controller_sha256": "8bc8554ed6743c5faf433deaa6de3f07fae27d183f62b04f49f9c3b9f1e8dcca",
"candidate_controller_sha256": "02b64f40a56cadefb181afbf7c3720affa3c7fe4ac18a2902dc9c546fd6b395d",
"fixed_c0_distance_m": 7.0
},
"sources_sha256": {
".cache/ford_route15b/rlog_full/route.npz": "f95e1c16a4677fee0ab57002d714e22716bf11873c308280553312fa9d18aab0",
".cache/ford_route15b/rlog_full/model_paths.npz": "a90be55943ce0009afbf38ef2035b3fff4de8ab00ea266636247bcc5a5012eae",
".cache/ford_route15b/rlog_full/metadata.json": "545ef8d6740e57237f35b2ca684111fc2d774e83aa1d6c8a5a0fff1b71270e85"
}
}
}
-98
View File
@@ -1,98 +0,0 @@
# Ford feedback timing follows comma's request-history pattern
The base C0/C1 mapping continues to use the latest selected curvature. C0 P,
C1 P and C1 I now calculate error from an earlier selected curvature, using
the same one-second request buffer and frame-index expression as comma's torque
controller. The buffer advances at the 100 Hz controls rate, including while
disengaged. The outgoing base request is never buffered for later transmission.
`controlsd` passes the same `lat_delay` value it passes to the upstream lateral
controller: `lateralDelay.lateralDelay + LAT_SMOOTH_SECONDS`. The existing extra
Ford model preview remains model-side; this change does not add it to feedback
delay or infer hardware delay from a turn's angle crossing. In the four replayed
routes the published delay was 0.1689463705 s and `LAT_SMOOTH_SECONDS` was zero.
Upstream's integer indexing selects the request 16 control cycles earlier,
nominally 0.16 s. The maximum lookback is 99 cycles, nominally 0.99 s.
The history is in curvature units because that is this controller's feedback
quantity. This adopts upstream's timing pattern, not its torque conversion,
acceleration gain schedule, friction/jerk feedforward or low-speed integral
freeze. Ford gains, formulas, anti-windup, integration cadence, field bounds,
upstream curvature limiting and driver/PSCM arbitration remain unchanged.
Action C0 P remains 1.0; direct-path C0 P remains 0.5; C1 P=0.75 and I=1.0.
Both Ford reference modes use the new timing.
The history starts at zero, like upstream. Ordinary disengagement clears P/I
while continuing to record requests. Existing invalid-input, timing and path
resets also clear request history, preventing invalid retained references from
surviving recovery. A nonfinite delay rejects the active command; finite delay
values use upstream's bounded frame selection. An explicit zero delay recovers
the previous command law exactly. Existing offline feedback-reference overrides
remain available; production supplies delay rather than an override.
Diagnostics identify `v21-delayed-feedback` and report the requested delay,
nominal frame delay, delayed feedback curvature/error and latest requested
curvature separately. The reported frame delay is not a measured physical delay.
## Verification and limitations
The delay-tracking regression failed against the old feedback behavior: a wheel
following a request with the configured delay still received correction. It
passes with delayed feedback through entry, reversal and release, in both turn
directions and both reference modes. A separate test executes the actual
upstream torque controller's buffer-selection expressions as an oracle.
Integration tests execute controlsd's actual delay wiring, source selection,
limiter, adapter and Float32 publication through the Ford CAN sender. A
delay-matched wheel trace generates zero P/I while the current base commands
reach CAN immediately. Persistent error still integrates; repeated measurements
do not integrate twice; driver and fresh PSCM override still clear correction
immediately. Exact suite counts are in the validation JSON.
Four native-time route replays cover 583,599 control cycles and 58,362 CAN
pack/decode checks. The baseline is the filtered-driver controller at
`baeabfaa807c7a07baf183968e570f2c1d3fd665`. Baseline commands match the archived
v20 replay exactly on all four routes. A third controller with zero delay
matches that baseline's commands and integral on every cycle. With delay,
command validity, base heading/overflow and feedback arbitration remain equal.
All commands remain bounded with C2=C3=0.
| Route | Low-speed C0 changes >0.25 m, v20 → v21 | Low-speed C1 changes >0.05 rad, v20 → v21 | C1 at field bound, v20 → v21 |
| --- | --- | --- | --- |
| 162 | 70 → 68 | 50 → 44 | 1.06 → 0.92 s |
| 157 | 137 → 110 | 59 → 51 | 6.52 → 5.94 s |
| 151 | 77 → 65 | 42 → 36 | 4.04 → 3.89 s |
| 149 | 197 → 159 | 111 → 95 | 16.08 → 13.23 s |
Low speed means below 15 mph; these counts include valid driver-interaction
periods and are command-continuity measurements, not autonomous tracking scores.
Neither baseline nor candidate was driven in these recordings; both command
streams use the same recorded requests and vehicle motion.
The change reduces transient correction. At route-162 time 302.649 s, requesting
141.4 degrees with the wheel at 87.3 degrees, replay C0 changes from -2.19 to
-1.73 m and C1 from -0.3695 to -0.3135 rad. At 307.502 s, after driver input,
opposite-direction release C0 falls from +1.52 to +1.04 m and C1 from +0.0655
to +0.0355 rad. The base request is unchanged in each example.
This can mean less correction during both entry and unwind. Fewer large command
steps do not prove reduced physical oscillation, maintained turn authority or
better release. These are frozen-motion replays, not a validated PSCM simulator.
The supplied delay has not been identified specifically for the combined C0/C1
response. The next physical evaluation must distinguish these effects; this is
not a demonstrated death-wobble fix.
Reproduce with the project's Python environment and built dependencies:
```sh
PYTHONPATH=.:opendbc_repo:.cache/ford_geometry_deps python \
tools/ford_pscm_lab/filtered_driver_replay.py \
--source .cache/ford_route162/full \
--delay-intake .cache/ford_route162/intake.npz \
--baseline baeabfaa807c7a07baf183968e570f2c1d3fd665 \
--output .cache/ford_feedback_delay_v21/162
```
Pull and restart the software while offroad. The existing master toggle still
selects upstream Ford control when disabled. For the current action trial,
leave Model Geometry Reference and C0 one-second distance disabled.
@@ -1,391 +0,0 @@
{
"baseline_commit": "baeabfaa807c7a07baf183968e570f2c1d3fd665",
"tests": {
"passed": 722,
"subtests_passed": 2
},
"total_cycles": 583599,
"total_wire_checks": 58362,
"code_sha256": {
"openpilot/selfdrive/controls/lib/ford_model_action.py": "d1730a79307297b32b44ba18e9792df216a1961a3f32927c5e6acf28014d288d",
"openpilot/selfdrive/controls/controlsd.py": "3427212538fcb1cf4d8a44f6be62c54b31ab5ba1a3eb16d958bb58f9c52abecf",
"openpilot/selfdrive/controls/lib/latcontrol_torque.py": "83ea33d37c349b10eca994150dae2a5841ec56b7038402432205a74054c917ee",
"tools/ford_pscm_lab/filtered_driver_replay.py": "9c210b31df62cb6f41e6cf10d76e2e7ba15eefbbfcb30a8ea0244e095365543a"
},
"method": "Same frozen recorded inputs for v20, v21 using logged lateralDelay, and v21 with zero delay. The latter must exactly match v20 at every cycle. No counterfactual wheel response is predicted.",
"limitations": "Supplied delay is not an identified C0/C1-specific response model. Lower command steps do not establish physical stability. Feedback demand is lower on rising and falling requests; entry and unwind can weaken.",
"points": [
{
"meaning": "Clean entry shortfall",
"t": 302.64934647200005,
"desired_angle": 141.37510681152344,
"old_c0": -2.1900000000000004,
"new_c0": -1.7300000000000004,
"old_c1": -0.36950000000000005,
"new_c1": -0.3135,
"old_i": -0.05458394920016066,
"new_i": -0.02249363859059498,
"old_reference": -0.03498964384198189,
"new_reference": -0.030434442684054375
},
{
"meaning": "Release following driver input; command comparison only",
"t": 307.502298498,
"desired_angle": 22.443912506103516,
"old_c0": 1.52,
"new_c0": 1.04,
"old_c1": 0.0655,
"new_c1": 0.035499999999999976,
"old_i": 0.018708751669361232,
"new_i": 0.01363232660438634,
"old_reference": -0.005546241067349911,
"new_reference": -0.010331000201404095
}
],
"routes": {
"162": {
"cycles": 49003,
"wire_checks": 4901,
"zero_delay_matches_baseline": true,
"feedback_delay_requested_min_median_max": [
0.16894637048244476,
0.16894637048244476,
0.16894637048244476
],
"feedback_delay_used_min_median_max": [
0.16,
0.16,
0.16
],
"baseline_recorded_error_p50_p95_p99_max": {
"c0": [
2.3841861818141297e-09,
5.2452087118126656e-08,
0.39000000458657785,
2.5800000023841863
],
"c1": [
0.001499999597668611,
0.07550000331401825,
0.11149999844551088,
0.24500000381469733
]
},
"latched_angle_match_fraction": 1.0,
"command_changes": {
"c0": [
0.019999999999999574,
0.22000000000000064,
0.4300000000000006,
0.839999999999999
],
"c1": [
0.003500000000000003,
0.02899999999999997,
0.04049999999999998,
0.0635
]
},
"variants": {
"old": {
"feedback_switches_active": 218,
"feedback_switches_low_speed": 95,
"low_speed_c0_steps_over_025m": 70,
"low_speed_c1_steps_over_005rad": 50,
"low_speed_step_p99": {
"c0": 0.16000000000000014,
"c1": 0.019949999999999843
},
"raw_crossing_c0_steps_over_025m": 0,
"c0_bound_active_s": 0.0,
"c1_bound_active_s": 1.0580744659999937
},
"new": {
"feedback_switches_active": 218,
"feedback_switches_low_speed": 95,
"low_speed_c0_steps_over_025m": 68,
"low_speed_c1_steps_over_005rad": 44,
"low_speed_step_p99": {
"c0": 0.13999999999999968,
"c1": 0.018449999999999786
},
"raw_crossing_c0_steps_over_025m": 0,
"c0_bound_active_s": 0.0,
"c1_bound_active_s": 0.9192208439999945
}
},
"method": "Compare Ford feedback variants on recorded references and frozen vehicle motion.\n\nInput: extract.py route.npz/model_paths.npz/metadata.json directories. This checks\ncommand continuity and overrides; it does not predict a changed wheel response.\nBoth controllers receive the logged selected curvature. Recorded-command agreement\nis expected only for routes driven with the selected baseline and fixed 7 m C0.\n",
"provenance": {
"baseline_commit": "baeabfaa807c7a07baf183968e570f2c1d3fd665",
"baseline_controller_sha256": "b34a55f84fdbf6c432ce89cdc378aaaf9bbd765ca4e6934845059f586e6f0773",
"candidate_controller_sha256": "d1730a79307297b32b44ba18e9792df216a1961a3f32927c5e6acf28014d288d",
"fixed_c0_distance_m": 7.0
},
"sources_sha256": {
".cache/ford_route162/full/route.npz": "a725f9ca309b90a2d750d51a9b9d5b459d99119a54ab048631bc0fce07348af8",
".cache/ford_route162/full/model_paths.npz": "cffe310b122d9ae26a53ee504608156b4242a6bba5f681898d5b661951ac5ce5",
".cache/ford_route162/full/metadata.json": "ef3ac1caca0eaec31f7140a0743bd3b98885ce143cb0f97a8eee615b33943d7f",
".cache/ford_route162/intake.npz": "9455b01e1d1d3dcba08276b382a8c6cd54ff6fb19cb417b72d0ac89cf193034b"
},
"baseline_matches_archived_v20_commands_and_integral": true
},
"157": {
"cycles": 76554,
"wire_checks": 7656,
"zero_delay_matches_baseline": true,
"feedback_delay_requested_min_median_max": [
0.0,
0.16894637048244476,
0.16894637048244476
],
"feedback_delay_used_min_median_max": [
0.16,
0.16,
0.16
],
"baseline_recorded_error_p50_p95_p99_max": {
"c0": [
0.009999999999999787,
0.41999999010562905,
1.3956999619007082,
5.39999999165535
],
"c1": [
4.291534405620467e-09,
0.03549999962002037,
0.06600000168576836,
0.4085000002384186
]
},
"latched_angle_match_fraction": 1.0,
"command_changes": {
"c0": [
0.009999999999999787,
0.21999999999999975,
0.4900000000000002,
1.5100000000000007
],
"c1": [
0.0030000000000000027,
0.028999999999999915,
0.039000000000000035,
0.09049999999999997
]
},
"variants": {
"old": {
"feedback_switches_active": 224,
"feedback_switches_low_speed": 148,
"low_speed_c0_steps_over_025m": 137,
"low_speed_c1_steps_over_005rad": 59,
"low_speed_step_p99": {
"c0": 0.20000000000000018,
"c1": 0.022499999999999964
},
"raw_crossing_c0_steps_over_025m": 0,
"c0_bound_active_s": 0.2999506119999751,
"c1_bound_active_s": 6.521285587999955
},
"new": {
"feedback_switches_active": 224,
"feedback_switches_low_speed": 148,
"low_speed_c0_steps_over_025m": 110,
"low_speed_c1_steps_over_005rad": 51,
"low_speed_step_p99": {
"c0": 0.16000000000000014,
"c1": 0.01750000000000007
},
"raw_crossing_c0_steps_over_025m": 0,
"c0_bound_active_s": 0.2999506119999751,
"c1_bound_active_s": 5.943273473999852
}
},
"method": "Compare Ford feedback variants on recorded references and frozen vehicle motion.\n\nInput: extract.py route.npz/model_paths.npz/metadata.json directories. This checks\ncommand continuity and overrides; it does not predict a changed wheel response.\nBoth controllers receive the logged selected curvature. Recorded-command agreement\nis expected only for routes driven with the selected baseline and fixed 7 m C0.\n",
"provenance": {
"baseline_commit": "baeabfaa807c7a07baf183968e570f2c1d3fd665",
"baseline_controller_sha256": "b34a55f84fdbf6c432ce89cdc378aaaf9bbd765ca4e6934845059f586e6f0773",
"candidate_controller_sha256": "d1730a79307297b32b44ba18e9792df216a1961a3f32927c5e6acf28014d288d",
"fixed_c0_distance_m": 7.0
},
"sources_sha256": {
".cache/ford_route157/full/route.npz": "e2e2573f904aa11ee9e10450e7f5b965d475657b61127e827a67eadcd6b857fb",
".cache/ford_route157/full/model_paths.npz": "25fc2526e092fde5ceb5aab63a5aa4f73a2c989a3263cedb0a26faf8da61c47b",
".cache/ford_route157/full/metadata.json": "f2d72c9a877ed6694e4da6831841113dc0c05987e2c76cca51f358d12411b235",
".cache/ford_route157/intake.npz": "5dffd86fde68197727d6c6b9c5eae82320a456fa1206c4cf18e5e9c69419dde4"
},
"baseline_matches_archived_v20_commands_and_integral": true
},
"151": {
"cycles": 325708,
"wire_checks": 32571,
"zero_delay_matches_baseline": true,
"feedback_delay_requested_min_median_max": [
0.16894637048244476,
0.16894637048244476,
0.16894637048244476
],
"feedback_delay_used_min_median_max": [
0.16,
0.16,
0.16
],
"baseline_recorded_error_p50_p95_p99_max": {
"c0": [
0.010000000223516992,
0.2000000013411043,
1.010000009727478,
4.550000023841858
],
"c1": [
0.004999999240040742,
0.02900000113248826,
0.07250000010803342,
0.28449999523162844
]
},
"latched_angle_match_fraction": 1.0,
"command_changes": {
"c0": [
0.009999999999999787,
0.07999999999999999,
0.2900000000000002,
1.2599999999999998
],
"c1": [
0.0025000000000000022,
0.02250000000000002,
0.038500000000000034,
0.07650000000000001
]
},
"variants": {
"old": {
"feedback_switches_active": 259,
"feedback_switches_low_speed": 101,
"low_speed_c0_steps_over_025m": 77,
"low_speed_c1_steps_over_005rad": 42,
"low_speed_step_p99": {
"c0": 0.13999999999999968,
"c1": 0.014499999999999957
},
"raw_crossing_c0_steps_over_025m": 0,
"c0_bound_active_s": 0.029249633000290487,
"c1_bound_active_s": 4.042117826999856
},
"new": {
"feedback_switches_active": 259,
"feedback_switches_low_speed": 101,
"low_speed_c0_steps_over_025m": 65,
"low_speed_c1_steps_over_005rad": 36,
"low_speed_step_p99": {
"c0": 0.10000000000000053,
"c1": 0.01050000000000001
},
"raw_crossing_c0_steps_over_025m": 0,
"c0_bound_active_s": 0.0,
"c1_bound_active_s": 3.8897953380001127
}
},
"method": "Compare Ford feedback variants on recorded references and frozen vehicle motion.\n\nInput: extract.py route.npz/model_paths.npz/metadata.json directories. This checks\ncommand continuity and overrides; it does not predict a changed wheel response.\nBoth controllers receive the logged selected curvature. Recorded-command agreement\nis expected only for routes driven with the selected baseline and fixed 7 m C0.\n",
"provenance": {
"baseline_commit": "baeabfaa807c7a07baf183968e570f2c1d3fd665",
"baseline_controller_sha256": "b34a55f84fdbf6c432ce89cdc378aaaf9bbd765ca4e6934845059f586e6f0773",
"candidate_controller_sha256": "d1730a79307297b32b44ba18e9792df216a1961a3f32927c5e6acf28014d288d",
"fixed_c0_distance_m": 7.0
},
"sources_sha256": {
".cache/ford_route151/full/route.npz": "41a5b8bd388cf5a3d553f784542376ac9355fcdc5be4f427053d0504537babe1",
".cache/ford_route151/full/model_paths.npz": "980b3843cec04254280d744a5801cee1bf0f8ef371052398327ff245f9eee01b",
".cache/ford_route151/full/metadata.json": "937825317a0edd470c54647240b922be8f79dda5b3365ffdd61281f0aca877a1",
".cache/ford_route151/intake.npz": "18bbefb7738cebd0071dc987e90f74469a0c8ed456f9412324030592cafd68c9"
},
"baseline_matches_archived_v20_commands_and_integral": true
},
"149": {
"cycles": 132334,
"wire_checks": 13234,
"zero_delay_matches_baseline": true,
"feedback_delay_requested_min_median_max": [
0.16894637048244476,
0.16894637048244476,
0.16894637048244476
],
"feedback_delay_used_min_median_max": [
0.16,
0.16,
0.16
],
"baseline_recorded_error_p50_p95_p99_max": {
"c0": [
0.02999999910593054,
0.8400000008046626,
2.1400000276565554,
4.560000002980233
],
"c1": [
0.015500000141560999,
0.10499999982118613,
0.32499999746203423,
0.43149999833107
]
},
"latched_angle_match_fraction": 1.0,
"command_changes": {
"c0": [
0.009999999999999787,
0.1999999999999993,
0.4900000000000002,
1.3100000000000005
],
"c1": [
0.0040000000000000036,
0.03200000000000003,
0.04999999999999999,
0.08850000000000002
]
},
"variants": {
"old": {
"feedback_switches_active": 354,
"feedback_switches_low_speed": 171,
"low_speed_c0_steps_over_025m": 197,
"low_speed_c1_steps_over_005rad": 111,
"low_speed_step_p99": {
"c0": 0.1999999999999993,
"c1": 0.02150000000000002
},
"raw_crossing_c0_steps_over_025m": 0,
"c0_bound_active_s": 0.0,
"c1_bound_active_s": 16.08094779300012
},
"new": {
"feedback_switches_active": 354,
"feedback_switches_low_speed": 171,
"low_speed_c0_steps_over_025m": 159,
"low_speed_c1_steps_over_005rad": 95,
"low_speed_step_p99": {
"c0": 0.16000000000000014,
"c1": 0.017500000000000016
},
"raw_crossing_c0_steps_over_025m": 0,
"c0_bound_active_s": 0.0,
"c1_bound_active_s": 13.22902962499984
}
},
"method": "Compare Ford feedback variants on recorded references and frozen vehicle motion.\n\nInput: extract.py route.npz/model_paths.npz/metadata.json directories. This checks\ncommand continuity and overrides; it does not predict a changed wheel response.\nBoth controllers receive the logged selected curvature. Recorded-command agreement\nis expected only for routes driven with the selected baseline and fixed 7 m C0.\n",
"provenance": {
"baseline_commit": "baeabfaa807c7a07baf183968e570f2c1d3fd665",
"baseline_controller_sha256": "b34a55f84fdbf6c432ce89cdc378aaaf9bbd765ca4e6934845059f586e6f0773",
"candidate_controller_sha256": "d1730a79307297b32b44ba18e9792df216a1961a3f32927c5e6acf28014d288d",
"fixed_c0_distance_m": 7.0
},
"sources_sha256": {
".cache/ford_route149/full/route.npz": "aa5902877343cd033ee286b3668d91a85336ffbf740861849fb0b76b0ca24ade",
".cache/ford_route149/full/model_paths.npz": "19827800b8fb5983f3d6b72fcfaf36e35a40170bb17cd7c1e49374744fb449fb",
".cache/ford_route149/full/metadata.json": "624fff03c25eb298661cb7b25f3dbe6d214d863f799d93635c0f4f05fc0d2b32",
".cache/ford_route149/intake.npz": "503919c4f1566ef850007c59f16ce074697ba57732f21beb7b158c1ca7be3a9d"
},
"baseline_matches_archived_v20_commands_and_integral": true
}
}
}
-75
View File
@@ -1,75 +0,0 @@
# Ford feedback: use filtered driver input
Route 162 ran action mode at C0 P=1.0. Raw torque crossings above 1 Nm
repeatedly removed C0/C1 feedback even while Ford's `steeringPressed` remained
false. The controller duplicated the torque threshold without Ford's existing
filter, so short crossings discarded the integral and abruptly removed P.
The adapter now uses `steeringPressed` for this decision. The existing Ford
threshold and filter are unchanged. From a zero counter, sustained torque
crosses that filter on the sixth sample. Fresh PSCM driver override (limit=3)
and invalid torque still clear feedback immediately. PSCM denial/inactive
status, freshness checks and input validity retain their previous behavior.
Clearing feedback removes correction; the base path request remains.
This applies to action and direct-path modes. Gains, reference selection,
distances, bounds, integration/unwind math, upstream request limits and CAN
cadence are unchanged. Action C0 P remains 1.0; direct-path C0 P remains 0.5.
Diagnostic names end in `v20-filtered-driver` so the next drive can confirm the
new arbitration actually ran.
## Validation
The short-torque-pulse regression failed against the old adapter before the
change. Afterward, the Ford controller, integration/CAN, geometry and Sunnylink
suites passed: 658 tests and 2 subtests. Tests exercise the actual shared Ford
filter, both torque signs, both reference modes, immediate PSCM override and
invalid torque. Ruff and `git diff --check` passed.
Native-time replay compares the new adapter with
`4900c0a40c87c72000b7168a6cf4fe6dd98ea6d0`, supplying identical recorded selected
curvature, vehicle measurements, driver flags and PSCM status. Four routes
cover 583,599 control cycles and 58,362 CAN pack/decode checks. Every command
stays in its field bounds with C2=C3=0, and filtered driver input or fresh PSCM
override clears all feedback.
Route 162's baseline reproduces recorded C0/C1 to Float32 precision (maximum
errors 1.15e-7 m and 1.48e-8 rad). Older routes were driven with earlier gains;
their comparisons below are two counterfactual command streams on the same
recorded motion, not a reproduction of those older deployed controllers.
| Route | C0 changes >0.25 m below 15 mph, old → new | C1 changes >0.05 rad below 15 mph, old → new | C1 at field bound, old → new |
| --- | --- | --- | --- |
| 162 | 152 → 70 | 83 → 50 | 0.97 → 1.06 s |
| 157 | 229 → 137 | 105 → 59 | 5.83 → 6.52 s |
| 151 | 142 → 77 | 86 → 42 | 3.39 → 4.04 s |
| 149 | 375 → 197 | 226 → 111 | 10.44 → 16.08 s |
For route 162, feedback on/off transitions fall from 538 to 218. At raw-torque
threshold crossings with unchanged model frame, nearly unchanged target and
no filtered driver/PSCM override, large C0 changes fall from 99 to zero.
Remaining transitions include legitimate driver input and PSCM status changes.
Retained correction changes C1 after a brief torque crossing has ended. This
can increase holding and time at the field limit, especially on route 149.
Replay proves command continuity and override behavior on frozen measurements;
it cannot establish improved physical tracking, stability or unwinding. Route
162 also had lag without any torque-triggered reset, which this change alone
does not explain.
Detailed metrics, source hashes and controller provenance are in
`ford_filtered_driver_v20_validation.json`. Reproduce a route with:
```sh
PYTHONPATH=.:opendbc_repo:.cache/ford_geometry_deps \
/Users/ibpersonal/dev/sunnypilot/.venv/bin/python \
tools/ford_pscm_lab/filtered_driver_replay.py \
--source .cache/ford_route162/full --output .cache/ford_filtered_driver_v20/162
```
## Deployment
Pull and restart the updated software while offroad. Keep Selected-Action Path
Tracking enabled, Model Geometry Reference disabled and C0 one-second distance
disabled for the current action/fixed-7-m trial. Turning the master controller
toggle off continues to select upstream Ford control.
@@ -1,308 +0,0 @@
{
"baseline_commit": "4900c0a40c87c72000b7168a6cf4fe6dd98ea6d0",
"tests": {
"passed": 658,
"subtests_passed": 2
},
"total_cycles": 583599,
"total_wire_checks": 58362,
"limitations": "Frozen recorded motion; no predicted wheel response. Only route 162 ran the baseline gain/version. Older-route baseline differences from recorded commands are expected.",
"routes": {
"162": {
"cycles": 49003,
"wire_checks": 4901,
"baseline_recorded_error_p50_p95_p99_max": {
"c0": [
2.3841861818141297e-09,
3.33786012163273e-08,
5.7220459037665705e-08,
1.1444091807533141e-07
],
"c1": [
4.6193593394860955e-10,
7.152557435219364e-09,
1.2874603272372553e-08,
1.478195188475695e-08
]
},
"latched_angle_match_fraction": 1.0,
"command_changes": {
"c0": [
0.0,
0.0,
0.3899999999999997,
2.58
],
"c1": [
0.0014999999999999458,
0.07550000000000001,
0.11149999999999999,
0.2450000000000001
]
},
"variants": {
"old": {
"feedback_switches_active": 538,
"feedback_switches_low_speed": 219,
"low_speed_c0_steps_over_025m": 152,
"low_speed_c1_steps_over_005rad": 83,
"low_speed_step_p99": {
"c0": 0.5400000000000005,
"c1": 0.033499999999999974
},
"raw_crossing_c0_steps_over_025m": 99,
"c0_bound_active_s": 0.0,
"c1_bound_active_s": 0.9656279909999625
},
"new": {
"feedback_switches_active": 218,
"feedback_switches_low_speed": 95,
"low_speed_c0_steps_over_025m": 70,
"low_speed_c1_steps_over_005rad": 50,
"low_speed_step_p99": {
"c0": 0.16000000000000014,
"c1": 0.019949999999999843
},
"raw_crossing_c0_steps_over_025m": 0,
"c0_bound_active_s": 0.0,
"c1_bound_active_s": 1.0580744659999937
}
},
"method": "Compare driver-input arbitration on recorded references and frozen vehicle motion.\n\nInput: extract.py route.npz/model_paths.npz/metadata.json directories. This checks\ncommand continuity and overrides; it does not predict a changed wheel response.\nBoth controllers receive the logged selected curvature. Recorded-command agreement\nis expected only for routes driven with the selected baseline and fixed 7 m C0.\n",
"provenance": {
"baseline_commit": "4900c0a40c87c72000b7168a6cf4fe6dd98ea6d0",
"baseline_controller_sha256": "d5841c1f6a4368de20ff14de89a48d7dce737c6a41d79628ccb4f8b19f28f91b",
"candidate_controller_sha256": "b34a55f84fdbf6c432ce89cdc378aaaf9bbd765ca4e6934845059f586e6f0773",
"fixed_c0_distance_m": 7.0
},
"sources_sha256": {
".cache/ford_route162/full/route.npz": "a725f9ca309b90a2d750d51a9b9d5b459d99119a54ab048631bc0fce07348af8",
".cache/ford_route162/full/model_paths.npz": "cffe310b122d9ae26a53ee504608156b4242a6bba5f681898d5b661951ac5ce5",
".cache/ford_route162/full/metadata.json": "ef3ac1caca0eaec31f7140a0743bd3b98885ce143cb0f97a8eee615b33943d7f"
},
"valid_active_seconds": 319.0762965260001
},
"157": {
"cycles": 76554,
"wire_checks": 7656,
"baseline_recorded_error_p50_p95_p99_max": {
"c0": [
0.009999999999999787,
0.3699999867081644,
1.1900000762939458,
2.7099999666213987
],
"c1": [
2.5331975406217566e-10,
7.152557379708213e-09,
1.3113021890553966e-08,
1.478195188475695e-08
]
},
"latched_angle_match_fraction": 1.0,
"command_changes": {
"c0": [
0.0,
0.0,
0.18569999999999706,
5.4
],
"c1": [
0.0,
0.035499999999999976,
0.066,
0.40850000000000003
]
},
"variants": {
"old": {
"feedback_switches_active": 484,
"feedback_switches_low_speed": 312,
"low_speed_c0_steps_over_025m": 229,
"low_speed_c1_steps_over_005rad": 105,
"low_speed_step_p99": {
"c0": 0.3100000000000005,
"c1": 0.028000000000000025
},
"raw_crossing_c0_steps_over_025m": 99,
"c0_bound_active_s": 0.25064877399995567,
"c1_bound_active_s": 5.832844185000681
},
"new": {
"feedback_switches_active": 224,
"feedback_switches_low_speed": 148,
"low_speed_c0_steps_over_025m": 137,
"low_speed_c1_steps_over_005rad": 59,
"low_speed_step_p99": {
"c0": 0.20000000000000018,
"c1": 0.022499999999999964
},
"raw_crossing_c0_steps_over_025m": 0,
"c0_bound_active_s": 0.2999506119999751,
"c1_bound_active_s": 6.521285587999955
}
},
"method": "Compare driver-input arbitration on recorded references and frozen vehicle motion.\n\nInput: extract.py route.npz/model_paths.npz/metadata.json directories. This checks\ncommand continuity and overrides; it does not predict a changed wheel response.\nBoth controllers receive the logged selected curvature. Recorded-command agreement\nis expected only for routes driven with the selected baseline and fixed 7 m C0.\n",
"provenance": {
"baseline_commit": "4900c0a40c87c72000b7168a6cf4fe6dd98ea6d0",
"baseline_controller_sha256": "d5841c1f6a4368de20ff14de89a48d7dce737c6a41d79628ccb4f8b19f28f91b",
"candidate_controller_sha256": "b34a55f84fdbf6c432ce89cdc378aaaf9bbd765ca4e6934845059f586e6f0773",
"fixed_c0_distance_m": 7.0
},
"sources_sha256": {
".cache/ford_route157/full/route.npz": "e2e2573f904aa11ee9e10450e7f5b965d475657b61127e827a67eadcd6b857fb",
".cache/ford_route157/full/model_paths.npz": "25fc2526e092fde5ceb5aab63a5aa4f73a2c989a3263cedb0a26faf8da61c47b",
".cache/ford_route157/full/metadata.json": "f2d72c9a877ed6694e4da6831841113dc0c05987e2c76cca51f358d12411b235"
},
"valid_active_seconds": 527.940320743
},
"151": {
"cycles": 325708,
"wire_checks": 32571,
"baseline_recorded_error_p50_p95_p99_max": {
"c0": [
0.010000000223516992,
0.18000000834465002,
0.9099999795913697,
4.419999957084657
],
"c1": [
0.002999999523162822,
0.016500000025611382,
0.04399999969661239,
0.09699999737739562
]
},
"latched_angle_match_fraction": 1.0,
"command_changes": {
"c0": [
0.0,
0.0,
0.0,
4.55
],
"c1": [
0.0,
0.020500000000000018,
0.050000000000000044,
0.28450000000000003
]
},
"variants": {
"old": {
"feedback_switches_active": 727,
"feedback_switches_low_speed": 225,
"low_speed_c0_steps_over_025m": 142,
"low_speed_c1_steps_over_005rad": 86,
"low_speed_step_p99": {
"c0": 0.17999999999999972,
"c1": 0.016500000000000015
},
"raw_crossing_c0_steps_over_025m": 80,
"c0_bound_active_s": 0.0,
"c1_bound_active_s": 3.3938434989995585
},
"new": {
"feedback_switches_active": 259,
"feedback_switches_low_speed": 101,
"low_speed_c0_steps_over_025m": 77,
"low_speed_c1_steps_over_005rad": 42,
"low_speed_step_p99": {
"c0": 0.13999999999999968,
"c1": 0.014499999999999957
},
"raw_crossing_c0_steps_over_025m": 0,
"c0_bound_active_s": 0.029249633000290487,
"c1_bound_active_s": 4.042117826999856
}
},
"method": "Compare driver-input arbitration on recorded references and frozen vehicle motion.\n\nInput: extract.py route.npz/model_paths.npz/metadata.json directories. This checks\ncommand continuity and overrides; it does not predict a changed wheel response.\nBoth controllers receive the logged selected curvature. Recorded-command agreement\nis expected only for routes driven with the selected baseline and fixed 7 m C0.\n",
"provenance": {
"baseline_commit": "4900c0a40c87c72000b7168a6cf4fe6dd98ea6d0",
"baseline_controller_sha256": "d5841c1f6a4368de20ff14de89a48d7dce737c6a41d79628ccb4f8b19f28f91b",
"candidate_controller_sha256": "b34a55f84fdbf6c432ce89cdc378aaaf9bbd765ca4e6934845059f586e6f0773",
"fixed_c0_distance_m": 7.0
},
"sources_sha256": {
".cache/ford_route151/full/route.npz": "41a5b8bd388cf5a3d553f784542376ac9355fcdc5be4f427053d0504537babe1",
".cache/ford_route151/full/model_paths.npz": "980b3843cec04254280d744a5801cee1bf0f8ef371052398327ff245f9eee01b",
".cache/ford_route151/full/metadata.json": "937825317a0edd470c54647240b922be8f79dda5b3365ffdd61281f0aca877a1"
},
"valid_active_seconds": 1355.493226190003
},
"149": {
"cycles": 132334,
"wire_checks": 13234,
"baseline_recorded_error_p50_p95_p99_max": {
"c0": [
0.020000001788138988,
0.7800000047683717,
2.070000008535385,
4.560000002980233
],
"c1": [
0.005500000000000005,
0.048999999627471036,
0.09199999963641169,
0.1995000131130219
]
},
"latched_angle_match_fraction": 1.0,
"command_changes": {
"c0": [
0.0,
0.0,
0.10000000000000053,
4.499999999999999
],
"c1": [
0.0,
0.07950000000000002,
0.3125,
0.4315
]
},
"variants": {
"old": {
"feedback_switches_active": 1024,
"feedback_switches_low_speed": 393,
"low_speed_c0_steps_over_025m": 375,
"low_speed_c1_steps_over_005rad": 226,
"low_speed_step_p99": {
"c0": 0.44079999999998143,
"c1": 0.031500000000000035
},
"raw_crossing_c0_steps_over_025m": 217,
"c0_bound_active_s": 0.0,
"c1_bound_active_s": 10.436320454000196
},
"new": {
"feedback_switches_active": 354,
"feedback_switches_low_speed": 171,
"low_speed_c0_steps_over_025m": 197,
"low_speed_c1_steps_over_005rad": 111,
"low_speed_step_p99": {
"c0": 0.1999999999999993,
"c1": 0.02150000000000002
},
"raw_crossing_c0_steps_over_025m": 0,
"c0_bound_active_s": 0.0,
"c1_bound_active_s": 16.08094779300012
}
},
"method": "Compare driver-input arbitration on recorded references and frozen vehicle motion.\n\nInput: extract.py route.npz/model_paths.npz/metadata.json directories. This checks\ncommand continuity and overrides; it does not predict a changed wheel response.\nBoth controllers receive the logged selected curvature. Recorded-command agreement\nis expected only for routes driven with the selected baseline and fixed 7 m C0.\n",
"provenance": {
"baseline_commit": "4900c0a40c87c72000b7168a6cf4fe6dd98ea6d0",
"baseline_controller_sha256": "d5841c1f6a4368de20ff14de89a48d7dce737c6a41d79628ccb4f8b19f28f91b",
"candidate_controller_sha256": "b34a55f84fdbf6c432ce89cdc378aaaf9bbd765ca4e6934845059f586e6f0773",
"fixed_c0_distance_m": 7.0
},
"sources_sha256": {
".cache/ford_route149/full/route.npz": "aa5902877343cd033ee286b3668d91a85336ffbf740861849fb0b76b0ca24ade",
".cache/ford_route149/full/model_paths.npz": "19827800b8fb5983f3d6b72fcfaf36e35a40170bb17cd7c1e49374744fb449fb",
".cache/ford_route149/full/metadata.json": "624fff03c25eb298661cb7b25f3dbe6d214d863f799d93635c0f4f05fc0d2b32"
},
"valid_active_seconds": 1247.577060015
}
}
}
-87
View File
@@ -1,87 +0,0 @@
# Ford model geometry reference trial
This documents the initial curvature-reference trial at `18ded0380`. The
subsequent [direct model-path trial](ford_direct_path.md) reuses the geometry
toggle and changes controlsd's reference and C0/C1 mapping. modeld telemetry
described here remains available for comparison.
`FordGeometryReference` changes the steering reference used by the existing
Ford C0/C1 feedback controller. It is default off and requires
`FordModelActionController` and a Ford CAN FD vehicle.
In Sunnylink's Ford settings, enable **Model Geometry Reference (Experimental)**
while offroad, then cycle offroad to onroad. Keep **Selected-Action Path Tracking**
enabled. Disable only the geometry toggle to compare with the previous model
action. Disable the controller toggle to restore upstream Ford control.
## Reference calculation
After modeld publishes the plan into the message builder, use its orientation
and orientation rate with the existing `get_curvature_from_plan` function:
```
curvature = 2 * heading_at_preview / (max(speed, 1) * preview)
- initial_heading_rate / max(speed, 1)
```
The existing function stabilizes previews shorter than 0.3 seconds using the
0.3-second heading. The experiment uses the exact `lat_action_t` already supplied
to inference, including learned/fixed delay, model-specific smoothing allowance,
the Ford speed-dependent 0.4-second preview addition, and 75 ms frame/action
timing compensation. CTMV2 on the recent route has roughly 0.744 seconds of
preview below 15 mph. This is a time preview, not direct sampling at seven metres.
Apply the same model-specific lateral smoothing and standstill hold as the
original action, once per published model frame. Use the published plan itself,
without a separate Planplus multiplier. Both `modelV2.action` and
`drivingModelData.action` receive the selected curvature. Preserve the original
action's independent smoothing history and its acceleration/stop fields.
controlsd still applies its existing curvature/acceleration/jerk limits and
lateral maneuver override. Its desired curvature and desired steering angle now
describe the selected reference. The unchanged controller converts that same
reference into C0/C1 and closes the loop on measured steering. C0 distance,
C1 distance, P/I gains, driver arbitration, PSCM limit behavior, CAN cadence,
and field bounds are unchanged; C2/C3 stay zero.
Malformed/nonfinite/incomplete orientation data retains the original action
for that frame and resets geometry smoothing history. Existing model validity
and freshness gates still apply. There is no runtime mode swap during engagement;
the toggle is read at modeld startup.
## Logging
`modelDataV2SP.fordGeometryReference` records enabled/valid, associated model
publication time, original action curvature, raw geometric curvature, selected
curvature, preview, and smoothing time. `valid=false` while enabled identifies
fallback. A startup log also identifies action or geometry mode. The existing
Ford diagnostics continue to identify the unchanged v15 feedback controller.
## Offline validation
The tests cover signs/units across speed and preview, smoothing, release and
reversal, standstill, invalid geometry, message serialization, preservation of
the original action/longitudinal control, selection through actual controlsd
code, desired-angle logging, maneuver priority, and upstream fallback.
Three Lightning routes (149, 151, 157; CTMV2 and Tee Time) were replayed through
the current reference selection, upstream limiter, and controller using frozen
recorded steering/driver/PSCM measurements. Native controller cadence is retained;
every tenth command gets a CAN pack/decode check. Reference timing uses recorded
delay and the speed of the first control sample consuming each model. That speed
can differ slightly from modeld's original inference input. This is not a neural
replay or a prediction of wheel motion under modified commands.
Results and input hashes: `ford_geometry_reference_validation.json`.
```
PYTHONPATH=.:opendbc_repo python tools/ford_pscm_lab/geometry_reference_validate.py \
--output .cache/ford_geometry
```
Geometry is not uniformly earlier or stronger. The route 157 right-turn example
requests a large angle earlier; the problematic segment 9 left initially asks
for less and later asks for more. It hits command field bounds more often in
the frozen-measurement replay. Straight cohorts are defined by the original
action, so they also include disagreements about turn entry/exit. No claim of
improved physical tracking or stability follows from these offline results.
@@ -1,212 +0,0 @@
{
"scope": "Replay the geometry reference and current C0/C1 controller on frozen route measurements.\n\nRequires cached full/route.npz, full/metadata.json, intake.npz and intake.json.\nThis is command validation, not a simulation of the truck following new requests.\n",
"routes": [
{
"route": "157",
"model": "CTMV2",
"cycles": 76554,
"model_frames": 14892,
"fallback_frames": 0,
"wire_round_trips": 15312,
"metrics": {
"all": {
"samples": 44971,
"angle_reference_difference_deg_p50_p95_max": [
0.7096582083526739,
37.323791775018584,
473.2113566966026
],
"action_c0_bound_samples": 0,
"geometry_c0_bound_samples": 26,
"action_c1_bound_samples": 433,
"geometry_c1_bound_samples": 1393
},
"straight": {
"samples": 29296,
"angle_reference_difference_deg_p50_p95_max": [
0.3907433332260141,
10.800464754067871,
172.34883032860316
],
"action_c0_bound_samples": 0,
"geometry_c0_bound_samples": 0,
"action_c1_bound_samples": 0,
"geometry_c1_bound_samples": 0
},
"turn": {
"samples": 5773,
"angle_reference_difference_deg_p50_p95_max": [
18.222567807080637,
104.0235491521133,
473.2113566966026
],
"action_c0_bound_samples": 0,
"geometry_c0_bound_samples": 26,
"action_c1_bound_samples": 433,
"geometry_c1_bound_samples": 1393
}
},
"sources_sha256": {
".cache/ford_route157/full/route.npz": "e2e2573f904aa11ee9e10450e7f5b965d475657b61127e827a67eadcd6b857fb",
".cache/ford_route157/full/metadata.json": "f2d72c9a877ed6694e4da6831841113dc0c05987e2c76cca51f358d12411b235",
".cache/ford_route157/intake.npz": "5dffd86fde68197727d6c6b9c5eae82320a456fa1206c4cf18e5e9c69419dde4",
".cache/ford_route157/intake.json": "1d82c7f87d42229b96771bce3dd43c6b1651aa4fb8bb0ca3b1886d5a1a1c1a3c",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/geometry_reference_validate.py": "84dae09c158f5aaa3b88b5141545cd16829022437027d3d86c60a0acf7d78f56",
"openpilot/sunnypilot/modeld_v2/ford_geometry.py": "9a51519533b7e4a6b5aa5ff58ad5b646afb2267c943d5a790e86d8b20324d84b",
"openpilot/selfdrive/controls/lib/ford_model_action.py": "8bc8554ed6743c5faf433deaa6de3f07fae27d183f62b04f49f9c3b9f1e8dcca"
}
},
{
"route": "149",
"model": "CTMV2",
"cycles": 132334,
"model_frames": 26104,
"fallback_frames": 0,
"wire_round_trips": 26468,
"metrics": {
"all": {
"samples": 109224,
"angle_reference_difference_deg_p50_p95_max": [
1.0809205342304384,
21.80654129599715,
180.71838950537807
],
"action_c0_bound_samples": 0,
"geometry_c0_bound_samples": 142,
"action_c1_bound_samples": 845,
"geometry_c1_bound_samples": 2430
},
"straight": {
"samples": 67435,
"angle_reference_difference_deg_p50_p95_max": [
0.6422363701463203,
6.246569735165232,
62.050131143674285
],
"action_c0_bound_samples": 0,
"geometry_c0_bound_samples": 0,
"action_c1_bound_samples": 0,
"geometry_c1_bound_samples": 0
},
"turn": {
"samples": 15161,
"angle_reference_difference_deg_p50_p95_max": [
6.8859610951543,
87.51133149734092,
180.71838950537807
],
"action_c0_bound_samples": 0,
"geometry_c0_bound_samples": 142,
"action_c1_bound_samples": 845,
"geometry_c1_bound_samples": 2430
}
},
"sources_sha256": {
".cache/ford_route149/full/route.npz": "aa5902877343cd033ee286b3668d91a85336ffbf740861849fb0b76b0ca24ade",
".cache/ford_route149/full/metadata.json": "624fff03c25eb298661cb7b25f3dbe6d214d863f799d93635c0f4f05fc0d2b32",
".cache/ford_route149/intake.npz": "503919c4f1566ef850007c59f16ce074697ba57732f21beb7b158c1ca7be3a9d",
".cache/ford_route149/intake.json": "80f8c4ba69b4259ed308e0db110123063c6917357d26b49c9b6585695c9b24ae",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/geometry_reference_validate.py": "84dae09c158f5aaa3b88b5141545cd16829022437027d3d86c60a0acf7d78f56",
"openpilot/sunnypilot/modeld_v2/ford_geometry.py": "9a51519533b7e4a6b5aa5ff58ad5b646afb2267c943d5a790e86d8b20324d84b",
"openpilot/selfdrive/controls/lib/ford_model_action.py": "8bc8554ed6743c5faf433deaa6de3f07fae27d183f62b04f49f9c3b9f1e8dcca"
}
},
{
"route": "151",
"model": "TT",
"cycles": 325708,
"model_frames": 64837,
"fallback_frames": 0,
"wire_round_trips": 65142,
"metrics": {
"all": {
"samples": 124806,
"angle_reference_difference_deg_p50_p95_max": [
0.6014079626170221,
8.687564671100564,
233.5485828527104
],
"action_c0_bound_samples": 0,
"geometry_c0_bound_samples": 6,
"action_c1_bound_samples": 162,
"geometry_c1_bound_samples": 588
},
"straight": {
"samples": 105881,
"angle_reference_difference_deg_p50_p95_max": [
0.5210065989727145,
4.103656620119354,
143.66919002099516
],
"action_c0_bound_samples": 0,
"geometry_c0_bound_samples": 0,
"action_c1_bound_samples": 0,
"geometry_c1_bound_samples": 0
},
"turn": {
"samples": 3715,
"angle_reference_difference_deg_p50_p95_max": [
9.423421334227854,
66.85200116463068,
233.5485828527104
],
"action_c0_bound_samples": 0,
"geometry_c0_bound_samples": 6,
"action_c1_bound_samples": 162,
"geometry_c1_bound_samples": 588
}
},
"sources_sha256": {
".cache/ford_route151/full/route.npz": "41a5b8bd388cf5a3d553f784542376ac9355fcdc5be4f427053d0504537babe1",
".cache/ford_route151/full/metadata.json": "937825317a0edd470c54647240b922be8f79dda5b3365ffdd61281f0aca877a1",
".cache/ford_route151/intake.npz": "18bbefb7738cebd0071dc987e90f74469a0c8ed456f9412324030592cafd68c9",
".cache/ford_route151/intake.json": "0cfe327b5889f2603d902d2a01e915bf9a27b7a312137111d892a5e8540e03aa",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/geometry_reference_validate.py": "84dae09c158f5aaa3b88b5141545cd16829022437027d3d86c60a0acf7d78f56",
"openpilot/sunnypilot/modeld_v2/ford_geometry.py": "9a51519533b7e4a6b5aa5ff58ad5b646afb2267c943d5a790e86d8b20324d84b",
"openpilot/selfdrive/controls/lib/ford_model_action.py": "8bc8554ed6743c5faf433deaa6de3f07fae27d183f62b04f49f9c3b9f1e8dcca"
}
}
],
"tests_passed": 605,
"route157_timing": [
{
"event": 9,
"action": {
"entry_75deg": 509.71654137700006,
"release_below_25deg": 515.628442615
},
"geometry": {
"entry_75deg": 509.26702728900005,
"release_below_25deg": 515.7717539590001
}
},
{
"event": 11,
"action": {
"entry_75deg": 595.0217840170001,
"release_below_25deg": 601.3182963449999
},
"geometry": {
"entry_75deg": 595.2721063070001,
"release_below_25deg": 601.874657156
}
},
{
"event": 16,
"action": {
"entry_75deg": 700.319324972,
"release_below_25deg": 708.5712472329999
},
"geometry": {
"entry_75deg": 700.2793086730001,
"release_below_25deg": 709.1140404539999
}
}
],
"totals": {
"cycles": 534596,
"model_frames": 105833,
"fallback_frames": 0,
"wire_round_trips": 106922
}
}
-155
View File
@@ -1,155 +0,0 @@
# 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. The current experiment adds [measured-curvature C1 feedback](ford_c1_feedback.md)
to this original mapping; the historical no-feedback description below is
not the current controller specification.
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.
-125
View File
@@ -1,125 +0,0 @@
# Ford selected-action drive-test branch
This v14 trial increases C1's integral gain from 0.25 to **1.0**, retaining
P=0.75, [curvature-derived C0](ford_curvature_c0_v8.md), direct C0/C1 requests,
and [continuous C1 PI feedback](ford_c1_minimal_pi.md).
Only integrated tracking error accumulates correction; C0/C1 reflect the current bounded request. C0 defaults to a 7 m circular arc from selected desired curvature. An on-device toggle can instead use max(7 m, speed × 1 second).
[Base C1 overflow allocation to C0](ford_c1_overflow.md) remains.
It is selectable on **any Ford CAN FD vehicle**
through the existing persistent, default-off Sunnylink
toggle. Offline checks establish software behavior; physical tracking,
turn-exit behavior and closed-loop stability remain unvalidated.
Both base commands use selected, upstream-limited desired curvature. The +0.40 s
low-speed model preview from `b720e9f1b` remains: full offset at 15 mph and below,
tapering to zero at 30 mph. The trial multiplies each fresh integral error increment
by four, for both accumulation and retirement. P, PSCM `LimitReached` handling,
integral arithmetic, field bounds, and selection are retained.
See [I=1.0 replay results](ford_c1_i1_trial.md) for scope, tradeoffs, and reproduction.
## Select and restore
1. Install branch `hiimisaac-dev` from `sunnypilot/sunnypilot` using the device's
normal branch-switch process and allow its build to finish.
2. While offroad, open Sunnylink device settings → Vehicle → Ford and enable
**Selected-Action Path Tracking (Experimental)** (`FordModelActionController`).
3. Complete a real offroad-to-onroad cycle. Selection occurs when `controlsd`
starts; a stored toggle change or disengagement alone cannot swap an active
controller. Initial physical evaluation remains controlled testing.
The startup event `Ford path controller selected` should report
`FordModelActionController`. Periodic `Ford C2-free path tracking` events
identify **`hypothesis=model-action-curvature-c0-distance-pi-v14`**. They report desired and measured
curvature, base heading, proportional and accumulated correction, applied heading,
feedback timing and driver/PSCM gating. `proportional_gain=0.75` and
`integral_gain=1.0` identify the trial. `offset_overflow` reports the extra C0
target in meters before C0 amplitude limits. `calibration_approved=false`
remains. The retired request/unwind/reversal diagnostic fields are removed.
Turning the toggle off and completing another offroad-to-onroad cycle restores
**upstream Ford curvature control**: 20 Hz steering messages, limited mode on
CAN FD, zero C0/C1/C3, and upstream curvature limiting and platform-specific
overshoot handling. Stored observer or retired controller settings cannot select
a custom controller. The observer toggle is no longer exposed. The experiment
only runs on Ford CAN FD vehicles; legacy Ford uses upstream control as well.
See [toggle-off validation](ford_upstream_fallback.md).
## C0 distance toggle on comma four
With the experimental Ford controller enabled, open **Settings → toggles → C0: 1 second**.
The toggle is visible for Ford CAN FD vehicles and can be changed while disengaged.
- **Off (default):** C0 uses a fixed 7 m arc.
- **On:** C0 uses a distance of max(7 m, speed × 1 second), matching the base C1 distance.
Disengage assistance, change the toggle, and remain disengaged for at least three seconds
before reengaging. This setting uses the existing three-second runtime parameter refresh;
**no ignition cycle or controlsd restart is required**. Engaged or paused MADS and stale
engagement messages prevent applying a change. A mode change resets the PI correction and
adapter timestamps. Reapplying the same value does not reset anything.
The persistent parameter is `FordC0TimeBased`. It cannot enable the experimental controller
by itself. The existing Sunnylink controller-selection toggle still requires an onroad cycle.
C1, the gains, the 7 m heading-overflow allocation, the upstream reference limits and the CAN
field bounds are unchanged. Below 7 m/s (about 15.7 mph), both distance modes are identical.
At 20/30/60 mph the enabled distance is approximately 8.9/13.4/26.8 m, respectively; C0 can
therefore be substantially larger, especially at higher speeds. Its release still follows the
current selected curvature immediately, with no additional slew.
The `Ford C0 distance changed` event records an applied switch. Periodic tracking events
include `c0_time_based` and the actual `offset_distance` in meters, including the default mode.
Offline checks verify selection, runtime switching, resets, unchanged C1 and CAN encoding;
they do not establish which distance the PSCM follows better.
Validation on 2026-09-14: 410 tests and 25 subtests passed, plus Ruff and the local comma four
UI construction/write/refresh/visibility/render check. The 54,146-cycle maneuver-route replay
(`84865544361f55cb/0000011c--99f4537696`) matched `775012167` exactly with the new toggle off.
With it on, C0 changed in 35,668 cycles (maximum difference 0.74 m), while C1 and accumulated
correction remained identical on the same recorded motion. The two comparisons completed
216,584 controller updates and CAN round trips. No vehicle build, installation or road test
was performed for this change.
## Wiring and validation
`controlsd` supplies the selected, upstream-limited desired curvature and the
measured steering-derived curvature already used in its tracking diagnostics.
Fresh steering publications advance C1 integration. P responds to the current
error without accumulating. Repeated publications use current feedforward and P
but cannot integrate the same elapsed interval twice.
Driver override clears P and I. A fresh PSCM reached-limit flag stops
extra outward accumulation while preserving unwind and base model changes.
With fresh feedback, the part of the error increment that cancels existing I
is applied before the ordinary accumulation clamp. Any remainder must fit the
combined feedforward/P/I amplitude envelope. There is no C0 confirmation
threshold or remembered turn direction. Zero error removes P and holds I; it
does not trigger a release. Final command limits still apply.
C0 starts with the selected-distance circular arc of selected desired curvature. It does not
add independent live model-path position or heading. Valid model geometry is
still required as a health gate. When the raw base heading
exceeds ±0.5 rad, C0 additionally receives 7 m times the clipped-away heading.
Accumulated C1 feedback does not spill into C0. The extra target returns to zero
as the base heading falls below the cap. Applied C0 changes in that same update.
C2 and C3 remain zero. The
existing field bounds, 100 Hz custom sender and Float32 publication remain in
place. An explicit selection flag distinguishes upstream mode from an invalid
experimental command; invalid experimental input cannot switch to upstream.
The opendbc sender restores upstream behavior when that flag is false.
[Direct-command validation](ford_direct_path_v9.md) records the same command law introduced in v9.
[Curvature-C0](ford_curvature_c0_v8.md) and its validation JSON record v8.
[Continuous PI](ford_c1_minimal_pi.md) and its validation JSON record v7.
[Proportional feedback](ford_c1_pi.md) and its validation JSON record v6.
[Completed-unwind release](ford_unwind_catchup.md) and
`ford_unwind_catchup_validation.json` record v5. [Changed-request release](ford_c1_request_release.md) and its
validation JSON record v4. The [overflow specification](ford_c1_overflow.md) and
`ford_c1_overflow_validation.json` record v3. The carryover specification and `ford_c1_carryover_validation.json`
record the previous experiment. `ford_c1_feedback_validation.json` records the initial feedback
version at `5fbb583e5`. `ford_model_action_validation.json` and
`ford_model_action_drive_test_validation.json` are historical records for the
original offline candidate and its first wiring, respectively; their counts
and coverage are not claims about the current version.
The full hardware build and device boot are not performed by these offline
checks. Pushing the branch does not install it on the device or change its
stored toggle.
@@ -1,145 +0,0 @@
{
"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"
}
}
-220
View File
@@ -1,220 +0,0 @@
{
"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"
}
}
-122
View File
@@ -1,122 +0,0 @@
# Ford completed-unwind correction release
Version `model-action-c1-feedback-v5` releases dominant C1 correction after a
confirmed unwind reaches the selected curvature. This fixes stored correction
continuing to request a new turn after its original unwind is complete.
Route 115 (`codex-last2`) ran v4, `6df5eabb7`. Near 2:13.2, desired and actual
steering were both near zero after a right turn, but C1 still requested about
0.1165 rad left. About 0.114 rad was accumulated unwind correction. V4's
changed-request release did not apply: the small new left request was increasing
while the measured error called for less left steering. The earlier reversal
release also did not apply because measured and requested curvature were
already on the same side.
## Rule
On a fresh steering measurement, remember an unwind direction when the selected
curvature relaxes toward zero (or crosses it), measured curvature remains on
the previous side, and measured error calls for leaving that old turn.
When measured error reaches or passes zero in that unwind direction, release
the stored correction only if all of these agree:
- The selected curvature has reached zero or crossed into the unwind direction.
- Correction points in that direction and exceeds the magnitude of base C1.
- Original model C0 and applied C0 both confirm that direction by at least the
existing 0.01 m DBC step.
Consume the unwind marker at this first catch-up, even if the other conditions
prevent release. A steady old-side bend, neutral/conflicting C0, or a correction
smaller than base C1 retains its correction. Steady requests cannot arm the
marker. Duplicate steering publications cannot arm or consume it. Driver/PSCM
feedback inhibition and controller resets clear it; correction reversing
direction also clears it.
After retirement, the v4 command law still runs: bounded changed-request
release, reversal release, measured-error integration, PSCM arbitration, and
final C1 slew. Removing stored correction therefore does not jump the output.
There is one additional control state, `unwind_direction`; `unwind_release`
only reports the signed correction retired on the current cycle. Periodic
diagnostics may miss individual release cycles.
This is a conditional correction-reset policy, not a PSCM plant model. It adds
no strength multiplier. The existing 1:1 feedback choice, C0 mapping/overflow,
C2/C3 zeroing, amplitude/slew limits, sender cadence and input gates remain.
Toggle off still selects upstream Ford control; toggle on selects the experiment
on Ford CAN FD platforms. See [selection and restore](ford_model_action_drive_test.md).
## Exact exit replay
Frozen route 115 measurements trigger one release at **2:13.203501**. The
selected steering angle is 0.469 degrees left and measured angle is 0.500 degrees
left. The controller retires 0.114026 rad of left unwind correction. Its first
C1 output moves from 0.1165 to 0.1110 rad left, respecting the original slew.
At **2:13.594615**, old C1 is **0.1240 rad left**, versus **0.0105 rad left** in
v5. C0 is identical. The earlier unwind (2:09 through 2:13.2), comparison turn
(3:20 through 3:34), and comparison bend (5:33 through 5:45) have identical
commands throughout their windows.
The recorded wheel motion stays fixed in this replay. It does not predict a
new steering angle, prove stability, or establish that the full overshoot is
fixed. This maneuver also includes driver input and a changing C0 request;
neither its whole swing nor every hanging exit can be attributed to stored I.
## Validation
Four targeted regressions failed on v4 because correction persisted after
catch-up; all now pass. Expanded tests cover both directions, fresh/duplicate
feedback, catch-up confirmation, steady tracking/noise, old-side bends, C0
agreement, dominant correction, reset/override, limit-reached behavior and slew.
Integration exercises actual controlsd request selection/limiting for model
and maneuver sources, Float32 publication, and Ford CAN packing/checksums.
The combined suite passes **753 tests and 9,145 subtests**, with **178 inherited
or unsupported safety-test skips**. Random feedback stress covers 200,000 cycles
and their mirrors. Each cycle exactly matches v4 after only the declared new
retirement; 59 cycles retire correction. Independent zero-error checks cover
200,000 cycles and 18,138 field-boundary cases. Ruff, controller Ty and settings
compilation checks pass.
| Frozen route | Cycles | New releases | Changed C1 cycles | Largest C1 difference |
| --- | ---: | ---: | ---: | ---: |
| 114 | 61,027 | 4 | 2,849 | 0.0150 rad |
| 115 | 40,037 | 1 | 384 | 0.1140 rad |
| 112 | 108,971 | 14 | 30,036 | 0.0720 rad |
| 113 | 49,614 | 1 | 951 | 0.0050 rad |
| Historical b9 | 90,774 | 16 | 7,013 | 0.1435 rad |
All routes compare v5 against v4 with the same recorded inputs. Activation and
C0 match exactly on all 350,423 cycles. Releases also occur at smaller exits;
changed history can affect subsequent ordinary bends. These results do not
establish unchanged physical centering. Route 114's large segment-2 overshoot
does not trigger this new release, so it remains a separate unresolved case.
The five replays and two stress runs verify **768,561 Float32/CAN round trips**,
separately from the integration suite. Exact source hashes and numerical
results are in `ford_unwind_catchup_validation.json`.
## Reproduction
Use the native project Python dependencies and unchanged opendbc revision
`64aa61b9b3fd26e70a7caa915acab207ff3cd64a`. Route replay requires the full-rlog
extracts identified by validation hashes; the original logs are not modified.
```sh
export PYTHONDONTWRITEBYTECODE=1
export PYTHONPATH=.:opendbc_repo
export PARAMS_ROOT=/tmp/ford-v5-test-params
export LOG_ROOT=/tmp/ford-v5-test-logs
python -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 openpilot/sunnypilot/sunnylink/tests openpilot/common/tests/test_params.py opendbc_repo/opendbc/car/ford/tests/test_ford.py opendbc_repo/opendbc/safety/tests/test_ford.py
python -m tools.ford_pscm_lab.feedback_replay stress --cycles 200000 --output .cache/ford_unwind_catchup/stress.json
python -m tools.ford_pscm_lab.stress_model_action --cycles 200000 --seed 20260913 --opendbc-revision 64aa61b9b3fd26e70a7caa915acab207ff3cd64a --output .cache/ford_unwind_catchup/zero_error.json
for route in 114 115 112 113 b9; do
python -m tools.ford_pscm_lab.feedback_replay route .cache/ford_route${route} --baseline 6df5eabb7e7f6bc4e206644d5ca9069df820124e --output .cache/ford_unwind_catchup/route${route}
done
```
Publication time approximates the computation clock; full SubMaster health is
not reconstructable. These route replays do not reconstruct selected maneuver
messages; integration tests cover that source. No device build, boot,
installation or physical steering test is performed offline.
-346
View File
@@ -1,346 +0,0 @@
{
"hypothesis": "model-action-c1-feedback-v5",
"baseline_revision": "6df5eabb7e7f6bc4e206644d5ca9069df820124e",
"opendbc_revision": "64aa61b9b3fd26e70a7caa915acab207ff3cd64a",
"scope": "Software command release and numerical invariants only; no counterfactual steering motion, physical stability or improved tracking claim.",
"calibration_approved": false,
"tests": {
"passed": 753,
"subtests_passed": 9145,
"skipped": 178,
"log_sha256": "64c683622cc91125e32cb0d78f4a5340b8d58fa149d90b06361629394489731d",
"initial_regression": "4 failed on v4: stored unwind correction remains after catch-up; all pass on v5",
"regression_log_sha256": "0ced2a0686cccba3c321c58749a679843a3179fa57078a6b030d20f0f9ae33e2"
},
"feedback_stress": {
"cycles": 200000,
"mirrored_updates": 200000,
"can_round_trips": 200000,
"carryover_release_count": 181,
"baseline_revision": "6df5eabb7e7f6bc4e206644d5ca9069df820124e",
"baseline_source_sha256": "1a4ce5f5f63b4d2f1f6e0537c9b2ca7c463ca44b427349d71d28fb8a1138b00f",
"seed": 20260913,
"request_release_cycles": 20214,
"unwind_release_cycles": 59,
"exact_unchanged_state_and_commands_without_unwind_release": 199941,
"exact_v4_match_after_only_declared_retirement": 200000,
"checks": "Symmetry, resets, amplitude/slew, bounded retirement, carryover confirmation, integration, PSCM limits, CAN.",
"scope": "Numerical software invariants only; no model of vehicle motion.",
"calibration_approved": false,
"controller_sha256": "2ceb4cd8717bb3325f9b22189c78605ad5d42a1061dec9ca2e4dbb90fd256d1e"
},
"zero_error_stress": {
"seed": 20260913,
"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.4000000000000019,
0.05000000000000002
],
"calibration_approved": false,
"scope": "Zero-error numerical construction: measured equals requested curvature. No PSCM response claims.",
"opendbc_import_head": "64aa61b9b3fd26e70a7caa915acab207ff3cd64a",
"source_sha256": {
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/stress_model_action.py": "cec2619285dd41274562ac035ee8ea0a389269a0c4ef1b62efa6252ad1a714aa",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/openpilot/selfdrive/controls/lib/ford_model_action.py": "2ceb4cd8717bb3325f9b22189c78605ad5d42a1061dec9ca2e4dbb90fd256d1e",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/model_action_replay.py": "af97c665f342c66b1be2502e188c63e6f3ee106d0a0d5e80997bc3040373ff9f"
}
},
"replays": {
"114": {
"baseline_revision": "6df5eabb7e7f6bc4e206644d5ca9069df820124e",
"baseline_source_sha256": "1a4ce5f5f63b4d2f1f6e0537c9b2ca7c463ca44b427349d71d28fb8a1138b00f",
"calibration_approved": false,
"cycles": 61027,
"active_cycles": 48419,
"validity_matches_baseline_exactly": true,
"status_counts": {
"inactive": 12608,
"active": 48419
},
"c0_matches_baseline_exactly": true,
"c0_changed_cycles": 0,
"max_abs_c0_change_m": 0.0,
"offset_overflow_seconds": 6.548916987999917,
"max_abs_offset_overflow_target_m": 1.233181856572628,
"request_release_cycles": 288,
"request_release_seconds": 2.98796386799998,
"max_abs_request_release_rad": 0.006280367394214892,
"unwind_release_cycles": 4,
"max_abs_unwind_release_rad": 0.01457856219656457,
"feedback_enabled_seconds": 438.3550780740002,
"pscm_limit_2_seconds": 8.604224759000118,
"c1_changed_cycles": 2849,
"max_abs_c1_change_rad": 0.015000000000000013,
"max_abs_correction_rad": 0.2921633626620207,
"can_round_trips": 61027,
"source_sha256": {
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route114/route.npz": "83524f07d61104b84b004ddc46bb751ca7f61da67798aa47db56d307765f5b3e",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route114/model_paths.npz": "14d14362d1a3e46398edd8e22b7cc4e36277a7596a73f546192d0e14c6642b07",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route114/metadata.json": "ec677b9275c1707e477ebd0ffa235d49b5ec63a1ee717b16040c3acdbcd3bdc0",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/feedback_replay.py": "e869520839aef948ccbf20b44e3efb6ec854a539ec0d66c07779825bcd8037a7",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/openpilot/selfdrive/controls/lib/ford_model_action.py": "2ceb4cd8717bb3325f9b22189c78605ad5d42a1061dec9ca2e4dbb90fd256d1e"
},
"timing_limit": "Controls publication time proxies the computation clock; full SubMaster checks are unavailable.",
"reference_limit": "Uses exact consumed model publication as reference; selected maneuver-plan messages are not reconstructed.",
"report_sha256": "9a9d7a6c92ec2cc1e19dc6b6e628e402cbe3f6344e09ccbee1c5c76deedebc7e",
"commands_sha256": "fc1a5249cc9a15e3369f177a88781377c327e27b681e1028ad26063cb70167a5"
},
"115": {
"baseline_revision": "6df5eabb7e7f6bc4e206644d5ca9069df820124e",
"baseline_source_sha256": "1a4ce5f5f63b4d2f1f6e0537c9b2ca7c463ca44b427349d71d28fb8a1138b00f",
"calibration_approved": false,
"cycles": 40037,
"active_cycles": 33976,
"validity_matches_baseline_exactly": true,
"status_counts": {
"inactive": 6061,
"active": 33976
},
"c0_matches_baseline_exactly": true,
"c0_changed_cycles": 0,
"max_abs_c0_change_m": 0.0,
"offset_overflow_seconds": 2.095635206999873,
"max_abs_offset_overflow_target_m": 0.6360401958227158,
"request_release_cycles": 181,
"request_release_seconds": 1.7931359259980582,
"max_abs_request_release_rad": 0.005393094571379859,
"unwind_release_cycles": 1,
"max_abs_unwind_release_rad": 0.11402585053291069,
"unwind_releases": [
{
"time_s": 133.203500567,
"retired_rad": -0.11402585053291069,
"correction_after_rad": 0.0,
"base_c1_rad": -0.0025691102200653404,
"desired_angle_deg": 0.46853354573249817,
"actual_angle_deg": 0.5,
"baseline_c0_c1": [
-0.04999999999999982,
-0.11650000000000005
],
"candidate_c0_c1": [
-0.04999999999999982,
-0.11099999999999999
]
}
],
"feedback_enabled_seconds": 306.2414766659987,
"pscm_limit_2_seconds": 6.4724571650002645,
"c1_changed_cycles": 384,
"max_abs_c1_change_rad": 0.1140000000000001,
"max_abs_correction_rad": 0.13572457044904282,
"can_round_trips": 40037,
"source_sha256": {
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route115/route.npz": "e0df32d9e80f1c6b7d56327b37cf07af60070ffc212b8d111e343306148bff23",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route115/model_paths.npz": "52f3ed9e188e4947618a57a3ed872fd1966a887fe1014999df741553b6c13bc0",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route115/metadata.json": "d3c73035bad8eb5059e07962fd274c19cb70c8952b6f1514835d312d08b87a16",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/feedback_replay.py": "e869520839aef948ccbf20b44e3efb6ec854a539ec0d66c07779825bcd8037a7",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/openpilot/selfdrive/controls/lib/ford_model_action.py": "2ceb4cd8717bb3325f9b22189c78605ad5d42a1061dec9ca2e4dbb90fd256d1e"
},
"timing_limit": "Controls publication time proxies the computation clock; full SubMaster checks are unavailable.",
"reference_limit": "Uses exact consumed model publication as reference; selected maneuver-plan messages are not reconstructed.",
"report_sha256": "e3e4183e14ce20b2d3b0938028a5a4ab3200f72dd46e898174751aa94912ad84",
"commands_sha256": "ba9b6411a432430bb1380f63fe011ba91235b393f3785c17d2904657c95d27d9"
},
"112": {
"baseline_revision": "6df5eabb7e7f6bc4e206644d5ca9069df820124e",
"baseline_source_sha256": "1a4ce5f5f63b4d2f1f6e0537c9b2ca7c463ca44b427349d71d28fb8a1138b00f",
"calibration_approved": false,
"cycles": 108971,
"active_cycles": 91414,
"validity_matches_baseline_exactly": true,
"status_counts": {
"inactive": 17557,
"active": 91414
},
"c0_matches_baseline_exactly": true,
"c0_changed_cycles": 0,
"max_abs_c0_change_m": 0.0,
"offset_overflow_seconds": 1.43945091800002,
"max_abs_offset_overflow_target_m": 0.5727187991142273,
"request_release_cycles": 381,
"request_release_seconds": 3.8611647240012985,
"max_abs_request_release_rad": 0.008723706007003784,
"unwind_release_cycles": 14,
"max_abs_unwind_release_rad": 0.07267236868778636,
"feedback_enabled_seconds": 840.7664582650004,
"pscm_limit_2_seconds": 14.441855805999936,
"c1_changed_cycles": 30036,
"max_abs_c1_change_rad": 0.07200000000000006,
"max_abs_correction_rad": 0.205313389369823,
"can_round_trips": 108971,
"source_sha256": {
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route112/route.npz": "2b08a2fb636f7d14556d7df4035eafc1d1b97932237955528562a16db2b31d3e",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route112/model_paths.npz": "9837afe78aab4cad288cad98a595a5777fa8a66bb235986b1272a7f7c54e559a",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route112/metadata.json": "726d78a7e7aa45307dcfe27cb00775c20ecc9d54538eb7f26a0166fc216226ce",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/feedback_replay.py": "e869520839aef948ccbf20b44e3efb6ec854a539ec0d66c07779825bcd8037a7",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/openpilot/selfdrive/controls/lib/ford_model_action.py": "2ceb4cd8717bb3325f9b22189c78605ad5d42a1061dec9ca2e4dbb90fd256d1e"
},
"timing_limit": "Controls publication time proxies the computation clock; full SubMaster checks are unavailable.",
"reference_limit": "Uses exact consumed model publication as reference; selected maneuver-plan messages are not reconstructed.",
"report_sha256": "aaa1d819cf344de668ea92eb58714a6ae537054a0feadc05a0fa15097a57c553",
"commands_sha256": "9f191699d19af8f123e9245a77827a2eca27d4b3191f9acddf9389edbda0ef24"
},
"113": {
"baseline_revision": "6df5eabb7e7f6bc4e206644d5ca9069df820124e",
"baseline_source_sha256": "1a4ce5f5f63b4d2f1f6e0537c9b2ca7c463ca44b427349d71d28fb8a1138b00f",
"calibration_approved": false,
"cycles": 49614,
"active_cycles": 27207,
"validity_matches_baseline_exactly": true,
"status_counts": {
"inactive": 22407,
"active": 27207
},
"c0_matches_baseline_exactly": true,
"c0_changed_cycles": 0,
"max_abs_c0_change_m": 0.0,
"offset_overflow_seconds": 2.7607263189997866,
"max_abs_offset_overflow_target_m": 0.6065444126725197,
"request_release_cycles": 119,
"request_release_seconds": 1.2281319819985583,
"max_abs_request_release_rad": 0.009946223348379135,
"unwind_release_cycles": 1,
"max_abs_unwind_release_rad": 0.005007614799767142,
"feedback_enabled_seconds": 243.3033761190004,
"pscm_limit_2_seconds": 14.003248144999816,
"c1_changed_cycles": 951,
"max_abs_c1_change_rad": 0.00500000000000006,
"max_abs_correction_rad": 0.16966817302181283,
"can_round_trips": 49614,
"source_sha256": {
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route113/route.npz": "774ca4a21b7113c2706d6130bc180c3216ea4833155300ab01e75b3486e36327",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route113/model_paths.npz": "93c41761eb85263f534f5371b905482cf7c948582eb1e9149966594be1d3768f",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route113/metadata.json": "1c0ca74dd48b90ab9d5444c5ca7f8aa9361700bbdf98bd5e853be50ad2895d7f",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/feedback_replay.py": "e869520839aef948ccbf20b44e3efb6ec854a539ec0d66c07779825bcd8037a7",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/openpilot/selfdrive/controls/lib/ford_model_action.py": "2ceb4cd8717bb3325f9b22189c78605ad5d42a1061dec9ca2e4dbb90fd256d1e"
},
"timing_limit": "Controls publication time proxies the computation clock; full SubMaster checks are unavailable.",
"reference_limit": "Uses exact consumed model publication as reference; selected maneuver-plan messages are not reconstructed.",
"report_sha256": "032aaf4b2dd3a54264330c9c6367ad4a63f49d779ce9acd4c3c1d355d731d545",
"commands_sha256": "c1d4e59718400d21d724d502e2befa315db56802a7622d3d68e8b3af7daea319"
},
"b9": {
"baseline_revision": "6df5eabb7e7f6bc4e206644d5ca9069df820124e",
"baseline_source_sha256": "1a4ce5f5f63b4d2f1f6e0537c9b2ca7c463ca44b427349d71d28fb8a1138b00f",
"calibration_approved": false,
"cycles": 90774,
"active_cycles": 86474,
"validity_matches_baseline_exactly": true,
"status_counts": {
"inactive": 4300,
"active": 86474
},
"c0_matches_baseline_exactly": true,
"c0_changed_cycles": 0,
"max_abs_c0_change_m": 0.0,
"offset_overflow_seconds": 5.703841178999909,
"max_abs_offset_overflow_target_m": 4.280821338295937,
"request_release_cycles": 416,
"request_release_seconds": 4.241454379000558,
"max_abs_request_release_rad": 0.017186015844345093,
"unwind_release_cycles": 16,
"max_abs_unwind_release_rad": 0.15648483206475247,
"feedback_enabled_seconds": 816.0284774219999,
"pscm_limit_2_seconds": 9.308613716000167,
"c1_changed_cycles": 7013,
"max_abs_c1_change_rad": 0.14349999999999996,
"max_abs_correction_rad": 0.18457476562660308,
"can_round_trips": 90774,
"source_sha256": {
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_routeb9/route.npz": "b07c789d8155335f5d120d0262fced6e4d5803fe767b0ff49b6413dce4140b5c",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_routeb9/model_paths.npz": "6b1f87897c050273fdc05af051307a049b6fc3a93072e7cda1721195ce7c3861",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_routeb9/metadata.json": "9ce452220cab61b81883f32fc2fcaf5db6c78a674cb255a49cc77d5029580fee",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/feedback_replay.py": "e869520839aef948ccbf20b44e3efb6ec854a539ec0d66c07779825bcd8037a7",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/openpilot/selfdrive/controls/lib/ford_model_action.py": "2ceb4cd8717bb3325f9b22189c78605ad5d42a1061dec9ca2e4dbb90fd256d1e"
},
"timing_limit": "Controls publication time proxies the computation clock; full SubMaster checks are unavailable.",
"reference_limit": "Uses exact consumed model publication as reference; selected maneuver-plan messages are not reconstructed.",
"report_sha256": "d66308cefae9956d2b67b8d9ceb83804054e838c70e84999ba758ab1cbf21d5d",
"commands_sha256": "406275e9c230a2975fd8c9d0824d3a902283d3a5cb597328026578db66c2cb6b"
}
},
"checks": {
"ruff": true,
"controller_ty": true,
"settings_compilation": true
},
"limitations": [
"Recorded driver input and PSCM response remain fixed during replay.",
"No device build, boot or physical test performed.",
"Releases occur at smaller exits too; unchanged real-world centering is not established.",
"Route 114 segment-2 overshoot does not trigger this release."
],
"float32_can_round_trips_excluding_integration_tests": 768561,
"exit_points_left_positive": [
{
"time_s": 131.19208205699988,
"baseline_c1_rad": -0.10949999999999999,
"candidate_c1_rad": -0.10949999999999999,
"baseline_correction_rad": 0.04102452737994574,
"candidate_correction_rad": 0.04102452737994574
},
{
"time_s": 133.19194825599993,
"baseline_c1_rad": 0.11650000000000005,
"candidate_c1_rad": 0.11650000000000005,
"baseline_correction_rad": 0.11402585053291069,
"candidate_correction_rad": 0.11402585053291069
},
{
"time_s": 133.203500567,
"baseline_c1_rad": 0.11650000000000005,
"candidate_c1_rad": 0.11099999999999999,
"baseline_correction_rad": 0.1140256728569634,
"candidate_correction_rad": -0.0
},
{
"time_s": 133.40571050699987,
"baseline_c1_rad": 0.118,
"candidate_c1_rad": 0.009500000000000064,
"baseline_correction_rad": 0.11367557589137142,
"candidate_correction_rad": -0.0
},
{
"time_s": 133.59461515599992,
"baseline_c1_rad": 0.124,
"candidate_c1_rad": 0.010500000000000065,
"baseline_correction_rad": 0.1120761218192909,
"candidate_correction_rad": -0.0015653052344988395
},
{
"time_s": 207.90007524899988,
"baseline_c1_rad": 0.16449999999999998,
"candidate_c1_rad": 0.16449999999999998,
"baseline_correction_rad": 0.016763380618580685,
"candidate_correction_rad": 0.016763380618580685
},
{
"time_s": 338.168560822,
"baseline_c1_rad": -0.173,
"candidate_c1_rad": -0.173,
"baseline_correction_rad": -0.024831306563109386,
"candidate_correction_rad": -0.024831306563109386
}
],
"identical_route115_command_windows_s": [
[
129.0,
133.2
],
[
200.0,
214.0
],
[
333.0,
345.0
]
]
}
-89
View File
@@ -1,89 +0,0 @@
# Ford toggle-off upstream restoration
`FordModelActionController` is the only setting that can select custom Ford
steering. It defaults false. With it false or absent, no custom path controller
is created and normal lateral-control curvature passes unchanged to the Ford
sender. A stored `FordPscmObserver` or retired virtual-angle setting cannot
override that choice. The observer toggle is removed from Sunnylink; its stored
parameter remains readable for compatibility but has no selection effect.
Selection remains fixed for the lifetime of controlsd. Sunnylink changes require
a real offroad-to-onroad cycle, as before. The startup diagnostic reports
`controller=upstream` when the experiment is not selected.
## Sender behavior
The new `fordLateralPath.enabled` field conveys startup selection independently
of `valid`. Its default is false. The sender uses custom mode only when this
field is true on a Ford CAN FD vehicle. This prevents invalid model geometry
or disengagement in the selected experiment from choosing a different controller.
Toggle-off restores the upstream Ford sender:
- 20 Hz steering messages on both CAN FD and legacy Ford.
- CAN FD limited mode 1 while active, mode 0 while inactive, with upstream ramp
type 0, counters and checksums.
- C0, C1 and C3 zero; C2 follows upstream actuator curvature.
- Upstream curvature amplitude/rate limits and the measured-curvature error
clamp above 9 m/s.
- Upstream anti-overshoot handling for Bronco Sport and F-150 MK14.
The reference is the upstream implementation already merged into this branch,
opendbc `f95f996f5917dcbbf2e32fe51b606a24cf836af6`. Its Ford sender differs from
the locally available comma opendbc `3e92d112129507debe45364891954db70238997a`
only in sunnypilot's additional `CP_SP`/`CC_SP` interface arguments. This change
restores that implementation; it does not upgrade unrelated upstream code.
Toggle-on retains the previous custom 100 Hz sender, mode 2, ramp type 3 and
existing path limits. The model-action controller's command law and diagnostic
identity `model-action-c1-feedback-v3` are unchanged. Legacy Ford always uses
upstream control. The opendbc dependency is now
`64aa61b9b3fd26e70a7caa915acab207ff3cd64a`. No Panda safety code is changed;
its existing limited-mode checks already use 20 Hz curvature limits.
## Validation
- Combined Ford, Sunnylink, parameter, logging, replay-tool and Ford safety
suite: **683 passed, 178 existing skips, 9,145 subtests passed**.
- Real startup → controlsd → Float32 publication → conversion → Ford sender:
14 new toggle-off cases, covering all six CAN FD platforms plus legacy
Escape, with both stored observer settings. They preserve the upstream
actuator output, including when custom model geometry is missing, and verify
20 Hz cadence, engage/disengage/reengage, zero path terms, mode, ramp,
counters and checksums across 4,200 control cycles / 840 steering messages.
- The existing toggle-on, stale-input, invalid-input and 100 Hz integration
regressions continue to pass.
- Additional Ford interface fuzz checks: **11 passed**, 60 generated examples
each, with real Cap'n Proto conversion and car-interface application. The
initially missing neural-network-data dependency was initialized at the
repository's existing pin `03cac2d30e111e0689c0429cb8c1fe6cb5a905af`.
- Packet equivalence: **55,000 toggle-off cycles across all 11 Ford platforms**
match the pinned upstream sender exactly. **30,000 toggle-on cycles across
six CAN FD platforms** match the previous custom sender exactly. All 90,305
outgoing packets and returned actuator values match, including invalid paths,
inactive periods, both turn directions and speed boundaries. Disabled
selection also ignores deliberately nonzero, valid custom path fields.
- Ruff, controller type check, generated Sunnylink schema check and diff
whitespace checks pass.
The packet comparison loads the exact old controller **and its old CAN builder**
from trusted local Git sources. It does not compare two aliases of the modified
code. Results and source hashes are in `ford_upstream_fallback_validation.json`.
No device build, boot or physical steering validation is claimed.
## Reproduction
Use the pinned opendbc dependency and native project dependencies:
```sh
export PYTHONDONTWRITEBYTECODE=1
export PYTHONPATH=.:opendbc_repo
python -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 openpilot/sunnypilot/sunnylink/tests openpilot/common/tests/test_params.py opendbc_repo/opendbc/car/ford/tests/test_ford.py opendbc_repo/opendbc/safety/tests/test_ford.py
FUZZ_SEED=20260911 python -m pytest -q -p no:cacheprovider openpilot/selfdrive/car/tests/test_car_interfaces.py -k FORD
python -m tools.ford_pscm_lab.upstream_fallback_check --cycles 5000 --output .cache/ford_upstream_fallback/equivalence.json
python openpilot/sunnypilot/sunnylink/tools/compile_settings_ui.py --check
```
The comparison requires both pinned baseline commits in the local opendbc Git
object store. The previous overflow/replay records describe their historical
source hashes; the comparison here establishes unchanged toggle-on sender output.
-190
View File
@@ -1,190 +0,0 @@
{
"created_at_utc": "2026-09-11T16:11:23.341760+00:00",
"scope": "Default-off upstream Ford control, including CAN sender; exact software comparison only.",
"parent_commit": "b81c00f5b9c3658d72675ec3ee0ac07e0ef14807",
"opendbc_commit": "64aa61b9b3fd26e70a7caa915acab207ff3cd64a",
"target": {
"repository": "sunnypilot/sunnypilot",
"branch": "hiimisaac-dev"
},
"selection": {
"param": "FordModelActionController",
"default": false,
"off": "upstream",
"on": "Ford CAN FD model-action v3",
"activation": "Existing controlsd startup; real offroad-to-onroad cycle",
"observer_setting": "Ignored for control selection; no longer exposed in Sunnylink",
"sender_field": "fordLateralPath.enabled defaults false, independent of valid"
},
"custom_command_law_ast_matches_parent": [
"_packed",
"_finite",
"encode_model_action",
"ModelActionController",
"FordModelActionController"
],
"tests": {
"combined": "683 passed, 178 skipped, 9145 subtests passed in 9.61s",
"additional_ford_interface_fuzz": "11 passed, 258 non-Ford deselected in 8.15s; 60 examples per Ford platform",
"fuzz_seed": 20260911,
"dependency_setup": "Initialized existing neural-network-data pin 03cac2d30e111e0689c0429cb8c1fe6cb5a905af after missing-model-data failure.",
"new_toggle_off_integration_cases": 14,
"new_integration_cycles": 4200,
"new_steering_frames": 840,
"ruff_changed_python": "pass",
"ty_controller": "pass",
"settings_compiler_check": "pass",
"diff_check": "pass"
},
"packet_equivalence": {
"scope": "Compare all outgoing Ford packets with pinned upstream and custom senders.\n\nUses trusted local Git sources, identical synthetic inputs, and the actual CAN\npackers. Establishes software equivalence, not physical steering performance.\n",
"seed": 20260911,
"upstream_revision": "f95f996f5917dcbbf2e32fe51b606a24cf836af6",
"previous_custom_revision": "c21a9013700734dd20b09e05aa68329ad8cc20f9",
"upstream_source_sha256": {
"opendbc/car/ford/fordcan.py": "8b3c74bff68146cf9f97d17203b7deebb9254561d9591a4a92d978bf01808a75",
"opendbc/car/ford/carcontroller.py": "c7e590c13cfe2434d77d6224359d659bb5092a3fdd65b12c7d8d9eddfb3deada"
},
"previous_custom_source_sha256": {
"opendbc/car/ford/fordcan.py": "5b73c568149bde299f71f92f034f3032a94ecae4ee4bae8af938f34ef9590062",
"opendbc/car/ford/carcontroller.py": "b2d327a1833fb1f0d09ee17f54c9c8d45517fa29beb04a4543cfbf1b43f1a65e"
},
"results": [
{
"fingerprint": "FORD_BRONCO_SPORT_MK1",
"custom_enabled": false,
"cycles": 5000,
"identical_packets": 5665
},
{
"fingerprint": "FORD_ESCAPE_MK4",
"custom_enabled": false,
"cycles": 5000,
"identical_packets": 5665
},
{
"fingerprint": "FORD_ESCAPE_MK4_5",
"custom_enabled": false,
"cycles": 5000,
"identical_packets": 3165
},
{
"fingerprint": "FORD_ESCAPE_MK4_5",
"custom_enabled": true,
"cycles": 5000,
"identical_packets": 7165
},
{
"fingerprint": "FORD_EXPLORER_MK6",
"custom_enabled": false,
"cycles": 5000,
"identical_packets": 5665
},
{
"fingerprint": "FORD_EXPEDITION_MK4",
"custom_enabled": false,
"cycles": 5000,
"identical_packets": 3165
},
{
"fingerprint": "FORD_EXPEDITION_MK4",
"custom_enabled": true,
"cycles": 5000,
"identical_packets": 7165
},
{
"fingerprint": "FORD_F_150_MK14",
"custom_enabled": false,
"cycles": 5000,
"identical_packets": 3165
},
{
"fingerprint": "FORD_F_150_MK14",
"custom_enabled": true,
"cycles": 5000,
"identical_packets": 7165
},
{
"fingerprint": "FORD_F_150_LIGHTNING_MK1",
"custom_enabled": false,
"cycles": 5000,
"identical_packets": 3165
},
{
"fingerprint": "FORD_F_150_LIGHTNING_MK1",
"custom_enabled": true,
"cycles": 5000,
"identical_packets": 7165
},
{
"fingerprint": "FORD_FOCUS_MK4",
"custom_enabled": false,
"cycles": 5000,
"identical_packets": 5665
},
{
"fingerprint": "FORD_MAVERICK_MK1",
"custom_enabled": false,
"cycles": 5000,
"identical_packets": 5665
},
{
"fingerprint": "FORD_MUSTANG_MACH_E_MK1",
"custom_enabled": false,
"cycles": 5000,
"identical_packets": 3165
},
{
"fingerprint": "FORD_MUSTANG_MACH_E_MK1",
"custom_enabled": true,
"cycles": 5000,
"identical_packets": 7165
},
{
"fingerprint": "FORD_RANGER_MK2",
"custom_enabled": false,
"cycles": 5000,
"identical_packets": 3165
},
{
"fingerprint": "FORD_RANGER_MK2",
"custom_enabled": true,
"cycles": 5000,
"identical_packets": 7165
}
],
"total_cycles": 85000,
"total_identical_packets": 90305,
"candidate_source_sha256": {
"opendbc/car/ford/carcontroller.py": "6d33f288de87e3baa69e9161b1b85367dc1d8542c06c3d3a9ce2d1f2347dbd30",
"opendbc/car/ford/fordcan.py": "0e241f19f152df897b294d4562bfd729dbfcbc56bcc9770379f76922f2864cb8",
"opendbc/car/structs.py": "82ecc4de1e5fda486d68fcf67903098dd083a56b78e65866744f42d5fb97b385"
},
"checker_sha256": "1d33a07cc5e6188c6d1b5de2a2a603efaee691d25d91b7bcd843ab4909753ae1"
},
"source_sha256": {
"openpilot/selfdrive/controls/lib/ford_model_action.py": "167ae5a01fdd7ea014e6ad3fe9d0b6e31c67de8ba057ec5ecf18ab38fc16353f",
"openpilot/selfdrive/controls/controlsd.py": "c9b68431d212178ae2177b16ddba4cf023ece84c7c0ea8a1db02a2527dc27aba",
"openpilot/cereal/custom.capnp": "c877eac4a77ea4cb42447edcf38da4baf20708e8993124852234008e0a084664",
"openpilot/sunnypilot/selfdrive/controls/controlsd_ext.py": "45bfaafa9a96d3ccbb56f34ec0b9a71abb7cd7f01e7e80620796d13c222e4720",
"openpilot/selfdrive/controls/tests/test_ford_model_action_selection.py": "3dc4f7236937358aad09b544577a796e47c15dbc96607739ff0874b900d55089",
"openpilot/selfdrive/controls/tests/test_ford_model_action_adapter.py": "073b16ffa611d654b954711f9e4f24df95477dfc0c09e75eff89d02eb29d7f2a",
"openpilot/selfdrive/controls/tests/test_ford_controlsd_logging.py": "5b082f3c1f6dc596a40a2011c71928debeb04fa70af36841f6f9a237a9ca439e",
"openpilot/sunnypilot/sunnylink/settings_ui_src/pages/vehicle.yaml": "e37a662618b6ccd2620ac355b4cc40a2253707bb4ba1d5d4631fdc89fd01a800",
"openpilot/sunnypilot/sunnylink/settings_ui.json": "36ac7f6177de2679d35c5f7f77234336e17a8632d31b195f40aec6014efb8577",
"openpilot/sunnypilot/sunnylink/tests/test_settings_schema.py": "53a3f1f807c638661c8ef60b5dc5c28ecf5604a9d35b610b8e4a5f5d1d99eedb",
"tools/ford_pscm_lab/feedback_replay.py": "860aff9fd00d26b2bd7c2b627286918d3b52c25cc31b0b0768a51fc55b0df37e"
},
"validation_environment": {
"python": "/Users/ibpersonal/dev/sunnypilot/.venv/bin/python",
"PYTHONPATH": ".:opendbc_repo:.cache/ford_v6/test_deps",
"PYTHONDONTWRITEBYTECODE": "1",
"LOG_ROOT": "/private/tmp/ford-upstream-logs",
"PARAMS_ROOT": "/private/tmp/ford-upstream-params"
},
"panda_safety_changed": false,
"limitations": [
"No full device build, boot, installation or physical steering validation.",
"Upstream means the pinned upstream implementation merged into this branch, not an upgrade to unrelated latest source."
]
}
-327
View File
@@ -1,327 +0,0 @@
# 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.
+10 -10
View File
@@ -18,21 +18,21 @@ function agnos_init {
sudo chmod 660 /dev/adsprpc-smd /dev/ion /dev/kgsl-3d0
# Check if AGNOS update is required
if [ $(< /VERSION) != "$AGNOS_VERSION" ]; then
if [ "$(< /VERSION)" != "$AGNOS_VERSION" ]; then
AGNOS_PY="$DIR/openpilot/common/hardware/comma/agnos.py"
MANIFEST="$DIR/openpilot/system/hardware/comma/agnos.json"
if $AGNOS_PY --verify $MANIFEST; then
if "$AGNOS_PY" --verify "$MANIFEST"; then
sudo reboot
fi
while true; do
$DIR/openpilot/common/hardware/comma/updater $AGNOS_PY $MANIFEST
"$DIR/openpilot/common/hardware/comma/updater" "$AGNOS_PY" "$MANIFEST"
done
fi
}
function launch {
# Remove orphaned git lock if it exists on boot
[ -f "$DIR/.git/index.lock" ] && rm -f $DIR/.git/index.lock
[ -f "$DIR/.git/index.lock" ] && rm -f "$DIR/.git/index.lock"
# Check to see if there's a valid overlay-based update available. Conditions
# are as follows:
@@ -44,7 +44,7 @@ function launch {
# that completed successfully and synced to disk.
if [ -f "${DIR}/.overlay_init" ]; then
find ${DIR}/.git -newer ${DIR}/.overlay_init | grep -q '.' 2> /dev/null
find "${DIR}/.git" -newer "${DIR}/.overlay_init" | grep -q '.' 2> /dev/null
if [ $? -eq 0 ]; then
echo "${DIR} has been modified, skipping overlay update installation"
else
@@ -53,9 +53,9 @@ function launch {
echo "Valid overlay update found, installing"
LAUNCHER_LOCATION="${BASH_SOURCE[0]}"
mv $DIR /data/safe_staging/old_openpilot
mv "${STAGING_ROOT}/finalized" $DIR
cd $DIR
mv "$DIR" /data/safe_staging/old_openpilot
mv "${STAGING_ROOT}/finalized" "$DIR"
cd "$DIR"
echo "Restarting launch script ${LAUNCHER_LOCATION}"
unset AGNOS_VERSION
@@ -69,7 +69,7 @@ function launch {
fi
# handle pythonpath
ln -sfn $(pwd) /data/pythonpath
ln -sfn "$(pwd)" /data/pythonpath
export PYTHONPATH="$PWD"
# submodule package symlinks for PYTHONPATH imports on device.
@@ -90,7 +90,7 @@ function launch {
# start manager
cd openpilot/system/manager
if [ ! -f $DIR/prebuilt ]; then
if [ ! -f "$DIR/prebuilt" ]; then
./build.py
fi
./manager.py
+1 -56
View File
@@ -383,7 +383,6 @@ struct CarControlSP @0xa5cd762cd951a455 {
leadOne @2 :LeadData;
leadTwo @3 :LeadData;
intelligentCruiseButtonManagement @4 :IntelligentCruiseButtonManagement;
fordLateralPath @5 :FordLateralPath;
struct Param {
key @0 :Text;
@@ -404,15 +403,6 @@ 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;
enabled @5 :Bool; # Startup-selected custom controller; independent of command validity.
}
struct BackupManagerSP @0xf98d843bfd7004a3 {
backupStatus @0 :Status;
restoreStatus @1 :Status;
@@ -457,16 +447,6 @@ 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 {
@@ -482,18 +462,6 @@ struct ModelDataV2SP @0xa1680744031fdb2d {
laneTurnDirection @0 :TurnDirection;
leftLaneChangeEdgeBlock @1 :Bool;
rightLaneChangeEdgeBlock @2 :Bool;
fordGeometryReference @3 :FordGeometryReference;
struct FordGeometryReference {
enabled @0 :Bool;
valid @1 :Bool; # False while enabled means invalid geometry; original action is retained.
modelMonoTime @2 :UInt64;
actionDesiredCurvature @3 :Float32; # Original model action, with its own unchanged smoothing history.
rawCurvature @4 :Float32;
selectedCurvature @5 :Float32; # Published modelV2.action, before controlsd's normal limits/maneuver override.
previewSeconds @6 :Float32;
smoothSeconds @7 :Float32;
}
enum TurnDirection {
none @0;
@@ -502,30 +470,7 @@ struct ModelDataV2SP @0xa1680744031fdb2d {
}
}
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 CustomReserved10 @0xcb9fd56c7057593a {
}
struct CustomReserved11 @0xc2243c65e0340384 {
+2 -4
View File
@@ -2119,9 +2119,6 @@ struct Joystick {
# convenient for debug and live tuning
axes @0: List(Float32);
buttons @1: List(Bool);
fordChannel @2 :FordChannel;
enum FordChannel { standard @0; c0 @1; c1 @2; }
}
struct DriverStateV2 {
@@ -2597,6 +2594,7 @@ struct Event {
clocks @35 :Clocks;
deviceState @6 :DeviceState;
chestnutState @152 :ChestnutState;
chestnutGpuState @153 :ChestnutState;
logMessage @18 :Text;
errorLogMessage @85 :Text;
@@ -2645,7 +2643,7 @@ struct Event {
carStateSP @114 :Custom.CarStateSP;
liveMapDataSP @115 :Custom.LiveMapDataSP;
modelDataV2SP @116 :Custom.ModelDataV2SP;
assistedDrivingMilestoneState @136 :Custom.AssistedDrivingMilestoneState;
customReserved10 @136 :Custom.CustomReserved10;
customReserved11 @137 :Custom.CustomReserved11;
customReserved12 @138 :Custom.CustomReserved12;
customReserved13 @139 :Custom.CustomReserved13;
+2 -2
View File
@@ -25,7 +25,8 @@ _services: dict[str, tuple] = {
"accelerometer": (True, 104., 104),
"temperatureSensor": (True, 2., 200),
"deviceState": (True, 2., 1),
"chestnutState": (True, 10., 10),
"chestnutState": (True, 10., 1),
"chestnutGpuState": (False, 10.),
"touch": (True, 20., 1),
"can": (True, 100., 2053, QueueSize.BIG), # decimation gives ~3 msgs in a full segment
"controlsState": (True, 100., 10, QueueSize.MEDIUM),
@@ -90,7 +91,6 @@ _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.),
+7
View File
@@ -21,6 +21,13 @@ class Profile:
def is_comma(self) -> bool:
return self.provider == 'Webbing' and self.iccid.startswith('8985235')
@property
def display_name(self) -> str:
if self.is_comma:
return "comma prime"
name = self.nickname or self.provider or "<unnamed>"
return f"{name} (...{self.iccid[-4:]})"
class LPABase(ABC):
@abstractmethod
+1 -1
View File
@@ -613,7 +613,7 @@ def parse_lpa_activation_code(activation_code: str) -> tuple[str, str]:
if not activation_code.startswith("LPA:"):
raise ValueError("Invalid activation code format")
parts = activation_code[4:].split("$")
if len(parts) != 3:
if len(parts) != 3 or not all(parts):
raise ValueError("Invalid activation code format")
return parts[1], parts[2]
+4 -11
View File
@@ -24,6 +24,7 @@ def chunk_file(path, targets):
manifest_path, *chunk_paths = targets
actual_num_chunks = max(1, math.ceil(os.path.getsize(path) / CHUNK_SIZE))
assert len(chunk_paths) >= actual_num_chunks, f"Allowed {len(chunk_paths)} chunks but needs at least {actual_num_chunks}, for path {path}"
Path(manifest_path).unlink(missing_ok=True)
with open(path, 'rb') as f:
for chunk_path in chunk_paths:
with open(chunk_path, 'wb') as out:
@@ -31,14 +32,6 @@ def chunk_file(path, targets):
Path(manifest_path).write_text(str(len(chunk_paths)))
os.remove(path)
def get_existing_chunks(path):
if os.path.isfile(path):
return [path]
if os.path.isfile(manifest := get_manifest_path(path)):
num_chunks = int(Path(manifest).read_text().strip())
return _chunk_paths(path, num_chunks)
raise FileNotFoundError(path)
class ChunkStream(io.RawIOBase):
def __init__(self, paths):
self._paths = iter(paths)
@@ -66,11 +59,11 @@ class ChunkStream(io.RawIOBase):
def open_file_chunked(path):
manifest_path = get_manifest_path(path)
if os.path.isfile(manifest_path):
if os.path.isfile(path):
paths = [path]
elif os.path.isfile(manifest_path):
num_chunks = int(Path(manifest_path).read_text().strip())
paths = [get_chunk_name(path, i, num_chunks) for i in range(num_chunks)]
elif os.path.isfile(path):
paths = [path]
else:
raise FileNotFoundError(path)
return io.BufferedReader(ChunkStream(paths))
+3
View File
@@ -145,6 +145,9 @@ class HardwareBase(ABC):
def get_modem_temperatures(self):
return []
def get_modem_state(self) -> dict:
return {}
def initialize_hardware(self):
pass
@@ -9,22 +9,25 @@ from openpilot.common.realtime import Ratekeeper
from openpilot.common.filter_simple import FirstOrderFilter
def read_power():
def read_power(panda=None):
if panda is not None and panda.get_type() == panda.HW_TYPE_CUATRO:
health = panda.health()
return health['voltage'] * health['current'] / 1e6
with open("/sys/bus/i2c/devices/0-0040/hwmon/hwmon1/power1_input") as f:
return int(f.read()) / 1e6
def sample_power(seconds=5) -> list[float]:
def sample_power(seconds=5, panda=None) -> list[float]:
rate = 123
rk = Ratekeeper(rate, print_delay_threshold=None)
pwrs = []
for _ in range(rate*seconds):
pwrs.append(read_power())
pwrs.append(read_power(panda))
rk.keep_time()
return pwrs
def get_power(seconds=5):
pwrs = sample_power(seconds)
def get_power(seconds=5, panda=None):
pwrs = sample_power(seconds, panda)
return np.mean(pwrs)
def wait_for_power(min_pwr, max_pwr, min_secs_in_range, timeout):
+2 -2
View File
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:3a94ab8395f20d20a9d5a2a2bacca0694f072df8421cf13adca6250d28065bdc
size 24709205
oid sha256:6a7adb302d378dda7b1788a841b89e4f905c872550a70a76650dcde977b4ece0
size 24709209
-4
View File
@@ -97,10 +97,6 @@ Params::Params(const std::string &path) {
}
Params::~Params() {
flushNonBlockingWrites();
}
void Params::flushNonBlockingWrites() {
if (future.valid()) {
future.wait();
}
-1
View File
@@ -75,7 +75,6 @@ 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,7 +73,6 @@ 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)
@@ -179,10 +178,6 @@ 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))
+5 -14
View File
@@ -133,12 +133,6 @@ 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);
@@ -168,15 +162,12 @@ 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}, [&]() {
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;
}
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]);
}
return count;
return filtered.size();
});
}
+1 -10
View File
@@ -28,6 +28,7 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
{"ControlsReady", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, BOOL}},
{"CurrentBootlog", {PERSISTENT, STRING}},
{"CurrentRoute", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, STRING}},
{"DisableDriverCameraIR", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, BOOL}},
{"DisableLogging", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, BOOL}},
{"DisablePowerDown", {PERSISTENT | BACKUP, BOOL}},
{"DisableUpdates", {PERSISTENT | BACKUP, BOOL, "0"}},
@@ -138,13 +139,10 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
{"UptimeOnroad", {PERSISTENT, FLOAT, "0.0"}},
{"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
@@ -165,7 +163,6 @@ 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"}},
@@ -175,9 +172,7 @@ 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}},
@@ -237,10 +232,6 @@ 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"}},
{"FordC0TimeBased", {PERSISTENT | BACKUP, BOOL, "0"}},
{"FordGeometryReference", {PERSISTENT | BACKUP, BOOL, "0"}},
{"HyundaiLongitudinalTuning", {PERSISTENT | BACKUP, INT, "0"}},
{"SubaruStopAndGo", {PERSISTENT | BACKUP, BOOL, "0"}},
{"SubaruStopAndGoManualParkingBrake", {PERSISTENT | BACKUP, BOOL, "0"}},
+556 -35
View File
@@ -1,16 +1,47 @@
"""Small QR encoder for the UI's byte-mode, error-correction-level-L codes."""
"""QR code encoding, decoding, and UI textures."""
import functools
import itertools
import numpy as np
import pyray as rl
# Indexes are QR versions. These are the only two Reed-Solomon parameters needed
# for error-correction level L.
_ECC_LEN = (0, 7, 10, 15, 20, 26, 18, 20, 24, 30, 18, 20, 24, 26, 30, 22, 24, 28, 30, 28, 28)
_NUM_BLOCKS = (0, 1, 1, 1, 1, 1, 2, 2, 2, 2, 4, 4, 4, 4, 4, 6, 6, 6, 6, 7, 8)
# (ec codewords per block, block count) for levels L, M, Q, H, versions 1-40
_EC = [
((7, 1), (10, 1), (13, 1), (17, 1)), ((10, 1), (16, 1), (22, 1), (28, 1)), ((15, 1), (26, 1), (18, 2), (22, 2)),
((20, 1), (18, 2), (26, 2), (16, 4)), ((26, 1), (24, 2), (18, 4), (22, 4)), ((18, 2), (16, 4), (24, 4), (28, 4)),
((20, 2), (18, 4), (18, 6), (26, 5)), ((24, 2), (22, 4), (22, 6), (26, 6)), ((30, 2), (22, 5), (20, 8), (24, 8)),
((18, 4), (26, 5), (24, 8), (28, 8)), ((20, 4), (30, 5), (28, 8), (24, 11)), ((24, 4), (22, 8), (26, 10), (28, 11)),
((26, 4), (22, 9), (24, 12), (22, 16)), ((30, 4), (24, 9), (20, 16), (24, 16)), ((22, 6), (24, 10), (30, 12), (24, 18)),
((24, 6), (28, 10), (24, 17), (30, 16)), ((28, 6), (28, 11), (28, 16), (28, 19)), ((30, 6), (26, 13), (28, 18), (28, 21)),
((28, 7), (26, 14), (26, 21), (26, 25)), ((28, 8), (26, 16), (30, 20), (28, 25)), ((28, 8), (26, 17), (28, 23), (30, 25)),
((28, 9), (28, 17), (30, 23), (24, 34)), ((30, 9), (28, 18), (30, 25), (30, 30)), ((30, 10), (28, 20), (30, 27), (30, 32)),
((26, 12), (28, 21), (30, 29), (30, 35)), ((28, 12), (28, 23), (28, 34), (30, 37)), ((30, 12), (28, 25), (30, 34), (30, 40)),
((30, 13), (28, 26), (30, 35), (30, 42)), ((30, 14), (28, 28), (30, 38), (30, 45)), ((30, 15), (28, 29), (30, 40), (30, 48)),
((30, 16), (28, 31), (30, 43), (30, 51)), ((30, 17), (28, 33), (30, 45), (30, 54)), ((30, 18), (28, 35), (30, 48), (30, 57)),
((30, 19), (28, 37), (30, 51), (30, 60)), ((30, 19), (28, 38), (30, 53), (30, 63)), ((30, 20), (28, 40), (30, 56), (30, 66)),
((30, 21), (28, 43), (30, 59), (30, 70)), ((30, 22), (28, 45), (30, 62), (30, 74)), ((30, 24), (28, 47), (30, 65), (30, 77)),
((30, 25), (28, 49), (30, 68), (30, 81)),
]
# 15 format-info bits for level L (01) with mask 0: ((0x08 << 10) | bch_remainder) ^ 0x5412
_FORMAT_BITS = 0b111011111000100
# GF(256) with the QR polynomial x^8 + x^4 + x^3 + x^2 + 1: powers of alpha and their logs
_EXP = [1]
for _ in range(254):
_EXP.append(_EXP[-1] << 1 ^ (0x11D if _EXP[-1] & 0x80 else 0))
_LOG = {v: i for i, v in enumerate(_EXP)}
def _bch_format(data: int) -> int:
v = data << 10
for shift in range(14, 9, -1):
if v >> shift & 1:
v ^= 0x537 << (shift - 10)
return (data << 10 | v) ^ 0x5412
# 15-bit format info indexed by (level bits << 3 | mask). Level bits: L=01, M=00, Q=11, H=10.
_FORMATS = [_bch_format(d) for d in range(32)]
def _raw_modules(version: int) -> int:
@@ -21,8 +52,24 @@ def _raw_modules(version: int) -> int:
return result - (36 if version >= 7 else 0)
def _block_lengths(version: int, level: int) -> list[int]:
"""Data codewords per Reed-Solomon block. The last blocks may be one longer."""
ec, nblocks = _EC[version - 1][level]
total = _raw_modules(version) // 8 - ec * nblocks
return [total // nblocks + (i >= nblocks - total % nblocks) for i in range(nblocks)]
def _interleaved(version: int, level: int) -> list[tuple[int, int]]:
"""(block, index within block) of each transmitted codeword: data column-major, then ECC column-major."""
ec, nblocks = _EC[version - 1][level]
lens = _block_lengths(version, level)
data = [(b, i) for i in range(max(lens)) for b in range(nblocks) if i < lens[b]]
ecc = [(b, lens[b] + i) for i in range(ec) for b in range(nblocks)]
return data + ecc
def _capacity(version: int) -> int:
return _raw_modules(version) // 8 - _ECC_LEN[version] * _NUM_BLOCKS[version]
return sum(_block_lengths(version, 0))
def _append_bits(bits: list[int], value: int, length: int) -> None:
@@ -49,37 +96,18 @@ def _data_codewords(data: bytes, version: int) -> bytes:
def _codewords(data: bytes, version: int) -> bytes:
"""Split data codewords into Reed-Solomon blocks and interleave data + ECC."""
data = _data_codewords(data, version)
num_blocks = _NUM_BLOCKS[version]
ecc_len = _ECC_LEN[version]
raw_codewords = _raw_modules(version) // 8
short_len = raw_codewords // num_blocks
num_short = num_blocks - raw_codewords % num_blocks
divisor = _divisor(ecc_len)
blocks: list[tuple[bytes, bytes]] = []
divisor = _divisor(_EC[version - 1][0][0])
blocks = []
offset = 0
for i in range(num_blocks):
length = short_len - ecc_len + (0 if i < num_short else 1)
for length in _block_lengths(version, 0):
block = data[offset:offset + length]
blocks.append((block, _remainder(block, divisor)))
blocks.append(block + _remainder(block, divisor))
offset += length
result = bytearray()
for i in range(short_len - ecc_len + 1):
for block, _ in blocks:
result.extend(block[i:i + 1])
for i in range(ecc_len):
for _, ecc in blocks:
result.append(ecc[i])
return bytes(result)
return bytes(blocks[b][i] for b, i in _interleaved(version, 0))
def _multiply(x: int, y: int) -> int:
result = 0
for _ in range(8):
result = (result << 1) ^ (0x11D if result & 0x80 else 0)
if y & 0x80:
result ^= x
y <<= 1
return result
return _EXP[(_LOG[x] + _LOG[y]) % 255] if x and y else 0
def _divisor(degree: int) -> bytes:
@@ -108,7 +136,7 @@ def _alignment_positions(version: int) -> list[int]:
if version == 1:
return []
count = version // 7 + 2
step = ((version * 4 + count * 2 + 1) // (count * 2 - 2)) * 2
step = (version * 8 + count * 3 + 5) // (count * 4 - 4) * 2
return [6] + [version * 4 + 10 - step * i for i in range(count - 1)][::-1]
@@ -171,7 +199,7 @@ class _Qr:
def _format(self) -> None:
for i in range(15):
bit = ((_FORMAT_BITS >> i) & 1) != 0
bit = ((_FORMATS[1 << 3 | 0] >> i) & 1) != 0 # level L, mask 0
y_pos = i if i < 6 else i + 1 if i < 8 else self.size - 15 + i
self._set_function(8, y_pos, bit)
x_pos = self.size - 1 - i if i < 8 else 15 - i if i < 9 else 14 - i
@@ -216,3 +244,496 @@ def make_texture(data: str, inverted: bool = False) -> rl.Texture:
rl_image.mipmaps = 1
rl_image.format = rl.PixelFormat.PIXELFORMAT_UNCOMPRESSED_R8G8B8A8
return rl.load_texture_from_image(rl_image)
# ---- Symbol structure for decoding ----
class QRError(Exception):
pass
_LEVELS = (1, 0, 3, 2) # format info level bits -> column in _EC
_MASKS = [
lambda i, j: (i + j) % 2 == 0,
lambda i, j: i % 2 == 0,
lambda i, j: j % 3 == 0,
lambda i, j: (i + j) % 3 == 0,
lambda i, j: (i // 2 + j // 3) % 2 == 0,
lambda i, j: (i * j) % 2 + (i * j) % 3 == 0,
lambda i, j: ((i * j) % 2 + (i * j) % 3) % 2 == 0,
lambda i, j: ((i + j) % 2 + (i * j) % 3) % 2 == 0,
]
_ALIGNMENT = np.ones((5, 5), dtype=bool)
_ALIGNMENT[1:4, 1:4] = False
_ALIGNMENT[2, 2] = True
def _gf_inv(a: int) -> int:
return _EXP[-_LOG[a] % 255]
@functools.lru_cache
def _data_coords(version: int) -> tuple[np.ndarray, np.ndarray]:
"""(rows, cols) of the data and error correction modules in placement order: two-column zigzag from the right."""
dim = version * 4 + 17
func = np.zeros((dim, dim), dtype=bool) # finder, timing, alignment, format, and version modules
func[:9, :9] = func[:9, dim - 8:] = func[dim - 8:, :9] = True
func[6, :] = func[:, 6] = True
positions = _alignment_positions(version)
for r, c in itertools.product(positions, positions):
if (r, c) not in ((6, 6), (6, dim - 7), (dim - 7, 6)):
func[r - 2:r + 3, c - 2:c + 3] = True
if version >= 7:
func[:6, dim - 11:dim - 8] = func[dim - 11:dim - 8, :6] = True
ys = np.arange(dim)
rows, cols = [], []
# the vertical timing column is skipped, so the pairs left of it start at odd columns
for i, right in enumerate(col if col > 6 else col - 1 for col in range(dim - 1, 0, -2)):
r = np.repeat(ys[::-1] if i % 2 == 0 else ys, 2)
c = np.tile((right, right - 1), dim)
keep = ~func[r, c]
rows.append(r[keep])
cols.append(c[keep])
return np.concatenate(rows), np.concatenate(cols)
# ---- Matrix decoding ----
def _poly_eval(p: list[int], x: int) -> int:
# p is highest degree first
y = 0
for c in p:
y = _multiply(y, x) ^ c
return y
_EXP_TABLE = np.array(_EXP)
_LOG_TABLE = np.array([_LOG.get(v, 0) for v in range(256)])
def _syndromes(msg: list[int], nsym: int) -> list[int]:
"""syn[i] = msg(alpha^i), msg highest degree first."""
m = np.array(msg)
exponents = np.arange(nsym)[:, None] * (len(msg) - 1 - np.arange(len(msg)))
return np.bitwise_xor.reduce(_EXP_TABLE[(_LOG_TABLE[m] + exponents) % 255] * (m != 0), axis=1).tolist()
def _rs_correct(msg: list[int], nsym: int) -> list[int]:
"""Corrects up to nsym // 2 errors in a Reed-Solomon codeword, in place."""
n = len(msg)
syn = _syndromes(msg, nsym)
if not any(syn):
return msg
# Berlekamp-Massey, sigma is lowest degree first
sigma, prev, L, m, b = [1], [1], 0, 1, 1
for r in range(nsym):
d = syn[r]
for i in range(1, L + 1):
d ^= _multiply(sigma[i], syn[r - i])
if d == 0:
m += 1
continue
coef = _multiply(d, _gf_inv(b))
shifted = [0] * m + prev
saved = sigma[:]
sigma = sigma + [0] * max(0, len(shifted) - len(sigma))
for i, c in enumerate(shifted):
sigma[i] ^= _multiply(coef, c)
if 2 * L <= r:
L, prev, b, m = r + 1 - L, saved, d, 1
else:
m += 1
sigma = sigma[:L + 1]
if 2 * L > nsym:
raise QRError("too many errors")
# Chien search: codeword position p has locator alpha^(n-1-p)
positions = [p for p in range(n) if _poly_eval(sigma[::-1], _EXP[(p - n + 1) % 255]) == 0]
if len(positions) != L:
raise QRError("error locator mismatch")
# solve syn[i] = sum_k e_k * X_k^i for the magnitudes e_k
xlog = [(n - 1 - p) % 255 for p in positions]
A = [[_EXP[(xlog[k] * i) % 255] for k in range(L)] + [syn[i]] for i in range(L)]
for col in range(L):
piv = next((r for r in range(col, L) if A[r][col]), None)
if piv is None:
raise QRError("singular")
A[col], A[piv] = A[piv], A[col]
inv = _gf_inv(A[col][col])
A[col] = [_multiply(inv, v) for v in A[col]]
for r in range(L):
if r != col and A[r][col]:
f = A[r][col]
A[r] = [a ^ _multiply(f, c) for a, c in zip(A[r], A[col], strict=True)]
for k, p in enumerate(positions):
msg[p] ^= A[k][L]
if any(_syndromes(msg, nsym)):
raise QRError("uncorrectable")
return msg
def _read_format(m: np.ndarray) -> int:
"""Returns the closest format info (level bits << 3 | mask) from either copy."""
dim = m.shape[0]
copies = ([(8, i) for i in range(6)] + [(8, 7), (8, 8), (7, 8)] + [(5 - i, 8) for i in range(6)],
[(dim - 1 - i, 8) for i in range(7)] + [(8, dim - 8 + i) for i in range(8)]) # (row, col), msb first
candidates = []
for coords in copies:
bits = int("".join(str(int(m[r, c])) for r, c in coords), 2)
candidates += [((bits ^ f).bit_count(), i) for i, f in enumerate(_FORMATS)]
distance, fmt = min(candidates)
if distance > 3:
raise QRError("bad format info")
return fmt
_ALNUM = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:"
_ECI_ENCODINGS = {
0: "cp437", 2: "cp437", 1: "iso8859-1", 3: "iso8859-1",
**{i + 2: f"iso8859-{i}" for i in range(2, 17) if i != 12},
20: "shift_jis", 21: "cp1250", 22: "cp1251", 23: "cp1252", 24: "cp1256",
25: "utf-16-be", 26: "utf-8", 27: "ascii", 170: "ascii", 28: "big5", 29: "gb18030", 30: "euc_kr",
}
class _Bits:
def __init__(self, data: list[int]):
self._value = int.from_bytes(bytes(data), "big")
self.remaining = len(data) * 8
def read(self, n: int) -> int:
if n > self.remaining:
raise QRError("bitstream underflow")
self.remaining -= n
return self._value >> self.remaining & (1 << n) - 1
def read_below(self, n: int, limit: int) -> int:
v = self.read(n)
if v >= limit:
raise QRError("value out of range")
return v
def _parse_data(data: list[int], version: int) -> str:
bits = _Bits(data)
out: list[str] = []
encoding = None
band = 0 if version <= 9 else 1 if version <= 26 else 2
while bits.remaining >= 4:
mode = bits.read(4)
if mode == 0:
break
if mode == 7: # ECI character set assignment
first = bits.read(8)
extra = 0 if first < 0x80 else 8 if first < 0xC0 else 16 if first < 0xE0 else -1 # 1, 2, or 3 byte assignment
if extra < 0:
raise QRError("bad ECI assignment")
assignment = (first & 0x7F >> extra // 8) << extra | bits.read(extra)
encoding = _ECI_ENCODINGS.get(assignment)
if encoding is None:
raise QRError(f"unsupported ECI assignment {assignment}")
elif mode == 1:
n = bits.read((10, 12, 14)[band])
while n > 0:
k = min(n, 3) # 3 digits in 10 bits, the last 2 or 1 in 7 or 4
out.append(f"{bits.read_below((4, 7, 10)[k - 1], 10 ** k):0{k}d}")
n -= k
elif mode == 2:
n = bits.read((9, 11, 13)[band])
while n > 0:
k = min(n, 2) # 2 characters in 11 bits, a last one in 6
v = bits.read_below((6, 11)[k - 1], 45 ** k)
out.append(_ALNUM[v // 45] * (k - 1) + _ALNUM[v % 45])
n -= k
elif mode == 4:
n = bits.read((8, 16, 16)[band])
segment = bytes(bits.read(8) for _ in range(n))
try:
out.append(segment.decode(encoding or "utf-8"))
except UnicodeDecodeError as e:
if encoding is not None:
raise QRError("invalid ECI byte segment") from e
out.append(segment.decode("latin-1"))
elif mode == 8:
n = bits.read((8, 10, 12)[band])
for _ in range(n):
v = bits.read(13)
c = (v // 0xC0) << 8 | v % 0xC0
c += 0x8140 if c < 0x1F00 else 0xC140
try:
out.append(c.to_bytes(2, "big").decode("shift_jis"))
except UnicodeDecodeError as e:
raise QRError("invalid Kanji character") from e
else:
raise QRError(f"unsupported mode {mode}")
return "".join(out)
def decode_matrix(m: np.ndarray) -> str:
"""Decodes a square boolean module matrix (True = dark) without a quiet zone."""
dim = m.shape[0]
if m.shape != (dim, dim) or dim % 4 != 1 or not 21 <= dim <= 177:
raise QRError("bad matrix size")
version = (dim - 17) // 4
fmt = _read_format(m)
level = _LEVELS[fmt >> 3]
rows, cols = _data_coords(version)
bits = m[rows, cols] ^ _MASKS[fmt & 7](rows, cols)
codewords = np.packbits(bits[:len(bits) // 8 * 8]).tolist()
ec, _ = _EC[version - 1][level]
lens = _block_lengths(version, level)
blocks = [[0] * (n + ec) for n in lens]
for (b, i), codeword in zip(_interleaved(version, level), codewords, strict=True):
blocks[b][i] = codeword
data: list[int] = []
for block, n in zip(blocks, lens, strict=True):
data += _rs_correct(block, ec)[:n]
return _parse_data(data, version)
# ---- Image decoding ----
def _box_sums(a: np.ndarray, radii: tuple[int, ...]) -> list[np.ndarray]:
"""Sums over (2r + 1)^2 neighborhoods of the last two axes, edge padded, from one integral image."""
P = max(radii)
lead = [(0, 0)] * (a.ndim - 2)
cs = np.pad(np.cumsum(np.cumsum(np.pad(a, lead + [(P, P), (P, P)], mode="edge"), -2), -1), lead + [(1, 0), (1, 0)])
H, W = a.shape[-2:]
out = []
for r in radii:
lo, hi = P - r, P + r + 1
out.append(cs[..., hi:hi + H, hi:hi + W] - cs[..., lo:lo + H, hi:hi + W] - cs[..., hi:hi + H, lo:lo + W] + cs[..., lo:lo + H, lo:lo + W])
return out
def _binarize(gray: np.ndarray) -> np.ndarray:
"""Adaptive threshold: each pixel against the mean of the surrounding tiles that have contrast."""
h, w = gray.shape
if h < 21 or w < 21:
raise QRError("image too small")
B = max(8, min(h, w) // 128 * 2)
H, W = -(-h // B), -(-w // B)
padded = np.pad(gray, ((0, H * B - h), (0, W * B - w)), mode="edge")
# block statistics from a subsample are plenty
sub = np.ascontiguousarray(padded[::2, ::2].reshape(H, B // 2, W, B // 2).transpose(0, 2, 1, 3)).reshape(H, W, -1)
blocks = sub.sum(axis=2, dtype=np.uint32) / sub.shape[2]
known = sub.max(axis=2) - sub.min(axis=2) >= 32
# Flat tiles cannot estimate their own threshold: use the tiles with contrast nearby, then
# further out, then the global midrange. A flat tile is then all dark or all light.
est = np.full((H, W), (blocks.min() + blocks.max()) / 2)
filled = np.zeros((H, W), dtype=bool)
for total, count in _box_sums(np.stack((known * blocks, known.astype(float))), (2, 6)):
fill = ~filled & (count > 0)
est[fill] = total[fill] / count[fill]
filled |= fill
thr = np.where(known, np.minimum(est, 254) + 1, np.where(blocks <= est, 255, 0)).astype(np.uint8)
return (padded.reshape(H, B, W, B) < thr[:, None, :, None]).reshape(H * B, W * B)[:h, :w]
class _Runs:
"""Run-length table of a padded, flattened binary image with a per-pixel run index."""
def __init__(self, padded: np.ndarray):
self.flat = padded.ravel()
self.lines, self.stride = padded.shape
change = self.flat[1:] != self.flat[:-1]
self.starts = np.concatenate(([0], np.flatnonzero(change) + 1))
self.lengths = np.diff(np.append(self.starts, self.flat.size)).astype(np.int32)
def run_at(self, line: np.ndarray, pos: np.ndarray) -> np.ndarray:
"""Index of the run containing the pixel at `pos` along `line`."""
return np.searchsorted(self.starts, line * self.stride + pos + 1, side="right") - 1
@staticmethod
def _match(lengths: list[np.ndarray], ratios: tuple[int, ...]) -> tuple[np.ndarray, np.ndarray]:
"""Checks windows of runs against the ratios, given the length of each run. Returns (ok, module size)."""
S = sum(ratios)
total = sum(lengths[1:], start=lengths[0])
ok = total >= 2 * S # modules need to be at least 2 px
for L, r in zip(lengths, ratios, strict=True):
ok &= np.abs(2 * S * L - 2 * r * total) <= r * total # integer form of |L - r * total / S| <= r * total / (2 * S)
return ok, total / S
def scan(self, ratios: tuple[int, ...]) -> np.ndarray:
"""Returns the indices of all dark runs starting a window of runs matching the ratios."""
n = len(ratios)
N = len(self.lengths) - n + 1
if N <= 0:
return np.zeros(0, dtype=int)
ok, _ = self._match([self.lengths[k:N + k] for k in range(n)], ratios)
ok &= self.flat[self.starts[:N]]
first = np.flatnonzero(ok)
return first[self.starts[first] // self.stride == self.starts[first + n - 1] // self.stride]
def check(self, first: np.ndarray, ratios: tuple[int, ...]) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
"""Checks the run windows starting at run index `first`. Returns (ok, center position along the line, module size)."""
n, half = len(ratios), len(ratios) // 2
ok = (first >= 0) & (first + n <= len(self.starts))
idx = np.clip(first[:, None] + np.arange(n), 0, len(self.starts) - 1)
matched, module = self._match([self.lengths[idx[:, k]] for k in range(n)], ratios)
ok &= matched & self.flat[self.starts[idx[:, 0]]]
ok &= self.starts[idx[:, 0]] // self.stride == self.starts[idx[:, -1]] // self.stride
center = self.starts[idx[:, half]] % self.stride - 1 + self.lengths[idx[:, half]] / 2
return ok, center, module
def _find_patterns(binary: np.ndarray, ratios: tuple[int, ...]) -> list[tuple[float, float, float]]:
"""Finds dark/light run patterns with the given module ratios. Returns (x, y, module size)."""
half = len(ratios) // 2
step = 2 # the center rows of a 2 px finder pattern still get scanned twice
rows_t = _Runs(np.pad(binary[::step], ((0, 0), (1, 1))))
first = rows_t.scan(ratios)
if len(first) == 0:
return []
_, cx, hmod = rows_t.check(first, ratios)
row = rows_t.starts[first] // rows_t.stride * step
xi = cx.astype(int)
xs, col = np.unique(xi, return_inverse=True)
cols_t = _Runs(np.pad(binary[:, xs].T, ((0, 0), (1, 1))))
ok, cy, vmod = cols_t.check(cols_t.run_at(col, row) - half, ratios)
ok &= (0.5 <= vmod / hmod) & (vmod / hmod <= 2)
line = np.clip(np.rint(cy / step), 0, rows_t.lines - 1).astype(int)
ok2, cx2, hmod2 = rows_t.check(rows_t.run_at(line, xi) - half, ratios)
ok &= ok2 & (0.5 <= hmod2 / vmod) & (hmod2 / vmod <= 2)
found: list[list[float]] = [] # [x, y, module, count]
for x, y, module in zip(cx2[ok], cy[ok], (hmod2[ok] + vmod[ok]) / 2, strict=True):
for f in found:
if abs(f[0] - x) <= f[2] and abs(f[1] - y) <= f[2] and 0.5 <= f[2] / module <= 2:
c = f[3]
f[0], f[1], f[2], f[3] = (f[0] * c + x) / (c + 1), (f[1] * c + y) / (c + 1), (f[2] * c + module) / (c + 1), c + 1
break
else:
found.append([x, y, module, 1])
found.sort(key=lambda f: -f[3])
return [(f[0], f[1], f[2]) for f in found if f[3] >= 2]
def _pick_finders(patterns: list[tuple[float, float, float]]) -> tuple[np.ndarray, np.ndarray, np.ndarray, float]:
"""Returns (top-left, top-right, bottom-left) centers and the module size of the most square-looking triple."""
best = None
for a, b, c in itertools.combinations(patterns[:10], 3):
mods = sorted((a[2], b[2], c[2]))
if mods[2] / mods[0] > 1.5:
continue
pts = [np.array(p[:2]) for p in (a, b, c)]
d = [np.linalg.norm(pts[(i + 1) % 3] - pts[(i + 2) % 3]) for i in range(3)]
tl = int(np.argmax(d)) # opposite the hypotenuse
p1, p2 = pts[(tl + 1) % 3], pts[(tl + 2) % 3]
v1, v2 = p1 - pts[tl], p2 - pts[tl]
n1, n2 = np.linalg.norm(v1), np.linalg.norm(v2)
if n1 == 0 or n2 == 0:
continue
cos = abs(np.dot(v1, v2)) / (n1 * n2)
if cos > 0.35 or not 0.6 <= n1 / n2 <= 1.6:
continue
score = cos + abs(np.log(n1 / n2)) + np.log(mods[2] / mods[0])
if best is not None and score >= best[0]:
continue
if v1[0] * v2[1] - v1[1] * v2[0] < 0:
p1, p2 = p2, p1
best = (score, pts[tl], p1, p2, float(sum(mods) / 3))
if best is None:
raise QRError("no finder patterns")
return best[1:]
def _perspective(src: np.ndarray, dst: np.ndarray) -> np.ndarray:
"""Homography mapping the four src points onto the four dst points."""
A = [row for (x, y), (u, v) in zip(src, dst, strict=True)
for row in ([x, y, 1, 0, 0, 0, -u * x, -u * y], [0, 0, 0, x, y, 1, -v * x, -v * y])]
try:
h = np.linalg.solve(np.array(A, dtype=float), np.asarray(dst, dtype=float).ravel())
except np.linalg.LinAlgError as e:
raise QRError("degenerate geometry") from e
return np.append(h, 1).reshape(3, 3)
def _transform(H: np.ndarray, pts: np.ndarray) -> np.ndarray:
p = np.column_stack((pts, np.ones(len(pts)))) @ H.T
return p[:, :2] / p[:, 2:3]
def _match_alignment(binary: np.ndarray, est: np.ndarray, offs: np.ndarray, r: int, module: float) -> np.ndarray | None:
h, w = binary.shape
dy = np.arange(max(0, int(est[1]) - r), min(h, int(est[1]) + r)) - est[1]
dx = np.arange(max(0, int(est[0]) - r), min(w, int(est[0]) + r)) - est[0]
if len(dy) == 0 or len(dx) == 0:
return None
y = np.rint(est[1] + dy[:, None, None] + offs[None, None, :, 1]).astype(int)
x = np.rint(est[0] + dx[None, :, None] + offs[None, None, :, 0]).astype(int)
valid = ((y >= 0) & (y < h) & (x >= 0) & (x < w)).all(axis=2)
samples = binary[np.clip(y, 0, h - 1), np.clip(x, 0, w - 1)]
score = np.where(valid, (samples == _ALIGNMENT.ravel()).sum(axis=2), 0)
if score.max() < 23:
return None
hits = np.argwhere(score == score.max())
centers = np.column_stack((est[0] + dx[hits[:, 1]], est[1] + dy[hits[:, 0]]))
closest = centers[np.argmin(np.linalg.norm(centers - est, axis=1))]
return centers[np.linalg.norm(centers - closest, axis=1) <= module / 2].mean(axis=0)
def _locate_alignment(binary: np.ndarray, H: np.ndarray, center: float, module: float) -> np.ndarray | None:
"""Template matches the 5x5 alignment pattern around its position estimated from H."""
grid = np.mgrid[-2:3, -2:3].reshape(2, -1).T[:, ::-1] + center # (25, 2) module coords (x, y)
pts = _transform(H, grid)
# the affine estimate can be off in both position and local scale under perspective
for radius in (2, 4, 8, 16):
for scale in (1.0, 0.8, 1.25, 0.65, 1.5):
found = _match_alignment(binary, pts[12], (pts - pts[12]) * scale, int(module * radius), module)
if found is not None:
return found
return None
def _sample(binary: np.ndarray, tl: np.ndarray, tr: np.ndarray, bl: np.ndarray, module: float, dim: int, use_alignment: bool) -> np.ndarray:
src = np.array([(3.5, 3.5), (dim - 3.5, 3.5), (3.5, dim - 3.5), (dim - 3.5, dim - 3.5)])
dst = np.array([tl, tr, bl, tr + bl - tl])
H = _perspective(src, dst)
if use_alignment and dim > 21:
align = _locate_alignment(binary, H, dim - 6.5, module)
if align is not None:
src[3], dst[3] = (dim - 6.5, dim - 6.5), align
H = _perspective(src, dst)
rows, cols = np.mgrid[0:dim, 0:dim]
pts = _transform(H, np.column_stack((cols.ravel() + 0.5, rows.ravel() + 0.5)))
xy = np.rint(pts).astype(int)
h, w = binary.shape
if (xy < 0).any() or (xy[:, 0] >= w).any() or (xy[:, 1] >= h).any():
raise QRError("code extends outside image")
return binary[xy[:, 1], xy[:, 0]].reshape(dim, dim)
def decode(gray: np.ndarray) -> str | None:
"""Decodes the QR code in a 2D uint8 grayscale image. Modules need to be at least 2 px.
Returns None if nothing could be decoded."""
try:
binary = _binarize(gray)
tl, tr, bl, module = _pick_finders(_find_patterns(binary, (1, 1, 3, 1, 1)))
except QRError:
return None
d = (np.linalg.norm(tr - tl) + np.linalg.norm(bl - tl)) / 2
dim = int(round((d / module + 7 - 17) / 4)) * 4 + 17
dims = [cand for cand in (dim, dim - 4, dim + 4) if 21 <= cand <= 177]
for cand, use_alignment, transpose in itertools.product(dims, (True, False), (False, True)):
try:
m = _sample(binary, tl, tr, bl, module, cand, use_alignment)
return decode_matrix(m.T if transpose else m)
except QRError:
pass
return None
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
-17
View File
@@ -106,13 +106,6 @@ 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()
@@ -133,16 +126,6 @@ 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)
+178
View File
@@ -0,0 +1,178 @@
import hashlib
import math
from pathlib import Path
import numpy as np
from openpilot.common import qrcode as qr
from openpilot.common.test import OpenpilotTestCase
LPA = "LPA:1$rsp.truphone.com$QRF-BETTERROAMING-PMRDGIR2EARDEIT5"
# Matrices generated with python-qrcode 8.2, covering all versions and EC levels.
# Packed fixtures keep the decoder tests independent of our encoder.
FIXTURES = {}
for path in Path(__file__).with_name("fixtures").glob("qrcode_*.npz"):
with np.load(path) as fixtures:
FIXTURES.update({key: fixtures[key] for key in fixtures.files})
def fixture(key: str) -> np.ndarray:
bits = np.unpackbits(FIXTURES[key])
size = math.isqrt(len(bits))
return bits[:size * size].reshape(size, size).astype(bool)
def render(matrix: np.ndarray, box: int = 6, border: int = 4) -> np.ndarray:
img = np.repeat(np.repeat(np.pad(matrix, border), box, axis=0), box, axis=1)
return np.where(img, 0, 255).astype(np.uint8)
def make(data: str, version: int | None = None, level: int = 0, box: int = 6, border: int = 4):
matrix = fixture(hashlib.sha256(f"{version}:{level}:{data}".encode()).hexdigest())
return matrix, render(matrix, box, border)
def warp(img: np.ndarray, H: np.ndarray) -> np.ndarray:
"""Bilinear resampling through the output -> input homography H, white outside the image."""
h, w = img.shape
rows, cols = np.mgrid[0:h, 0:w]
pts = qr._transform(H, np.column_stack((cols.ravel() + 0.5, rows.ravel() + 0.5))) - 0.5
x0, y0 = np.floor(pts[:, 0]).astype(int), np.floor(pts[:, 1]).astype(int)
fx, fy = pts[:, 0] - x0, pts[:, 1] - y0
padded = np.pad(img.astype(float), 1, constant_values=255)
def at(y, x):
return padded[np.clip(y + 1, 0, h + 1), np.clip(x + 1, 0, w + 1)]
out = at(y0, x0) * (1 - fx) * (1 - fy) + at(y0, x0 + 1) * fx * (1 - fy) + at(y0 + 1, x0) * (1 - fx) * fy + at(y0 + 1, x0 + 1) * fx * fy
return np.clip(out, 0, 255).astype(np.uint8).reshape(h, w)
def rotate(img: np.ndarray, angle: float) -> np.ndarray:
h, w = img.shape
t = np.radians(angle)
R = np.array([[np.cos(t), -np.sin(t)], [np.sin(t), np.cos(t)]])
center = np.array([w / 2, h / 2])
corners = np.array([(0, 0), (w, 0), (w, h), (0, h)], dtype=float)
return warp(img, qr._perspective((corners - center) @ R.T + center, corners))
class TestQRCode(OpenpilotTestCase):
def test_alignment_positions(self):
assert qr._alignment_positions(7) == [6, 22, 38]
assert qr._alignment_positions(32) == [6, 34, 60, 86, 112, 138]
assert qr._alignment_positions(40) == [6, 30, 58, 86, 114, 142, 170]
def test_all_versions(self):
for version in range(1, 41):
for level in range(4):
with self.subTest(version=version, level=level):
data = "".join(chr(ord("a") + i % 26) for i in range(version))
matrix, img = make(data, version, level, box=3)
assert qr.decode_matrix(matrix) == data
assert qr.decode(img) == data
def test_modes(self):
for data in ["0123456789012345", "HELLO WORLD $1.50", LPA, "こんにちは", "ünïcødé", "mixed 123 ABC xyz"]:
with self.subTest(data=data):
matrix, img = make(data)
assert qr.decode_matrix(matrix) == data
assert qr.decode(img) == data
def test_error_correction(self):
matrix, _ = make(LPA, level=2)
rng = np.random.default_rng(0)
flipped = matrix.copy()
for r, c in rng.integers(9, matrix.shape[0] - 9, size=(40, 2)):
flipped[r, c] ^= True
assert qr.decode_matrix(flipped) == LPA
def test_large_modules(self):
for data in ["0123456789012345", "HELLO WORLD $1.50", LPA, "mixed 123 ABC xyz"]:
for box in [16, 20, 24, 32]:
for dark, light in [(0, 255), (60, 200), (140, 250)]:
with self.subTest(data=data, box=box, dark=dark):
_, img = make(data, box=box)
img = np.where(img == 0, dark, light).astype(np.uint8)
assert qr.decode(img) == data
def test_image_edges(self):
# a code touching the image edge must not lose the rows and columns left over from tiling
matrix, _ = make(LPA)
for size in (200, 203):
with self.subTest(size=size):
img = np.full((size, size), 255, dtype=np.uint8)
code = render(matrix, box=5, border=0)
img[size - code.shape[0]:, size - code.shape[1]:] = code
assert qr.decode(img) == LPA
def test_eci(self):
# qrcode_eci.npz: packed Segno 1.6.6 matrices, generated with mode='byte',
# eci=True, micro=False and the named encoding. Mixed also includes numeric,
# alphanumeric, and Kanji segments after changing the byte encoding twice.
cases = {
"iso8859-5": "Привет", "utf-16-be": "héllo", "utf-8": "こんにちは",
"shift_jis": "日本語", "cp1251": "Привет", "iso8859-1": "héllo",
"mixed": "hélloПривет日本語123ABC漢字",
}
for encoding, expected in cases.items():
with self.subTest(encoding=encoding):
matrix = fixture(encoding)
assert qr.decode_matrix(matrix) == expected
assert qr.decode(render(matrix)) == expected
def test_parse_data(self):
def parse(stream: str) -> str:
stream += '0' * (-len(stream) % 8)
return qr._parse_data([int(stream[i:i + 8], 2) for i in range(0, len(stream), 8)], 1)
def eci(assignment: str, payload: bytes = b'A') -> str:
return parse('0111' + assignment + '0100' + f'{len(payload):08b}' + ''.join(f'{b:08b}' for b in payload) + '0000')
# ASCII assignment 170 uses the two-byte ECI representation.
assert eci('1000000010101010') == 'A'
for assignment in ['00001110', '1000001111100111', '110000010000000000000000', '11100000']:
with self.subTest(assignment=assignment), self.assertRaises(qr.QRError):
eci(assignment)
with self.assertRaises(qr.QRError):
eci('00011010', b'\xff') # Invalid UTF-8 must not fall back to Latin-1.
# out-of-range numeric, alphanumeric, and Kanji values are format errors, not crashes
for stream in ['0001' + '0000000011' + '1111111111', '0001' + '0000000010' + '1111111',
'0010' + '000000010' + '11111111111', '0010' + '000000001' + '111111',
'1000' + '00000001' + '0000000111111']:
with self.subTest(stream=stream), self.assertRaises(qr.QRError):
parse(stream)
def test_rotation(self):
for angle in [0, 90, 180, 270, 25, 110]:
with self.subTest(angle=angle):
_, img = make(LPA, box=8, border=12)
assert qr.decode(rotate(img, angle)) == LPA
def test_mirrored(self):
_, img = make(LPA)
assert qr.decode(img[:, ::-1]) == LPA
def test_perspective_and_noise(self):
_, img = make(LPA, box=10, border=8)
h, w = img.shape
corners = np.array([(40, 60), (w - 20, 30), (w - 60, h - 40), (30, h - 90)])
arr = warp(img, qr._perspective(corners, np.array([(0, 0), (w, 0), (w, h), (0, h)]))).astype(float)
rng = np.random.default_rng(1)
arr = arr * 0.6 + 60 + rng.normal(0, 12, arr.shape) # low contrast + noise
# uneven lighting
arr += np.linspace(-40, 40, w)[None, :]
assert qr.decode(np.clip(arr, 0, 255).astype(np.uint8)) == LPA
def test_no_code(self):
rng = np.random.default_rng(2)
assert qr.decode(rng.integers(0, 256, size=(240, 320), dtype=np.uint8)) is None
assert qr.decode(np.full((240, 320), 200, dtype=np.uint8)) is None
def test_encoder_roundtrip(self):
for version in range(1, 21):
with self.subTest(version=version):
assert qr.decode_matrix(np.array(qr._Qr(version, b"hello").modules)) == "hello"
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:845c40ff0d37612e8f2f482a36845744b5ae91ce2fcfc8117990d7d278b59820
size 13079
oid sha256:bf97a6738b294ac0aed9b2d075916cee0b7d3215bcd23381760900ede6a92748
size 13256
+2 -2
View File
@@ -23,8 +23,8 @@ done
# sudo apt install inkscape
for svg in $(find $DIR -type f | grep svg$); do
bunx svgo $svg --multipass --pretty --indent 2
for svg in $(find "$DIR" -type f | grep svg$); do
bunx svgo "$svg" --multipass --pretty --indent 2
# convert to PNG
png="${svg%.svg}.png"
Binary file not shown.
-5
View File
@@ -21,7 +21,6 @@ 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
@@ -187,9 +186,6 @@ class Car:
# card is driven by can recv, expected at 100Hz
self.rk = Ratekeeper(100, print_delay_threshold=None)
# log fingerprint in sentry
sunnypilot_interfaces.log_fingerprint(self.CP)
def state_update(self) -> tuple[car.CarState, custom.CarStateSP, structs.RadarDataT | None]:
"""carState update loop, driven by can"""
@@ -199,7 +195,6 @@ 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)
@@ -1,36 +0,0 @@
"""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,6 +63,5 @@ 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
@@ -1,109 +0,0 @@
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()
+1 -40
View File
@@ -1,6 +1,5 @@
#!/usr/bin/env python3
import math
import time
from numbers import Number
from openpilot.cereal import log
@@ -14,8 +13,6 @@ from openpilot.common.swaglog import cloudlog
from opendbc.car.car_helpers import interfaces
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
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
@@ -47,7 +44,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', 'carStateSP', 'carOutput',
'extrinsicsCalibration', 'deviceMotion', 'longitudinalPlan', 'lateralManeuverPlan', 'carState', 'carOutput',
'driverMonitoringState', 'onroadEvents', 'driverAssistance'] + self.sm_services_ext,
poll='selfdriveState')
self.pm = messaging.PubMaster(['carControl', 'controlsState'] + self.pm_services_ext)
@@ -55,14 +52,6 @@ class Controls(ControlsExt):
self.steer_limited_by_safety = False
self.curvature = 0.0
self.desired_curvature = 0.0
self.ford_path_controller = select_model_action_controller(self.CP, self.params.get_bool("FordModelActionController"),
c0_time_based=self.params.get_bool("FordC0TimeBased"),
direct_path=self.params.get_bool("FordGeometryReference"))
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__ if self.ford_model_action else "upstream")
self.ford_path = FordPath()
self.pose_calibrator = PoseCalibrator()
self.calibrated_pose: Pose | None = None
@@ -152,8 +141,6 @@ class Controls(ControlsExt):
# Reset desired curvature to current to avoid violating the limits on engage
if self.sm.valid['lateralManeuverPlan']:
new_desired_curvature = self.sm['lateralManeuverPlan'].desiredCurvature if CC.latActive else self.curvature
elif self.ford_model_action and self.ford_path_controller.direct_path:
new_desired_curvature = self.ford_path_controller.path_curvature(model_v2, CS.vEgo) if CC.latActive else self.curvature
else:
new_desired_curvature = model_v2.action.desiredCurvature if CC.latActive else self.curvature
self.desired_curvature, curvature_limited = clip_curvature(CS.vEgo, self.desired_curvature, new_desired_curvature, lp.roll)
@@ -168,32 +155,6 @@ 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:
reference_service = 'lateralManeuverPlan' if self.sm.valid['lateralManeuverPlan'] else 'modelV2'
self.ford_path = self.ford_path_controller.update(
ford_model, self.desired_curvature, current_curvature=self.curvature, yaw_rate=-CS.yawRate, speed=CS.vEgo, now=time.monotonic(),
# Roll/angle offset cancel in the error; retain the normal steering-angle conversion's speed and stiffness effects.
curvature_scale=self.VM.get_steer_from_curvature(1., CS.vEgo, 0.) / (self.CP.steerRatio*self.CP.wheelbase),
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,
active=CC.latActive, valid=CS.canValid and self.sm.all_checks(['carState', 'vehicleParameters', 'modelV2', reference_service]),
lat_delay=lat_delay,
driver_pressed=CS.steeringPressed, driver_torque=CS.steeringTorque,
reference_source=reference_service, roll=lp.roll,
pscm_status=self.sm['carStateSP'].fordPscmStatus if self.sm.valid['carStateSP'] else None,
)
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)
actuators.curvature = float(self.ford_path.curvature)
# Ensure no NaNs/Infs
for p in ACTUATOR_FIELDS:
attr = getattr(actuators, p)
@@ -1,320 +0,0 @@
"""Opt-in Ford C2-free model mapping with measured-curvature PI feedback.
C0 samples a desired-curvature arc at 7 m, optionally max(7 m, v*1s),
including base-heading overflow, plus proportional tracking correction softened
near zero error without filtering or a deadband. C1
combines the selected curvature's heading with proportional and integrated
tracking error. Reference distance and gains are explicit trial choices.
Commands use the current bounded request without an additional C0/C1 slew.
The direct-path trial samples model position and heading separately, retains
independent upstream request limits, and uses heading-equivalent feedback.
"""
from collections import deque
import math
import struct
import numpy as np
from opendbc.car.ford.values import FordFlags
from openpilot.common.realtime import DT_CTRL
from openpilot.selfdrive.controls.lib.drive_helpers import clip_curvature
from openpilot.selfdrive.controls.lib.ford_path import FordPath, _model_path
OFFSET_STATION_M = 7.0
HEADING_TIME_S = 1.0
C1_PROPORTIONAL_GAIN = 0.75 # Drive-trial gains, not a learned calibration.
C1_INTEGRAL_GAIN = 1.0
C0_PROPORTIONAL_GAIN = 1.0 # Action trial: stronger immediate correction for the same tracking error.
DIRECT_PATH_C0_PROPORTIONAL_GAIN = 0.5
# Action-only trial: soften the error expressed as metres of C0 before gain.
# The slope grows smoothly from 0.5 to 1; correction reduction is at most 0.125 m * gain.
C0_SMALL_ERROR_GAIN = 0.5
C0_SMALL_ERROR_SCALE_M = 0.25
# Offline Lightning fit: geometric curvature per metre of C0, with v in m/s.
C0_RESPONSE_CONSTANT = 0.010717679293424373
C0_RESPONSE_INVERSE_SPEED_SQUARED = 0.018122981795212647
CALIBRATION_APPROVED = False
CURVATURE_REQUEST_BUFFER_SECONDS = 1.0
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, *, c0_time_based=False):
"""Encode a circular-arc offset and max(7, v*1s)*selected curvature.
The arc starts at zero lateral position and heading. Original model geometry
remains a health gate; selected curvature supplies both path commands.
"""
if not _finite(desired_curvature, speed) or not .3 <= speed <= 55 or abs(desired_curvature) > 1:
return FordPath()
try:
path = _model_path(model)
except OverflowError:
return FordPath()
if path is None or not all(_finite(*values) for values in path):
return FordPath()
# (1-cos(S*k))/k, using sinc to avoid cancellation near zero curvature.
distance = max(OFFSET_STATION_M, speed*HEADING_TIME_S) if c0_time_based else OFFSET_STATION_M
half_heading = .5*distance*desired_curvature
sinc = math.sin(half_heading)/half_heading if half_heading else 1.
c0 = .5*desired_curvature*distance**2*sinc**2
c1 = max(OFFSET_STATION_M, speed*HEADING_TIME_S)*desired_curvature
return FordPath(True, c0, c1, 0., 0.) if _finite(c0, c1) else FordPath()
def encode_model_path(model, speed, *, c0_time_based=False):
"""Sample lateral position and heading independently in the model's frame.
Preserve the existing distance choices and hold a short path's endpoint.
These preview points are trial choices, not identified Ford reference points.
"""
if not _finite(speed) or not .3 <= speed <= 55:
return FordPath()
try:
path = _model_path(model)
except OverflowError:
return FordPath()
if path is None or not all(_finite(*values) for values in path):
return FordPath()
distance, _, lateral, heading = path
heading_distance = max(OFFSET_STATION_M, speed*HEADING_TIME_S)
offset_distance = heading_distance if c0_time_based else OFFSET_STATION_M
return FordPath(True, float(np.interp(offset_distance, distance, lateral)),
float(np.interp(heading_distance, distance, heading)), 0., 0.)
class ModelActionController:
"""Integrated tracking error is the only accumulated correction.
Freshness, measurement cadence and driver/PSCM arbitration belong to the caller.
"""
__slots__ = ('c0', 'c1', 'correction', 'proportional_gain', 'integral_gain', 'proportional', 'feedback_curvature', 'c0_time_based',
'c0_proportional_gain', 'offset_proportional', 'offset_proportional_linear', 'offset_reference')
def __init__(self, proportional_gain=C1_PROPORTIONAL_GAIN, integral_gain=C1_INTEGRAL_GAIN, *, c0_time_based=False,
c0_proportional_gain=C0_PROPORTIONAL_GAIN):
if not _finite(proportional_gain, integral_gain, c0_proportional_gain) or min(proportional_gain, integral_gain, c0_proportional_gain) < 0.:
raise ValueError('PI gains must be finite and nonnegative')
self.proportional_gain, self.integral_gain = float(proportional_gain), float(integral_gain)
self.c0_proportional_gain = float(c0_proportional_gain)
self.c0_time_based = bool(c0_time_based)
self.reset()
def reset(self):
self.c0 = self.c1 = self.correction = self.proportional = self.feedback_curvature = 0.
self.offset_proportional = self.offset_proportional_linear = 0.
self.offset_reference = None
def update(self, model, desired_curvature, *, current_curvature, speed, dt, active=True, valid=True,
feedback_dt=None, feedback_enabled=True, pscm_limited=False, feedback_curvature=None, curvature_scale=1.,
direct_path=False, roll=0.):
feedback_dt = dt if feedback_dt is None else feedback_dt
reference = desired_curvature if feedback_curvature is None else feedback_curvature
if (not active or not valid or not _finite(dt, feedback_dt, desired_curvature, current_curvature, reference, curvature_scale, roll)
or not .002 <= dt <= .1 or not 0. <= feedback_dt <= .15
or abs(desired_curvature) > 1. or abs(current_curvature) > 1. or abs(reference) > 1.):
self.reset()
return FordPath()
if curvature_scale <= 0.:
self.reset()
return FordPath()
target = (encode_model_path(model, speed, c0_time_based=self.c0_time_based) if direct_path else
encode_model_action(model, desired_curvature, speed, c0_time_based=self.c0_time_based))
if not target.valid:
self.reset()
return FordPath()
if direct_path:
# Limit the channels independently, without rebuilding C0 from C1.
# Normalizing C0 by distance only applies the existing curvature envelope;
# when unconstrained, the output is exactly the sampled model offset.
distance = max(OFFSET_STATION_M, speed*HEADING_TIME_S) if self.c0_time_based else OFFSET_STATION_M
previous = current_curvature if self.offset_reference is None else self.offset_reference
self.offset_reference, _ = clip_curvature(speed, previous, 2.*target.path_offset/distance**2, roll)
target = FordPath(True, .5*self.offset_reference*distance**2,
max(OFFSET_STATION_M, speed*HEADING_TIME_S)*desired_curvature, 0., 0.)
else:
self.offset_reference = None
self.feedback_curvature = reference
error = reference-current_curvature
self.proportional = self.proportional_gain*max(OFFSET_STATION_M, speed*HEADING_TIME_S)*error if feedback_enabled else 0.
# Road-curvature error -> geometric steering error -> metres of C0.
# This is proportional only: nothing is accumulated or carried into a release.
response = C0_RESPONSE_CONSTANT+C0_RESPONSE_INVERSE_SPEED_SQUARED/max(speed, 1.34)**2
offset_error = error*curvature_scale/response
self.offset_proportional_linear = self.c0_proportional_gain*error*curvature_scale/response if feedback_enabled else 0.
self.offset_proportional = self.offset_proportional_linear
if not _finite(self.proportional, self.offset_proportional):
self.reset()
return FordPath()
if feedback_enabled and not direct_path:
# Unlike blending gains back to 1, this never introduces a slope above the
# original gain. Large corrections lose only a bounded amount, not a fraction.
self.offset_proportional -= self.c0_proportional_gain*(1.-C0_SMALL_ERROR_GAIN)*C0_SMALL_ERROR_SCALE_M*math.tanh(
offset_error/C0_SMALL_ERROR_SCALE_M)
base = float(np.clip(target.path_angle, -.5, .5))
offset = float(np.clip(target.path_offset+OFFSET_STATION_M*(target.path_angle-base), -5.11, 5.11))
self.c0 = float(np.clip(offset+self.offset_proportional, -5.11, 5.11))
if feedback_enabled:
increment = self.integral_gain*error*speed*feedback_dt
if not _finite(increment):
self.reset()
return FordPath()
direction = current_curvature if current_curvature else self.c1
if pscm_limited and increment*direction > 0.:
increment = float(np.clip(increment, min(-self.correction, 0.), max(-self.correction, 0.)))
# Retire existing I before limiting new accumulation; never cross zero
# through this step. New I is bounded by the combined command's range.
relief = float(np.clip(increment, min(-self.correction, 0.), max(-self.correction, 0.)))
self.correction += relief
increment -= relief
request = base+self.proportional+self.correction
self.correction += float(np.clip(increment, min(-.5-request, 0.), max(.5-request, 0.)))
else:
self.correction = 0.
self.c1 = float(np.clip(base+self.proportional+self.correction, -.5, .5))
return FordPath(True, _packed(self.c0, .01, -5.12), _packed(self.c1, .0005, -.5), 0., 0.)
class FordModelActionController:
"""Input adapter for the opt-in selected-action controller.
controlsd owns upstream selection/limiting and service health. This adapter
checks ages and clock order, then supplies elapsed time to the core.
Base commands use the latest request. Like comma's torque controller, feedback
uses a one-second request buffer indexed by lateralDelay at the 100 Hz control
cadence. Only integration waits for fresh steering measurements.
CAN yaw remains a health gate, not the feedback measurement. Ford's filtered
steeringPressed and fresh PSCM driver overrides clear the correction.
Fresh PSCM limits only inhibit outward integration;
neither a limit nor a repeated measurement freezes the model request.
"""
def __init__(self, proportional_gain=C1_PROPORTIONAL_GAIN, integral_gain=C1_INTEGRAL_GAIN, *, c0_time_based=False,
c0_proportional_gain=None, direct_path=False):
if c0_proportional_gain is None:
c0_proportional_gain = DIRECT_PATH_C0_PROPORTIONAL_GAIN if direct_path else C0_PROPORTIONAL_GAIN
self.core = ModelActionController(proportional_gain=proportional_gain, integral_gain=integral_gain, c0_time_based=c0_time_based,
c0_proportional_gain=c0_proportional_gain)
self.direct_path = bool(direct_path)
self.request_buffer_size = int(CURVATURE_REQUEST_BUFFER_SECONDS / DT_CTRL)
self.request_buffer = deque([0.] * self.request_buffer_size, maxlen=self.request_buffer_size)
self.hypothesis = ('model-path-direct-feedback-v22-soft-c0' if self.direct_path else
'model-action-curvature-c0-feedback-v22-soft-c0')
self.reset()
def path_curvature(self, model, speed):
"""Heading-equivalent feedback target; controlsd limits and logs this value."""
target = encode_model_path(model, speed, c0_time_based=self.core.c0_time_based)
return target.path_angle/max(OFFSET_STATION_M, speed*HEADING_TIME_S) if target.valid else 0.
def set_c0_time_based(self, enabled, *, lateral_engaged):
"""Apply a distance change only after lateral assistance is disengaged."""
if lateral_engaged or self.core.c0_time_based == bool(enabled):
return False
self.core.c0_time_based = bool(enabled)
self.reset('c0_distance_changed')
return True
def reset(self, status='inactive'):
self.core.reset()
if status != 'inactive':
self.request_buffer = deque([0.] * self.request_buffer_size, maxlen=self.request_buffer_size)
self.last_time = self.last_measurement_time = self.last_model_time = None
self.diagnostics = {'status': status, 'hypothesis': self.hypothesis,
'c0_time_based': self.core.c0_time_based,
'calibration_approved': CALIBRATION_APPROVED, 'command': (0., 0., 0., 0.)}
def update(self, model, desired_curvature, *, current_curvature, yaw_rate, speed, now, measurement_time, model_time,
reference_time, active, valid=True, driver_pressed=False, driver_torque=0., pscm_status=None,
feedback_curvature=None, curvature_scale=1., reference_source='modelV2', roll=0., lat_delay=0.):
# Record on every control cycle, including disengagement, as upstream does.
# This buffer changes feedback only; it never queues the outgoing base path.
feedback_delay = 0.
if _finite(desired_curvature, lat_delay) and abs(desired_curvature) <= 1.:
self.request_buffer.append(desired_curvature)
delay_frames = int(np.clip(lat_delay / DT_CTRL + 1, 1, self.request_buffer_size))
if feedback_curvature is None:
feedback_curvature = self.request_buffer[-delay_frames]
feedback_delay = (delay_frames - 1) * DT_CTRL
reason = None
if not active:
reason = 'inactive'
elif not valid:
reason = 'invalid_service'
elif not _finite(desired_curvature, current_curvature, yaw_rate, speed, now, measurement_time, model_time, reference_time, lat_delay):
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 or abs(current_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
feedback_dt = 0. if self.last_measurement_time is None else measurement_time-self.last_measurement_time
if not .002 <= dt <= .1 or not 0. <= feedback_dt <= .15 or (
self.last_model_time is not None and model_time < self.last_model_time
):
self.reset('timing_reset')
return FordPath()
status_fresh = (pscm_status is not None and pscm_status.valid and pscm_status.canMonoTime > 0
and -.005 <= now-pscm_status.canMonoTime*1e-9 <= .15)
pscm_limited = bool(status_fresh and pscm_status.limit == 2)
# CarState already filters Ford's torque threshold into steeringPressed.
# Rechecking raw torque here bypasses that filter and abruptly clears P/I.
driver_override = bool(driver_pressed or not _finite(driver_torque)
or (status_fresh and pscm_status.limit == 3))
feedback_enabled = not (driver_override or (status_fresh and (pscm_status.denied or pscm_status.lateralState != 2)))
direct_path = self.direct_path and reference_source == 'modelV2'
command = self.core.update(model, desired_curvature, current_curvature=current_curvature, speed=speed, dt=dt,
feedback_dt=feedback_dt, feedback_enabled=feedback_enabled, pscm_limited=pscm_limited,
feedback_curvature=feedback_curvature, curvature_scale=curvature_scale, direct_path=direct_path, roll=roll)
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
raw_heading = max(OFFSET_STATION_M, speed*HEADING_TIME_S)*desired_curvature
base_heading = float(np.clip(raw_heading, -.5, .5))
self.diagnostics = {'status': 'active', 'hypothesis': self.hypothesis,
'direct_path': direct_path, 'offset_reference': self.core.offset_reference,
'c0_time_based': self.core.c0_time_based,
'offset_distance': max(OFFSET_STATION_M, speed*HEADING_TIME_S) if self.core.c0_time_based else OFFSET_STATION_M,
'calibration_approved': CALIBRATION_APPROVED, 'desired_curvature': desired_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,
'curvature_error': desired_curvature-current_curvature, 'feedback_dt': feedback_dt,
'feedback_delay_requested': lat_delay, 'feedback_delay': feedback_delay,
'heading_feedforward': base_heading,
'offset_overflow': OFFSET_STATION_M*(raw_heading-base_heading),
'offset_proportional': self.core.offset_proportional, 'c0_proportional_gain': self.core.c0_proportional_gain,
'offset_proportional_linear': self.core.offset_proportional_linear,
'curvature_scale': curvature_scale,
'heading_correction': self.core.correction, 'feedback_enabled': feedback_enabled,
'heading_proportional': self.core.proportional, 'proportional_gain': self.core.proportional_gain,
'integral_gain': self.core.integral_gain, 'feedback_curvature': self.core.feedback_curvature,
'feedback_error': self.core.feedback_curvature-current_curvature,
'driver_override': driver_override, 'pscm_limited': pscm_limited, 'pscm_status_fresh': bool(status_fresh),
'command': (command.path_offset, command.path_angle, 0., 0.)}
return command
def select_model_action_controller(CP, enabled, *, c0_time_based=False, direct_path=False):
"""Only opt-in Ford CAN FD vehicles override upstream curvature control."""
compatible = CP.brand == 'ford' and CP.flags & FordFlags.CANFD
if enabled and compatible:
return FordModelActionController(proportional_gain=C1_PROPORTIONAL_GAIN, integral_gain=C1_INTEGRAL_GAIN,
c0_time_based=c0_time_based, direct_path=direct_path)
return None
@@ -1,368 +0,0 @@
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)
@@ -1,229 +0,0 @@
{
"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."
}
@@ -1,67 +0,0 @@
{
"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."
}
@@ -1,13 +0,0 @@
{
"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."
}

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