Compare commits

...

194 Commits

Author SHA1 Message Date
Isaac Barham df5e2bbae2 Ford: hold joint endpoint C0 at the supervisor saturation
The equal-arrival endpoint held 2-4 m of C0 in large turns. In the reference
ML3V/ML34 calibration the path supervisor's C0 term clip(0.5*C0, +-0.5)
saturates at 1.0 m, so that extra C0 added primary curvature but no
turn-direction allowance, while on release the supervisor kept excluding
zero demand until held C0 slewed (1.5 m/s) back below about 0.41 m.

Cap the static endpoint's C0 at 1.0 m and give the rest to C1, returning to
C0 only at C1's bound. The paired selector still builds with both fields,
then trades C0 for C1 at constant curvature before a release is needed.
Endpoints whose equal-arrival C0 is at most 1.0 m are unchanged.

Offline evidence on reference-firmware instructions (no wheel, assist or
vehicle model): zero demand excluded after release 1.48/1.88/2.17 s ->
0.39/0.39/0.48 s for large 15/20/25 km/h steps with an unchanged 50%/90%
rise; replayed routes 194-196 cut forced old-direction output after large
releases by 50-75%; packets away from large turns are identical. Three
frozen selector costs are refreshed; their selected packets are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-22 14:57:18 -04:00
Isaac Barham b30793bfb1 Ford: coordinate endpoint buildup and retain stop requests through speed undershoot 2026-09-21 22:55:37 -04:00
Isaac Barham d8c5639616 Ford: preserve stop requests and add bounded geometry entry preview
Keep coordinated steering active below 0.3 m/s by repeating the last transmitted active C0/C1 request. Preserve disengagement, freshness, fault and PSCM override release; avoid the moving inverse at zero speed.

Extend the existing signaled-turn preview with the offline boot08 geometry candidate: a 0.8-second local path reference, bounded addition and action-takeover fade. Keep the action base and upstream fallback.

Validation: 715 focused tests passed across controller, transport and settings suites; 20,000 policy parity cases, 3,000 continuous CAN/state comparisons and 4,854 geometry samples across 12 archived routes matched the retained candidate. Offline checks do not simulate changed wheel response.
2026-09-21 19:42:32 -04:00
Isaac Barham 36fe2cf854 Ford: add opt-in signaled-turn action preview 2026-09-21 14:23:50 -04:00
Isaac Barham 251e46e657 Ford: release native encoder arguments with cyclic GC disabled
NumPy ctypes pointer casts create reference cycles retained indefinitely by card. Preserve dtype and contiguity validation while giving synchronous native calls an acyclic array owner. The full controller stays flat through 300000 GC-disabled updates; nine route replays retain exact steering commands. Shared hardwared sensor changes are excluded.
2026-09-21 10:39:28 -04:00
Isaac Barham 07affcf934 Ford: add opt-in bounded assistance for large turn entries 2026-09-21 08:09:52 -04:00
Isaac Barham d203a109a7 hardwared: keep routine persistence off the health heartbeat 2026-09-18 19:00:49 -04:00
Isaac Barham ee3d0d33a0 Ford: avoid duplicate preview search and expose long health stalls 2026-09-18 17:32:16 -04:00
Isaac Barham 2fe8559208 Ford: preview the action trend for coordinated C0/C1 allocation 2026-09-18 16:33:29 -04:00
Isaac Barham 539fd716e5 Ford: retain centering trim through small wheel transients
The trim-release condition reused the wheel-motion learning gate, so brief
measured wheel movements discarded learned correction even when the
requested angle stayed nearly steady on route 17c.

Use filtered requested-angle motion to identify a fast reference change.
Keep large opposing-error release, measured-motion learning inhibition,
trim bounds, overrides and the coordinated C0/C1 allocator intact.

Validation: 707 tests and 25 subtests; seven frozen route comparisons with
527,215 updates per variant (two archived raw-yaw sensitivity tapes).
Both observed highway resets are avoided, the roundabout-entry packets
match, and the bookmarked unwind retains its nominal exit timings.
Offline checks do not establish a new physical steering response.
2026-09-18 13:07:32 -04:00
Isaac Barham bd9c0a8b47 Ford: correct small residual angle errors in coordinated control
Add a bounded slow integral trim around the nominal inverse. Route 17a exposes sustained wheel-angle errors despite accurate modeled command delivery. Restrict learning to small, slow errors; derive wheel rate because Ford leaves steeringRateDeg unset; clear opposing trim during reversal or fast unwind. Preserve existing activation, override, field and acceleration limits.

Validate with 538 focused tests, four calibrated-yaw route replays, and two archived raw-yaw sensitivity runs. Frozen commands and a synthetic biased-wheel regression do not establish onroad centering or stability.
2026-09-18 10:53:24 -04:00
Isaac Barham 6aa35bc1f1 Ford: correct v24 yaw input and retain commands on light touch 2026-09-18 01:49:16 -04:00
Isaac Barham a761165e31 Ford: reduce v24 Python overhead and trace health-loop stalls 2026-09-18 00:56:44 -04:00
Isaac Barham 99b3fb03e9 Ford: reduce v24 search work during turns
Reject candidates failing the existing immediate-error bound before simulating their full return. Reuse identical state and prefix-cost evaluations within each selection, preserving candidate order, scoring, and tie breaks.

Route 174 shows card, controlsd, and selfdrived sharing a saturated core during System Lagging alerts. This reduces encoder work without changing steering targets, commands, rates, or alert thresholds.

Validation: all nine native outputs exactly match the original library in 600 seeded cases and 720 recorded-state comparisons. Recorded-state p95 chooser runtime improves from 0.317 ms to 0.090 ms on the development Mac. 492 regression tests, Ruff, strict C++ compilation, and diff checks pass. Post-fix device timing remains to be measured.
2026-09-18 00:21:32 -04:00
Isaac Barham 528ed36155 Ford: fix v24 reader handoff to car interface
Return the coordinated CarControl copy as a Capnp reader. Returning a builder crashed the Ford sender when it called actuators.as_builder().

Pass prepared controls unchanged through CarInterfaceBase.apply in the CAN pipeline and real card-hook tests. Cover active, inactive, and override cases and verify the incoming message and longitudinal fields are preserved. The previous harness reader conversion masked this crash.

Validation: reproduced the reported traceback before the fix; 486 controller/status/selection checks and 30 sunnylink schema checks pass. Ruff and git diff --check pass.
2026-09-18 00:03:45 -04:00
Isaac Barham 3b657f6de9 Ford: gate coordinated C0/C1 encoder behind sunnylink trial toggle 2026-09-17 20:22:05 -04:00
Isaac Barham 42ef386729 Ford: soften small C1 feedback corrections
Apply the existing C0 small-error shape to action-mode C1 proportional
feedback. Corrections remain continuous and signed, with half the current
slope near zero error and the original slope approached at larger error.
The base request, C0 output, feedback delay, integral equations, arbitration,
field bounds and direct-path feedback stay unchanged. Identify the action
trial as v23-soft-c0-c1 in controller diagnostics.

Validation: 571 Ford tests and two subtests pass; Ruff and diff checks pass.
Production commands and controller state match the previously reviewed
offline candidate exactly over 216,184 recorded cycles from routes 162,
166 and 16a, including 2,164 CAN round-trip checks. This verifies command
behavior, not physical stability or an on-road wobble fix. The older PSCM
request reconstruction retains the reported release tradeoff: up to 40 ms
later in the selected forced-fast exit scenarios.
2026-09-17 13:02:04 -04:00
Isaac Barham 7bf3b9b428 Ford: soften small C0 feedback corrections
Reduce incremental C0 proportional gain near zero error, smoothly returning toward the existing slope for large errors. Preserve C1 feedback, request timing, base mapping, field bounds, and the upstream fallback.

Validate with 719 tests and 25 subtests, plus five route replays covering 646178 cycles and 64620 CAN round trips. Replay confirms command behavior; physical stability remains an onroad trial.
2026-09-16 20:33:02 -04:00
Isaac Barham ed9c44f575 Ford: align feedback with upstream request-history timing 2026-09-16 18:16:45 -04:00
Isaac Barham baeabfaa80 Ford: use filtered driver input for feedback continuity 2026-09-16 17:51:41 -04:00
Isaac Barham 4900c0a40c Ford: increase action-mode C0 proportional correction 2026-09-16 16:43:26 -04:00
Isaac Barham 0020c0c419 Ford: revert geometry-assisted action trial
Restore the runtime, schema, and toggle behavior of 01f5d5429 after the reported driving regression. Retain only the corrected v15 logging-test expectation. All 643 regression tests pass.
2026-09-16 14:51:04 -04:00
Isaac Barham 9d34f77c86 Ford: blend geometry assistance into the action reference 2026-09-16 12:59:09 -04:00
Isaac Barham 01f5d54292 Ford: trial direct model-path offset and heading
Sample position and orientation at the existing distance stations, retain measured-steering feedback against the path heading, and apply request limits independently to C0 and C1. Reuse the geometry toggle with upstream and maneuver fallback unchanged. Defer the untested raw-torque filtering change so this trial compares against the driven baseline. Validate with 593 tests and paired full-route replays; no physical tracking improvement is claimed.
2026-09-16 11:59:09 -04:00
Isaac Barham 4f7d2b8d2f Ford: use filtered driver input for feedback arbitration
The duplicate raw 1 Nm check cleared P/I on short torque crossings while Ford steeringPressed remained false. Use the existing filtered signal and retain immediate PSCM and invalid-torque overrides. Add regression and CAN integration coverage plus paired full-rlog replay results.
2026-09-16 11:43:30 -04:00
Isaac Barham 18ded03800 Ford: add selectable model geometry reference 2026-09-15 22:08:20 -04:00
Isaac Barham 1336171a20 Ford: add proportional C0 tracking correction
Add stateless C0 feedback using an offline Lightning response fit and the existing vehicle-model curvature conversion. Keep C1 P=0.75/I=1.0, the current feedforward geometry, field bounds, and upstream fallback.

Validate with 439 tests and native-time replay over ten Lightning routes: 1,170,113 cycles, 117,016 CAN round trips, and identical C1 output. Physical improvement remains an on-road trial.
2026-09-15 20:04:11 -04:00
Isaac Barham d425b3260b Ford: build C1 error correction four times faster
Raise I from 0.25 to 1.0 with P held at 0.75. Route 151 showed persistent undertracking with unused C1 range and slow integral buildup. Preserve C0, bounds, anti-windup and upstream fallback; identify the trial as v14.

Validate with 22 fixed-motion route replays, 5,201,912 CAN round trips, and 468 tests plus 25 subtests. Replay verifies commands, not improved physical tracking or stable release.
2026-09-15 17:05:50 -04:00
Isaac Barham 9a1d06061b Merge sunnypilot master into hiimisaac-dev
Routine sync: bring hiimisaac-dev up to date with master while preserving Ford-related work on this branch.
2026-09-15 14:54:54 +00:00
Isaac Barham 31cdfa12a0 Ford: increase immediate C1 error correction to P=0.75 2026-09-15 01:11:15 -04:00
Isaac Barham b720e9f1bb Ford: add low-speed model delay preview
Add up to 0.4 seconds to lateral model preview at or below 15 mph, tapering to zero at 30 mph. Apply only when the Ford CAN-FD C0/C1 controller is enabled, using the same startup toggle semantics as controlsd. Preserve the underlying delay estimate and longitudinal action time.

Validation: 397 helper, Ford controller, model recovery and parser tests passed; lint and whitespace checks passed. Actual model delay construction matches the offline +0.4-second sweep across 2,560 recorded frames at float32 and model float16 precision.
2026-09-14 23:29:37 -04:00
Isaac Barham 14bb8b0b1f joystick: keep manager from replacing manual input publisher 2026-09-14 20:26:14 -04:00
Isaac Barham 94a460f672 Ford: add independent C0/C1 to existing joystick mode
Add an opt-in joystick channel selector for CAN FD Fords. The steering axis directly controls C0 or C1 with the other path fields zero; keyboard keys select the channel and the existing on-screen joystick alert identifies it. Preserve standard joystick outputs and existing engagement and longitudinal behavior. Require centered input after switching, disengagement, or invalid/stale input.

Validation: 287 offline tests passed; 1,590 standard-mode frames matched the previous control outputs. Verified selected fields through the real CAN encoder at zero and moving speeds, schema compilation, and lint. Driving controller and lateral maneuver tools are unchanged.
2026-09-14 19:30:24 -04:00
Isaac Barham 16b2a3e996 Ford: remove diagnostic tests and restore stock maneuver tools
Revert the six Ford channel-test commits and discard the unfinished keyboard standstill changes. Restore the original lateral maneuver and joystick tools, removing test toggles, parameters, schema additions, reports, and control overrides. Preserve the driving controller and C0 distance toggle.

Validation: 232 Ford controller tests passed; tracked tree matches de23ac008.
2026-09-14 19:20:46 -04:00
Isaac Barham 506e9dd651 Ford: add keyboard channel steps with MADS support
Trigger bounded baseline/step/release targets through the existing isolated C0/C1 controller path. Preserve ACC and MADS, permit manual throttle only in keyboard mode, and abort on stale input or driver intervention. Fix the manager-imported Ford maneuver entry point, add response/status reporting, and cover the keyboard lifecycle and CAN output offline.
2026-09-14 18:56:32 -04:00
Isaac Barham b4a2b19aeb Ford: run normal lateral maneuvers through isolated channels 2026-09-14 13:33:30 -04:00
Isaac Barham 6b5661f1a1 Ford: test C0 and C1 at quarter field magnitude
Increase isolated diagnostic increments to 1.28 m C0 and 0.125 rad C1, corresponding to 25% of symmetric controller field limits after CAN rounding. Hold each pulse for one second and retain existing release and abort conditions. Verify CAN amplitudes, midpulse aborts and reports for both old and new probes.
2026-09-14 13:01:24 -04:00
Isaac Barham 0d0bb7eb33 Ford: match channel test health checks to 20 Hz polling
The default 100 Hz subscriber configuration marked conflated vehicle messages unhealthy at the diagnostic polling rate, preventing every trial from starting. Configure the actual 20 Hz rate and exercise real SubMaster health checks in the full-suite regression instead of mocking them healthy.
2026-09-14 12:28:50 -04:00
Isaac Barham e697a5522e Ford: use 15 mph for the lower channel test speed 2026-09-14 12:19:34 -04:00
Isaac Barham a89c9c814f Ford: add C0/C1 diagnostic toggle and response report 2026-09-14 12:10:28 -04:00
Amy Jeanes a5f44653d7 mici: add a refresh models button to the models panel (#2018)
models: add a refresh models button to mici, gate and show progress on both panels

Adds the refresh-models tile to the mici models panel (it was missing there),
factoring the sync-key trigger + in-progress check into refresh_model_list()
and refresh_in_progress() shared with the big UI.

On both UIs the refresh button is now gated on offroad + not-downloading +
not-refreshing (the manager runs offroad-only and its per-tick manifest fetch
sits above a blocking download loop, so a refresh queued in either state would
stick), and shows progress while the manager refetches: mici shows "fetching..."
on the tile, the big UI flips its button from REFRESH to "FETCHING..." to match
its FETCHING.../SELECT/CLEAR label style (as in the OSM panel), replacing the
old fire-and-forget popup.


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

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: James Vecellio-Grant <159560811+Discountchubbs@users.noreply.github.com>
2026-09-14 08:33:48 -07:00
Isaac Barham de23ac008e Ford: add live C0 distance toggle on comma four 2026-09-14 08:12:49 -04:00
Isaac Barham 7750121675 Ford: restore desired-curvature C0 with direct commands 2026-09-14 06:49:13 -04:00
Isaac Barham 5db3e3c9a0 Ford: restore model-path C0 with direct requests 2026-09-13 23:19:00 -04:00
Isaac Barham 26e5c9f532 Ford: send current C0/C1 requests without extra slew 2026-09-13 23:00:17 -04:00
Isaac Barham 57ae29f257 Ford: update curvature-C0 toggle description and install notes 2026-09-13 19:09:45 -04:00
Amy Jeanes 63a2a3868e models: don't freeze the ui on an unset LagdToggleDelay (#2026)
models: don't block the ui on an unset LagdToggleDelay

Params.get's second positional is `block`, not a fallback value, so
get("LagdToggleDelay", "0.2") passes block=True and does a blocking read.
When the param is unset this spins the ui thread until it appears, freezing
the models panel (the description is rebuilt every frame). Read it the same
way livedelay/lagd_toggle.py does.


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

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: James Vecellio-Grant <159560811+Discountchubbs@users.noreply.github.com>
2026-09-13 13:31:13 -07:00
Matt Purnell 5484f7f4a7 modeld: set the valid flag on modelDataV2SP (#2017)
* modeld: set the valid flag on modelDataV2SP

modelDataV2SP was published with new_message's default valid=False, so
the message was permanently invalid. Nothing acts on that today because
selfdrived lists it under ignore_valid, but it shows up as invalid in
every commIssue dump and hides any real problem behind a false one.

Copy modelV2's flag, the same way fill_model_msg already does for
drivingModelData.

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

* modeld_v2: set the valid flag on modelDataV2SP

The sunnypilot model runner publishes the same message and had the same
gap. Copy modelV2's flag here too, so both daemons agree.

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

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Co-authored-by: James Vecellio-Grant <159560811+Discountchubbs@users.noreply.github.com>
2026-09-13 12:49:59 -07:00
dzid26 347238b307 camera offset: use real horizon for the shear center (#2016) 2026-09-13 12:38:20 -07:00
Amy Jeanes c57f9a7f4e workflows: let forks run their own model builds (#2009)
build-single-tinygrad-model and build-all-tinygrad-models gain a docs_repo
input (default sunnypilot/sunnypilot-models) so a fork can run either against
its own gh-pages catalog, next to the existing hf_repo input for the dataset.

build-all's setup job now checks out the repo and branch it was dispatched
from instead of sunnypilot/sunnypilot's default branch. That keeps the
manifest's tinygrad_ref tied to the code that compiled the models, and lets a
fork run the full rebuild against its own dataset and docs repo.


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

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Co-authored-by: James Vecellio-Grant <159560811+Discountchubbs@users.noreply.github.com>
2026-09-13 10:31:27 -07:00
Amy Jeanes 3c24eeea25 ci: remove stale disabled workflows (#1994)
Both are disabled in the Actions tab and have not run in months:

- Release Drafter (release-drafter.yml) and its config
  .github/release-drafter.yml: last run 2025-12-18
- Debug Discourse Posting (test-discourse.yaml.yml): one-off debug
  workflow from #1435, last run 2025-10-28

The post-to-discourse composite action is kept; the prebuilt workflow
still uses it. docs, stale and jenkins scan are also disabled here but
are inherited from commaai/openpilot and left in place to avoid
modify/delete conflicts on every upstream sync.


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

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Co-authored-by: James Vecellio-Grant <159560811+Discountchubbs@users.noreply.github.com>
2026-09-13 10:16:44 -07:00
Isaac Barham 20485134e1 Ford: derive C0 from selected desired curvature 2026-09-13 13:08:46 -04:00
Isaac Barham 08b3a14ad4 Ford: simplify C1 feedback with continuous PI unwind 2026-09-13 12:30:22 -04:00
Amy Jeanes 7430f245c7 mici: add a clear cache button to the models panel (#2008)
models: add a clear cache button to mici, gate and show progress on both panels

Adds the clear-cache tile to the mici models panel (trash slide-to-confirm),
factoring the cache-size math into model_cache_size_mb() shared with the big UI.

On both UIs the clear button is now gated on offroad + not-downloading +
not-clearing (the manager runs offroad-only, so a clear queued onroad would
never be serviced and would stick), and shows progress while the manager works:
mici shows "clearing..." on the tile, the big UI flips its button to
"CLEARING..." to match its FETCHING.../SELECT/CLEAR label style (as in the OSM
panel).


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

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-09-13 08:12:56 -07:00
Isaac Barham 5e6993aabb Merge sunnypilot master into hiimisaac-dev
Bring in models: test tinygrad concurrency (#2006) while preserving Ford work on hiimisaac-dev.
2026-09-13 14:25:38 +00:00
Isaac Barham bf00bc691d Ford: add explicit proportional C1 feedback trial 2026-09-13 08:18:42 -04:00
Isaac Barham 22d188776c Ford: release completed unwind correction after catch-up 2026-09-13 00:45:55 -04:00
Isaac Barham 6df5eabb7e Ford: retire opposing C1 correction as the selected request changes 2026-09-12 23:52:25 -04:00
James Vecellio-Grant b67898fac4 models: test tinygrad concurrency (#2006) 2026-09-11 20:24:12 -07:00
Isaac Barham 17a86842f9 Ford: restore upstream control when the experimental toggle is off 2026-09-11 12:12:48 -04:00
Isaac Barham b81c00f5b9 Ford: allocate clipped base heading to C0 2026-09-10 18:04:51 -04:00
Isaac Barham 959ae3d6e7 Ford: allow model-action controller on all CAN FD vehicles
Select the feedback controller on any Ford CAN FD vehicle when its Sunnylink toggle is enabled. Cover startup priority, toggle restarts, and downstream CAN publication across all six current CAN FD platforms; update the settings description and drive-test documentation.

Validation: 612 tests passed, 178 skipped, and 9146 subtests passed. Ruff and git diff checks passed. Offline validation only.
2026-09-10 11:16:30 -04:00
Isaac Barham cf69210bb8 Ford: release conflicting C1 correction when the path agrees
An accumulated correction can outweigh a new C1 request while measured
curvature still points the other way. Release that correction only with fresh
feedback and agreement from both target and slewed C0. Keep steady-target
correction, the existing integral strength, output limits and arbitration.

Record releases in the v2 diagnostic identity. The change adds one release
condition and a diagnostic counter; the command law still has three states.

Validation: 567 tests and 9,146 subtests pass, with 178 inherited or unsupported
skips. Randomized and b8/b9 replay checks cover 669,343 Float32/CAN round trips.
Activation and C0 match the previous controller exactly. Replay verifies
command behavior only; no physical response or stability claim is made.
2026-09-10 10:01:37 -04:00
Isaac Barham 5fbb583e59 Ford: add measured-curvature feedback to C1
Add a distance-integrated steering-curvature correction to the restored v1
heading request. Hold the correction at zero error, allow unwind at limits,
and clear it for driver override or inactive PSCM control. Preserve C0,
C2=C3=0, final output limits, the 100 Hz sender and the existing opt-in toggle.

Validate build/hold/unwind, measurement cadence, anti-windup and the actual
controlsd-to-CAN path. The combined suite passes 511 tests and 9,146 subtests;
178 inherited or unsupported safety variants skip. Stress and frozen b8
replay pass 578,569 Float32/CAN round trips. These checks do not establish
physical tracking or stability. Record reproduction steps and source hashes.
2026-09-09 10:46:41 -04:00
Isaac Barham a7d70e2b08 Ford: restore original selected-curvature v1 controller
Restore the exact tracked tree from 5fc16abc7, identified in the
9b and 9e strong-tracking routes, including opendbc c21a9013 and
the original 100 Hz CAN FD cadence. C0 samples model y at 7 m;
C1 is max(7 m, speed times 1 s) times selected desired curvature.

This removes the later model-orientation, forecast, damping and
cadence experiments. Model selection/bundles and Panda safety are
unchanged. Restoring the prior behavior does not establish the
physical cause of the reported wobble.

Validation: exact original tree and submodule match; 494 tests and
9146 subtests passed, 178 inapplicable safety skips; canonical
Sunnylink schema; 100 real-sender messages in 100 control cycles,
with counters/checksums, C2/C3 zero and unchanged Panda TX checks.

Assisted-by: OpenAI Codex
2026-09-08 21:44:44 -04:00
Isaac Barham 00de176331 Ford: restore v7 model-point controller
Revert the C1 early-release experiment in 7ca2df481 after reported
centering degradation. Restore the exact tracked tree from 4bd841ecc;
model selection and model bundles are unchanged. A different driving
model remains a possible confound, so this does not assign a physical
root cause to either model or controller.

Validation: exact baseline tree match, 267 tests and 3 subtests passed,
and Sunnylink generated schema matches its source.

Assisted-by: OpenAI Codex
2026-09-08 20:37:34 -04:00
Isaac Barham 7ca2df4819 Ford: experiment with earlier C1 release from model path shape
Bound the model heading request toward zero using terminal spatial
curvature at the existing preview point. Preserve C0, command limits,
slew, 20 Hz cadence, and the existing Sunnylink selection.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

* ui: remove raygui usage

* match previous gui_text_box line spacing

* Revert "match previous gui_text_box line spacing"

This reverts commit ffd2fe31725c6d50bffaebc621c1e170d0926c66.

* Reapply "match previous gui_text_box line spacing"

This reverts commit d41404f09607e225f43868f7747f22dc0bb2cf16.

* log chestnut supply fault (#38711)

* log chestnut INA supply fault

* ci

* bump raylib (#38712)

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

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

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

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

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

* cabana: remove Qt from livestream (#38722)

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

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

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

* cabana: de-QT streams (#38718)

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

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

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

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

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

ui: sync gpu loading state

* add chestnut offroad alerts (#38706)

* system: add chestnut offroad alerts

* system: refine chestnut offroad alerts

* system: refine chestnut power alerts

* system: confirm chestnut power recovery from PCIe

* system: detect missing chestnut power from INA voltage

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

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

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

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

Pass the flags first.

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

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

pandad: support compact health packet

* BMRLNAP (#38681)

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

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

* mici: name updater signal constants

* drop SIGNAL_ prefix

* self contained

---------

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

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

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

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

* TGC (#38739)

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

* here

* monitor chestnut USB in hardwared (#38741)

hardwared: monitor chestnut USB independently

* modeld: wait for stable chestnut (#38742)

modeld: wait for stable chestnut

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

This reverts commit 7d5596d5c3.

* amd warp (#38684)

* modeld: fuse warp and policy TinyJit

* bump tg

* fix?

* this simple trick...

* debug 1

* bump tg

* pack all

* wips

* fix

* BIG_INTO_SMALL remove

* slower

* ui: show usb connection (#38745)

* ui: show USB status

* ui: resize USB icon

* ui: classify USB device once

* ui: debounce USB disconnect

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

* ui: show one GPU status (#38748)

ui: show one GPU status icon

* AGNOS 19.7 (#38750)

---------

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

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

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

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

* bump tg

* bump

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

* hack, remove before merge

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

* why were they hard coded

---------

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

* ui: resize USB icon

* ui: classify USB device once

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

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

* bump tg

* fix?

* this simple trick...

* debug 1

* bump tg

* pack all

* wips

* fix

* BIG_INTO_SMALL remove

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

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

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

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

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

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

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

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

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

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

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

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

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

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

* drop SIGNAL_ prefix

* self contained

---------

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

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

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

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

Assisted-by: Codex
2026-08-31 13:22:02 -04:00
Isaac Barham 33e70080ad Ford: update CAN-FD path control rate
Assisted-by: Codex
2026-08-31 12:57:00 -04:00
Harald Schäfer 4adbb85742 BMRLNAP (#38681) 2026-08-31 09:25:32 -07:00
Isaac Barham b7f0e3fbdc Ford: restore full C2 gentle path following
Assisted-by: Codex
2026-08-31 11:41:29 -04:00
Robbe Derks 70df7f227b bump panda (new health packet) (#38736)
pandad: support compact health packet
2026-08-31 14:01:20 +02:00
Isaac Barham 7f371b8acd Ford: hold path authority through turns
Assisted-by: Codex
2026-08-30 15:45:21 -04:00
Trey Moen 0e32059484 replay: capture downloader's stderr so download progress is reported again (#38734) 2026-08-30 09:32:36 -07:00
Isaac Barham 24c858e618 Ford: strengthen bounded path tracking feedback
Keep action curvature authoritative while increasing bounded C0/C1 feedback when measured curvature is behind. Keep C2 allocation tied to maneuver demand instead of tracking error.

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

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

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

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

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

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

* system: refine chestnut offroad alerts

* system: refine chestnut power alerts

* system: confirm chestnut power recovery from PCIe

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

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

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

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

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

* match previous gui_text_box line spacing

* Revert "match previous gui_text_box line spacing"

This reverts commit ffd2fe31725c6d50bffaebc621c1e170d0926c66.

* Reapply "match previous gui_text_box line spacing"

This reverts commit d41404f09607e225f43868f7747f22dc0bb2cf16.
2026-08-27 10:54:52 -07:00
Isaac Barham 25d0d0f1ff Ford: embed model path in rolling reference
Assisted-by: Codex
2026-08-27 13:08:15 -04:00
337 changed files with 32474 additions and 2833 deletions
+1
View File
@@ -9,6 +9,7 @@
*.ttf filter=lfs diff=lfs merge=lfs -text
*.otf filter=lfs diff=lfs merge=lfs -text
*.wav filter=lfs diff=lfs merge=lfs -text
openpilot/selfdrive/assets/sounds/milestone.wav -filter -diff -merge -text
openpilot/selfdrive/car/tests/test_models_segs.txt filter=lfs diff=lfs merge=lfs -text
openpilot/common/hardware/comma/updater filter=lfs diff=lfs merge=lfs -text
-11
View File
@@ -1,11 +0,0 @@
* @sunnypilot/dev-internal
/.github/ @devtekve @sunnyhaibin
/release/ci/ @devtekve @sunnyhaibin
/tinygrad_repo @devtekve @Discountchubbs
/tinygrad/ @devtekve @Discountchubbs
/selfdrive/controls/lib/longitudinal_planner.py @devtekve @Discountchubbs
/selfdrive/controls/lib/longitudinal_mpc_lib/long_mpc.py @devtekve @Discountchubbs
/selfdrive/modeld/ @devtekve @Discountchubbs
/sunnypilot/model* @devtekve @Discountchubbs
/sunnypilot/sunnylink/ @devtekve
/system/athena/ @devtekve
-43
View File
@@ -1,43 +0,0 @@
exclude-labels:
- 'no-changelog'
categories:
- title: '🚀 Features'
labels:
- 'feature'
- 'enhancement'
- title: '🐛 Bug Fixes'
collapse-after: 5
labels:
- 'fix'
- 'bugfix'
- 'bug'
- title: '🧰 Maintenance'
collapse-after: 5
label: 'chore'
change-template: '- $TITLE @$AUTHOR (#$NUMBER)'
change-title-escapes: '\<*_&'
replacers:
- search: '/[Ss][Uu][Nn][Nn][Yy][Pp][Ii][Ll][Oo][Tt]/g'
replace: 'sunnypilot'
- search: '/\b[Ss][Pp]\b/g'
replace: 'SP'
version-resolver:
major:
labels:
- 'major'
minor:
labels:
- 'minor'
patch:
labels:
- 'patch'
default: patch
name-template: 'v$RESOLVED_VERSION 🚀'
tag-template: 'v$RESOLVED_VERSION'
version-template: "0.$MAJOR.$MINOR.$PATCH" # The day OP becomes v1, we need to bump this
tag-prefix: "v0." # The day OP becomes v1, we need to bump this
prerelease-identifier: "staging"
template: |
## Changes
$CHANGES
@@ -20,6 +20,11 @@ on:
required: false
type: string
default: 'sunnypilot/sunnypilot_models_v1'
docs_repo:
description: 'GitHub repo holding the driving_models JSON on its gh-pages branch'
required: false
type: string
default: 'sunnypilot/sunnypilot-models'
jobs:
setup:
@@ -34,7 +39,6 @@ jobs:
- name: Checkout sunnypilot repo
uses: actions/checkout@v4
with:
repository: sunnypilot/sunnypilot
path: sunnypilot
submodules: recursive
@@ -47,10 +51,10 @@ jobs:
echo "tinygrad_ref=$ref" >> $GITHUB_OUTPUT
echo "tinygrad_ref is $ref"
- name: Checkout docs repo (sunnypilot-models, gh-pages)
- name: Checkout docs repo (gh-pages)
uses: actions/checkout@v4
with:
repository: sunnypilot/sunnypilot-models
repository: ${{ inputs.docs_repo }}
ref: gh-pages
path: docs
ssh-key: ${{ secrets.CI_SUNNYPILOT_DOCS_PRIVATE_KEY }}
@@ -78,6 +82,7 @@ jobs:
- name: Get next recompiled dir number
id: create-recompiled-dir
env:
HF_TOKEN: ${{ secrets.HF_TOKEN }}
HF_REPO: ${{ github.event.inputs.hf_repo }}
run: |
pip install huggingface_hub
@@ -117,6 +122,7 @@ jobs:
json_version: ${{ needs.setup.outputs.json_version }}
target_hardware: ${{ github.event.inputs.target_hardware }}
hf_repo: ${{ github.event.inputs.hf_repo }}
docs_repo: ${{ inputs.docs_repo }}
set_min_version: ${{ github.event.inputs.set_min_version }}
tinygrad_ref: ${{ needs.setup.outputs.tinygrad_ref }}
secrets: inherit
@@ -161,6 +167,7 @@ jobs:
target_hardware: ${{ github.event.inputs.target_hardware }}
artifact_suffix: -retry
hf_repo: ${{ github.event.inputs.hf_repo }}
docs_repo: ${{ inputs.docs_repo }}
set_min_version: ${{ github.event.inputs.set_min_version }}
tinygrad_ref: ${{ needs.setup.outputs.tinygrad_ref }}
secrets: inherit
+30 -9
View File
@@ -30,6 +30,7 @@ jobs:
runs-on: ubuntu-24.04
outputs:
model_name: ${{ steps.resolve.outputs.model_name }}
safe_model_name: ${{ steps.resolve.outputs.safe_model_name }}
onnx_ref: ${{ steps.resolve.outputs.onnx_ref }}
onnx_path: ${{ steps.resolve.outputs.onnx_path }}
hf_defaults_path: ${{ steps.resolve.outputs.hf_defaults_path }}
@@ -64,7 +65,9 @@ jobs:
exit 1
fi
SAFE_NAME="${NAME// /-}"
echo "model_name=${NAME}" >> $GITHUB_OUTPUT
echo "safe_model_name=${SAFE_NAME}" >> $GITHUB_OUTPUT
echo "onnx_ref=${ONNX_REF}" >> $GITHUB_OUTPUT
echo "onnx_path=${ONNX_PATH}" >> $GITHUB_OUTPUT
echo "hf_defaults_path=${HF_DEFAULTS_PATH}" >> $GITHUB_OUTPUT
@@ -135,7 +138,7 @@ jobs:
- name: Prepare output
env:
MODEL_NAME: ${{ needs.resolve.outputs.model_name }}
MODEL_NAME: ${{ needs.resolve.outputs.safe_model_name }}
run: |
source ${UV_PROJECT_ENVIRONMENT}/bin/activate
export PYTHONPATH=${{ github.workspace }}
@@ -158,13 +161,13 @@ jobs:
- name: Upload small model artifact
uses: actions/upload-artifact@v4
with:
name: model-${{ needs.resolve.outputs.model_name }}-${{ github.run_number }}
name: model-${{ needs.resolve.outputs.safe_model_name }}-${{ github.run_number }}
path: ${{ github.workspace }}/small_output/
- name: Upload artifact name file
uses: actions/upload-artifact@v4
with:
name: artifact-name-${{ needs.resolve.outputs.model_name }}
name: artifact-name-${{ needs.resolve.outputs.safe_model_name }}
path: ${{ github.workspace }}/small_output/artifact_name.txt
- name: Re-enable powersave
@@ -254,7 +257,7 @@ jobs:
- name: Prepare output
env:
MODEL_NAME: ${{ needs.resolve.outputs.model_name }}
MODEL_NAME: ${{ needs.resolve.outputs.safe_model_name }}
run: |
source ${UV_PROJECT_ENVIRONMENT}/bin/activate
export PYTHONPATH=${{ github.workspace }}
@@ -277,13 +280,13 @@ jobs:
- name: Upload big model artifact
uses: actions/upload-artifact@v4
with:
name: model-${{ needs.resolve.outputs.model_name }}-${{ github.run_number }}
name: model-${{ needs.resolve.outputs.safe_model_name }}-${{ github.run_number }}
path: ${{ github.workspace }}/big_output/
- name: Upload artifact name file
uses: actions/upload-artifact@v4
with:
name: artifact-name-${{ needs.resolve.outputs.model_name }}
name: artifact-name-${{ needs.resolve.outputs.safe_model_name }}
path: ${{ github.workspace }}/big_output/artifact_name.txt
- name: Re-enable powersave
@@ -318,7 +321,7 @@ jobs:
if: ${{ inputs.target == 'small' || inputs.target == 'big' }}
uses: actions/download-artifact@v4
with:
name: artifact-name-${{ needs.resolve.outputs.model_name }}
name: artifact-name-${{ needs.resolve.outputs.safe_model_name }}
path: artifact_name
- name: Read artifact name
@@ -338,7 +341,7 @@ jobs:
- name: Upload model to HF
if: ${{ inputs.target == 'small' || inputs.target == 'big' }}
env:
HF_OIDC_RESOURCE: datasets/${{ env.HF_REPO }}
HF_TOKEN: ${{ secrets.HF_TOKEN }}
ARTIFACT_NAME: ${{ steps.artifact.outputs.artifact_name }}
run: |
rm -f output/artifact_name.txt
@@ -364,7 +367,7 @@ jobs:
- name: Generate DM metadata and upload to HF
if: ${{ inputs.target == 'dm' }}
env:
HF_OIDC_RESOURCE: datasets/${{ env.HF_REPO }}
HF_TOKEN: ${{ secrets.HF_TOKEN }}
run: |
export PYTHONPATH=$(pwd)
python3 -c "
@@ -481,11 +484,29 @@ jobs:
print(f'Chunked {pkl} into {len(targets)} chunks')
"
- name: Compile DM warp
run: |
source ${UV_PROJECT_ENVIRONMENT}/bin/activate
export PYTHONPATH="${PYTHONPATH}:${{ github.workspace }}/tinygrad_repo:${{ github.workspace }}"
TG_FLAGS="DEV=QCOM IMAGE=1 FLOAT16=1 NOLOCALS=1 JIT_BATCH_SIZE=0 OPENPILOT_HACKS=1"
MODEL_DIR="${{ github.workspace }}/openpilot/selfdrive/modeld"
DM_SIZE=$(python3 -c "from openpilot.common.transformations.model import DM_INPUT_SIZE as s; print(f'{s[0]}x{s[1]}')")
for res in $(python3 -c "from openpilot.common.transformations.camera import _ar_ox_fisheye as a, _os_fisheye as o; print(f'{a.width}x{a.height} {o.width}x{o.height}')"); do
WARP_PKL="${MODEL_DIR}/models/dm_warp_${res}_tinygrad.pkl"
taskset -c 7 env ${TG_FLAGS} python3 ${MODEL_DIR}/compile_dm_warp.py \
--camera-resolution ${res} \
--warp-to ${DM_SIZE} \
--output ${WARP_PKL}
done
- name: Prepare DM output
run: |
mkdir -p dm_output
cp ${{ github.workspace }}/${{ env.DM_PKL }}.chunk* dm_output/
cp ${{ github.workspace }}/${{ env.DM_PKL }}.chunkmanifest dm_output/
cp ${{ github.workspace }}/openpilot/selfdrive/modeld/models/dm_warp_* dm_output/
- name: Upload DM artifact
uses: actions/upload-artifact@v4
@@ -39,6 +39,11 @@ on:
required: false
type: string
default: 'sunnypilot/sunnypilot_models_v1'
docs_repo:
description: 'GitHub repo holding the driving_models JSON on its gh-pages branch'
required: false
type: string
default: 'sunnypilot/sunnypilot-models'
set_min_version:
description: 'Minimum selector version'
required: false
@@ -107,6 +112,11 @@ on:
required: false
type: string
default: 'sunnypilot/sunnypilot_models_v1'
docs_repo:
description: 'GitHub repo holding the driving_models JSON on its gh-pages branch'
required: false
type: string
default: 'sunnypilot/sunnypilot-models'
env:
RECOMPILED_DIR: recompiled${{ inputs.recompiled_dir }}
JSON_FILE: docs/docs/driving_models_${{ inputs.target_hardware == 'chestnut' && 'chestnut_v' || 'v' }}${{ inputs.json_version }}.json
@@ -136,7 +146,7 @@ jobs:
- name: Checkout docs repo
uses: actions/checkout@v4
with:
repository: sunnypilot/sunnypilot-models
repository: ${{ inputs.docs_repo }}
ref: gh-pages
path: docs
ssh-key: ${{ secrets.CI_SUNNYPILOT_DOCS_PRIVATE_KEY }}
@@ -146,7 +156,7 @@ jobs:
- name: Validate hf_repo and JSON version
env:
HF_OIDC_RESOURCE: datasets/${{ inputs.hf_repo }}
HF_TOKEN: ${{ secrets.HF_TOKEN }}
run: |
if [ ! -f "$JSON_FILE" ]; then
echo "JSON file $JSON_FILE does not exist!"
@@ -155,13 +165,8 @@ jobs:
python3 -c "
import sys
from huggingface_hub import HfApi
try:
api = HfApi()
api.repo_info(repo_id=sys.argv[1], repo_type='dataset')
print(f'Success: Repo {sys.argv[1]} exists.')
except Exception as e:
print('HF validation failed:', e)
sys.exit(1)
HfApi().repo_info(repo_id=sys.argv[1], repo_type='dataset')
print(f'Success: Repo {sys.argv[1]} exists.')
" "${{ inputs.hf_repo }}"
- name: Download artifact name file
@@ -192,7 +197,7 @@ jobs:
- name: Upload to Hugging Face
env:
HF_OIDC_RESOURCE: datasets/${{ inputs.hf_repo }}
HF_TOKEN: ${{ secrets.HF_TOKEN }}
ARTIFACT_NAME: ${{ steps.read-artifact-name.outputs.artifact_name }}
run: |
hf upload ${{ inputs.hf_repo }} \
@@ -46,6 +46,13 @@ runs:
printf '%s\t%s\n' "$ENCODED_URL" "${DEST_DIR}/${CANONICAL}.chunk${CHUNK_IDX}" >> "$DOWNLOAD_LIST"
done < <(echo "$ARTIFACT" | jq -r '.chunks[].file_name')
echo "$NUM_CHUNKS" > "${DEST_DIR}/${CANONICAL}.chunkmanifest"
if [ "$CANONICAL" = "dmonitoring_model_tinygrad.pkl" ]; then
for warp in dm_warp_1928x1208_tinygrad.pkl dm_warp_1344x760_tinygrad.pkl; do
ENCODED_URL=$(python3 -c "import urllib.parse; print(urllib.parse.quote('${BASE_URL}/${warp}', safe=':/'))")
printf '%s\t%s\n' "$ENCODED_URL" "${DEST_DIR}/${warp}" >> "$DOWNLOAD_LIST"
done
fi
}
echo "$MODELS_JSON" | jq -c '.[]' | while IFS= read -r model; do
-28
View File
@@ -1,28 +0,0 @@
name: Release Drafter
on:
push:
branches:
- master
tags:
- 'v*'
pull_request_target:
types: [opened, reopened, synchronize]
workflow_dispatch:
permissions:
contents: read
jobs:
update_release_draft:
permissions:
contents: write
pull-requests: write
runs-on: ubuntu-latest
steps:
- uses: release-drafter/release-drafter@v6
with:
config-name: release-drafter.yml
prerelease: ${{ !startsWith(github.ref, 'refs/tags/v') }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -121,7 +121,7 @@ jobs:
if-no-files-found: error
build_model:
runs-on: [self-hosted, chestnut]
runs-on: [self-hosted, "${{ inputs.target_hardware == 'chestnut' && 'chestnut' || 'tici' }}"]
needs: get_model
env:
MODEL_NAME: ${{ inputs.custom_name || inputs.upstream_branch }} (${{ needs.get_model.outputs.model_date }})
@@ -188,7 +188,7 @@ jobs:
if [ "${{ inputs.target_hardware }}" == "chestnut" ]; then
echo "CHESTNUT build"
export CHESTNUT=1
TG_FLAGS="DEBUG=1 DEV=USB+AMD:LLVM WARP_DEV=QCOM FLOAT16=1 JIT_BATCH_SIZE=0 GMMU=0 TC_OPT=2"
TG_FLAGS="DEBUG=1 DEV=USB+AMD:LLVM FLOAT16=1 JIT_BATCH_SIZE=0 GMMU=0 TC_OPT=2 TC_OCCUPANCY_OPT=1"
OUTPUT_PKL="${{ env.MODELS_DIR }}/big_driving_tinygrad.pkl"
else
echo "QCOM build"
@@ -216,6 +216,9 @@ jobs:
needs: [ prepare_strategy ]
runs-on: ubuntu-24.04
if: ${{ needs.prepare_strategy.outputs.include_big_model == 'true' }}
concurrency:
group: prepare-chestnut
cancel-in-progress: false
outputs:
onnx_sha256: ${{ steps.resolve.outputs.onnx_sha256 }}
env:
@@ -228,8 +231,10 @@ jobs:
run: |
REF="${{ github.head_ref || github.ref_name }}"
ONNX_HASH=$(gh api "repos/${GH_REPO}/contents/openpilot/selfdrive/modeld/models/big_driving_supercombo.onnx?ref=${REF}" --jq '.content' | base64 -d | grep '^oid sha256:' | cut -d: -f2)
BLOB_SHA=$(gh api "repos/${GH_REPO}/contents/openpilot/selfdrive/modeld/models/big_driving_supercombo.onnx?ref=${REF}" --jq '.sha')
ONNX_HASH=$(gh api "repos/${GH_REPO}/git/blobs/${BLOB_SHA}" --jq '.content' | base64 -d | grep '^oid sha256:' | cut -d: -f2)
echo "ONNX hash: $ONNX_HASH"
[ -n "$ONNX_HASH" ] || { echo "::error::Failed to extract ONNX hash"; exit 1; }
echo "onnx_sha256=$ONNX_HASH" >> $GITHUB_OUTPUT
TINYGRAD_REF=$(gh api "repos/${GH_REPO}/contents/tinygrad_repo?ref=${REF}" --jq '.sha')
@@ -238,7 +243,7 @@ jobs:
JSON_URL="https://huggingface.co/datasets/${HF_REPO}/resolve/main/${HF_DEFAULTS_PATH}/default_models.json"
check_defaults() {
DEFAULTS=$(curl -fsSL "$JSON_URL" 2>/dev/null) || return 1
DEFAULTS=$(curl -fsSL "${JSON_URL}?t=$(date +%s)" 2>/dev/null) || return 1
TINYGRAD_MATCH=$(echo "$DEFAULTS" | jq -r --arg ref "$TINYGRAD_REF" '.tinygrad_ref == $ref' 2>/dev/null)
[ "$TINYGRAD_MATCH" = "true" ] || return 1
BUNDLE=$(echo "$DEFAULTS" | jq --arg hash "$ONNX_HASH" '.bundles[] | select(.onnx_sha256 == $hash)' 2>/dev/null)
@@ -252,18 +257,35 @@ jobs:
echo "No matching model on HF — dispatching build"
gh workflow run build-default-models.yaml --ref "$REF" -f target=big
sleep 10
echo "Polling HF for big model availability..."
BUILD_RUN_ID=$(gh run list --workflow build-default-models.yaml --branch "$REF" --limit 1 --json databaseId --jq '.[0].databaseId')
echo "Dispatched build run: $BUILD_RUN_ID"
echo "Waiting for build run to complete..."
for i in $(seq 1 90); do
sleep 30
if check_defaults; then
echo "Big model available on HF after $((i * 30))s"
exit 0
STATUS=$(gh api "repos/${GH_REPO}/actions/runs/${BUILD_RUN_ID}" --jq '.status')
CONCLUSION=$(gh api "repos/${GH_REPO}/actions/runs/${BUILD_RUN_ID}" --jq '.conclusion')
echo "Poll $i/90: status=$STATUS conclusion=$CONCLUSION"
if [ "$STATUS" = "completed" ]; then
if [ "$CONCLUSION" = "success" ]; then
echo "Build run succeeded, verifying HF..."
sleep 10
if check_defaults; then
echo "Big model verified on HF"
exit 0
fi
echo "::error::Build succeeded but model not found on HF"
exit 1
else
echo "::error::Build run failed with conclusion=$CONCLUSION"
exit 1
fi
fi
echo "Poll $i/90: not yet available"
done
echo "::error::Big model not available on HF after 45 minutes"
echo "::error::Build run did not complete within 45 minutes"
exit 1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -277,6 +299,9 @@ jobs:
prepare_small_model:
needs: [ prepare_strategy ]
runs-on: ubuntu-24.04
concurrency:
group: prepare-small-model
cancel-in-progress: false
outputs:
driving_onnx_sha256: ${{ steps.resolve.outputs.driving_onnx_sha256 }}
env:
@@ -289,8 +314,10 @@ jobs:
run: |
REF="${{ github.head_ref || github.ref_name }}"
DRIVING_HASH=$(gh api "repos/${GH_REPO}/contents/openpilot/selfdrive/modeld/models/driving_supercombo.onnx?ref=${REF}" --jq '.content' | base64 -d | grep '^oid sha256:' | cut -d: -f2)
BLOB_SHA=$(gh api "repos/${GH_REPO}/contents/openpilot/selfdrive/modeld/models/driving_supercombo.onnx?ref=${REF}" --jq '.sha')
DRIVING_HASH=$(gh api "repos/${GH_REPO}/git/blobs/${BLOB_SHA}" --jq '.content' | base64 -d | grep '^oid sha256:' | cut -d: -f2)
echo "Driving ONNX hash: $DRIVING_HASH"
[ -n "$DRIVING_HASH" ] || { echo "::error::Failed to extract driving ONNX hash"; exit 1; }
echo "driving_onnx_sha256=$DRIVING_HASH" >> $GITHUB_OUTPUT
TINYGRAD_REF=$(gh api "repos/${GH_REPO}/contents/tinygrad_repo?ref=${REF}" --jq '.sha')
@@ -299,7 +326,7 @@ jobs:
JSON_URL="https://huggingface.co/datasets/${HF_REPO}/resolve/main/${HF_DEFAULTS_PATH}/default_models.json"
check_defaults() {
DEFAULTS=$(curl -fsSL "$JSON_URL" 2>/dev/null) || return 1
DEFAULTS=$(curl -fsSL "${JSON_URL}?t=$(date +%s)" 2>/dev/null) || return 1
TINYGRAD_MATCH=$(echo "$DEFAULTS" | jq -r --arg ref "$TINYGRAD_REF" '.tinygrad_ref == $ref' 2>/dev/null)
[ "$TINYGRAD_MATCH" = "true" ] || return 1
DRIVING=$(echo "$DEFAULTS" | jq --arg hash "$DRIVING_HASH" '.bundles[] | select(.onnx_sha256 == $hash)' 2>/dev/null)
@@ -313,18 +340,35 @@ jobs:
echo "No matching model on HF — dispatching build"
gh workflow run build-default-models.yaml --ref "$REF" -f target=small
sleep 10
echo "Polling HF for model availability..."
BUILD_RUN_ID=$(gh run list --workflow build-default-models.yaml --branch "$REF" --limit 1 --json databaseId --jq '.[0].databaseId')
echo "Dispatched build run: $BUILD_RUN_ID"
echo "Waiting for build run to complete..."
for i in $(seq 1 60); do
sleep 30
if check_defaults; then
echo "Model available on HF after $((i * 30))s"
exit 0
STATUS=$(gh api "repos/${GH_REPO}/actions/runs/${BUILD_RUN_ID}" --jq '.status')
CONCLUSION=$(gh api "repos/${GH_REPO}/actions/runs/${BUILD_RUN_ID}" --jq '.conclusion')
echo "Poll $i/60: status=$STATUS conclusion=$CONCLUSION"
if [ "$STATUS" = "completed" ]; then
if [ "$CONCLUSION" = "success" ]; then
echo "Build run succeeded, verifying HF..."
sleep 10
if check_defaults; then
echo "Small model verified on HF"
exit 0
fi
echo "::error::Build succeeded but model not found on HF"
exit 1
else
echo "::error::Build run failed with conclusion=$CONCLUSION"
exit 1
fi
fi
echo "Poll $i/60: not yet available"
done
echo "::error::Small driving model not available on HF after 30 minutes"
echo "::error::Small model build did not complete within 30 minutes"
exit 1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -338,6 +382,9 @@ jobs:
prepare_dm_model:
needs: [ prepare_strategy ]
runs-on: ubuntu-24.04
concurrency:
group: prepare-dm-model
cancel-in-progress: false
outputs:
dm_onnx_sha256: ${{ steps.resolve.outputs.dm_onnx_sha256 }}
env:
@@ -350,8 +397,10 @@ jobs:
run: |
REF="${{ github.head_ref || github.ref_name }}"
DM_HASH=$(gh api "repos/${GH_REPO}/contents/openpilot/selfdrive/modeld/models/dmonitoring_model.onnx?ref=${REF}" --jq '.content' | base64 -d | grep '^oid sha256:' | cut -d: -f2)
BLOB_SHA=$(gh api "repos/${GH_REPO}/contents/openpilot/selfdrive/modeld/models/dmonitoring_model.onnx?ref=${REF}" --jq '.sha')
DM_HASH=$(gh api "repos/${GH_REPO}/git/blobs/${BLOB_SHA}" --jq '.content' | base64 -d | grep '^oid sha256:' | cut -d: -f2)
echo "DM ONNX hash: $DM_HASH"
[ -n "$DM_HASH" ] || { echo "::error::Failed to extract DM ONNX hash"; exit 1; }
echo "dm_onnx_sha256=$DM_HASH" >> $GITHUB_OUTPUT
TINYGRAD_REF=$(gh api "repos/${GH_REPO}/contents/tinygrad_repo?ref=${REF}" --jq '.sha')
@@ -360,7 +409,7 @@ jobs:
JSON_URL="https://huggingface.co/datasets/${HF_REPO}/resolve/main/${HF_DEFAULTS_PATH}/default_models.json"
check_defaults() {
DEFAULTS=$(curl -fsSL "$JSON_URL" 2>/dev/null) || return 1
DEFAULTS=$(curl -fsSL "${JSON_URL}?t=$(date +%s)" 2>/dev/null) || return 1
TINYGRAD_MATCH=$(echo "$DEFAULTS" | jq -r --arg ref "$TINYGRAD_REF" '.tinygrad_ref == $ref' 2>/dev/null)
[ "$TINYGRAD_MATCH" = "true" ] || return 1
DM=$(echo "$DEFAULTS" | jq --arg hash "$DM_HASH" '.bundles[] | select(.onnx_sha256 == $hash)' 2>/dev/null)
@@ -374,18 +423,35 @@ jobs:
echo "No matching DM model on HF — dispatching build"
gh workflow run build-default-models.yaml --ref "$REF" -f target=dm
sleep 10
echo "Polling HF for DM model availability..."
BUILD_RUN_ID=$(gh run list --workflow build-default-models.yaml --branch "$REF" --limit 1 --json databaseId --jq '.[0].databaseId')
echo "Dispatched build run: $BUILD_RUN_ID"
echo "Waiting for build run to complete..."
for i in $(seq 1 60); do
sleep 30
if check_defaults; then
echo "DM model available on HF after $((i * 30))s"
exit 0
STATUS=$(gh api "repos/${GH_REPO}/actions/runs/${BUILD_RUN_ID}" --jq '.status')
CONCLUSION=$(gh api "repos/${GH_REPO}/actions/runs/${BUILD_RUN_ID}" --jq '.conclusion')
echo "Poll $i/60: status=$STATUS conclusion=$CONCLUSION"
if [ "$STATUS" = "completed" ]; then
if [ "$CONCLUSION" = "success" ]; then
echo "Build run succeeded, verifying HF..."
sleep 10
if check_defaults; then
echo "DM model verified on HF"
exit 0
fi
echo "::error::Build succeeded but DM model not found on HF"
exit 1
else
echo "::error::Build run failed with conclusion=$CONCLUSION"
exit 1
fi
fi
echo "Poll $i/60: not yet available"
done
echo "::error::DM model not available on HF after 30 minutes"
echo "::error::DM model build did not complete within 30 minutes"
exit 1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
-78
View File
@@ -1,78 +0,0 @@
name: Debug Discourse Posting
on:
push:
jobs:
test-discourse-post:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Post test message to Discourse
uses: ./.github/workflows/post-to-discourse
with:
discourse-url: ${{ vars.DISCOURSE_URL }}
api-key: ${{ secrets.DISCOURSE_API_KEY }}
api-username: ${{ secrets.DISCOURSE_API_USERNAME }}
topic-id: ${{ vars.DISCOURSE_UPDATES_TOPIC_ID }}
message: |
## 🧪 Test Post from GitHub Actions
**This is a test post to verify Discourse integration**
- **Workflow**: ${{ github.workflow }}
- **Run Number**: #${{ github.run_number }}
- **Branch**: `${{ github.ref_name }}`
- **Commit**: ${{ github.sha }}
- **Actor**: @${{ github.actor }}
- **Timestamp**: ${{ github.event.head_commit.timestamp }}
---
### Fake Build Info (for testing)
- **Version**: 0.9.8-test
- **Build**: #42
- **Branch**: release-test
[View workflow run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})
*This is an automated test message. Drive safe! 🚗💨*
- name: Create topic on Discourse
uses: ./.github/workflows/post-to-discourse
with:
discourse-url: ${{ vars.DISCOURSE_URL }}
api-key: ${{ secrets.DISCOURSE_API_KEY }}
api-username: ${{ secrets.DISCOURSE_API_USERNAME }}
#topic-id: ${{ vars.DISCOURSE_UPDATES_TOPIC_ID }}
category-id: 4
title: "This is a test of a new topic instead of a reply"
message: |
## 🧪 Test Post from GitHub Actions
**This is a test post to verify Discourse integration**
- **Workflow**: ${{ github.workflow }}
- **Run Number**: #${{ github.run_number }}
- **Branch**: `${{ github.ref_name }}`
- **Commit**: ${{ github.sha }}
- **Actor**: @${{ github.actor }}
- **Timestamp**: ${{ github.event.head_commit.timestamp }}
---
### Fake Build Info (for testing)
- **Version**: 0.9.8-test
- **Build**: #42
- **Branch**: release-test
[View workflow run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})
*This is an automated test message. Drive safe! 🚗💨*
- name: Display results
if: always()
run: |
echo "::notice::Discourse post test completed"
echo "Check your Discourse topic to verify the post appeared correctly"
+79
View File
@@ -0,0 +1,79 @@
name: Test Models Compatibility With Tinygrad Changes
on:
pull_request:
paths:
- 'tinygrad_repo'
workflow_dispatch:
jobs:
generate-matrix:
runs-on: ubuntu-latest
outputs:
models: ${{ steps.set-matrix.outputs.models }}
steps:
- uses: actions/checkout@v4
- name: Fetch and Parse json
id: set-matrix
run: |
python3 -c '
import json, urllib.request, os, re
with open("openpilot/sunnypilot/models/fetcher.py", "r") as f:
urls = re.findall(r"MODEL_URL(?:_CHESTNUT)?\s*=\s*[\"'"'"']([^\"'"'"']+)[\"'"'"']", f.read())
artifacts = []
for url in urls:
data = json.loads(urllib.request.urlopen(url).read())
for bundle in data.get("bundles", []):
for model in bundle.get("models", []):
if "artifact" in model:
artifacts.append(model["artifact"])
with open(os.environ["GITHUB_OUTPUT"], "a") as f:
f.write(f"models={json.dumps(artifacts)}\n")
'
test-model:
name: Test ${{ matrix.artifact.file_name }}
needs: generate-matrix
runs-on: ubuntu-latest
container: ghcr.io/commaai/openpilot-base:latest
strategy:
fail-fast: false
matrix:
artifact: ${{ fromJson(needs.generate-matrix.outputs.models) }}
steps:
- uses: actions/checkout@v4
with:
submodules: true
- name: Download Model Chunks in Parallel
run: |
mkdir -p /tmp/model_chunks
echo '${{ toJson(matrix.artifact.chunks) }}' > chunks.json
BASE_URL="${{ matrix.artifact.download_uri.url }}"
export BASE_DIR=$(dirname "$BASE_URL")
python3 -c '
import json, os
with open("chunks.json") as f:
chunks = json.load(f)
manifest_path = f"/tmp/model_chunks/${{ matrix.artifact.file_name }}.chunkmanifest"
with open(manifest_path, "w") as f:
f.write(str(len(chunks)))
base_dir = os.environ["BASE_DIR"]
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")
'
curl -Z --parallel-immediate --parallel-max 16 -s -S -f -L -K /tmp/curl_config.txt
- name: Run Model Compatibility Test
env:
MODEL_BASE_NAME: ${{ matrix.artifact.file_name }}
MODEL_CHUNK_DIR: "/tmp/model_chunks"
PYTHONPATH: ".:./tinygrad_repo"
run: |
python3 -m pytest openpilot/sunnypilot/modeld_v2/tests/test_models.py
+1
View File
@@ -288,6 +288,7 @@ if arch == "comma_arm64":
SConscript([
'openpilot/selfdrive/pandad/SConscript',
'openpilot/selfdrive/controls/lib/longitudinal_mpc_lib/SConscript',
'openpilot/selfdrive/controls/lib/ford_joint/SConscript',
'openpilot/selfdrive/locationd/SConscript',
'openpilot/selfdrive/modeld/SConscript',
'openpilot/selfdrive/ui/SConscript',
+69
View File
@@ -0,0 +1,69 @@
# 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.
@@ -0,0 +1,621 @@
{
"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
@@ -0,0 +1,109 @@
# 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
@@ -0,0 +1,856 @@
{
"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
@@ -0,0 +1,85 @@
# 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
17–31 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
@@ -0,0 +1,415 @@
{
"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
@@ -0,0 +1,102 @@
# 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
@@ -0,0 +1,177 @@
{
"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
@@ -0,0 +1,129 @@
# 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
@@ -0,0 +1,258 @@
{
"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
@@ -0,0 +1,102 @@
# 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: 112–117, 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
@@ -0,0 +1,82 @@
# 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 112–117, 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
@@ -0,0 +1,332 @@
{
"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
@@ -0,0 +1,131 @@
# 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
@@ -0,0 +1,307 @@
{
"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
@@ -0,0 +1,98 @@
# 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: 112–117, 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 |
| 10–45 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
@@ -0,0 +1,707 @@
{
"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
@@ -0,0 +1,151 @@
# 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.0025–0.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
@@ -0,0 +1,373 @@
{
"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
@@ -0,0 +1,128 @@
# 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.
@@ -0,0 +1,214 @@
{
"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
@@ -0,0 +1,99 @@
# 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 112–117, 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% |
| 5–30° | 0.0965 m | 0.0608 m | 37.0% |
| 30–90° | 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
@@ -0,0 +1,890 @@
{
"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
@@ -0,0 +1,95 @@
# 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
@@ -0,0 +1,70 @@
# 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
@@ -0,0 +1,125 @@
{
"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
@@ -0,0 +1,154 @@
{
"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
@@ -0,0 +1,98 @@
# 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.
@@ -0,0 +1,391 @@
{
"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
@@ -0,0 +1,75 @@
# 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.
@@ -0,0 +1,308 @@
{
"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
@@ -0,0 +1,87 @@
# 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.
@@ -0,0 +1,212 @@
{
"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
}
}
+56
View File
@@ -0,0 +1,56 @@
# Route 18d: memory growth and health-message stalls
Route: `84865544361f55cb/0000018d--edff60065e`, running clean
`07affcf9340623d759f31915477b5176d21c0b14`. Examined 37 uploaded qlogs.
## Observed faults
- `card` RSS rose from 68.7 MiB to 871.2 MiB over about 40 minutes.
PSS also grew substantially, identifying retained process memory rather than
just shared mappings. `controlsd` remained comparatively stable.
- Communication-error diagnostics identified `deviceState` as below frequency.
A 4.005-second `gpu1-usr` temperature read coincided with a 4.436-second gap
between health messages. A separate SOM power read took 1.577 seconds.
## Memory fix
The NumPy `ndpointer` argument conversion reaches `ctypes.cast`, which creates
cyclic pointer references. `card` disables cyclic garbage collection through
`config_realtime_process`. Consequently, every native encoder call retained
small allocations indefinitely.
Keep NumPy's dtype/contiguity validation, but construct an acyclic pointer that
owns the input array until the synchronous native call finishes. No forced GC,
controller equations, gains, candidate search, bounds, or packet changes.
The regression test reproduced 3,000 retained pointers in 500 ordinary encoder
updates and 5,500 with preview. Both now retain zero. Before the fix, an
encoder-only run grew by 51.0 MB in 12,000 updates. The fixed full controller
stayed near 72.3 MB for 300,000 updates with GC disabled (50 minutes of updates
at 100 Hz, executed faster than real time).
## Communication fault: separate work
The temperature and power reads above are in shared upstream `hardwared`, not
in the Ford controller. A tested background-reader candidate is preserved on
local branch `codex/ford-health-18d-sensor-candidate`. It is deliberately
excluded from this controller fix because it changes shared sensor sampling,
freshness handling and thermal behavior and needs its own review.
This fix does not claim to eliminate the observed health-message stalls.
Reducing memory pressure may help general resource pressure, but is not proof
that the blocking sensor I/O will stop.
## Validation
- 156 focused Ford tests passed, including native argument lifetime, preview,
turn-entry assistance and the controller/transmit path.
- Nine frozen routes, 556,776 input samples: before/after C0/C1 and observer
results match exactly, with entry assist both enabled and disabled.
- Ruff and whitespace checks passed.
Frozen replays establish unchanged commands, not a new claim about vehicle
tracking. The memory reproduction was local; on-device memory still needs
confirmation after updating. Other logged processes also used memory, so this
identifies and fixes the dominant confirmed leak rather than proving the whole
system cannot grow.
+51
View File
@@ -0,0 +1,51 @@
# Coordinated controller: residual wheel-angle correction
Route 17a contains sustained small wheel-angle errors without PSCM limitReached. At 230–250 seconds the request is about -1.55 degrees while the wheel remains near zero. Other steady bends miss in either direction. The nominal inverse/encoder already closely meets its own internal curvature target, so precise encoding alone does not eliminate the observed residual. Actual PSCM calibration, load and unobserved state remain possible sources; a fixed directional offset would not fit the observations.
The coordinated controller now adds a bounded integral trim to the angle passed into its inverse. The model/action target remains the reference and remains unchanged in carControl. Both C0 and C1 are still allocated together. This is steering-angle feedback, not a lateral-position measurement, and cannot correct a model target that places the truck on a line.
## Tuning and lifecycle
- Integral gain: **0.2 / second**. This is new tuning, not a recovered firmware constant and not gainless control.
- Trim limit: **±2 steering-wheel degrees**. Clipping the integration error to ±2 degrees limits normal accumulation to **0.4 degrees/second**.
- Learn only with error ≤5 degrees, wheel speed ≤5 degrees/second, and vehicle speed ≥2 m/s. These trial thresholds focus adaptation on small, slow tracking instead of turn-entry lag.
- Ford does not populate `steeringRateDeg`. Wheel speed is derived from successive measured angles with a 0.1-second filter to suppress quantization. This filter guards trim learning; it does not filter the normal angle request.
- Freeze growth at the inverse acceleration limit, either command field bound, or fresh PSCM limitReached. Opposing error may reduce trim toward zero.
- A large opposing error (>5 degrees) or a fast-changing requested angle (>5 degrees/second) clears opposing trim **before** allocation. Requested-angle rate uses the same 0.1-second derivative filtering, with its history reset on inactivity. The angle request itself is unchanged. Fast wheel motion alone freezes learning and permits gradual relief, rather than discarding the learned correction. Resetting can still remove up to 2 degrees immediately; the accumulation-rate limit does not apply to this reset.
- steeringPressed freezes trim without cutting the base request. Actual disengagement, invalid inputs, faults, or fresh PSCM override/denial reset it with the normal inactive request.
- No new proportional correction. No persistent learning across engagements. Diagnostics include requested angle, trimmed inverse input, applied trim, measured-wheel and requested-angle rates, and whether learning is permitted.
The 2-degree bound covers the roughly 1–1.6-degree steady residuals motivating this experiment. The slow gain and small-error/motion conditions deliberately limit adaptation during transients. They are an initial experimental calibration, not universal Ford tuning or a proof of stability.
The existing default-off Coordinated C0/C1 gate remains the selector. Upstream fallback, v23, geometry and joystick selections are unchanged. Existing field bounds, acceleration allowance, mode, packet cadence, driver override and fault handling remain in force.
## Validation and limitations
The new regression drives the production adapter and Ford CAN packer with a persistent ±1.5-degree error. It fails on baseline `6aa35bc1f` because the command never builds residual correction, and passes after this change. A synthetic first-order wheel response with a known additive mismatch also tests convergence through the packed-command observer and nominal angle stage. That deliberately simple plant is a software regression, not an identified PSCM model or evidence of onroad smoothness.
538 focused controller tests pass. They cover selection and upstream fallback, corrected yaw, message-reader integration, actual packet observation, command bounds, persistent errors of both signs, no learning from large lag/fast motion/crawl, trim bounds, driver touch, override/disengagement reset, acceleration/PSCM anti-windup, and prompt removal of opposing trim during reversal/unwind. Ruff and whitespace checks pass.
Frozen replays compare this change with `6aa35bc1f` on routes 172, 175, 177 and 17a using their logged calibrated carControl yaw. Additional archived 166/16a wobble/turn sensitivity runs use retained raw CAN yaw in **both** variants: their original calibrated vectors were unavailable locally, so these are not exact replays of the current yaw-corrected controller. No production raw-yaw fallback is introduced. Missing history starts an independent replay interval; it is not bridged or represented as a vehicle restart. PSCM timestamps are converted from extracted seconds back into nanoseconds before freshness checks.
The four calibrated-yaw routes cover 206,019 adapter/packer updates per variant; the two sensitivity routes bring the total to 372,927 per variant. Activation matched baseline throughout, with no latched prediction faults, nonzero C2/C3, field-bound violations, or added acceleration-limit clipping. All fresh override/denial samples remained inactive. Across ten archived wobble windows the largest increase in fitted nominal internal-angle oscillation was 0.021 degrees peak-to-peak; this is a command-model proxy, not measured new wheel motion. Across 141 nominal 25/50/100-degree crossing comparisons, the largest later entry was 11.4 ms and later exit 19.8 ms. Changed angle levels can move threshold crossings; these are not measurements of a new plant delay. Candidate pipeline p95 on the development Mac was 0.125–0.140 ms across the four full routes, about 0.001–0.002 ms above their paired baselines. Comma timing is unmeasured.
The first candidate was rejected for retaining correction through reversal. A subsequent replay exposed that the Ford steering-rate field was unpopulated; the final code and regression derive rate from actual angle samples. All metrics above use that final implementation.
Artifacts and runnable local scripts are under `.cache/ford_joint_trim`. Measurements and model outputs stay frozen. A trim reaching its bound on that tape does not demonstrate that the truck would need the full trim: the recorded wheel cannot respond to the changed commands. Command/proxy comparisons can reject obvious regressions; they do not predict a new lane position, steering feel, or closed-loop stability. The next drive must establish whether residual wheel error and lane placement improve without reintroducing wobble or hanging exits.
## Route 17c: distinguish wheel motion from a requested release
Build `bd9c0a8b4` improved the measured small-request precision on route 17c, but at 1461.084 and 1469.555 seconds it discarded approximately 0.98 and 0.87 degrees of trim during brief wheel movements at 53–59 mph. The requested angles remained near -1.3 and +1.6 degrees. Both events had continuous mode-2 commands, no detected driver torque, and no PSCM limit or override. The previous reset condition reused the learning gate: fast **wheel** motion plus an opposing small error therefore erased a learned bias without a fast **request**.
The release condition now uses requested-angle motion instead. The gain, bound, learning conditions, inverse/encoder, and upstream target are unchanged. The two-sign production-pipeline regression fails on `bd9c0a8b4` with applied trim zero instead of ±0.985 degrees, then passes with this change. Reversal tests retain immediate release for a fast request, including before the wheel moves. Inactivity clears the new request-rate history.
Validation of this refinement:
- **707 focused tests and 25 subtests pass**, including the Ford packer, gates/fallbacks, trim regression, unwind, override and bounds. Ruff and whitespace checks pass.
- Frozen baseline/candidate comparisons cover **527,215 updates per variant** on seven routes. Routes 172, 175, 177, 17a and 17c supply 360,307 updates using recorded calibrated yaw; 166/16a remain the explicitly labelled archived raw-yaw sensitivity checks described above.
- Both highway trim losses disappear in the replay. Candidate trim can subsequently reach its unchanged 2-degree bound because the recorded wheel cannot respond; this is not an onroad trim requirement.
- Route 17c's two large roundabout-entry windows have identical packed C0/C1 commands. Its bookmarked release has unchanged 25/50/100-degree nominal exit crossings. This preserves the replayed behavior; it does not speed up the actual roundabout response.
- Across 150 nominal-angle crossing comparisons requiring 50 ms continuously beyond the threshold, the largest later entry is 11.2 ms and later exit 8.7 ms. A naive first-sample crossing flagged 119 ms on route 177 because baseline briefly reached 100.0005 degrees while the candidate reached 99.9999; that approximately 0.0006-degree difference is not a new physical delay.
- Across ten wobble windows, the largest increase in the fitted nominal-angle oscillation is **0.0157 degrees peak-to-peak**. It is small but not zero, and is a command-model proxy rather than measured wheel motion. Maximum consecutive-active C0/C1 packet steps do not increase; activation matches, no acceleration clipping is added, and all bounds and fresh override/denial checks pass.
Artifacts and runnable comparisons are under `.cache/ford_trim_release`. Offline results support a focused trim-release experiment, not a claim that centering or wobble is fixed on the truck.
+134
View File
@@ -0,0 +1,134 @@
# Coordinated C0/C1 steering trial (v24)
This default-off trial turns the normal action-derived desired steering angle into a jointly selected C0/C1 pair. It estimates the held fields and curvature filter of an older Ford PSCM and chooses one command whose predicted next-update error is no worse than a C1-anchored alternative. Among those candidates it scores a short action-trend forecast followed by the complete return to its steady pair, including the filter tail. It does not retain a future command plan or classify turns into maneuver states.
The later [residual wheel-angle trim](ford_joint_centering_trim.md) adds a small, bounded integral correction around this nominal encoder. The original validation below describes the earlier versions; the trim has separate validation and is not gainless.
## 0.10-second action-trend preview
The coordinated trial now extrapolates the existing filtered requested-angle rate for 0.10 seconds, then assumes that target holds. This changes allocation planning; the action-derived steering target itself is unchanged. Each forecast point uses the current inverse conversion, acceleration allowance and bounded/quantized C0/C1 mapping. Speed and measurement-driven states remain fixed within the forecast. The current-target immediate-error inequality is retained. The 0.10-second duration is a tuning parameter, not a firmware constant or additional model steerDelay.
The preview is included under the existing default-off coordinated-controller toggle; there is no new setting. Master-off still selects upstream Ford. Diagnostics add `target_preview_seconds`, `target_preview_delta` (unclipped extrapolated angle change, degrees), and `target_preview_limited` (forecast curvature clipped by the acceleration allowance). Disengagement resets the requested-rate history as before.
Frozen replays compared 0, 0.05, 0.10 and 0.15 seconds across routes 17c, 172, 177, 166 and 16a: 438,519 updates per variant through the real adapter and CAN packer. Under the nominal older-firmware model, the selected 0.10-second candidate changes internal-target MAE from 53.53 to 50.45 degrees and 33.00 to 30.36 degrees on two roundabout entries, and 8.00 to 4.94 degrees on the good unwind. Estimated release threshold crossings occur 19–152 ms earlier. These are internal target estimates with recorded vehicle motion frozen, not new wheel trajectories.
This candidate did **not** pass every offline reject check. In an older raw-yaw sensitivity rocking window, periodic tracking-error amplitude increases from 4.94 to 5.90 degrees peak-to-peak; a calibrated-yaw route-172 bookmark increases from 0.694 to 0.815 degrees. Whole-route averages improve, but local oscillation and response-rate sensitivity regressions remain. The user explicitly selected implementation and push of the 0.10-second candidate after reviewing this tradeoff. No road improvement or closed-loop stability is established. The report is `pscm-trend-preview.html`; local research artifacts are under `.cache/ford_trend_preview`.
Numerical checks compare the native score with an independent 1,600-tick forward calculation, preserve exact zero-preview/zero-trend behavior, and exercise timing jitter, duplicate timestamps, abrupt reversals, inactivity, touches, and PSCM override/denial through the real sender. Production exactly reproduces all 438,519 command/observer updates of the tested 0.10-second replay tapes. All 611 focused controller/settings tests pass, as do Ruff, diff checks and the native library's real SConscript build. No comma-device timing is claimed.
### Route 182 communication warnings
The next drive ran `2fe855920` with preview enabled. Its first six quick-log segments contain communication warnings identifying `deviceState`, followed by `managerState`. Device-state publication gaps are 5.23, 8.76 and 6.35 seconds. The unrelated absent `alertDebug` and `lateralManeuverPlan` topics listed in diagnostic dictionaries are ignored by the health checks. The warnings do not identify a PSCM communication failure. Quick logs alone do not identify the blocking health-loop operation; full segments were requested. The same logs show card/controlsd/selfdrived approaching one CPU core in several sampled intervals. These are distinct observations, not proof of a single cause.
Preview allocation no longer computes the entire ordinary C0/C1 search before computing its forecast search. It computes only the ordinary C1-anchored accuracy bound needed by the forecast. The skipped ordinary output already belonged to the original candidate grid, so removing it does not remove a candidate. All 3,600 deterministic before/after comparisons match commands, score, state, grid count and immediate-error bound exactly. All 438,519 updates of the five-route replay still match the released preview candidate. Local selection p95 falls from 0.273 to 0.180 ms, with 1.18× aggregate speedup; comma-device scheduling improvement remains unmeasured.
Existing hardware-loop stage records now enter quick logs when a stage exceeds one second; 100 ms–one-second timings remain in full logs. This changes logging severity only, not watchdog thresholds, UI alerts, process scheduling or hardware operations. It enables diagnosis without requiring full logs for every subsequent warning. The optimization and instrumentation are not a claim that the observed health-process stalls are fixed. Validation: 612 controller/settings tests and six hardware timing/alert tests pass.
## Select and revert
In sunnylink → Vehicle → Ford Settings, while offroad:
- **Selected-Action Path Tracking (Experimental): ON**
- **Model Geometry Reference: OFF**
- **Coordinated C0/C1 Steering (Experimental): ON**
Apply with an offroad-to-onroad cycle. The new `FordPscmJointControl` parameter defaults to false. Turning it off restores v23 selected-action control. Turning the master Selected-Action setting off restores upstream Ford control, regardless of this stored setting. Geometry mode and Joystick Debug Mode keep their existing paths. C0 distance selection does not affect this trial.
The integration accepts Ford CAN FD platforms, following the existing master gate. Compatibility with each PSCM calibration is not established.
## Live integration
`controlsd` retains the normal action source, curvature limits, vehicle-model conversion, input freshness checks and steering-angle target. With the trial selected, its existing Ford adapter is a validity carrier with zero P/I gains; it supplies no additional path correction. `card` performs the joint allocation immediately before the existing Ford CAN packer. It negates the encoder's CAN-coordinate output into the OP path interface; the packer negates it back on transmission.
C2/C3 remain zero. Existing field bounds, mode 2 and immediate ramp remain unchanged. The observer advances at an estimated 8 ms firmware cadence using the decoded packets actually queued to panda. Queueing is not a PSCM acknowledgment. It retains held/filter estimates while disengaged or overridden and applies the recovered inactive slew to mode-0 packets. A fresh limitReached status does not freeze the target; the PSCM's explicit override or denial inhibits this trial's lateral command. Like upstream Ford, steeringPressed alone does not zero the path. Longitudinal controls are untouched.
A command-history gap over 100 ms, nonmonotonic clock or invalid prediction latches the trial inactive until the next onroad process start. The normal steering-fault alert reports the latched fault. There is no mid-turn switch to another controller. Diagnostics use `Ford joint path tracking` / `ford-joint-v24` and distinguish requested angle, reachable angle, estimated held state, proposed pair and packed pair.
## What the estimate does and does not know
Packaged numerical calibration comes from ML3V-14D003-BD / ML34-14D007-EDL. No executable firmware or private firmware image is shipped. Equivalence to the Lightning's RL38 firmware remains unverified. Internal integral is assumed initially zero and frozen; interaction is nominally 1.0; acceleration uses measured yaw × speed (no measured bank input); vehicle geometry comes from CarParams. These are explicit assumptions, not measured PSCM RAM. The model's 3 m/s² acceleration allowance is retained. It estimates an internal angle target before subsequent PSCM filtering/torque stages, not the future wheel angle.
## Validation
The production Python/C++ port reproduces every C0/C1 sample of the audited prototype in seven frozen windows from routes 166 and 16a. Held fields and filtered curvature also match; the largest angle-stage numerical difference is below 6e-14 degrees.
A separate replay executes the actual 100 Hz adapter and Ford CAN packer with the reconstructed response ticking at 125 Hz. Mean internal-target error, degrees:
| Window | Audited prototype | Actual 100 Hz adapter |
|---|---:|---:|
| 16a wobble 1 | 0.520 | 0.521 |
| 16a wobble 2 | 0.240 | 0.248 |
| 16a exit | 3.509 | 3.503 |
| 16a turn | 9.762 | 9.981 |
| 166 wobble 1 | 3.491 | 3.545 |
| 166 wobble 2 | 0.530 | 0.587 |
| 166 good left | 4.124 | 4.222 |
These are frozen measurements and nominal active-mode reconstruction, not observed improvements on the truck. The actual adapter made 12,031 packed command updates without a latched fault. The slowest per-window 99th-percentile cycle was 0.95 ms on the development Mac; comma hardware timing is unmeasured. Firmware-rate/calibration uncertainty remains material.
The focused suite exercises default-off selection, rollback, zero second PI, CAN signs/bounds/cadence, inactive state retention, interventions, limitReached, stale/nonfinite inputs, timing faults, real card hooks, parameter/schema registration, and inverse/forward numerical agreement.
The first enabled on-device trial exposed a message-type crash: the adapter returned a Cap'n Proto builder, while Ford's sender expects a reader and copies its actuator fields with `as_builder()`. The adapter now returns a reader. Regression coverage passes its output unchanged through `CarInterfaceBase.apply` and the production Ford sender, including the actual card hook with active, inactive and driver-override inputs. The earlier harness converted the output to a reader itself and hid this integration error. The regression also checks that the incoming message is not mutated and all fields other than the selected lateral-active state are preserved.
Run:
```sh
python -m pytest openpilot/selfdrive/car/tests/test_ford_joint_control.py \
openpilot/selfdrive/car/tests/test_ford_pscm_status.py \
openpilot/selfdrive/controls/tests/test_ford_model_action*.py \
openpilot/sunnypilot/sunnylink/tests/test_settings_schema.py
```
Build the native kernel through SCons. On the development checkout, its real SConscript and native parameter library were compiled successfully. A complete root build could not run because the checkout lacks the msgq/rednose SCons tool submodules. No on-device build or road validation is claimed.
The trial's question is whether coordinating both channels preserves entry while reducing unnecessary correction during release. Offline results justify the opt-in comparison; they cannot establish smoothness or closed-loop stability.
## Runtime optimization after route 174
Route 174 ran v24 on `528ed3615` and recorded System Lagging during turns. Full rlogs show card using about 63% of one CPU core during the first alert; card, controlsd and selfdrived share that core and priority, and their combined measured load was about 101%. This supports CPU contention, not a PSCM limit, as the explanation for this alert. Comma timing must still be checked after the optimization.
The search now rejects candidates that fail its existing immediate-error constraint before evaluating their full return. Within each selection it also reuses return costs for identical predicted states and prefix costs. No cache survives the selection, and the candidate set, full-return policy, scoring, tie breaks and command cadence are unchanged.
Compared with the original native library built at the same optimization level, all nine outputs matched exactly in 600 deterministic stress cases and 720 comparisons using the 360 active logged states from route 174 at both firmware tick counts. On the development Mac, the recorded-state benchmark's 95th-percentile selection time fell from 0.317 ms to 0.090 ms; aggregate speedup was 1.47×. The broader stress benchmark improved 5.41× in aggregate. These are local encoder timings, not measured post-fix comma CPU utilization or a guarantee of system scheduling latency. Frozen pre-optimization commands and costs are also covered by the regression suite.
## Route 175 follow-up
Route `00000175--9529c33e36` ran `99b3fb03e` with v24 enabled. All ten full rlogs contain no System Lagging alert. Remaining communication warnings identify deviceState and, sometimes, managerState. DeviceState publication timestamps contain gaps up to 8.06 seconds; manager polls that service and falls back to its one-second timeout. The recorded CPU samples still show card/controlsd/selfdrived close to one core in sustained sections, so further optimization is useful independently of the health-process stalls.
Two wheel pauses near 100 and 115 degrees follow short steeringPressed detections. At about 175.35 and 176.94 seconds, the existing override handling transmits mode 0 / zero C0/C1 for approximately 87 and 117 ms, then rebuilds the request. Those pauses do not show limitReached. None of the 1,710 active v24 diagnostic samples reports the inverse acceleration allowance clipping the target. The user reported small left corrections against a rightward tendency, so these detections cannot be classified as false driver input. Override behavior is unchanged.
The bookmark marks a moderately good but slightly wide left turn. It contains brief limitReached states with continued mode-2 commands, rather than a frozen command. Wheel-tracking error remains; this route is not evidence of closed-loop equivalence to another steering platform. An 8.71-second carState recording gap around 290.55–299.26 seconds, with other card streams continuing and buffered diagnostics arriving later, is excluded from tracking statistics.
The additional encoder optimization reuses the already-computed C0/C1 calibration gains, constructs the same sorted candidate grid with fewer NumPy temporaries, and decodes fixed calibration entries once. Relative to `99b3fb03e`, all nine search outputs matched exactly in 600 deterministic stress cases and 3,420 comparisons using active route-175 states at both tick counts. Another 20,000 grid-boundary comparisons matched. A 6,000-cycle adapter/CAN/observer exercise, including stop, override, reengagement, steps and reversals, produced exactly the same packed commands and observer states.
On the development Mac, recorded-state selection p95 decreased from 0.174 to 0.102 ms (1.63× aggregate speedup). Full pipeline p95 decreased from 0.199 to 0.162 ms (1.24× aggregate speedup). These local measurements do not establish comma scheduling latency, and there are scheduler outliers in both runs. No target, gain, bound, override rule, or steering command was intentionally changed.
The hardwared patch avoids redundant synchronous Params removal for hidden alerts whose unused extra text changes, and routes the Tici-support alert through the same change-only helper. Initial cleanup, visible text updates, and clearing remain synchronous; failed writes are retried. An unchanged hidden-temperature alert reproduces the unnecessary blocking operation on the previous implementation. The logs do not identify which blocking call caused every on-device stall. Slow-stage timing now records operations exceeding 100 ms to distinguish thermal/system reads, Chestnut status, startup/engagement parameters, power reads, publication, and persistence. Watchdog thresholds and health checks remain unchanged.
Validation: 524 focused controller/hardware tests passed; Ruff and `git diff --check` passed. Diagnostic artifacts, route data, and benchmarks are local under `.cache/ford_v24_followup`; the visual report is `route-175.html` in the existing report directory. No truck validation of this follow-up optimization is claimed.
## Route 175 yaw bias and touch response
The previous inverse consumed raw Ford CAN yaw. In clean near-steady small-request samples, it differs from sign-normalized calibrated `carControl.angularVelocity[2]` by a median -0.00767 rad/s. Through the recovered steady angle equation, the difference corresponds to about -3.08 degrees of steering; the observed median actual-minus-requested error was -3.36 degrees. Four continuous steady windows show observed errors of -2.09, -2.39, -3.17 and -2.95 degrees, compared with nominal yaw-source effects of -1.86, -1.90, -2.97 and -3.45 degrees. This supports an input-bias explanation; it does not establish which internal yaw signal the actual PSCM uses or prove the onroad correction.
The coordinated controller now uses negative calibrated car-frame yaw Z, matching CAN/pinion coordinates. This is the existing pose published by controlsd, without a fitted trim or additional feedback gain. The v24 validity carrier requires calibrated, valid, healthy and fresh deviceMotion; missing or invalid yaw disables the request rather than falling back to the biased raw signal. Diagnostics record both yaw inputs. Other controller selections retain their existing input behavior.
The extra v24 steeringPressed gate is removed. A light touch no longer directly forces zero C0/C1 and mode 0, and the observer retains its held state. Actual disengagement, steering faults, freshness failures, and a fresh PSCM override (limit 3) or denial still inhibit the command. This changes the software request continuity, not the PSCM's internal torque blending or safety permissions.
Regression coverage includes biased raw yaw with unchanged calibrated motion, yaw sign, missing/nonfinite/out-of-range yaw, touch continuity, retained PSCM override/denial, and execution of controlsd's motion-health gate with v24 enabled and disabled. The original eight bias/touch cases failed before the fix. All 524 focused controller tests now pass. Frozen replay compares yaw-only, touch-only, combined and baseline commands through the actual adapter and CAN packer; measurements remain recorded, so it cannot demonstrate the resulting wheel motion or smoothness. Artifacts are under `.cache/ford_v25_bias`.
All four variants completed 52,526 recorded updates each without latched prediction faults, nonzero C2/C3 or field-bound violations. The missing carState interval is treated as an independent replay boundary, not bridged or claimed as a vehicle restart. All recorded active samples had the required calibrated yaw and recorded motion-health fields. Yaw-only preserved the baseline active count; the combined change added 5,988 active updates, all during steeringPressed with lateral control otherwise permitted. Fresh PSCM override/denial remained inactive. In the 174–178 second wheel-pause window, the extra 21 mode-0 updates disappeared. No new acceleration-allowance clipping occurred. The four steady windows shifted the requested curvature leftward, consistent with correcting the measured right bias. Closed-loop bias and touch feel still require truck validation.
Replay-audit correction: the original four-variant `.cache/ford_v25_bias/replay.py` passed an extracted PSCM timestamp in seconds where the adapter expects nanoseconds. Its fresh-status branch therefore did not substantiate the override/denial claim above. The production freshness code and explicit-status unit tests were unaffected. The later [centering-trim validation](ford_joint_centering_trim.md) converts timestamps correctly and asserts inhibition on every fresh recorded override/denial sample.
## Route 183 health-persistence fix
Route `00000183--892e38ca21` ran clean `ee3d0d33a` with the joint controller enabled. Quick logs identify deviceState communication failures alongside hardware-loop section durations of 3.604 seconds in engagement handling, 7.656 seconds in thermal reads, and 5.463 seconds in statistics/persistence. These section timings do not identify individual blocking syscalls. Controls CPU contention and other-core I/O wait are also present; the device did not report overheating.
Routine `IsEngaged`, uptime, and last-offroad-status writes now use the existing ordered asynchronous Params writer. Network-metered and runner-voltage flags are written at startup and on changes, eliminating four redundant writes per second during steady operation (each persistence operation includes file and directory fsync). Shutdown and ignition-cycle acknowledgements retain their synchronous writes. No controller command, preview duration, safety gate, thermal limit, or communication threshold changes.
Regression tests run the production hardware loop with delayed persistence. Before the change, a 5.5-second injected delay produces 6–11.5-second publication gaps. The fixed loop retains its 0.5-second cadence. A separate native integration test holds the actual Params file lock: the old loop cannot finish while the lock is held; the fixed loop publishes all 125 test messages and persists the final values in order after unlock. Flag transitions and overtemperature inhibition/fan behavior remain covered.
The thermal-read stall is not explained by this reproduction. Individual sensor reads taking over one second now report their sensor name, zone, and elapsed time in quick logs, preserving the original values and failure behavior. No stale-temperature cache or relaxed watchdog is introduced. Truck validation is still needed; this patch fixes demonstrated blocking-persistence behavior, not a proven diagnosis of every recorded stall.
Validation: 200 hardware, Params, thermal, and Ford tests passed; Ruff and diff checks passed. The broader suite initially hung after its success summary in an existing shared StatLog ZeroMQ-context destructor. A local-only pytest cleanup plugin closed both statistics sockets before their shared context; the same 200 tests then exited successfully. Production statistics code is unchanged. Reproduction, profiling, and validation artifacts are under `.cache/ford_health_fix`.
+39
View File
@@ -0,0 +1,39 @@
# Ford coordinated allocation trial
The existing `FordModelActionController` + `FordPscmJointControl` gate now uses
the recovered normal C0/C1 slew rates to choose its steady endpoint split:
```
t = desired_internal_curvature / (g0 * c0_rate + g1 * c1_rate)
c0 = t * c0_rate
c1 = t * c1_rate
```
When a field clips, allocate its missing contribution to the other field's
remaining capacity. The existing dynamic selector still handles held state,
heading-dependent filtering, target preview and release. No new gain, control
mode, search horizon or native-library ABI is added. DBC bounds and C2=C3=0 remain.
The normal upstream controller is unchanged with the existing master toggle off.
This is the exact endpoint policy tested against ML3V-14D003-BD /
ML34-14D007-EDL instructions. At 18 mph, the approximately 204-degree internal
target settles within 2.5 degrees in 2.864 rather than 3.384 seconds. The first
90% of its rise is unchanged. The approximately 25-degree release takes .624
rather than .456 seconds to settle: this is a known trial tradeoff, not a proven
improvement in all driving. These are internal target results, not wheel-motion
or closed-loop smoothness predictions. Actual steering feedback must assess both
large turns and ordinary correction/release.
For stopping, controlsd and card share the same normalization: a filtered speed
between -0.3 and 0 m/s counts as zero only when raw speed is in [0, 0.3) m/s and
reverse is not selected. This prevents small speed-filter undershoot from
invalidating and clearing the last active packet. Larger/inconsistent negative
speeds, reverse, stale inputs, faults, override and disengagement still release.
No global CarState speed or PSCM speed message is altered. Retaining the request
does not bypass the firmware's separate near-zero-speed output gate.
Validation: 926 Ford tests and two subtests passed; lint and diff checks passed.
The shipped encoder reproduces all 9,000 saved candidate command samples from
the native-verified step/release fixture (maximum difference 8.89e-16). Native
instruction validation and the underlying fixtures are recorded in the local
PSCM authority report. Diagnostics include `allocation: equal-arrival`.
+38
View File
@@ -0,0 +1,38 @@
# Ford joint endpoint: hold C0 at the supervisor saturation
The equal-arrival endpoint (`C0 = 1.5 t`, `C1 = 0.1 t`) holds 2-4 m of C0 in large
turns. In the reference ML3V-14D003-BD / ML34-14D007-EDL calibration the path
supervisor centre is
```
S(v) * (clip(0.5 * held_C0, +-0.5) + clip(10 * held_C1, +-0.349609))
```
with width `0.399994 * S + 5` below 40 km/h. Its C0 term saturates at 1.0 m, so
held C0 above that adds primary curvature but no turn-direction allowance. On
release, zero demand stays excluded until held C0 slews (1.5 m/s) below about
0.41 m while C1 is saturated, so extra C0 adds (C0 - 0.41) / 1.5 s of forced
old-direction output.
`static_pair` therefore caps its equal-arrival C0 at 1.0 m and gives the rest to
C1; the remainder returns to C0 only when C1 reaches its bound. Endpoints with
equal-arrival C0 of at most 1.0 m are unchanged. The paired selector, dynamics,
quantization, preview, gates, stop hold and DBC bounds are unchanged; it still
builds with both fields, then trades C0 for C1 at constant curvature.
Offline evidence (reference firmware, not the live RL38 calibration; no wheel,
assist or vehicle model):
- Native zero-demand supervisor probe, controlled 15/20/25 km/h large steps:
zero excluded after release 1.48/1.88/2.17 s -> 0.39/0.39/0.48 s. The first
50%/90% of the rise is unchanged; the last 1% can settle up to 0.5 s later.
- Native probe on replayed routes 194/195/196 packets: forced old-direction
output in the 3 s after large releases 24.9 -> 6.4, 19.6 -> 8.4,
15.0 -> 3.6, 16.6 -> 8.3, 11.0 -> 4.4 unit*s (one driver-involved release
9.6 -> 9.2).
- 81,340 replay updates per policy: with no |target| >= 30 deg within 10 s,
packets are identical on 194/196 and 98.9% identical on 195.
These are firmware-stage predictions. A physical A/B must show whether the wheel
releases large turns earlier; diagnostics report `allocation:
equal-arrival-supervisor-c0`.
+155
View File
@@ -0,0 +1,155 @@
# 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.45–6.94 m of path at 2.78–3.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.3–55 m/s), yaw sanity bound (±3 rad/s), selected curvature
sanity bound (±1/m), and control interval (2–100 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
@@ -0,0 +1,125 @@
# 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.
@@ -0,0 +1,145 @@
{
"date": "2026-09-07",
"baseline_commit": "7ca3c6e3b3e659c6f446039501c5826bbd14092e",
"branch": "codex/ford-model-action-drive-test",
"scope": "Default-off Sunnylink selection and v8 retirement; offline validation only. No device installation or physical performance validation.",
"calibration_approved": false,
"production_selector_changed": true,
"toggle": "FordModelActionController",
"default_enabled": false,
"v8_removed": true,
"panda_safety_changed": false,
"opendbc_submodule_changed": false,
"deployment_opendbc_head": "c21a9013700734dd20b09e05aa68329ad8cc20f9",
"controller_size": {
"total_lines": 145,
"code_lines_excluding_blanks_comments_docstrings": 95,
"core_persistent_values": 2,
"adapter_timestamps": 3,
"removed_v8_module_lines": 469
},
"tests": {
"combined_ford_params_sunnylink_suite": "284 passed, 26 subtests passed in 2.63s",
"suite_log_sha256": "2e223a507f0630481cf6f83b9f8893d226f3f4273a79a09fc35905aa875b1d2c",
"coverage": {
"covered_lines": 87,
"num_statements": 87,
"percent_covered": 100.0,
"percent_covered_display": "100",
"missing_lines": 0,
"excluded_lines": 0,
"percent_statements_covered": 100.0,
"percent_statements_covered_display": "100",
"num_branches": 26,
"num_partial_branches": 0,
"covered_branches": 26,
"missing_branches": 0,
"percent_branches_covered": 100.0,
"percent_branches_covered_display": "100"
},
"ruff": "pass",
"ty_controller_and_lab": "pass",
"settings_compiler_check": "pass",
"standards_review_remaining_findings": 0,
"spec_review_remaining_findings": 0,
"resolved_review_finding": "Updated YAML authoring source and regenerated settings JSON before final compiler/schema suite."
},
"routes": {
"route95": {
"cycles": 54738,
"core_active_cycles": 37614,
"core_exact_archived_match": true,
"cohorts_reproduced": true,
"adapter_active_cycles": 37614,
"adapter_exact_match_with_fresh_engagement_dt": true,
"adapter_validity_differs_from_archive_cycles": 0,
"adapter_command_differs_from_archive_cycles": 59,
"adapter_max_absolute_command_difference_c0_c1": [
0.010000000000000675,
0.0010000000000000009
],
"field_slew_zero_c2_c3_pass": true,
"float32_can_round_trips": 109476,
"opendbc_import_head": "72a775d35e54c21ff5c5798acef22016eedcc0a7",
"report_sha256": "72fab710dc81c8c7d9b75d371b97fec32402d3b01fa05517dfa814d8daff3134"
},
"route90": {
"cycles": 78812,
"core_active_cycles": 73055,
"core_exact_archived_match": true,
"cohorts_reproduced": true,
"adapter_active_cycles": 73055,
"adapter_exact_match_with_fresh_engagement_dt": true,
"adapter_validity_differs_from_archive_cycles": 0,
"adapter_command_differs_from_archive_cycles": 19,
"adapter_max_absolute_command_difference_c0_c1": [
0.020000000000000462,
0.0020000000000000018
],
"field_slew_zero_c2_c3_pass": true,
"float32_can_round_trips": 157624,
"opendbc_import_head": "72a775d35e54c21ff5c5798acef22016eedcc0a7",
"report_sha256": "cc2224bae597a341697a7681560a077cb209d77e4d060c0850997691c57d32fb"
}
},
"stress": {
"seed": 20260907,
"random_cycles": 200000,
"mirrored_core_updates": 200000,
"invalid_or_inactive_resets": 3537,
"field_boundary_cases": 18138,
"float32_can_round_trips": 218138,
"analytic_targets_scalar_slew_and_mirror_checks_pass": true,
"direct_raw_float32_packing_matches_host_output": true,
"max_continuous_step_c0_c1": [
0.40000000000000147,
0.05000000000000002
],
"calibration_approved": false,
"scope": "Numerical construction only; no PSCM response or closed-loop performance claims.",
"opendbc_import_head": "c21a9013700734dd20b09e05aa68329ad8cc20f9"
},
"total_float32_can_round_trips": 485238,
"native_params": {
"source": "Rebuilt locally from this branch with clang++ and generated Capnp headers; ignored test dependency, not committed binary.",
"library_sha256": "270bf43241cf7c02cc432cf78ec9411a62d7653ca445695efe785ae82241aa09",
"sources_sha256": {
"openpilot/common/params_c.cc": "57e3bcc7eba939bc91aadafb4ed1248b8123a8fe5c48fd8530298d966ea4db63",
"openpilot/common/params.cc": "a5adacb1d47cb3bf6e0d87d44ce158b41982d7eaf2e8114e32c48d3a6631304c",
"openpilot/common/params.h": "ed03d137e126ecd6f1608016020af18c0339fb987e27d0a2aa6830bba396970c",
"openpilot/common/params_keys.h": "39d36465f66405843b926ba18473fb6aee81c0f1c7bea87246aa08ffe3f67c58",
"openpilot/common/util.cc": "4479ecf72465e8f453d8af78447f7715f02d9397c58a49048f2bbc87a96d6b8a",
"openpilot/common/swaglog.cc": "9c2f88a2f1c3c4253b73defb264cc367a13ade23e02928e1d469b5c5833df176"
}
},
"test_dependency_notes": {
"python": "/Users/ibpersonal/dev/sunnypilot/.venv/bin/python",
"pyyaml": "6.0.3 from local uv cache",
"jsonschema": "Local cached package appended after venv to run schema validator without skips",
"hardware_build_and_device_boot": "not performed"
},
"source_sha256": {
"docs/ford_model_action_candidate.md": "c968132348d20891a9396f6e69db1315d570505796e19e09ffbb2da748e6e687",
"docs/ford_virtual_angle_experiment.md": "da6322f3c3d2d81463e44c50cc6cad1a962f97008ff9425c314da333ebe47a87",
"openpilot/common/params_c.cc": "57e3bcc7eba939bc91aadafb4ed1248b8123a8fe5c48fd8530298d966ea4db63",
"openpilot/common/params_keys.h": "39d36465f66405843b926ba18473fb6aee81c0f1c7bea87246aa08ffe3f67c58",
"openpilot/common/tests/test_params.py": "557a1f616af5fd9f5e623fbee0fd6f44cb059a30c290c68ce2b57e9bcceed081",
"openpilot/selfdrive/controls/controlsd.py": "102b383e5beff43b8dd7c219178bef62e8a4b54443ebe682694606862bcd4e7f",
"openpilot/selfdrive/controls/lib/ford_model_action.py": "8f3bc5d68e0051776f614a2ccffae84a88f7898dc95bdc12c23dcfe10dfe676a",
"openpilot/selfdrive/controls/tests/test_ford_controlsd_logging.py": "1c9448d88d8021e5d34a5dccd14a17c6c1bc64b5531342bfc6d15574d9d3e710",
"openpilot/selfdrive/controls/tests/test_ford_model_action_adapter.py": "2c5b14f814e84e59d749f61a43ef1dcfe06253f6e185443fa123c37525ac8466",
"openpilot/sunnypilot/sunnylink/settings_ui.json": "9974df3ac4cc58ae78d47848cd18ef4aca1bcbb00edb257f28b4220d92890528",
"openpilot/sunnypilot/sunnylink/settings_ui_src/pages/vehicle.yaml": "7a3fa18e562d5a03a5b85c72ee3f3ebeb836c497285dcd9aaad24f8fb4dd6942",
"openpilot/sunnypilot/sunnylink/tests/test_settings_schema.py": "3db566612381fd87d3655a3ccff470be7da998c4ea7f6365f53e58cdb9c0ffb7",
"tools/ford_pscm_lab/model_action_replay.py": "827a6dc488d554bdf6e87438c6a2a985b3195bf6d01ab09002bfd6049d22a868",
"tools/ford_pscm_lab/stress_model_action.py": "2d5c72cc4b8ae214f2f5a19a050fe138f5c2ab0294d92d24e2481af5f8185613",
"tools/ford_pscm_lab/test_model_action_replay.py": "ebf6bcd9260745100311521f8e11e85b7aebdd5561ab0876bfc2e802429d6896",
"openpilot/selfdrive/controls/tests/test_ford_model_action_selection.py": "f826c6328f0abac2a61f1a0a6f8d119fdbab858e363cb466d84ba9a7783059cd",
"docs/ford_model_action_drive_test.md": "d825b177cd099efd797fe89b7041695d6d41e4b9e8bba6bcaeb32aece164262b"
},
"deployment_target": {
"repository": "sunnypilot/sunnypilot",
"branch": "hiimisaac-dev",
"validated_code_commit": "ea1ed70c718d32539ef6b9a89b89c0e297c92e06"
}
}
+220
View File
@@ -0,0 +1,220 @@
{
"date": "2026-09-07",
"baseline_commit": "c4b3c55c826fca1ce09618e418e95f0a24478d96",
"calibration_approved": false,
"production_selector_changed": false,
"vehicle_settings_changed": false,
"panda_safety_changed": false,
"scope": "Offline command construction, adapter integration, numerical fault probes and Ford regression tests. Physical response remains unvalidated.",
"controller_size": {
"total_lines": 131,
"code_lines_excluding_blanks_comments_docstrings": 86,
"core_persistent_values": 2,
"adapter_timestamps": 3
},
"opendbc_import_head": "72a775d35e54c21ff5c5798acef22016eedcc0a7",
"tests": {
"ford_suite": "264 passed, 150 subtests passed in 14.29s",
"new_core_adapter_tests": 107,
"new_replay_validator_tests": 13,
"controller_coverage": {
"statements": 78,
"missing_statements": 0,
"branches": 24,
"partial_branches": 0,
"percent": 100
},
"ruff": "pass",
"ty_controller_and_lab": "pass"
},
"routes": {
"route95": {
"cycles": 54738,
"core_active_cycles": 37614,
"core_exact_archived_match": true,
"cohorts_reproduced": true,
"adapter_active_cycles": 37614,
"adapter_status_counts": {
"inactive": 17124,
"active": 37614
},
"adapter_exact_match_with_fresh_engagement_dt": true,
"core_active_path_shorter_than_7m_cycles": 44,
"adapter_validity_differs_from_archive_cycles": 0,
"adapter_command_differs_from_archive_cycles": 59,
"adapter_max_absolute_command_difference_c0_c1": [
0.010000000000000675,
0.0010000000000000009
],
"field_slew_zero_c2_c3_pass": true,
"float32_can_round_trips": 109476,
"turn_speed_15_55": {
"seconds": 33.332073582999925,
"core_c0_c1_rms": [
0.07257996778378971,
0.03628926246224237
],
"recorded_v8_c0_c1_rms": [
0.3416081588451539,
0.027322786376822172
],
"adapter_eligible_seconds": 33.332073582999925,
"adapter_c0_c1_rms": [
0.07257996778378971,
0.03628926246224237
]
},
"input_sha256": {
"route.npz": "6e5438867ea618e53ca395b60ff4b9b146256f1bf6f07dc8e92d663286074154",
"encoder_comparison.npz": "23bd05c4b6400299844acaba1d97051c96c23c682bf17b46e89e7f2cca5fce38",
"pose_replay.npz": "9cfbb3c6f0b1fdb7e3d38e6b64b8c94cffbcc9fabe41f6344d84a9b657b6af9b"
},
"report_sha256": "cf4e3804f2ebdb0e50f3c49636bf60a30e3c1180411b582826a115970ab972fc"
},
"route90": {
"cycles": 78812,
"core_active_cycles": 73055,
"core_exact_archived_match": true,
"cohorts_reproduced": true,
"adapter_active_cycles": 73055,
"adapter_status_counts": {
"inactive": 5757,
"active": 73055
},
"adapter_exact_match_with_fresh_engagement_dt": true,
"core_active_path_shorter_than_7m_cycles": 0,
"adapter_validity_differs_from_archive_cycles": 0,
"adapter_command_differs_from_archive_cycles": 19,
"adapter_max_absolute_command_difference_c0_c1": [
0.020000000000000462,
0.0020000000000000018
],
"field_slew_zero_c2_c3_pass": true,
"float32_can_round_trips": 157624,
"turn_speed_15_55": {
"seconds": 47.53485040600012,
"core_c0_c1_rms": [
0.08686835454709245,
0.043216347944464766
],
"recorded_v8_c0_c1_rms": [
0.4471065308273131,
0.030663943784303503
],
"adapter_eligible_seconds": 47.53485040600012,
"adapter_c0_c1_rms": [
0.08686835454709245,
0.043216347944464766
]
},
"input_sha256": {
"route.npz": "51e8c26eedde253e171af47d704c1967ba45ae6825d883393bec1fb9e00251c1",
"encoder_comparison.npz": "7a625d3ed5cbd8013d1028aa3bc421740551dcae5c3d60981208bd047af9794c",
"pose_replay.npz": "4457ccc0868354749da5b72c1dea0faf750f783dfdc87038101288fdcba1e707"
},
"report_sha256": "b95247bdf6bbec16e5dc4781eaf7a678aca787418251cd161f6438e3173ad490"
}
},
"stress": {
"seed": 20260907,
"random_cycles": 200000,
"mirrored_core_updates": 200000,
"invalid_or_inactive_resets": 3537,
"field_boundary_cases": 18138,
"float32_can_round_trips": 218138,
"analytic_targets_scalar_slew_and_mirror_checks_pass": true,
"direct_raw_float32_packing_matches_host_output": true,
"max_continuous_step_c0_c1": [
0.40000000000000147,
0.05000000000000002
],
"calibration_approved": false,
"scope": "Numerical construction only; no PSCM response or closed-loop performance claims.",
"opendbc_import_head": "72a775d35e54c21ff5c5798acef22016eedcc0a7"
},
"mutation_checks": {
"mutations": [
{
"mutation": "halve_heading",
"detected_by_tests": true,
"failed_tests": 9
},
{
"mutation": "cap_heading_preview",
"detected_by_tests": true,
"failed_tests": 9
},
{
"mutation": "erase_centering",
"detected_by_tests": true,
"failed_tests": 15
},
{
"mutation": "slow_c0_to_c1_rate",
"detected_by_tests": true,
"failed_tests": 11
},
{
"mutation": "retain_invalid_state",
"detected_by_tests": true,
"failed_tests": 18
},
{
"mutation": "ignore_model_freshness",
"detected_by_tests": true,
"failed_tests": 2
},
{
"mutation": "ignore_model_clock_rollback",
"detected_by_tests": true,
"failed_tests": 1
},
{
"mutation": "reverse_c0_wire_sign",
"detected_by_tests": true,
"failed_tests": 11
}
],
"all_detected": true
},
"native_test_dependency": {
"scope": "Native dependency for inherited Params selection test only; copied existing local build, not rebuilt.",
"source_library": "/Users/ibpersonal/.codex/worktrees/3548/sunnypilot/openpilot/common/libparams_c.dylib",
"sha256": "ddde738108eab18b75f085c865c43aa197fd79a0384b390b1515ff90116e20e0",
"byte_identical_source_files": [
"openpilot/common/params.cc",
"openpilot/common/params.h",
"openpilot/common/params_c.cc",
"openpilot/common/params.py",
"openpilot/common/params_keys.h",
"openpilot/common/queue.h",
"openpilot/common/util.cc",
"openpilot/common/util.h",
"openpilot/common/hardware/hw.h"
]
},
"review": {
"standards_findings": 0,
"spec_findings": 0,
"method": "Independent parallel read-only reviews; 120 focused tests independently passed."
},
"source_sha256": {
"openpilot/selfdrive/controls/lib/ford_model_action.py": "cb6353f00f2f5c84df4e606c6b7e20650f8c1e908b9fd71890f72aa4a5e42592",
"openpilot/selfdrive/controls/tests/test_ford_model_action.py": "c3b971622cc4041575aeec1826d45b76cebab9a2f77b2b9525c85d4184961295",
"openpilot/selfdrive/controls/tests/test_ford_model_action_adapter.py": "5aaa29c1f11080b7df0165fd0202a29b065053809e5f9c4b9ed8abfcce47e41b",
"tools/ford_pscm_lab/__init__.py": "db4b8b7d2e317ed34ca0ec220bf9d53e7b80a23e766f4e1cb2c8224dada45f2e",
"tools/ford_pscm_lab/model_action_replay.py": "c114c479bd22e4fc61a3e8d3ee7fae5d71d1d80ec4b952faad8f8f3692fb1508",
"tools/ford_pscm_lab/stress_model_action.py": "a78a50eed1f801f3b096d694ab8c2fd70804b6c250465b4152f38d83770a982b",
"tools/ford_pscm_lab/test_model_action_replay.py": "09d024c59d44b83ec081d6416d0f946a7719a73f22213af1f4ddc03dc4e6f4ac"
},
"artifacts": {
"directory": ".cache/ford_model_action",
"route_reports": [
"route95/report.json",
"route90/report.json"
],
"stress_report": "stress.json",
"mutation_report": "mutations/report.json",
"test_log": "ford_suite.txt"
}
}
+81
View File
@@ -0,0 +1,81 @@
# Signaled-turn preview trial
The installed option now also includes the independently tested geometry entry
candidate. See [stop continuity and geometry entry](ford_stop_geometry_entry.md)
for current behavior. The original action-only trial and evidence follow below.
The coordinated Ford controller can start a large intersection turn late even
when the model geometry already shows the turn. This opt-in trial uses geometry
as a timing cue for the existing action request. It does not replace the action
with path heading or lateral position.
## Selection
In sunnylink Ford settings, enable **Signaled-Turn Preview (Experimental)** with
**Selected-Action Path Tracking** and **Coordinated C0/C1 Steering** on, and
**Model Geometry Reference** off. Keep **Large-Turn Entry Assist** on to match the
offline candidate. Apply settings while offroad, then start a new onroad session.
The new parameter, `FordPscmTurnPreview`, defaults off. The existing turn-entry
assist remains a separate option. Turning the master selected-action option off
still selects upstream Ford control.
## Command change
`controlsd` samples heading change at 7 and 14 metres of cumulative model-path
distance, once per model message. The cue fades in above 5 degrees at 7 metres
and 20 degrees at 14 metres; both headings must have the same sign. Full weight
requires at least 10 and 30 degrees respectively. This uses path distance even
when forward X folds back during a turn.
The cue and original model timestamp travel in `carControlSP.fordTurnPreview`.
`card` adds at most 0.20 seconds of the existing filtered action-angle rate,
capped at 30 wheel degrees, to the trimmed inverse target. It requires exactly
one matching driver turn signal and both raw model action and filtered angle
request growing toward the turn. The original action, rate estimate, and trim
error remain unchanged. The existing inverse acceleration allowance, encoder,
CAN bounds, transmit observer, and optional post-encoder assist still apply.
C2 and C3 remain zero.
Missing, invalid, future or older-than-150-ms geometry removes this addition.
Lane changes, maneuver injection, driver steering input, or a fresh PSCM limit
of 2 or greater suppress it too. These cue failures do not invalidate the base
controller. New model array work stays out of the CAN loop. There are no new
subscriptions, logging frequencies, shared process changes, or correction
integrators.
## Offline evidence and limits
The frozen candidate's latest-route left entry first changed commands about
0.60 seconds earlier, but the reconstructed reference reached 90 degrees only
21 ms earlier. Another good left entry reached that reference 239 ms earlier.
The latest-route right entry retained identical packets. Those are estimates
from the older-firmware reconstruction, not measured wheel improvements.
Nine of ten bookmarked wobble windows retained identical packets. The remaining
window differed by at most 0.02 m C0 and 0.001 rad C1. Two camera-verified ordinary
lane-following sections retained identical packets with the matching-signal
gate. This does not establish unchanged behavior on all ordinary roads.
Retained command history can affect output after the signal/cue clears. In one
good unwind the reconstructed 30-degree crossing was 0.103 seconds later, with
the 5-degree crossing unchanged. Offline replay holds the measured wheel fixed;
it cannot establish stability, physical smoothness, or successful turn completion.
The installation checks exercise actual model messages, additive Cap'n Proto
transport, both process startup selectors, card's reader contract, and the real
Ford CAN packer. They compare full command histories with the retained candidate
when enabled and the prior controller when disabled. Route gaps reset only the
offline predictor; production timing-gap behavior is unchanged.
All 11 retained routes (18f, 190, 183, 185, 17c, 166, 16a, 172, 177, 175, 17a)
matched exactly in both configurations: 1,208,782 controller updates and 116,886
model messages. The installed helper's heading samples and action-trend signs
also matched the original offline extractor. The relevant controller,
integration, startup, and sunnylink suites passed 410 tests; lint and generated
settings consistency checks passed.
On the development Mac, the new cue helper's median/p99 update cost was
20.5/37.5 microseconds per new model message; cached calls were 0.125/0.25
microseconds. A 10,000-message Python allocation check retained 32 additional
bytes after warmup. These are local checks, not device realtime or whole-process
memory validation.
+64
View File
@@ -0,0 +1,64 @@
# Stop request continuity and geometry entry
The joint controller previously marked the path invalid below 0.3 m/s in both
controlsd's validity carrier and card. The sender then transmitted inactive mode
0 with zero C0/C1. The new joint path stays active through a stop while engagement,
freshness, CAN, motion, and steering-fault checks permit it.
Below 0.3 m/s, card repeats the last transmitted active C0/C1 packet. The observer
continues advancing at actual speed; no fictitious speed is supplied to the
inverse, which divides by speed squared. Trim is retained, the latest action
target is remembered, and the trend rate is zero while holding. At 0.3 m/s and
above, normal allocation resumes using the current action. Disengagement, stale
inputs, steering faults, and explicit PSCM override/denial still release the
request. Starting or reengaging at rest after an inactive packet begins with a
neutral active packet; it does not restore an old turn command.
This preserves the transmitted request, not a proven physical wheel angle. A
different model action on restart can still request straighter steering. The
legacy non-joint adapter's speed gate and master-off upstream path are unchanged.
## Geometry candidate
The existing **Signaled-Turn Preview (Experimental)** toggle now also enables the
offline `boot08` candidate. Keep Selected-Action Path Tracking, Coordinated C0/C1
Steering, and Large-Turn Entry Assist enabled, with Model Geometry Reference off.
Apply settings offroad and start a new onroad session.
On each new model message, controlsd measures local heading change over a complete
2 m path interval centered at speed times 0.8 seconds (start clamped to zero).
It converts this curvature through the existing VehicleModel, roll and angle
offset. Geometry and vehicle parameters must be valid; insufficient path disables
only the independent addition. The angle travels as additive carControlSP metadata.
The 7/14 m headings and exactly one matching signal must agree on a sharp turn.
The independent addition is capped at 90 wheel degrees, fades during action unwind,
and fades from full strength at 30 action degrees to zero at 90. Strong opposing
action suppresses it. Card selects the larger of this addition and the existing
bounded action-rate preview, rather than adding both. The action base, trim,
encoder, acceleration allowance, C0/C1 field bounds and zero C2/C3 remain.
The geometry calculation uses speed and vehicle parameters available when the
new model message is processed. The archived offline experiment sampled their
latest recorded values at the model timestamp. Transport timing can therefore
cause small input differences; installation parity uses identical sampled inputs.
## Validation
Nine new stop-continuity regression cases failed before the fix and pass with
active packets through the stop, retained commands/trim, and fault/disengagement
release without reviving old commands. The real Ford CAN packer is exercised.
The prior frozen experiment covered 12 routes (828,178 controller samples per
candidate). Installation checks match its geometry calculation on 4,854 sampled
paths across all 12 routes, its policy on 20,000 random cases, and its transmitted
commands and observer state on 3,000 continuous moving updates. Separate tests
cover flat-action entry through real model messages and Cap'n Proto transport,
takeover fade, health and signal gates, and toggle-off behavior.
The earlier experiment advanced several nominal entry crossings by roughly
0.07–0.39 seconds; some turns were unchanged. Nine of ten wobble windows retained
identical packets, and the known unwind's 90/60/30-degree reference crossings were
unchanged. A near-stop entry reference was busier. These are frozen command and
older-firmware reference comparisons, not a new physical wheel response, proof
of stability, or truck validation of the combined changes.
+106
View File
@@ -0,0 +1,106 @@
# Ford large-turn entry assist
The coordinated encoder can select the same C0/C1 packet even when its angle
target increases: larger fields sometimes have the same predicted next state,
and its tie break favors the smaller fields. This does not prove that the live
PSCM responds identically. This opt-in trial adds a bounded command after that
selection, making the turn-entry experiment observable at the CAN output.
## Enable and revert
In sunnylink Ford settings, keep **Selected-Action Path Tracking** and
**Coordinated C0/C1 Steering** enabled, and **Model Geometry Reference** disabled.
Enable **Large-Turn Entry Assist (Experimental)**, then make an offroad-to-onroad
transition. The setting is default off and read once at startup. Disabling only
the new setting and cycling offroad/onroad restores the previous coordinated
controller. Disabling Selected-Action Path Tracking restores upstream Ford
control. No hot switching or added steering-loop parameter I/O.
## Exactly what changes
The original desired steering angle, inverse, target-trend preview and centering
trim remain. An additional command is weighted by three continuous ramps:
- Requested angle magnitude: zero through 30 degrees, full at 60 degrees.
- Wheel lag into that turn: zero through 25 degrees, full at 50 degrees.
- Growing filtered target rate: zero at steady/relaxing target, full at
30 degrees/second.
Multiply the three weights. At full weight, add **0.60 m C0 and 0.04 rad C1**
in the requested turn direction, then quantize and clip to the existing DBC
bounds. These are trial tuning values, not recovered Ford constants or a
guaranteed conversion to wheel angle. The 4× label refers to the earlier offline
starting budget of 0.15 m / 0.01 rad, not a multiplication of all steering.
Only the extra term is suppressed on steeringPressed, fresh PSCM limit >=2,
or a target already limited by the existing inverse acceleration allowance.
Existing disengagement, fault and explicit override gates still govern the
whole command. C2/C3 stay zero. No new integrator, retained maneuver state or
encoder search is added. The observer continues consuming actual packed commands.
The extra goes to zero when the filtered target stops growing. **This does not
clear the consequences of earlier commands:** the observer and encoder retain
their state, so later requests can change. Normal path following and physical
wheel release are not guaranteed unchanged. Delayed unwind is an accepted trial
tradeoff, not claimed fixed.
## Why this strength
Nine recorded routes were compared: 183, 185, 17c, 166, 16a, 172, 177, 175 and
17a. Each run used 556,776 input rows, actual adapter/packer calls, frozen model
and vehicle measurements, and full available histories. Compare against the
unchanged coordinated controller at `d203a109a`.
| Extra budget | Weak right 2: max total C0/C1 difference | Changed ordinary samples |
|---|---|---|
| 0.15 m / 0.01 rad | 0.26 m / 0.0185 rad | 2,923 / 236,385 (1.24%) |
| 0.30 m / 0.02 rad | 0.41 m / 0.0285 rad | 2,071 / 236,385 (0.88%) |
| **0.60 m / 0.04 rad** | **0.71 m / 0.0485 rad** | **1,493 / 236,385 (0.63%)** |
Total differences can exceed the immediate additive budget because earlier
commands alter subsequent encoder decisions. The ordinary screen requires
requests within ±30 degrees, engaged control, no touch/limitReached, and two
seconds of continuously qualifying data on each side. It measures command
differences, not lane error. Lower counts are not proof of better centering.
The 4× trial changes 76/334 and 104/326 samples in the two weak-right windows.
Their peak absolute fields stay below 3.10 m C0 and 0.225 rad C1. Roundabout
windows reach maximum differences of 0.63–0.66 m / 0.0425–0.0445 rad.
Nine of ten marked wobble windows match exactly. In route172's bookmark, 98/396
samples change, up to 0.21 m C0 / 0.007 rad C1. Command total variation is slightly
lower (C0 5.95→5.85 m; C1 0.3605→0.3545 rad); maximum individual steps are unchanged.
This does not prove that the real wobble improves or remains equal.
Across all nine routes the trial touches a DBC field bound on 29 samples versus
12 for baseline, including a short interval on175. It never exceeds those
bounds. It changes the marked good unwind's channel split substantially:
maximum C0/C1 differences of 2.16 m / 0.058 rad. That is not a measured wheel
release delay. Larger requests reaching CAN do not establish more wheel motion.
## Verification and interpretation
Tests exercise the real Ford adapter and packer: startup/fallback, default-off
parameter, both turn signs, no extra for small requests/errors, DBC clipping,
target limits, overrides, fault gates, and clearing the added term while retaining
the observer's real history. Full-route production verification compares both
settings directly against the frozen 4× research output, including state and
transmitted commands. UI source and generated schema are checked together.
The deployment gate additionally suppresses the term when the inverse target
is acceleration-limited; no active extra in these nine research tapes coincided
with that condition. This does not introduce a new acceleration allowance.
`Ford joint path tracking` diagnostics identify the enabled experiment as
`ford-joint-turn-entry-v1`, with `turn_entry_enabled`, `turn_entry_weight`,
`turn_entry_c0`, `turn_entry_c1`, `base_wire_command` and actual `wire_sent`.
The existing `predicted_curvature` field describes the encoder prediction before
the added term; `filtered_curvature` is the observer advanced from actual sends.
Logging cadence is unchanged.
These are offline numerical checks, not a closed-loop steering validation.
166/16a retain raw-yaw sensitivity; 183 has incomplete earlier history; 185 has
log gaps. The recovered older firmware model is not proven identical to the
live truck. The physical question for this trial is whether the stronger entry
requests move the wheel farther/faster without unacceptable normal-driving
changes. Neither turn completion nor preserved driving feel is established.
+122
View File
@@ -0,0 +1,122 @@
# 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
@@ -0,0 +1,346 @@
{
"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
@@ -0,0 +1,89 @@
# 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
@@ -0,0 +1,190 @@
{
"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
@@ -0,0 +1,327 @@
# Ford C2-free model-pose tracking with measured feedback
This experiment is retired. Its implementation, setting and dedicated tests
were removed from the selected-action drive-test branch. For current setup,
see [Ford selected-action drive testing](ford_model_action_drive_test.md).
The material below is historical; it does not describe an available toggle.
Hypothesis `model-pose-c0-c1-feedback-v8` retains the model-pose C0/C1 base
and adds two guarded release policies. When measured turning exceeds both
current and delayed requests, a separate output guard prevents same-direction
C0/C1 growth, including while feedback history rebuilds after driver input.
When turning instead falls below both requests and is no longer increasing,
bounded C1 tracking can use remaining release-entry command headroom.
Existing opposing-bias recovery still stops at zero bias. Geometry, blending,
feedback gain, slew rates and field limits are unchanged; C2/C3 remain zero.
This is an experimental outer controller around the multivariable PSCM.
Its geometry does not define a calibrated C0/C1-to-wheel mapping or an angle
servo. V8 has offline validation only. Command replay cannot establish the
truck's response, closed-loop stability, or an overshoot improvement.
## Evidence and scope
Route80 ran v3 and contains both sustained under-response and over-response.
Representative eligible windows had median CAN response/request ratios of
0.78, 1.77 and 0.69 with a declared 0.2-second comparison interval. These
are descriptive tracking ratios, not identified controller gains.
V4 replaced separate model-heading C1 with selected-curvature C1 and reduced
heading demand in several large maneuvers. The user subsequently reported
weak turning and steering repeatedly stopping near 85 degrees. Older logs
contain larger wheel angles; the inspected host code has no fixed 85-degree
wheel stop, although upstream curvature limits depend on speed.
Route83 had the Sunnylink toggle on, but omitted EPS firmware responses.
The former firmware gate selected the default `FordPathController`; replay
reproduced its recorded C0/C1/C2 requests. Its favorable turns are evidence
for the existing model-pose construction, not validation of v5 or v6.
V6 reuses that construction while replacing its remaining C2 request with
C0/C1 geometry. Removing C2 changes the request received by the PSCM, so
matching large C0/C1 commands does not guarantee matching vehicle motion.
Route8a ran v6 and was reported as the best drive. Route8e ran v7 throughout
with the experiment enabled; it includes entry lag and excessive turning
while requests release. Fixed-input v6/v7 replay produced identical commands
in the main reversal and over-response examples, so the v7 recovery change
does not directly explain their command behavior. In the over-response
example, model C0/C1 grew while selected curvature fell and driver resets
repeatedly removed feedback history. Another exit remained deficient after
opposing bias reached zero. These observations motivate the v8 guards; they
do not isolate an EPS transfer function or demonstrate the proposed response.
## Base request
controlsd selects valid `lateralManeuverPlan.desiredCurvature`, otherwise
`modelV2.action.desiredCurvature`, after the existing curvature limiter.
This action already includes upstream delay handling; it receives no extra
response advance here.
The model contribution uses the existing allocator's raw forward pose and
bounded short-pose correction. `_model_pose` advances 0.1 seconds, retains
the model's remaining forward geometry, and separately corrects the short
pose using measured curvature and its recent change. Its offset preview is
up to 7 m and its heading preview is up to max(7 m, speed × 1 s), bounded by
available path length. This raw pose is not passed through a second model
filter. The filtered, ego-aligned reference remains available for comparison
and the existing geometry-validity checks.
```text
share(k) = clip((k - 0.006/m) / (0.012/m - 0.006/m), 0, 1)
aligned = desired_curvature × model_forward_heading > 0
model_share = min(share(abs(desired_curvature)), share(model_curvature_demand))
if aligned, otherwise 0
model_pair = existing_pose_encoder(model_pose, model_share, C2=0)
remaining_curvature = desired_curvature × (1 - model_share)
L0 = max(8 m, speed × 1 s)
L1 = max(7 m, speed × 1 s)
curvature_C0 = 0.5 × remaining_curvature × L0²
curvature_C1 = remaining_curvature × L1
C0_base = clip(model_pair.C0 + curvature_C0, ±5.11 m)
C1_base = clip(model_pair.C1 + curvature_C1, ±0.5 rad)
```
`model_curvature_demand` is the larger absolute curvature implied by the
forward offset and heading previews. The share uses the existing allocator's
0.006–0.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 v5–v7 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 103–110 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.
+1 -1
View File
@@ -16,7 +16,7 @@ export VECLIB_MAXIMUM_THREADS=1
export QCOM_PRIORITY=12
if [ -z "$AGNOS_VERSION" ]; then
export AGNOS_VERSION="19.6"
export AGNOS_VERSION="19.7"
fi
export STAGING_ROOT="/data/safe_staging"
+67 -1
View File
@@ -383,6 +383,8 @@ struct CarControlSP @0xa5cd762cd951a455 {
leadOne @2 :LeadData;
leadTwo @3 :LeadData;
intelligentCruiseButtonManagement @4 :IntelligentCruiseButtonManagement;
fordLateralPath @5 :FordLateralPath;
fordTurnPreview @6 :FordTurnPreview;
struct Param {
key @0 :Text;
@@ -403,6 +405,25 @@ struct CarControlSP @0xa5cd762cd951a455 {
}
}
struct FordTurnPreview {
valid @0 :Bool;
modelMonoTime @1 :UInt64;
heading7 @2 :Float64; # Heading change at 7 m of path distance, native pinion degrees.
heading14 @3 :Float64; # Heading change at 14 m of path distance, native pinion degrees.
actionRate @4 :Float64; # Negative raw model curvature derivative; only its sign is used.
geometryAngle @5 :Float64; # Local path curvature converted to native wheel degrees at v*0.8 s.
geometryValid @6 :Bool;
}
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;
@@ -447,6 +468,16 @@ struct BackupManagerSP @0xf98d843bfd7004a3 {
struct CarStateSP @0xb86e6369214c01c8 {
speedLimit @0 :Float32;
fordPscmStatus @1 :FordPscmStatus;
struct FordPscmStatus {
valid @0 :Bool;
canMonoTime @1 :UInt64; # Last accepted Lane_Assist_Data3_FD1 CAN receipt, not carStateSP publication time.
lateralState @2 :UInt8; # LatCtlSte_D_Stat
limit @3 :UInt8; # LatCtlLim_D_Stat: generic lateral limit, not a torque/rate diagnosis.
capability @4 :UInt8; # LatCtlCpblty_D_Stat
denied @5 :Bool; # LaActDeny_B_Actl
}
}
struct LiveMapDataSP @0xf416ec09499d9d19 {
@@ -462,6 +493,18 @@ 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;
@@ -470,7 +513,30 @@ struct ModelDataV2SP @0xa1680744031fdb2d {
}
}
struct CustomReserved10 @0xcb9fd56c7057593a {
struct AssistedDrivingMilestoneState @0xcb9fd56c7057593a {
enabled @0 :Bool;
madsDistanceMeters @1 :Float64;
fullAssistDistanceMeters @2 :Float64;
event @3 :Event;
struct Event {
id @0 :UInt64;
category @1 :Category;
distanceMeters @2 :Float64;
previousDistanceMeters @3 :Float64;
unit @4 :Unit;
}
enum Category {
none @0;
mads @1;
fullAssist @2;
}
enum Unit {
imperial @0;
metric @1;
}
}
struct CustomReserved11 @0xc2243c65e0340384 {
+6 -1
View File
@@ -725,6 +725,7 @@ struct ChestnutState {
pcieLtssm @7 :UInt8;
supplyVoltage @8 :UInt16; # mV
supplyCurrent @9 :Int16; # mA
supplyFault @10 :Bool;
}
struct RadarState @0x9a185389d6fdd05f {
@@ -1004,6 +1005,7 @@ struct DrivingModelData {
frameIdExtra @1 :UInt32;
frameDropPerc @6 :Float32;
modelExecutionTime @7 :Float32;
big @8 :Bool;
action @2 :ModelDataV2.Action;
@@ -2117,6 +2119,9 @@ 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 {
@@ -2640,7 +2645,7 @@ struct Event {
carStateSP @114 :Custom.CarStateSP;
liveMapDataSP @115 :Custom.LiveMapDataSP;
modelDataV2SP @116 :Custom.ModelDataV2SP;
customReserved10 @136 :Custom.CustomReserved10;
assistedDrivingMilestoneState @136 :Custom.AssistedDrivingMilestoneState;
customReserved11 @137 :Custom.CustomReserved11;
customReserved12 @138 :Custom.CustomReserved12;
customReserved13 @139 :Custom.CustomReserved13;
+1
View File
@@ -90,6 +90,7 @@ _services: dict[str, tuple] = {
"carParamsSP": (True, 0.02, 1),
"carControlSP": (True, 100., 10),
"carStateSP": (True, 100., 10),
"assistedDrivingMilestoneState": (True, 10., 1),
"liveMapDataSP": (True, 1., 1),
"modelDataV2SP": (True, 20., None, QueueSize.BIG),
"liveLocationKalman": (True, 20.),
+12
View File
@@ -1,4 +1,5 @@
import os
import time
from abc import abstractmethod, ABC
from dataclasses import dataclass, fields
@@ -16,6 +17,17 @@ class ThermalZone:
zone_number = -1
def read(self) -> float:
start = time.monotonic()
try:
return self._read()
finally:
elapsed = time.monotonic() - start
if elapsed > 1.0:
# Import lazily: swaglog's hardware paths depend on this module.
from openpilot.common.swaglog import cloudlog
cloudlog.event("Thermal sensor read slow", sensor=self.name, zone=self.zone_number, elapsed=elapsed, error=True)
def _read(self) -> float:
if self.zone_number < 0:
for n in os.listdir("/sys/devices/virtual/thermal"):
if not n.startswith("thermal_zone"):
+11 -11
View File
@@ -56,29 +56,29 @@
},
{
"name": "boot",
"url": "https://commadist.azureedge.net/agnosupdate/boot-b30f5eef65ec3878f3aa3dcaf2cc95c09e2c1e661cd3a38e94da37dee76f68bd.img.xz",
"hash": "b30f5eef65ec3878f3aa3dcaf2cc95c09e2c1e661cd3a38e94da37dee76f68bd",
"hash_raw": "b30f5eef65ec3878f3aa3dcaf2cc95c09e2c1e661cd3a38e94da37dee76f68bd",
"url": "https://commadist.azureedge.net/agnosupdate/boot-6ecf6f987cd11968104abcccabbe268485d329cdb73012dfd3c381a6b8deb27d.img.xz",
"hash": "6ecf6f987cd11968104abcccabbe268485d329cdb73012dfd3c381a6b8deb27d",
"hash_raw": "6ecf6f987cd11968104abcccabbe268485d329cdb73012dfd3c381a6b8deb27d",
"size": 46897152,
"sparse": false,
"full_check": true,
"has_ab": true,
"ondevice_hash": "6650e4c46df99ae6dfd6ee895a34b8a2a3cc490a8ce18e16cc3c451c3f822b6e"
"ondevice_hash": "d12e1e5b9455b62a1464558716493b33e470d7a7e88da1c4105a3b21d0961808"
},
{
"name": "system",
"url": "https://commadist.azureedge.net/agnosupdate/system-5b6ce7965904a157fd3a134ccfcb854f9ca5c1cc2a26b7cb80a4fa4e1cc4aaa3.img.xz",
"hash": "b134fd04e9da27fa1d359ea0f2742c216fa21a08b5c47e9be22ab3b0563d9b9b",
"hash_raw": "5b6ce7965904a157fd3a134ccfcb854f9ca5c1cc2a26b7cb80a4fa4e1cc4aaa3",
"url": "https://commadist.azureedge.net/agnosupdate/system-3c271e2b3d20d2f0a8bf6555a1319f3efb12845490967d6151195174a01e912f.img.xz",
"hash": "74ffc9c551e1f29cda897ace8a69080fe644f8039977c6885f2b48362e39b744",
"hash_raw": "3c271e2b3d20d2f0a8bf6555a1319f3efb12845490967d6151195174a01e912f",
"size": 4718592000,
"sparse": true,
"full_check": false,
"has_ab": true,
"ondevice_hash": "91242772af771ae96fe2eebc105f2b80a7e1dbaaf6003c2574b62d51b806f468",
"ondevice_hash": "6a992680183685eea9db99d915219a37935f45989330d9b619e880450257f448",
"alt": {
"hash": "5b6ce7965904a157fd3a134ccfcb854f9ca5c1cc2a26b7cb80a4fa4e1cc4aaa3",
"url": "https://commadist.azureedge.net/agnosupdate/system-5b6ce7965904a157fd3a134ccfcb854f9ca5c1cc2a26b7cb80a4fa4e1cc4aaa3.img",
"hash": "3c271e2b3d20d2f0a8bf6555a1319f3efb12845490967d6151195174a01e912f",
"url": "https://commadist.azureedge.net/agnosupdate/system-3c271e2b3d20d2f0a8bf6555a1319f3efb12845490967d6151195174a01e912f.img",
"size": 4718592000
}
}
]
]
+2 -1
View File
@@ -5,6 +5,7 @@ import logging
import os
import select
import signal
import string
import struct
import subprocess
import tempfile
@@ -354,7 +355,7 @@ class Modem:
imei = ""
iccid = (self._atv("AT+QCCID", "+QCCID:") or "").rstrip("F")
if not iccid.isdigit():
if not all(c in string.hexdigits for c in iccid):
iccid = ""
imsi = first_line("AT+CIMI")
+7 -1
View File
@@ -4,11 +4,17 @@ from pathlib import Path
CHESTNUT_FW_VERSION = "ed4e39b7"
CHESTNUT_USB_IDS = ((0xADD1, 0x0001), (0x3801, 0x0001))
CHESTNUT_ROM_USB_IDS = ((0x174C, 0x2464), (0x174C, 0x2463))
CHESTNUT_USB_PRODUCT = f"custom {CHESTNUT_FW_VERSION}-CLEAN"
USB_DEVICES_PATH = Path("/sys/bus/usb/devices")
TYPEC_CC_ORIENTATION_PATH = Path("/sys/class/power_supply/usb/typec_cc_orientation")
PRIMARY_USB_CONTROLLER = "a600000.ssusb"
def is_chestnut_usb_id(vendor_id: int, product_id: int, include_bootloader: bool = False) -> bool:
ids = CHESTNUT_USB_IDS + CHESTNUT_ROM_USB_IDS if include_bootloader else CHESTNUT_USB_IDS
return (vendor_id, product_id) in ids
def get_usb_topology() -> set[str]:
try:
return set(os.listdir(USB_DEVICES_PATH))
@@ -81,7 +87,7 @@ def set_usb_state(device_state, devices: list[dict]) -> None:
entry.linkErrorCount = device["linkErrorCount"]
entry.usb3Lane = device.get("usb3Lane", "unknown")
if (entry.vendorId, entry.productId) in CHESTNUT_USB_IDS:
if is_chestnut_usb_id(entry.vendorId, entry.productId):
chestnut_present = True
device_state.chestnutPresent = chestnut_present
+4
View File
@@ -97,6 +97,10 @@ Params::Params(const std::string &path) {
}
Params::~Params() {
flushNonBlockingWrites();
}
void Params::flushNonBlockingWrites() {
if (future.valid()) {
future.wait();
}
+1
View File
@@ -75,6 +75,7 @@ public:
return put(key.c_str(), val ? "1" : "0", 1);
}
void putNonBlocking(const std::string &key, const std::string &val);
void flushNonBlockingWrites();
inline void putBoolNonBlocking(const std::string &key, bool val) {
putNonBlocking(key, val ? "1" : "0");
}
+5
View File
@@ -73,6 +73,7 @@ params_get = _bind("params_get", [ParamsHandle, ctypes.c_char_p, ctypes.c_bool],
params_get_bool = _bind("params_get_bool", [ParamsHandle, ctypes.c_char_p, ctypes.c_bool], ctypes.c_bool)
params_put = _bind("params_put", [ParamsHandle, ctypes.c_char_p, ctypes.c_char_p, ctypes.c_size_t, ctypes.c_bool], ctypes.c_int)
params_put_bool = _bind("params_put_bool", [ParamsHandle, ctypes.c_char_p, ctypes.c_bool, ctypes.c_bool], ctypes.c_int)
params_flush = _bind("params_flush", [ParamsHandle])
params_remove = _bind("params_remove", [ParamsHandle, ctypes.c_char_p], ctypes.c_int)
params_get_path = _bind("params_get_path", [ParamsHandle, ctypes.c_char_p, ctypes.c_size_t], ParamsBuffer)
params_keys_size = _bind("params_keys_size", [ParamsHandle], ctypes.c_size_t)
@@ -178,6 +179,10 @@ class Params:
def put_bool(self, key, val, block=False):
params_put_bool(self.p, self.check_key(key), val, block)
def flush(self):
"""Wait for all prior nonblocking writes from this Params instance."""
params_flush(self.p)
def remove(self, key):
params_remove(self.p, self.check_key(key))
+14 -5
View File
@@ -133,6 +133,12 @@ int params_put_bool(ParamsHandle *handle, const char *key, bool value, bool bloc
});
}
void params_flush(ParamsHandle *handle) noexcept {
translate_exceptions([&]() {
handle->params.flushNonBlockingWrites();
});
}
int params_remove(ParamsHandle *handle, const char *key) noexcept {
return translate_exceptions(-1, [&]() {
return handle->params.remove(key);
@@ -162,12 +168,15 @@ ParamsBuffer params_key_at(ParamsHandle *handle, size_t index) noexcept {
size_t params_keys_by_flag(ParamsHandle *handle, uint32_t flag, ParamsBuffer *out, size_t out_size) noexcept {
return translate_exceptions(size_t{0}, [&]() {
auto filtered = handle->params.allKeys(static_cast<ParamKeyFlag>(flag));
size_t count = std::min(filtered.size(), out_size);
for (size_t i = 0; i < count; i++) {
out[i] = return_string(filtered[i]);
size_t count = 0;
for (const auto &key : handle->keys) {
if (flag == ALL || (handle->params.getKeyFlag(key) & flag)) {
// Each buffer borrows a different string, stable for the handle's lifetime.
if (count < out_size) out[count] = {key.data(), key.size()};
++count;
}
}
return filtered.size();
return count;
});
}
+19
View File
@@ -92,6 +92,12 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
{"ObdMultiplexingEnabled", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, BOOL}},
{"Offroad_CarUnrecognized", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, JSON}},
{"Offroad_ChestnutBranch", {CLEAR_ON_MANAGER_START, JSON}},
{"Offroad_ChestnutNotDetected", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, JSON}},
{"Offroad_ChestnutOverheated", {CLEAR_ON_MANAGER_START, JSON}},
{"Offroad_ChestnutPcieUnavailable", {CLEAR_ON_MANAGER_START, JSON}},
{"Offroad_ChestnutUncompiled", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, JSON}},
{"Offroad_ChestnutUpdateFailed", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, JSON}},
{"Offroad_ChestnutUsbSlow", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, JSON}},
{"Offroad_ConnectivityNeeded", {CLEAR_ON_MANAGER_START, JSON}},
{"Offroad_ConnectivityNeededPrompt", {CLEAR_ON_MANAGER_START, JSON}},
{"Offroad_ExcessiveActuation", {PERSISTENT, JSON}},
@@ -132,10 +138,13 @@ 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
@@ -156,6 +165,7 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
{"DevUIInfo", {PERSISTENT | BACKUP, INT, "0"}},
{"EnableCopyparty", {PERSISTENT | BACKUP, BOOL}},
{"EnableGithubRunner", {PERSISTENT | BACKUP, BOOL}},
{"FullAssistDrivenDistanceMeters", {PERSISTENT, FLOAT, "0.0"}},
{"GreenLightAlert", {PERSISTENT | BACKUP, BOOL, "0"}},
{"GithubRunnerSufficientVoltage", {CLEAR_ON_MANAGER_START , BOOL}},
{"HasAcceptedTermsSP", {PERSISTENT, STRING, "0"}},
@@ -165,7 +175,9 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
{"IsDevelopmentBranch", {CLEAR_ON_MANAGER_START, BOOL}},
{"IsReleaseSpBranch", {CLEAR_ON_MANAGER_START, BOOL}},
{"LastGPSPositionLLK", {PERSISTENT, STRING}},
{"LastDriveAssistedDrivingSummary", {PERSISTENT, JSON, "{}"}},
{"LeadDepartAlert", {PERSISTENT | BACKUP, BOOL, "0"}},
{"MadsDrivenDistanceMeters", {PERSISTENT, FLOAT, "0.0"}},
{"MaxTimeOffroad", {PERSISTENT | BACKUP, INT, "1800"}},
{"ModelRunnerTypeCache", {CLEAR_ON_ONROAD_TRANSITION, INT}},
{"OffroadMode", {CLEAR_ON_MANAGER_START, BOOL}},
@@ -225,6 +237,13 @@ 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"}},
{"FordPscmJointControl", {PERSISTENT | BACKUP, BOOL, "0"}},
{"FordPscmTurnEntryAssist", {PERSISTENT | BACKUP, BOOL, "0"}},
{"FordPscmTurnPreview", {PERSISTENT | BACKUP, BOOL, "0"}},
{"HyundaiLongitudinalTuning", {PERSISTENT | BACKUP, INT, "0"}},
{"SubaruStopAndGo", {PERSISTENT | BACKUP, BOOL, "0"}},
{"SubaruStopAndGoManualParkingBrake", {PERSISTENT | BACKUP, BOOL, "0"}},
+4 -4
View File
@@ -27,14 +27,14 @@ public:
auto param_path = Params().getParamPath();
if (util::file_exists(param_path)) {
std::string real_path = util::readlink(param_path);
util::check_system(util::string_format("rm %s -rf", real_path.c_str()));
util::check_system(util::string_format("rm -rf %s", real_path.c_str()));
unlink(param_path.c_str());
}
if (getenv("COMMA_CACHE") == nullptr) {
util::check_system(util::string_format("rm %s -rf", Path::download_cache_root().c_str()));
util::check_system(util::string_format("rm -rf %s", Path::download_cache_root().c_str()));
}
util::check_system(util::string_format("rm %s -rf", Path::comma_home().c_str()));
util::check_system(util::string_format("rm %s -rf", msgq_path.c_str()));
util::check_system(util::string_format("rm -rf %s", Path::comma_home().c_str()));
util::check_system(util::string_format("rm -rf %s", msgq_path.c_str()));
unsetenv("OPENPILOT_PREFIX");
}
+17
View File
@@ -106,6 +106,13 @@ class TestParams(OpenpilotTestCase):
assert q.get("CarParams") is None
assert q.get("CarParams", True) == b"1"
def test_flush_non_blocking_writes(self):
self.params.put("DongleId", "first")
self.params.put("DongleId", "last")
self.params.flush()
assert self.params.get("DongleId") == "last"
def test_params_all_keys(self):
keys = Params().all_keys()
@@ -126,6 +133,16 @@ class TestParams(OpenpilotTestCase):
assert self.params.get("LiveParametersV2") is None
assert self.params.get("LiveParametersV2", return_default=True) is None
def test_filtered_keys_are_distinct_registered_strings(self):
registered = set(self.params.all_keys())
for flag in (ParamKeyFlag.PERSISTENT, ParamKeyFlag.BACKUP, ParamKeyFlag.CLEAR_ON_MANAGER_START):
filtered = self.params.all_keys(flag)
assert len(filtered) > 1
assert len(filtered) == len(set(filtered))
assert set(filtered) <= registered
assert all(key.decode('utf-8') for key in filtered)
assert self.params.all_keys(flag) == filtered
def test_params_get_type(self):
# json
self.params.put("ApiCache_FirehoseStats", {"a": 0}, block=True)
+35
View File
@@ -0,0 +1,35 @@
from unittest.mock import Mock, mock_open
import pytest
from openpilot.common.hardware import base
from openpilot.common.swaglog import cloudlog
@pytest.mark.parametrize('elapsed', [.05, 1.5])
def test_thermal_read_preserves_value_and_identifies_slow_sensor(monkeypatch, elapsed):
sensor = base.ThermalZone('cpu0-gold-usr')
sensor.zone_number = 7
read = mock_open(read_data='62000')
report = Mock()
monkeypatch.setattr(base, 'open', read, raising=False)
monkeypatch.setattr(base.time, 'monotonic', Mock(side_effect=[0., elapsed]))
monkeypatch.setattr(cloudlog, 'event', report)
assert sensor.read() == 62.
read.assert_called_once_with('/sys/devices/virtual/thermal/thermal_zone7/temp')
if elapsed > 1.:
report.assert_called_once_with('Thermal sensor read slow', sensor='cpu0-gold-usr', zone=7, elapsed=elapsed, error=True)
else:
report.assert_not_called()
def test_slow_thermal_failure_is_reported_without_hiding_error(monkeypatch):
sensor = base.ThermalZone('memory')
sensor.zone_number = 8
monkeypatch.setattr(base, 'open', Mock(side_effect=OSError('sensor read failed')), raising=False)
monkeypatch.setattr(base.time, 'monotonic', Mock(side_effect=[0., 2.]))
report = Mock()
monkeypatch.setattr(cloudlog, 'event', report)
with pytest.raises(OSError, match='sensor read failed'):
sensor.read()
report.assert_called_once_with('Thermal sensor read slow', sensor='memory', zone=8, elapsed=2., error=True)
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:07bda2fe5d6be0b2854044053c384fe002e96406da119863a443b9344258b500
size 1544
Binary file not shown.
+20 -3
View File
@@ -21,6 +21,8 @@ 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.selfdrive.car.ford_joint_control import select_joint_control
from openpilot.sunnypilot.mads.helpers import set_alternative_experience, set_car_specific_params
from openpilot.sunnypilot.selfdrive.car import interfaces as sunnypilot_interfaces
@@ -123,6 +125,7 @@ class Car:
self.RI = RI
self.CP.alternativeExperience = 0
self.ford_joint_control = select_joint_control(self.CP, self.params)
# mads
set_alternative_experience(self.CP, self.CP_SP, self.params)
set_car_specific_params(self.CP, self.CP_SP, self.params)
@@ -198,6 +201,11 @@ 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)
if self.ford_joint_control is not None and self.ford_joint_control.fault:
# Surface a latched loss of command history through the normal steering
# fault alert; never silently leave the UI engaged with this trial disabled.
CS.steerFaultTemporary = True
# Update radar tracks from CAN
RD: structs.RadarDataT | None = self.RI.update(can_list)
@@ -267,7 +275,7 @@ class Car:
cs_sp_send.carStateSP = CS_SP
self.pm.send('carStateSP', cs_sp_send)
def controls_update(self, CS: car.CarState, CC: car.CarControl, CC_SP: custom.CarControlSP):
def controls_update(self, CS: car.CarState, CC: car.CarControl, CC_SP: custom.CarControlSP, pscm_status=None):
"""control update loop, driven by carControl"""
if not self.initialized_prev:
@@ -280,8 +288,17 @@ class Car:
if self.sm.all_alive(['carControl']):
# send car controls over can
now_nanos = self.can_log_mono_time if REPLAY else int(time.monotonic() * 1e9)
self.last_actuators_output, can_sends = self.CI.apply(CC, convert_carControlSP(CC_SP), now_nanos)
control_sp = convert_carControlSP(CC_SP)
if self.ford_joint_control is not None:
CC = self.ford_joint_control.prepare(CC, control_sp, CS, now_nanos * 1e-9,
fresh=self.sm.all_checks(['carControl', 'carControlSP']), pscm_status=pscm_status,
turn_preview=CC_SP.fordTurnPreview)
self.last_actuators_output, can_sends = self.CI.apply(CC, control_sp, now_nanos)
self.pm.send('sendcan', can_list_to_can_capnp(can_sends, msgtype='sendcan', valid=CS.canValid))
if self.ford_joint_control is not None:
self.ford_joint_control.record_sent(can_sends, now_nanos)
if self.sm.frame % 20 == 0:
cloudlog.event('Ford joint path tracking', **self.ford_joint_control.diagnostics)
self.CC_prev = CC
@@ -293,7 +310,7 @@ class Car:
initialized = (not any(e.name == EventName.selfdriveInitializing for e in self.sm['onroadEvents']) and
self.sm.seen['onroadEvents'])
if not self.CP.passive and initialized:
self.controls_update(CS, self.sm['carControl'], self.sm['carControlSP'])
self.controls_update(CS, self.sm['carControl'], self.sm['carControlSP'], CS_SP.fordPscmStatus)
self.initialized_prev = initialized
self.CS_prev = CS
@@ -0,0 +1,280 @@
"""Default-off joint C0/C1 trial at the existing Ford transmit boundary.
The observer consumes decoded packets queued to panda, not tentative requests.
It estimates older-firmware state; it cannot observe ECU RAM or prove acceptance.
"""
import math
from opendbc.car.ford.values import FordFlags
from openpilot.selfdrive.controls.lib.ford_path import joint_control_speed
# Small steering-wheel trim around the nominal inverse, in degrees. These are
# trial tuning values, not recovered PSCM constants. Entry assist is separately gated.
ANGLE_TRIM_MAX = 2.0
ANGLE_TRIM_KI = 0.2 # 1/s; error clipping limits adaptation to 0.4 deg/s.
ANGLE_TRIM_ERROR_MAX = 5.0
ANGLE_TRIM_RATE_MAX = 5.0 # deg/s: do not learn a bias from fast wheel motion.
TARGET_PREVIEW = 0.1 # seconds; causal action-trend forecast, then hold.
TURN_ENTRY_C0_MAX = 0.60 # metres; experimental post-encoder correction budget.
TURN_ENTRY_C1_MAX = 0.04 # radians; not a recovered Ford calibration value.
def turn_entry_weight(target, angle, requested_rate):
"""Fade in only for a large, growing turn request with substantial wheel lag."""
direction = math.copysign(1.0, target)
size = max(0.0, min(1.0, (abs(target) - 30.0) / 30.0))
lag = max(0.0, min(1.0, (direction * (target - angle) - 25.0) / 25.0))
growth = max(0.0, min(1.0, direction * requested_rate / 30.0))
return size * lag * growth
def joint_control_enabled(CP, params):
return bool(
CP.brand == 'ford'
and CP.flags & FordFlags.CANFD
and params.get_bool('FordModelActionController')
and params.get_bool('FordPscmJointControl')
and not params.get_bool('FordGeometryReference')
and not params.get_bool('JoystickDebugMode')
)
def select_joint_control(CP, params):
# No calibration or native-library load on the normal/default path.
if not joint_control_enabled(CP, params):
return None
return FordJointControl(CP, turn_entry_assist=params.get_bool('FordPscmTurnEntryAssist'),
turn_preview=params.get_bool('FordPscmTurnPreview'))
class FordJointControl:
def __init__(self, CP, *, turn_entry_assist=False, turn_preview=False):
from opendbc.can import CANParser
from opendbc.car.ford.fordcan import CanBus
from openpilot.selfdrive.controls.lib.ford_joint.model import MainRequest
from openpilot.selfdrive.controls.lib.ford_joint.angle import AngleModel
from openpilot.selfdrive.controls.lib.ford_joint.encoder import PairedRelease
self.turn_entry_assist = turn_entry_assist
self.turn_preview = turn_preview
self.wheelbase, self.ratio = CP.wheelbase, CP.steerRatio
if not all(math.isfinite(v) and v > 0 for v in (self.wheelbase, self.ratio)):
raise ValueError('Joint control requires finite positive vehicle geometry')
self.request = MainRequest(native_lookup=True)
self.angle = AngleModel(self.request.cal)
self.encoder = PairedRelease(self.request, preserve_now=True)
self.bus = CanBus(CP).main
self.parser = CANParser('ford_lincoln_base_pt', [('LateralMotionControl2', 100)], self.bus)
self.last_time = None
self.last_sent_time = None
self.phase = 0.0
self.sent = (0.0, 0.0, False)
self.measurement = (0.0, 0.0, 0.0)
self.fault = ''
self.angle_trim = 0.0
self.wheel_rate = 0.0
self.last_target = None
self.requested_rate = 0.0
self.diagnostics = {'hypothesis': 'ford-joint-v24', 'status': 'inactive'}
def advance(self, now):
"""Retain channel/filter state through inactive commands and driver override."""
if self.last_time is not None:
elapsed = now - self.last_time
if not 0.0 <= elapsed <= 0.1:
# Lost command history cannot be repaired by pretending the held state
# reset. Disable this trial until the next onroad process start.
self.fault = 'command_timing_gap'
else:
self.phase += elapsed
ticks = int((self.phase + 1e-12) / 0.008)
self.phase -= ticks * 0.008
speed, angle, yaw = self.measurement
for _ in range(ticks):
r = self.request.step(speed, self.sent[0], self.sent[1], active=self.sent[2], freeze_i=True)
self.angle.step(speed, r['filtered_curvature'], angle, yaw, yaw * speed / 3.6, self.wheelbase, self.ratio)
self.last_time = now
def prepare(self, CC, CC_SP, CS, now, *, fresh=True, pscm_status=None, turn_preview=None):
from openpilot.selfdrive.controls.lib.ford_joint.inverse import C0_BOUND, C1_BOUND, invert_angle, quantize
dt = now - self.last_time if self.last_time is not None else 0.0
self.advance(now)
path = CC_SP.fordLateralPath
target = float(CC.actuators.steeringAngleDeg)
speed_ms = joint_control_speed(CS)
# controlsd publishes calibrated car-frame motion and gates the path on its
# health/freshness. Raw Ford CAN yaw has a zero offset on the audited truck.
# Calibrated Z is opposite the CAN/pinion sign used by the recovered model.
yaw = -float(CC.angularVelocity[2]) if len(CC.angularVelocity) == 3 else math.nan
finite = all(math.isfinite(v) for v in (speed_ms, CS.steeringAngleDeg, CS.yawRate, yaw, target))
if finite:
# Ford does not populate CarState.steeringRateDeg. Derive motion from the
# measured angle; 0.1 s filtering suppresses its 0.1-degree quantization.
if 0.0 < dt <= 0.1:
rate = (CS.steeringAngleDeg - self.measurement[1]) / dt
self.wheel_rate += dt / (0.1 + dt) * (rate - self.wheel_rate)
self.measurement = (max(0.0, speed_ms * 3.6), CS.steeringAngleDeg, yaw)
else:
self.wheel_rate = 0.0
status_fresh = pscm_status is not None and pscm_status.valid and pscm_status.canMonoTime > 0 and -0.005 <= now - pscm_status.canMonoTime * 1e-9 <= 0.15
# Like upstream Ford, steeringPressed alone does not zero the path. Retain
# disengagement/fault gates and the PSCM's explicit override/denial status.
override = bool(status_fresh and (pscm_status.limit == 3 or pscm_status.denied))
valid = bool(
fresh
and CS.canValid
and finite
and 0.0 <= speed_ms <= 55
and abs(CS.yawRate) <= 3
and abs(yaw) <= 3
and path.enabled
and path.valid
and not CS.steerFaultTemporary
and not CS.steerFaultPermanent
)
# A missing previous transmit receipt means the prediction is not synchronized.
if self.last_sent_time is not None and now - self.last_sent_time > 0.1:
self.fault = 'missing_transmit_history'
active = bool(CC.latActive and valid and not override and not self.fault)
if not active:
self.angle_trim = 0.0
self.last_target = None
self.requested_rate = 0.0
command = (0.0, 0.0)
details = {}
stop_hold = active and speed_ms < 0.3
if stop_hold:
# The moving inverse divides by v**2. At a stop retain the last actual
# active packet, not an inactive/zero request or a fictitious speed.
# A disengagement/fault packet clears this naturally via record_sent.
command = self.sent[:2] if self.sent[2] else command
self.last_target = target
self.requested_rate = 0.0
elif active:
try:
speed, angle, yaw = self.measurement
if self.last_target is not None and 0.0 < dt <= 0.1:
rate = (target - self.last_target) / dt
self.requested_rate += dt / (0.1 + dt) * (rate - self.requested_rate)
self.last_target = target
error = target - angle
settling = abs(error) <= ANGLE_TRIM_ERROR_MAX and abs(self.wheel_rate) <= ANGLE_TRIM_RATE_MAX
# Brief wheel motion freezes learning but does not invalidate a learned
# bias. Clear opposing trim for a large error or a fast-moving request.
release = abs(error) > ANGLE_TRIM_ERROR_MAX or abs(self.requested_rate) > ANGLE_TRIM_RATE_MAX
if not CS.steeringPressed and release and error * self.angle_trim < 0.0:
self.angle_trim = 0.0
trimmed_target = target + self.angle_trim
entry_lead, cue = 0.0, 0.0
if self.turn_preview and not CS.steeringPressed and not (status_fresh and pscm_status.limit >= 2):
from openpilot.selfdrive.controls.lib.ford_turn_preview import geometry_entry_lead, turn_preview_lead
entry_lead, cue = turn_preview_lead(self.requested_rate, turn_preview, now)
independent = geometry_entry_lead(trimmed_target, self.requested_rate, turn_preview, now)
if abs(independent) > abs(entry_lead):
entry_lead = independent
signal = int(CS.leftBlinker) - int(CS.rightBlinker)
if signal * entry_lead <= 0.0:
entry_lead = 0.0
inverse_target = trimmed_target + entry_lead
inverse = invert_angle(self.angle, speed, inverse_target, angle, yaw, yaw * speed / 3.6, self.wheelbase, self.ratio)
# Firmware phase is estimated from elapsed time. Cover the 1/2 firmware
# ticks before the next nominal 100 Hz transmit; all phases are tested.
ticks = max(1, min(2, int((self.phase + 0.01 + 1e-12) / 0.008)))
command, info = self.encoder.choose(
speed, inverse['curvature'], phase=0 if ticks == 2 else 2,
curvature_rate=self.requested_rate / inverse['slope_per_curvature'], preview=TARGET_PREVIEW,
curvature_bound=inverse['allowance'] / (speed / 3.6)**2,
)
base_command = command
entry_weight = 0.0
if self.turn_entry_assist and not CS.steeringPressed and not inverse['accel_limited'] and not (status_fresh and pscm_status.limit >= 2):
entry_weight = turn_entry_weight(target, angle, self.requested_rate)
if entry_weight:
extra = math.copysign(entry_weight, target)
command = quantize((command[0] + extra * TURN_ENTRY_C0_MAX, command[1] + extra * TURN_ENTRY_C1_MAX))
# Apply after selection so nominal slew equivalence cannot discard the
# added request. record_sent/advance still observe the actual packets;
# their retained state can affect later commands after this term clears.
details = {
'geometry_lead': entry_lead,
'geometry_cue': cue,
'inverse_target': inverse_target,
'base_wire_command': tuple(map(float, base_command)),
'turn_entry_weight': entry_weight,
'turn_entry_c0': float(command[0] - base_command[0]),
'turn_entry_c1': float(command[1] - base_command[1]),
'requested_angle': target,
'trimmed_angle': trimmed_target,
'angle_trim': self.angle_trim,
'wheel_rate': self.wheel_rate,
'requested_rate': self.requested_rate,
'target_preview_seconds': TARGET_PREVIEW,
'target_preview_delta': self.requested_rate * TARGET_PREVIEW,
'target_preview_limited': info['preview_limited'],
'reachable_angle': inverse['reachable_target'],
'target_curvature': inverse['curvature'],
'predicted_curvature': float(info['first_state'][3]), # Encoder prediction before optional entry assist.
'accel_limited': inverse['accel_limited'],
}
# Learn only small residual errors while the wheel is settling. Large
# entry lag and saturated actuation must not wind up a centering trim.
# Opposing error can still bleed existing trim toward zero. A light
# touch freezes learning; it does not cut the base command or held trim.
limited = inverse['accel_limited'] or (status_fresh and pscm_status.limit == 2) or abs(command[0]) >= C0_BOUND or abs(command[1]) >= C1_BOUND
learning = CS.vEgo >= 2.0 and settling and not limited
if not CS.steeringPressed:
increment = ANGLE_TRIM_KI * max(-ANGLE_TRIM_MAX, min(ANGLE_TRIM_MAX, error)) * dt
if not learning:
increment = max(min(-self.angle_trim, 0.0), min(max(-self.angle_trim, 0.0), increment))
self.angle_trim = max(-ANGLE_TRIM_MAX, min(ANGLE_TRIM_MAX, self.angle_trim + increment))
details['trim_learning'] = bool(learning and not CS.steeringPressed)
except (ValueError, OverflowError, ArithmeticError):
self.fault = 'invalid_prediction'
self.angle_trim = 0.0
active = False
command = (0.0, 0.0)
# The inverse operates in CAN/pinion coordinates. CarController negates
# FordLateralPath on TX, so negate here exactly once to retain the tested sign.
path.pathOffset, path.pathAngle = -float(command[0]), -float(command[1])
path.curvature = path.curvatureRate = 0.0
path.valid = active
cc = CC.as_reader().as_builder() if hasattr(CC, 'as_reader') else CC.as_builder()
cc.latActive = active
self.diagnostics = {
'hypothesis': 'ford-joint-geometry-entry-v2' if self.turn_preview else ('ford-joint-turn-entry-v1' if self.turn_entry_assist else 'ford-joint-v24'),
'turn_preview_enabled': self.turn_preview,
'turn_entry_enabled': self.turn_entry_assist,
'turn_entry_weight': 0.0,
'turn_entry_c0': 0.0,
'turn_entry_c1': 0.0,
'status': self.fault or ('active' if active else 'inactive'),
'driver_override': override,
'driver_pressed': bool(CS.steeringPressed),
'stop_hold': bool(stop_hold),
'allocation': 'equal-arrival-supervisor-c0',
'yaw_source': 'calibrated_pose',
'yaw_rate': yaw if math.isfinite(yaw) else None,
'can_yaw_rate': float(CS.yawRate) if math.isfinite(CS.yawRate) else None,
'held_c0': self.request.c0,
'held_c1': self.request.c1,
'filtered_curvature': self.request.filtered,
'wire_command': tuple(map(float, command)),
**details,
}
# Preserve card's reader contract: CarController copies actuators with
# as_builder(), which is only available on a Cap'n Proto reader.
return cc.as_reader()
def record_sent(self, can_sends, now_nanos):
packets = [msg for msg in can_sends if msg[0] == 0x3D6 and msg[2] == self.bus]
if not packets:
return
self.parser.update([now_nanos, packets])
values = self.parser.vl['LateralMotionControl2']
if values['LatCtlCurv_No_Actl'] != 0.0 or values['LatCtlCrv_NoRate2_Actl'] != 0.0:
self.fault = 'unexpected_curvature_channel'
self.sent = (values['LatCtlPathOffst_L_Actl'], values['LatCtlPath_An_Actl'], values['LatCtl_D2_Rq'] == 2)
self.last_sent_time = now_nanos * 1e-9
self.diagnostics['wire_sent'] = self.sent
@@ -0,0 +1,36 @@
"""Publish the Ford PSCM's actual CAN status without changing opendbc structs."""
import math
from opendbc.car import Bus
from opendbc.car.ford.values import FordFlags
MESSAGE = 'Lane_Assist_Data3_FD1'
SIGNALS = ('LatCtlSte_D_Stat', 'LatCtlLim_D_Stat', 'LatCtlCpblty_D_Stat', 'LaActDeny_B_Actl')
def populate_ford_pscm_status(CP, can_parsers, CS_SP, can_valid):
if CP.brand != 'ford' or not CP.flags & FordFlags.CANFD:
return
status = CS_SP.init('fordPscmStatus')
parser = can_parsers.get(Bus.pt)
if parser is None:
return
values = parser.vl.get(MESSAGE, {})
timestamps = parser.ts_nanos.get(MESSAGE, {})
if any(signal not in values or signal not in timestamps for signal in SIGNALS):
return
received = timestamps[SIGNALS[0]]
if received <= 0 or any(timestamps[signal] != received for signal in SIGNALS):
return
decoded = [values[signal] for signal in SIGNALS]
if any(not math.isfinite(value) or int(value) != value or not 0 <= value <= maximum
for value, maximum in zip(decoded, (7, 3, 3, 1), strict=True)):
return
status.canMonoTime = received
status.lateralState, status.limit, status.capability = map(int, decoded[:3])
status.denied = bool(decoded[3])
# CI.update already checked all parser validity. Reading can_valid again here
# would advance the parser's invalid-message counter a second time per tick.
# Age is evaluated by the feedback consumer using this original CAN timestamp.
status.valid = bool(can_valid)
+3
View File
@@ -54,6 +54,8 @@ def convert_carControlSP(struct: capnp.lib.capnp._DynamicStructReader) -> struct
return {k: v for k, v in s.items() if not k.endswith('DEPRECATED')}
struct_dict = struct.to_dict()
# Consumed by card's Ford adapter, not by opendbc or the CAN packer.
struct_dict.pop('fordTurnPreview', None)
struct_dataclass = structs.CarControlSP(**remove_deprecated({k: v for k, v in struct_dict.items() if not isinstance(k, dict)}))
struct_dataclass.mads = structs.ModularAssistiveDrivingSystem(**remove_deprecated(struct_dict.get('mads', {})))
@@ -63,5 +65,6 @@ def convert_carControlSP(struct: capnp.lib.capnp._DynamicStructReader) -> struct
struct_dataclass.intelligentCruiseButtonManagement = structs.IntelligentCruiseButtonManagement(
**remove_deprecated(struct_dict.get('intelligentCruiseButtonManagement', {}))
)
struct_dataclass.fordLateralPath = structs.FordLateralPath(**remove_deprecated(struct_dict.get('fordLateralPath', {})))
return struct_dataclass
@@ -0,0 +1,47 @@
"""Native encoder calls must release their arguments with card's GC disabled."""
import ctypes
import gc
import weakref
import numpy as np
import pytest
from openpilot.selfdrive.controls.lib.ford_joint.encoder import DoubleArray, PairedRelease
from openpilot.selfdrive.controls.lib.ford_joint.model import MainRequest
@pytest.mark.parametrize('preview', [0., .1])
def test_native_arguments_do_not_accumulate_without_gc(preview):
encoder = PairedRelease(MainRequest(native_lookup=True))
kwargs = {'curvature_rate': .01, 'preview': preview, 'curvature_bound': .04}
encoder.choose(20., .01, **kwargs)
enabled = gc.isenabled()
gc.collect()
gc.disable()
try:
before = sum(isinstance(obj, ctypes.c_void_p) for obj in gc.get_objects())
for _ in range(500):
encoder.choose(20., .01, **kwargs)
after = sum(isinstance(obj, ctypes.c_void_p) for obj in gc.get_objects())
assert after <= before, f'{after - before} native pointer objects retained in 500 control updates'
finally:
gc.collect()
if enabled:
gc.enable()
@pytest.mark.parametrize('value', [[1., 2.], np.ones(3, dtype=np.float32), np.ones(6)[::2]])
def test_native_array_validation_is_retained(value):
with pytest.raises(TypeError):
DoubleArray.from_param(value)
def test_native_argument_owns_array_only_until_call_finishes():
value = np.array([1., 2.])
ref = weakref.ref(value)
pointer = DoubleArray.from_param(value)
assert pointer.value == value.ctypes.data
del value
assert ref() is not None
del pointer
assert ref() is None
@@ -0,0 +1,103 @@
"""A field reaching its bound must not discard a jointly reachable target."""
import pytest
from openpilot.selfdrive.controls.lib.ford_joint.angle import AngleModel
from openpilot.selfdrive.controls.lib.ford_joint.encoder import PairedRelease
from openpilot.selfdrive.controls.lib.ford_joint.inverse import C0_BOUND, C0_SUPERVISOR_SATURATION, C1_BOUND, invert_angle, static_pair
from openpilot.selfdrive.controls.lib.ford_joint.model import MainRequest
@pytest.mark.parametrize('sign', [-1., 1.])
def test_joint_encoder_preserves_large_reachable_target(sign):
request = MainRequest(native_lookup=True)
speed = 15 * 1.609344
inverse = invert_angle(AngleModel(request.cal), speed, sign * 280., 0., 0., 0., 3.7, 16.9)
assert not inverse['accel_limited']
probe = MainRequest(native_lookup=True).step(speed, 0., 0., freeze_i=True)
c0, c1 = static_pair(request, inverse['curvature'], speed)
assert abs(c1) <= C1_BOUND
assert abs(c0) <= C0_BOUND
assert probe['g0'] * c0 + probe['g1'] * c1 == pytest.approx(inverse['curvature'], abs=1e-12)
_, info = PairedRelease(request).choose(speed, inverse['curvature'])
# The planned target is the CAN-quantized steady pair, not next-tick wheel motion.
quantization_error = probe['g0'] * .005 + probe['g1'] * .00025
assert abs(info['planned_target'] - inverse['curvature']) <= quantization_error + 1e-12
@pytest.mark.parametrize('speed', [8., 15., 19.312128, 24.14016, 28.968192, 50., 100., 180.])
@pytest.mark.parametrize('fraction', [-2., -1., -.8, -.3, 0., .3, .8, 1., 2.])
def test_static_allocation_uses_available_combined_field_range(speed, fraction):
request = MainRequest(native_lookup=True)
probe = request.step(speed, 0., 0., freeze_i=True)
g0, g1 = probe['g0'], probe['g1']
capacity = g0 * C0_BOUND + g1 * C1_BOUND
c0, c1 = static_pair(request, fraction * capacity, speed)
assert abs(c0) <= C0_BOUND and abs(c1) <= C1_BOUND
expected = max(-capacity, min(capacity, fraction * capacity))
assert g0 * c0 + g1 * c1 == pytest.approx(expected, abs=1e-12)
@pytest.mark.parametrize('sign', [-1., 1.])
def test_unsaturated_pair_reaches_both_endpoints_together(sign):
# Below the supervisor's C0 saturation the endpoint is still equal arrival.
request = MainRequest(native_lookup=True)
speed = 15 * 1.609344
inverse = invert_angle(AngleModel(request.cal), speed, sign * 40., 0., 0., 0., 3.7, 16.9)
c0, c1 = static_pair(request, inverse['curvature'], speed)
assert abs(c0) < C0_SUPERVISOR_SATURATION
assert c0 / request.cal.f(0xFEF259F8) == pytest.approx(c1 / request.cal.f(0xFEF25A08))
probe = request.step(speed, 0., 0., freeze_i=True)
assert probe['g0'] * c0 + probe['g1'] * c1 == pytest.approx(inverse['curvature'])
@pytest.mark.parametrize('speed', [8., 15., 19.312128, 24.14016, 28.968192, 50., 100., 180.])
@pytest.mark.parametrize('fraction', [.05, .2, .4, .6, .8, .95, 1.])
@pytest.mark.parametrize('sign', [-1., 1.])
def test_endpoint_holds_c0_at_supervisor_saturation(speed, fraction, sign):
request = MainRequest(native_lookup=True)
probe = request.step(speed, 0., 0., freeze_i=True)
g0, g1 = probe['g0'], probe['g1']
curvature = sign * fraction * (g0 * C0_BOUND + g1 * C1_BOUND)
c0, c1 = static_pair(request, curvature, speed)
assert g0 * c0 + g1 * c1 == pytest.approx(curvature, abs=1e-12)
# Held C0 exceeds the supervisor saturation only when C1 alone cannot carry the rest.
assert abs(c0) <= C0_SUPERVISOR_SATURATION + 1e-12 or abs(c1) == C1_BOUND
assert static_pair(request, -curvature, speed) == pytest.approx((-c0, -c1), abs=1e-15)
def _equal_arrival(request, curvature, speed_kmh, *, gains):
r0, r1 = request.cal.f(0xFEF259F8), request.cal.f(0xFEF25A08)
duration = curvature / (gains[0] * r0 + gains[1] * r1)
return duration * r0, duration * r1
@pytest.mark.parametrize('speed,curvature', [(15., .06), (20., .05), (25., .04)])
def test_capped_endpoint_keeps_entry_and_parks_c0_before_release(monkeypatch, speed, curvature):
"""Production selector, quantized packets: same rise, less held C0 at the end of the hold."""
from openpilot.selfdrive.controls.lib.ford_joint import encoder
from openpilot.selfdrive.controls.lib.ford_joint.inverse import quantize
def run():
m = MainRequest(native_lookup=True)
selector = PairedRelease(m)
sent, phase, rise = (0., 0.), 0., None
for i in range(500): # 5 s step-and-hold at 100 Hz
phase += .01
ticks = int((phase + 1e-12) / .008)
phase -= ticks * .008
for _ in range(ticks):
m.step(speed, *sent, freeze_i=True)
if rise is None and m.filtered >= .9 * curvature:
rise = i * .01
count = max(1, min(2, int((phase + .01 + 1e-12) / .008)))
command, _ = selector.choose(speed, curvature, phase=0 if count == 2 else 2)
sent = quantize(command)
return rise, m.c0, m.filtered
capped = run()
monkeypatch.setattr(encoder, 'static_pair', _equal_arrival)
equal = run()
assert capped[0] == pytest.approx(equal[0], abs=.02)
assert capped[2] == pytest.approx(curvature, rel=.01)
assert abs(capped[1]) <= C0_SUPERVISOR_SATURATION + .15 < abs(equal[1])
@@ -0,0 +1,553 @@
"""Exercise the opt-in trial through the production Ford CAN packer."""
import copy
import itertools
import json
import math
from collections import defaultdict
from types import SimpleNamespace
import numpy as np
import pytest
from opendbc.car import Bus, structs
from opendbc.car.ford.carcontroller import CarController
from opendbc.car.ford.values import FordFlags
from opendbc.car.interfaces import CarInterfaceBase
from openpilot.common.params import Params, ParamKeyFlag
from openpilot.selfdrive.car.ford_joint_control import ANGLE_TRIM_MAX, FordJointControl, joint_control_enabled, select_joint_control
from openpilot.selfdrive.controls.lib.ford_joint.angle import AngleModel
from openpilot.selfdrive.controls.lib.ford_joint.encoder import PairedRelease, state
from openpilot.selfdrive.controls.lib.ford_joint.inverse import invert_angle
from openpilot.selfdrive.controls.lib.ford_joint.model import MainRequest
from openpilot.selfdrive.controls.tests.test_ford_model_action_adapter import update
from openpilot.selfdrive.controls.tests.test_ford_model_action_selection import car_params, startup
def cp():
return structs.CarParams(brand='ford', flags=int(FordFlags.CANFD), carFingerprint='FORD_F_150_LIGHTNING_MK1', wheelbase=3.7, steerRatio=16.9)
def controls(target=30., active=True):
cc = structs.CarControl(latActive=active, longActive=True)
cc.angularVelocity = [0., 0., 0.]
cc.actuators.steeringAngleDeg = target
cc.actuators.accel = .3
sp = structs.CarControlSP()
sp.fordLateralPath.enabled = sp.fordLateralPath.valid = True
return cc, sp
class Pipeline:
def __init__(self):
self.joint = FordJointControl(cp())
self.sender = CarController({Bus.pt: 'ford_lincoln_base_pt'}, cp(), structs.CarParamsSP())
self.cs = structs.CarState(vEgo=5.36, vEgoRaw=5.36, canValid=True)
self.vehicle = SimpleNamespace(out=self.cs, acc_tja_status_stock_values=defaultdict(int),
lkas_status_stock_values=defaultdict(int), buttons_stock_values=defaultdict(int))
self.interface = SimpleNamespace(CC=self.sender, CS=self.vehicle)
def tick(self, now, target=30., active=True, *, angular_velocity=None, **kwargs):
cc, sp = controls(target, active)
cc.angularVelocity = [0., 0., -self.cs.yawRate] if angular_velocity is None else angular_velocity
result = self.joint.prepare(cc.as_reader(), sp, self.cs, now, **kwargs)
_, packets = CarInterfaceBase.apply(self.interface, result, sp, round(now * 1e9))
assert sum(p[0] == 0x3d6 for p in packets) == 1
self.joint.record_sent(packets, round(now * 1e9))
assert result.longActive and result.actuators.accel == cc.actuators.accel
wire = self.joint.parser.vl['LateralMotionControl2']
assert wire['LatCtlCurv_No_Actl'] == wire['LatCtlCrv_NoRate2_Actl'] == 0.
assert abs(wire['LatCtlPathOffst_L_Actl']) <= 5.11 + 1e-10
assert abs(wire['LatCtlPath_An_Actl']) <= .5 + 1e-10
assert wire['LatCtl_D2_Rq'] == (2 if result.latActive else 0)
assert self.joint.sent == pytest.approx((*self.joint.diagnostics['wire_command'], result.latActive))
return result
@pytest.mark.parametrize('master,trial,geometry,joystick', itertools.product((False, True), repeat=4))
@pytest.mark.parametrize('compatible', [False, True])
def test_startup_gate_matches_both_processes(master, trial, geometry, joystick, compatible):
params = SimpleNamespace(get_bool=lambda k: {'FordModelActionController': master, 'FordPscmJointControl': trial,
'FordGeometryReference': geometry, 'JoystickDebugMode': joystick}.get(k, False))
params_cp = car_params(flags=FordFlags.CANFD if compatible else 0)
expected = compatible and master and trial and not geometry and not joystick
assert joint_control_enabled(params_cp, params) == expected
selected = startup(params_cp, params).ford_path_controller
assert bool(selected and selected.joint_control) == expected
if not expected:
assert select_joint_control(params_cp, params) is None
if not master:
assert selected is None
def test_parameter_is_persistent_default_off(tmp_path):
params = Params(str(tmp_path))
assert params.get_default_value('FordPscmJointControl') is False
assert not params.get_bool('FordPscmJointControl')
for flag in (ParamKeyFlag.PERSISTENT, ParamKeyFlag.BACKUP):
assert b'FordPscmJointControl' in params.all_keys(flag)
def test_reference_carrier_adds_no_second_feedback_loop():
from openpilot.selfdrive.controls.lib.ford_model_action import FordModelActionController
c = FordModelActionController(joint_control=True, c0_time_based=True)
for i in range(100):
p = update(c, now=1+i*.01, current_curvature=-.005)
assert p.valid and p.path_offset == p.path_angle == p.curvature == p.curvature_rate == 0.
assert c.core.correction == c.core.proportional == c.core.offset_proportional == 0.
assert not c.set_c0_time_based(True, lateral_engaged=False)
def test_joint_reference_carrier_stays_valid_through_stop():
from openpilot.selfdrive.controls.lib.ford_model_action import FordModelActionController
c = FordModelActionController(joint_control=True)
for i, speed in enumerate([5., .3, .29, .01, 0., 0., .01, .29, .3, 5.]):
path = update(c, now=1+i*.01, speed=speed)
assert path.valid, (speed, c.diagnostics)
assert path.path_offset == path.path_angle == 0.
assert not update(c, now=1.1, speed=0., valid=False).valid
@pytest.mark.parametrize('sign', [-1., 1.])
def test_stop_repeats_transmitted_request_and_resume_keeps_target(sign):
p = Pipeline()
p.cs.steeringAngleDeg = sign*120.
p.cs.yawRate = p.cs.vEgo*math.radians(sign*120.)/(3.7*16.9)
for i in range(200):
p.tick(1+i*.01, sign*120.)
sent = p.joint.sent
assert sent[2] and any(abs(v) > .01 for v in sent[:2])
p.joint.angle_trim = sign*.5
for i, speed in enumerate([.29, .01]+[0.]*200+[.01, .29]):
p.cs.vEgo = speed
p.cs.yawRate = speed*math.radians(sign*120.)/(3.7*16.9)
assert p.tick(3+i*.01, sign*120.).latActive
assert p.joint.sent == sent
assert p.joint.angle_trim == sign*.5
assert not p.joint.fault
json.dumps(p.joint.diagnostics, allow_nan=False)
p.cs.vEgo = .3
p.cs.yawRate = .3*math.radians(sign*120.)/(3.7*16.9)
assert p.tick(5.04, sign*120.).latActive
assert p.joint.diagnostics['requested_angle'] == sign*120.
assert p.joint.diagnostics['requested_rate'] == 0.
assert not p.joint.fault
@pytest.mark.parametrize('sign', [-1., 1.])
def test_stop_speed_filter_undershoot_keeps_actual_transmitted_hold(sign):
p = Pipeline()
p.cs.gearShifter = structs.CarState.GearShifter.drive
for i in range(30):
p.tick(1+i*.01, sign*120.)
sent = p.joint.sent
assert sent[2] and any(abs(v) > .01 for v in sent[:2])
p.cs.vEgoRaw, p.cs.standstill = 0., True
for i, speed in enumerate([.01, 0., -.01, -.04, -.01, 0., .01, .29]):
p.cs.vEgo = speed
assert p.tick(1.3+i*.01, sign*120.).latActive
assert p.joint.sent == sent
assert p.joint.diagnostics['stop_hold']
p.cs.vEgo = p.cs.vEgoRaw = .3
assert p.tick(1.38, sign*120.).latActive
assert p.joint.sent[0]*sign > 0 and p.joint.sent[1]*sign > 0
p.cs.gearShifter = structs.CarState.GearShifter.reverse
assert not p.tick(1.39, sign*120.).latActive
assert p.joint.sent == (0., 0., False)
@pytest.mark.parametrize('failure', ['disengage', 'stale', 'can', 'steering_fault', 'override', 'denied'])
def test_stopped_hold_still_releases_and_does_not_resurrect_old_command(failure):
p = Pipeline()
for i in range(20):
p.tick(1+i*.01, 120.)
previous = p.joint.sent
p.cs.vEgo = 0.
assert p.tick(1.2, 120.).latActive
assert p.joint.sent == previous
p.cs.canValid = failure != 'can'
p.cs.steerFaultTemporary = failure == 'steering_fault'
status = SimpleNamespace(valid=True, canMonoTime=1_210_000_000, limit=3 if failure == 'override' else 0, denied=failure == 'denied')
assert not p.tick(1.21, 120., active=failure != 'disengage', fresh=failure != 'stale', pscm_status=status).latActive
assert p.joint.sent == (0., 0., False)
p.cs.canValid, p.cs.steerFaultTemporary = True, False
assert p.tick(1.22, 120.).latActive
assert p.joint.sent == (0., 0., True)
def test_can_yaw_zero_offset_does_not_bias_the_command():
reference, biased = Pipeline(), Pipeline()
biased.cs.yawRate = -.008 # Route 175: raw CAN disagrees with calibrated yaw.
for i in range(200):
now = 1. + i*.01
reference.tick(now, 0., angular_velocity=[0., 0., 0.])
biased.tick(now, 0., angular_velocity=[0., 0., 0.])
assert biased.joint.sent == reference.joint.sent
assert biased.joint.measurement[2] == 0.
def test_calibrated_yaw_sign_matches_pinion_coordinates():
p = Pipeline()
p.cs.yawRate = .012
p.tick(1., angular_velocity=[.01, .02, -.02])
assert p.joint.measurement[2] == pytest.approx(.02)
@pytest.mark.parametrize('sign', [-1., 1.])
def test_persistent_small_wheel_error_builds_bounded_correction(sign):
# Route 17a: a steady request ~1.5 degrees from the wheel, no driver or
# PSCM limit. The nominal encoder alone has no way to remove that residual.
p = Pipeline()
p.cs.vEgo = 20.
before = None
for i in range(601):
p.tick(1. + i*.01, sign*1.5)
if i == 100:
before = p.joint.diagnostics['target_curvature']
assert sign*(p.joint.diagnostics['target_curvature'] - before) > 0.00005
assert 1.7 < sign*p.joint.angle_trim < ANGLE_TRIM_MAX
@pytest.mark.parametrize('bias', [-1.5, 1.5])
def test_trim_removes_static_mismatch_in_synthetic_angle_plant(bias):
# A regression for residual feedback, NOT a validated PSCM/wheel simulation.
# Use the actual packed-command observer and angle stage, then a deliberately
# simple 0.4 s wheel lag with a known additive mismatch.
p = Pipeline()
p.cs.vEgo = 20.
angle = rate = 0.
errors = []
for i in range(3000):
p.cs.steeringAngleDeg, p.cs.steeringRateDeg = angle, rate
p.cs.yawRate = math.radians(angle)/16.9*20./(3.7+(33/16384)*20**2)
p.tick(1.+i*.01, 0.)
nominal = copy.copy(p.joint.angle).step(72., p.joint.diagnostics['predicted_curvature'], angle,
p.cs.yawRate, p.cs.yawRate*20., 3.7, 16.9)
rate = (nominal+bias-angle)/.4
angle += rate*.01
errors.append(angle)
assert np.mean(np.abs(errors[-500:])) < .1
@pytest.mark.parametrize('target,rate,speed', [(60., 0., 5.), (1.5, 20., 5.), (1.5, 0., 1.)])
def test_trim_does_not_learn_large_lag_fast_motion_or_crawl(target, rate, speed):
p = Pipeline()
p.cs.vEgo = speed
for i in range(200):
# Real Ford CarState leaves steeringRateDeg at zero, even during motion.
p.cs.steeringAngleDeg = rate*i*.01
p.tick(1.+i*.01, target+p.cs.steeringAngleDeg)
assert abs(p.joint.angle_trim) < .015 # Only the derivative's initial settling.
if rate:
assert p.joint.wheel_rate == pytest.approx(rate, abs=.01)
assert not p.joint.diagnostics['trim_learning']
def test_trim_bounded_retained_at_zero_error_and_frozen_on_light_touch():
p = Pipeline()
for i in range(1201):
p.tick(1.+i*.01, 2.)
assert abs(p.joint.angle_trim) <= ANGLE_TRIM_MAX
assert p.joint.angle_trim == ANGLE_TRIM_MAX
p.cs.steeringPressed = True
assert p.tick(13.01, -2.).latActive
assert p.joint.angle_trim == ANGLE_TRIM_MAX
p.cs.steeringPressed = False
p.tick(13.02, 0.)
assert p.joint.angle_trim == ANGLE_TRIM_MAX
assert not p.tick(13.03, active=False).latActive
assert p.joint.angle_trim == 0.
@pytest.mark.parametrize('limit,denied', [(3, False), (0, True)])
def test_explicit_override_resets_trim(limit, denied):
p = Pipeline()
p.joint.angle_trim = 1.
status = SimpleNamespace(valid=True, canMonoTime=1_000_000_000, limit=limit, denied=denied)
assert not p.tick(1., 1.5, pscm_status=status).latActive
assert p.joint.angle_trim == 0.
def test_limit_reached_does_not_grow_trim_but_allows_relief():
p = Pipeline()
p.joint.angle_trim = 1.
for i in range(101):
now = 1.+i*.01
status = SimpleNamespace(valid=True, canMonoTime=round(now*1e9), limit=2, denied=False)
assert p.tick(now, 1.5, pscm_status=status).latActive
assert p.joint.angle_trim == 1.
p.cs.steeringAngleDeg = 3.
p.tick(2.01, 1.5, pscm_status=status)
assert 0.99 < p.joint.angle_trim < 1.
def test_acceleration_clipping_does_not_grow_trim():
p = Pipeline()
p.cs.vEgo = 50.
p.cs.steeringAngleDeg = 200.
for i in range(100):
p.tick(1.+i*.01, 201.5)
assert p.joint.diagnostics['accel_limited']
assert p.joint.angle_trim == 0.
@pytest.mark.parametrize('sign', [-1., 1.])
def test_small_wheel_motion_does_not_discard_steady_request_trim(sign):
# Route 17c, 1461 s: a ~1-degree trim disappears when a brief wheel
# movement crosses the target, although the requested angle barely changes.
p = Pipeline()
p.cs.vEgo = 24.
p.cs.steeringAngleDeg = sign * -2.1
for i in range(60):
p.tick(1. + i * .01, sign * -1.3)
p.joint.angle_trim = sign * .985
p.cs.steeringAngleDeg = sign * -.8
assert p.tick(1.6, sign * -1.3).latActive
assert abs(p.joint.wheel_rate) > 5.
assert not p.joint.diagnostics['trim_learning']
assert p.joint.diagnostics['angle_trim'] == pytest.approx(sign * .985)
assert .98 < sign * p.joint.angle_trim < .985 # Opposing error can bleed it.
@pytest.mark.parametrize('sign', [-1., 1.])
def test_fast_request_releases_opposing_trim_before_wheel_moves(sign):
p = Pipeline()
for i in range(60):
p.tick(1. + i * .01, 0.)
p.joint.angle_trim = sign
request = sign * -2.
p.tick(1.6, request)
assert p.joint.wheel_rate == 0.
assert p.joint.diagnostics['angle_trim'] == 0.
assert p.joint.diagnostics['trimmed_angle'] == request
def test_inactive_request_history_cannot_clear_trim_after_reengagement():
p = Pipeline()
p.tick(1., 0.)
p.tick(1.01, 60.)
assert abs(p.joint.requested_rate) > 5.
p.tick(1.02, 0., active=False)
assert p.joint.requested_rate == 0. and p.joint.last_target is None
p.tick(1.03, 1.)
p.joint.angle_trim = -.5
p.tick(1.04, 1.)
assert p.joint.diagnostics['angle_trim'] == -.5
@pytest.mark.parametrize('target,rate', [(-60., 0.), (-2., -30.)])
def test_reversal_or_fast_unwind_clears_old_trim_before_allocation(target, rate):
p = Pipeline()
for i in range(10):
p.cs.steeringAngleDeg = rate*i*.01
p.tick(1.+i*.01, target+p.cs.steeringAngleDeg)
p.joint.angle_trim = ANGLE_TRIM_MAX
for i in range(10, 101):
p.cs.steeringAngleDeg = rate*i*.01
request = target+p.cs.steeringAngleDeg
p.tick(1.+i*.01, request)
assert p.joint.angle_trim == 0.
assert p.joint.diagnostics['trimmed_angle'] == pytest.approx(request, abs=1e-5)
@pytest.mark.parametrize('angular_velocity', [[], [0., 0.], [0., 0., math.nan], [0., 0., math.inf], [0., 0., 3.1]])
def test_missing_or_invalid_calibrated_yaw_does_not_fall_back_to_raw_can(angular_velocity):
p = Pipeline()
p.tick(1.)
assert not p.tick(1.01, angular_velocity=angular_velocity).latActive
assert p.joint.sent == (0., 0., False)
assert p.tick(1.02).latActive
def test_touch_does_not_zero_the_command_or_reset_held_state():
reference, touched = Pipeline(), Pipeline()
for i in range(160):
touched.cs.steeringPressed = 40 <= i < 50 or 100 <= i < 115
target = 60. if i < 100 else -30.
reference.tick(1. + i*.01, target)
assert touched.tick(1. + i*.01, target).latActive
assert touched.joint.sent == reference.joint.sent
np.testing.assert_array_equal(state(touched.joint.request), state(reference.joint.request))
@pytest.mark.parametrize('sign', [-1., 1.])
def test_can_coordinates_entry_reversal_release_and_100hz(sign):
p = Pipeline()
for i in range(250):
target = sign * (30. if i < 80 else -30. if i < 160 else 0.)
assert p.tick(1+i*.01, target).latActive
if i == 0:
# The existing sender negates OP path fields; the adapter compensates once.
assert sign * p.joint.sent[0] > 0 and sign * p.joint.sent[1] > 0
json.dumps(p.joint.diagnostics, allow_nan=False)
assert not p.joint.fault
def test_observer_follows_packed_command_only_and_inactive_slew():
p = Pipeline()
cc, sp = controls()
before = state(p.joint.request).copy()
p.joint.prepare(cc, sp, p.cs, 1.) # An untransmitted candidate is not held input.
p.joint.advance(1.01)
np.testing.assert_array_equal(state(p.joint.request), before)
for i in range(100):
p.tick(1.02+i*.01, 60.)
held = copy.copy(p.joint.request)
result = p.tick(2.02, 60., active=False)
assert not result.latActive and p.joint.sent == (0., 0., False)
assert abs(p.joint.request.c0) > 0 or abs(p.joint.request.c1) > 0
held = copy.copy(p.joint.request)
phase = p.joint.phase
p.tick(2.03, 60., active=False)
ticks = int((phase+.01+1e-12)/.008)
for _ in range(ticks):
held.step(5.36*3.6, 0., 0., active=False, freeze_i=True)
np.testing.assert_allclose(state(p.joint.request), state(held), atol=1e-12)
assert p.tick(2.04, -30.).latActive
assert p.joint.diagnostics['requested_angle'] == -30.
@pytest.mark.parametrize('limit,denied,active', [(0, False, True), (2, False, True), (3, False, False), (0, True, False)])
def test_limit_reached_does_not_freeze_but_override_inhibits(limit, denied, active):
p = Pipeline()
status = SimpleNamespace(valid=True, canMonoTime=1_000_000_000, limit=limit, denied=denied)
assert p.tick(1., pscm_status=status).latActive == active
@pytest.mark.parametrize('field,value', [('vEgo', -.01), ('vEgo', 56.), ('yawRate', 3.1),
('steeringAngleDeg', math.nan), ('vEgo', math.inf), ('canValid', False),
('steerFaultTemporary', True), ('steerFaultPermanent', True)])
def test_bad_measurements_never_send_previous_request(field, value):
p = Pipeline()
p.tick(1.)
setattr(p.cs, field, value)
assert not p.tick(1.01).latActive
assert p.joint.sent == (0., 0., False)
json.dumps(p.joint.diagnostics, allow_nan=False)
@pytest.mark.parametrize('target', [math.nan, math.inf, -math.inf])
def test_nonfinite_target_and_stale_control_are_inactive(target):
p = Pipeline()
assert not p.tick(1., target).latActive
assert not p.tick(1.01, fresh=False).latActive
assert p.tick(1.02).latActive
@pytest.mark.parametrize('next_time', [.9, 1.101])
def test_unknown_clock_history_latches_inactive(next_time):
p = Pipeline()
p.tick(1.)
assert not p.tick(next_time).latActive
assert not p.tick(next_time+.01).latActive
assert p.joint.fault
def test_angle_inverse_matches_forward_stage_across_speeds():
rng = np.random.default_rng(418)
for _ in range(80):
m = AngleModel(MainRequest().cal)
m.yaw_acc, m.bank_residual, m.angle_bias = rng.uniform(-1., 1., 3)
speed = float(rng.uniform(1.1, 180.))
target, angle = map(float, rng.uniform(-300., 300., 2))
yaw = float(rng.uniform(-.5, .5))
result = invert_angle(m, speed, target, angle, yaw, yaw*speed/3.6, 3.7, 16.9)
actual = m.step(speed, result['curvature'], angle, yaw, yaw*speed/3.6, 3.7, 16.9)
assert actual == pytest.approx(result['reachable_target'], abs=1e-9)
assert abs(result['bounded_accel']) <= result['allowance']
def test_candidate_does_not_mutate_state_and_native_step_matches_python():
rng = np.random.default_rng(919)
for j in range(40):
m = MainRequest(native_lookup=True)
m.c0, m.c1, m.filtered = map(float, rng.uniform([-5., -.49, -.08], [5., .49, .08]))
m.fast = bool(j % 2)
speed, curvature = float(rng.uniform(1.1, 180.)), float(rng.uniform(-.1, .1))
old = state(m).copy()
pair, info = PairedRelease(m).choose(speed, curvature, phase=j % 10)
np.testing.assert_array_equal(state(m), old)
for _ in range(info['count']):
m.step(speed, *pair, freeze_i=True)
np.testing.assert_allclose(state(m), info['first_state'], atol=1e-12, rtol=0)
assert abs(info['first_state'][3]-info['planned_target']) <= info['immediate_error_bound']+1e-12
@pytest.mark.parametrize('speed,c0,c1,filtered,fast,target,phase,pair,cost', [
(20., 0., 0., 0., False, .03, 0, (.03, .002), 5.206693787207076),
(20., 3., .4, .03, False, -.03, 2, (2.98, .399), 149.0975016923494),
(40., 3., .4, .03, True, 0., 0, (2.95, .395), 12.167867914611671),
(6., 5.11, .5, .1, False, -.1, 0, (5.08, .498), 1423.8735141422555),
(80., -5.11, -.5, -.1, True, .03, 2, (-5.08, -.4975), 39.42744549117561),
(40., 3., -.4, 0., False, 0., 2, (3.02, -.399), 6.184073685045232),
])
def test_optimized_selection_matches_frozen_cases(speed, c0, c1, filtered, fast, target, phase, pair, cost):
# Frozen outputs from 528ed3615 before pruning/caching the full-return search.
# Cover entry, reversal, release, saturation, cancellation and both tick counts.
# Costs refreshed for the equal-buildup endpoint and residual allocation at
# field bounds, then for the supervisor-saturation C0 endpoint (its return
# path differs). All six immediate selected packets remain unchanged.
m = MainRequest(native_lookup=True)
m.c0, m.c1, m.filtered, m.fast = c0, c1, filtered, fast
command, info = PairedRelease(m).choose(speed, target, phase)
assert command == pytest.approx(pair, abs=1e-15, rel=0)
assert info['cost'] == pytest.approx(cost, abs=1e-12, rel=1e-12)
@pytest.mark.parametrize('active,override', list(itertools.product((False, True), repeat=2)))
def test_card_transmit_hook_and_fault_alert_use_actual_source(active, override):
import ast
from pathlib import Path
from openpilot.cereal import custom
from openpilot.selfdrive.car.helpers import convert_carControlSP
source_path = Path(__file__).resolve().parents[1] / 'card.py'
tree = ast.parse(source_path.read_text())
car = next(n for n in tree.body if isinstance(n, ast.ClassDef) and n.name == 'Car')
method = next(n for n in car.body if isinstance(n, ast.FunctionDef) and n.name == 'controls_update')
branch = next(n for n in method.body if isinstance(n, ast.If) and ast.unparse(n.test) == "self.sm.all_alive(['carControl'])")
code = compile(ast.Module(body=[branch], type_ignores=[]), str(source_path), 'exec')
p = Pipeline()
p.cs.steeringPressed = override
sent = []
owner = SimpleNamespace(ford_joint_control=p.joint, sm=SimpleNamespace(all_alive=lambda _: True, all_checks=lambda _: True, frame=1),
CI=SimpleNamespace(apply=lambda cc, sp, now: CarInterfaceBase.apply(p.interface, cc, sp, now)),
pm=SimpleNamespace(send=lambda service, data: sent.append((service, data))))
cc, _ = controls(active=active)
original = cc.to_dict()
sp = custom.CarControlSP.new_message()
sp.fordLateralPath.enabled = sp.fordLateralPath.valid = True
exec(code, {'self': owner, 'CC': cc.as_reader(), 'CC_SP': sp, 'CS': p.cs, 'pscm_status': None, 'REPLAY': False,
'time': SimpleNamespace(monotonic=lambda: 1.), 'convert_carControlSP': convert_carControlSP,
'can_list_to_can_capnp': lambda data, **kwargs: data})
assert sent[0][0] == 'sendcan'
assert p.joint.sent[2] == active
assert p.joint.last_sent_time == 1.
assert owner.CC_prev.to_dict() == {**original, 'latActive': active}
assert cc.to_dict() == original
update_method = next(n for n in car.body if isinstance(n, ast.FunctionDef) and n.name == 'state_update')
alert = next(n for n in update_method.body if isinstance(n, ast.If) and 'self.ford_joint_control.fault' in ast.unparse(n.test))
alert_code = compile(ast.Module(body=[alert], type_ignores=[]), str(source_path), 'exec')
p.joint.fault = 'command_timing_gap'
exec(alert_code, {'self': owner, 'CS': p.cs})
assert p.cs.steerFaultTemporary
def test_nonfinite_native_result_latches_inactive(monkeypatch):
from openpilot.selfdrive.controls.lib.ford_joint.encoder import LIB
p = Pipeline()
monkeypatch.setattr(LIB, 'paired_select', lambda *args: None)
assert not p.tick(1.).latActive
assert p.joint.fault == 'invalid_prediction'
def test_duplicate_clock_and_missing_receipts_cannot_advance_unknown_history():
p = Pipeline()
p.tick(1.)
assert p.tick(1.).latActive
assert state(p.joint.request).tolist() == [0., 0., 0., 0., 0.]
# Prepare can run without sending, but stale TX history must end the trial.
for now in (1.05, 1.10, 1.15):
cc, sp = controls()
result = p.joint.prepare(cc, sp, p.cs, now)
assert not result.latActive and p.joint.fault == 'missing_transmit_history'
@@ -0,0 +1,89 @@
"""Causal target preview: numerical scoring and the real Ford transmit boundary."""
import copy
import math
import numpy as np
import pytest
from openpilot.selfdrive.car.tests.test_ford_joint_control import Pipeline
from openpilot.selfdrive.controls.lib.ford_joint.encoder import PairedRelease, parameters, state
from openpilot.selfdrive.controls.lib.ford_joint.inverse import static_pair, quantize
from openpilot.selfdrive.controls.lib.ford_joint.model import MainRequest
@pytest.mark.parametrize('speed', [18., 36., 90.])
@pytest.mark.parametrize('phase', range(10))
def test_preview_score_matches_explicit_forward_recurrence(speed, phase):
m = MainRequest(native_lookup=True)
m.c0, m.c1, m.filtered = .7, -.03, -.001
curvature, rate, preview = .002, .015, .1
bound = 3 / (speed / 3.6)**2
encoder = PairedRelease(m)
_, baseline = encoder.choose(speed, curvature, phase)
pair, info = encoder.choose(speed, curvature, phase, curvature_rate=rate, preview=preview, curvature_bound=bound)
p = parameters(m, speed)
targets, prefs = [], []
n = math.ceil(preview / .008)
for j in range(n + 1):
k = min(bound, max(-bound, curvature + rate * min(j * .008, preview)))
pref = quantize(static_pair(m, k, speed, gains=(p[0], p[1])))
prefs.append(pref)
targets.append(p[0] * pref[0] + p[1] * pref[1])
model, cost = copy.copy(m), 0.
for j in range(1600):
command = pair if j < info['count'] else prefs[min(j + 1, n)]
model.step(speed, *command, freeze_i=True)
cost += .008 * ((model.filtered - targets[min(j + 1, n)]) / .01)**2
if j + 1 == info['count']:
np.testing.assert_allclose(state(model), info['first_state'], atol=1e-14, rtol=0)
assert info['cost'] == pytest.approx(cost, abs=2e-10, rel=2e-12)
assert abs(info['first_state'][3] - baseline['planned_target']) <= baseline['immediate_error_bound'] + 1.1e-12
@pytest.mark.parametrize('curvature_rate,preview', [(0., .1), (.02, 0.)])
def test_constant_or_disabled_preview_preserves_ordinary_encoder(curvature_rate, preview):
m = MainRequest(native_lookup=True)
m.c0, m.c1, m.filtered = -1., .1, .004
encoder = PairedRelease(m)
expected, before = encoder.choose(36., .01)
actual, after = encoder.choose(36., .01, curvature_rate=curvature_rate, preview=preview, curvature_bound=.03)
assert actual == expected and after['cost'] == before['cost']
np.testing.assert_array_equal(after['first_state'], before['first_state'])
@pytest.mark.parametrize('kwargs', [
{'curvature_rate': math.nan}, {'preview': -.1}, {'preview': .16},
{'preview': .1, 'curvature_bound': 0.}, {'preview': .1, 'curvature_bound': math.inf},
])
def test_invalid_forecast_is_rejected(kwargs):
with pytest.raises(ValueError):
PairedRelease(MainRequest(native_lookup=True)).choose(36., .002, **kwargs)
def test_target_trend_enters_real_adapter_without_changing_requested_angle():
p = Pipeline()
for i in range(81):
p.tick(1. + i * .01, i * .5)
d = p.joint.diagnostics
assert d['requested_angle'] == 40.
assert d['trimmed_angle'] == 40. + d['angle_trim']
assert d['target_preview_seconds'] == .1
assert 4.9 < d['target_preview_delta'] < 5.1
assert d['wheel_rate'] == 0.
p.tick(1.81, 40., active=False)
p.tick(1.82, -40.)
assert p.joint.diagnostics['target_preview_delta'] == 0.
def test_preview_does_not_run_an_unused_full_constant_target_search(monkeypatch):
from openpilot.selfdrive.controls.lib.ford_joint.encoder import LIB
def unused_search(*args):
pytest.fail('Preview needs only the current-target accuracy bound, not a second full search')
monkeypatch.setattr(LIB, 'paired_select', unused_search)
command, info = PairedRelease(MainRequest(native_lookup=True)).choose(
36., .002, curvature_rate=.015, preview=.1, curvature_bound=.03,
)
assert np.isfinite(command).all()
assert abs(info['first_state'][3] - info['planned_target']) <= info['immediate_error_bound'] + 1.1e-12
@@ -0,0 +1,109 @@
import ast
from pathlib import Path
from types import SimpleNamespace
import unittest
from openpilot.cereal import custom
from openpilot.selfdrive.car.ford_pscm_status import MESSAGE, SIGNALS, populate_ford_pscm_status
from openpilot.selfdrive.car.helpers import convert_to_capnp
from opendbc.can import CANPacker, CANParser
from opendbc.car import Bus, structs
from opendbc.car.ford.values import FordFlags
class TestFordPscmStatus(unittest.TestCase):
def setUp(self):
self.cp = SimpleNamespace(brand='ford', flags=FordFlags.CANFD)
self.packer = CANPacker('ford_lincoln_base_pt')
self.parser = CANParser('ford_lincoln_base_pt', [(MESSAGE, 33), ('Yaw_Data_FD1', 100)], 0)
def update_status(self, timestamp, *, lateral_state=2, limit=0, capability=2, denied=False):
status = self.packer.make_can_msg(MESSAGE, 0, dict(zip(SIGNALS, (lateral_state, limit, capability, denied), strict=True)))
yaw = self.packer.make_can_msg('Yaw_Data_FD1', 0, {'VehYaw_W_Actl': 0.1})
self.parser.update([(timestamp, [status, yaw])])
def publish(self, *, can_valid=True):
state_sp = convert_to_capnp(structs.CarStateSP(speedLimit=13.5))
populate_ford_pscm_status(self.cp, {Bus.pt: self.parser}, state_sp, can_valid)
return state_sp
def test_decodes_status_and_preserves_receipt_time_across_other_can_messages(self):
self.update_status(1_000_000_000, limit=2, capability=1, denied=True)
original = self.publish()
self.assertEqual(original.speedLimit, 13.5)
status = original.fordPscmStatus
self.assertTrue(status.valid)
self.assertEqual(status.canMonoTime, 1_000_000_000)
self.assertEqual((status.lateralState, status.limit, status.capability, status.denied), (2, 2, 1, True))
# carStateSP may publish at 100 Hz while this 33 Hz message is absent. New
# unrelated CAN must not freshen the timestamp of an old PSCM status.
yaw = self.packer.make_can_msg('Yaw_Data_FD1', 0, {'VehYaw_W_Actl': .2})
self.parser.update([(1_080_000_000, [yaw])])
copied = self.publish().fordPscmStatus
self.assertEqual(copied.canMonoTime, 1_000_000_000)
self.assertEqual((copied.limit, copied.capability, copied.denied), (2, 1, True))
self.update_status(1_090_000_000, lateral_state=3, limit=3, capability=2)
next_state = self.publish()
with custom.CarStateSP.from_bytes(next_state.to_bytes()) as decoded:
latest = decoded.fordPscmStatus
self.assertTrue(latest.valid)
self.assertEqual(latest.canMonoTime, 1_090_000_000)
self.assertEqual((latest.lateralState, latest.limit, latest.capability, latest.denied), (3, 3, 2, False))
def test_absent_parser_unseen_message_and_invalid_can_do_not_claim_valid_status(self):
state = custom.CarStateSP.new_message()
populate_ford_pscm_status(self.cp, {}, state, True)
self.assertFalse(state.fordPscmStatus.valid)
self.assertEqual(state.fordPscmStatus.canMonoTime, 0)
self.assertFalse(self.publish().fordPscmStatus.valid)
self.update_status(1_000_000_000)
invalid = self.publish(can_valid=False).fordPscmStatus
self.assertFalse(invalid.valid)
self.assertEqual(invalid.canMonoTime, 1_000_000_000)
def test_mixed_timestamps_or_malformed_status_cannot_enable_feedback(self):
self.update_status(1_000_000_000)
self.parser.ts_nanos[MESSAGE][SIGNALS[-1]] = 990_000_000
self.assertFalse(self.publish().fordPscmStatus.valid)
self.parser.ts_nanos[MESSAGE][SIGNALS[-1]] = 1_000_000_000
for value in (float('nan'), -1, 1.5, 4):
self.parser.vl[MESSAGE]['LatCtlLim_D_Stat'] = value
self.assertFalse(self.publish().fordPscmStatus.valid)
def test_other_vehicles_and_legacy_messages_default_to_unavailable(self):
for cp in (SimpleNamespace(brand='toyota'), SimpleNamespace(brand='ford', flags=0)):
state = custom.CarStateSP.new_message(speedLimit=10.)
populate_ford_pscm_status(cp, {}, state, True)
self.assertFalse(state.fordPscmStatus.valid)
self.assertEqual(state.fordPscmStatus.canMonoTime, 0)
self.assertEqual(state.speedLimit, 10.)
# Old recordings/readers have no appended status pointer; defaults must
# remain unavailable rather than interpreting zeroed enums as fresh data.
self.assertFalse(custom.CarStateSP.new_message().fordPscmStatus.valid)
def test_actual_card_update_populates_status_after_dataclass_conversion(self):
self.update_status(1_000_000_000, limit=1)
source_path = Path(__file__).resolve().parents[1] / 'card.py'
source = ast.parse(source_path.read_text())
car_class = next(n for n in source.body if isinstance(n, ast.ClassDef) and n.name == 'Car')
method = next(n for n in car_class.body if isinstance(n, ast.FunctionDef) and n.name == 'state_update')
statements = method.body
first = next(i for i, n in enumerate(statements) if isinstance(n, ast.Assign) and ast.unparse(n.value) == 'self.CI.update(can_list)')
last = next(i for i, n in enumerate(statements) if isinstance(n, ast.Expr) and isinstance(n.value, ast.Call)
and isinstance(n.value.func, ast.Name) and n.value.func.id == 'populate_ford_pscm_status')
self.assertGreater(last, first)
code = compile(ast.Module(body=statements[first:last + 1], type_ignores=[]), str(source_path), 'exec')
ci = SimpleNamespace(update=lambda _: (SimpleNamespace(canValid=True), structs.CarStateSP(speedLimit=11.)),
can_parsers={Bus.pt: self.parser})
environment = {'self': SimpleNamespace(CP=self.cp, CI=ci), 'can_list': [], 'convert_to_capnp': convert_to_capnp,
'populate_ford_pscm_status': populate_ford_pscm_status}
exec(code, environment)
self.assertTrue(environment['CS_SP'].fordPscmStatus.valid)
self.assertEqual(environment['CS_SP'].fordPscmStatus.canMonoTime, 1_000_000_000)
self.assertEqual(environment['CS_SP'].fordPscmStatus.limit, 1)
if __name__ == '__main__':
unittest.main()
@@ -0,0 +1,110 @@
"""Opt-in entry assist at the real Ford adapter and CAN packing boundary."""
from types import SimpleNamespace
import numpy as np
import pytest
from openpilot.common.params import Params, ParamKeyFlag
from openpilot.selfdrive.car.ford_joint_control import FordJointControl, select_joint_control
from openpilot.selfdrive.car.tests.test_ford_joint_control import Pipeline, cp
from openpilot.selfdrive.controls.lib.ford_joint.encoder import state
def assisted():
p = Pipeline()
p.joint = FordJointControl(cp(), turn_entry_assist=True)
return p
def test_turn_entry_is_default_off_and_requires_the_existing_joint_controller(tmp_path):
params = Params(str(tmp_path))
assert params.get_default_value('FordPscmTurnEntryAssist') is False
assert not params.get_bool('FordPscmTurnEntryAssist')
assert b'FordPscmTurnEntryAssist' in params.all_keys(ParamKeyFlag.PERSISTENT | ParamKeyFlag.BACKUP)
params.put_bool('FordPscmTurnEntryAssist', True, block=True)
assert select_joint_control(cp(), params) is None
params.put_bool('FordModelActionController', True, block=True)
assert select_joint_control(cp(), params) is None
params.put_bool('FordPscmJointControl', True, block=True)
assert select_joint_control(cp(), params).turn_entry_assist
params.put_bool('FordPscmTurnEntryAssist', False, block=True)
assert not select_joint_control(cp(), params).turn_entry_assist
params.put_bool('FordPscmTurnEntryAssist', True, block=True)
for key in ('FordGeometryReference', 'JoystickDebugMode'):
params.put_bool(key, True, block=True)
assert select_joint_control(cp(), params) is None
params.put_bool(key, False, block=True)
@pytest.mark.parametrize('sign', [-1., 1.])
def test_large_growing_undertracked_request_adds_to_real_wire_commands(sign):
base, trial = Pipeline(), assisted()
for p in (base, trial):
p.tick(1., 0.)
p.tick(1.01, sign * 120.)
np.testing.assert_allclose(np.array(trial.joint.sent[:2]) - base.joint.sent[:2], sign * np.array([.60, .04]), atol=1e-12)
assert trial.joint.diagnostics['requested_angle'] == sign * 120.
assert trial.joint.diagnostics['trimmed_angle'] == base.joint.diagnostics['trimmed_angle']
assert trial.joint.diagnostics['requested_rate'] == base.joint.diagnostics['requested_rate']
assert trial.joint.diagnostics['turn_entry_weight'] == 1.
assert trial.joint.sent[2] and base.joint.sent[2]
@pytest.mark.parametrize('sign', [-1., 1.])
def test_extra_clips_at_dbc_bounds_through_real_packer(sign, monkeypatch):
p = assisted()
p.tick(1., 0.)
choose = p.joint.encoder.choose
def near_bounds(*args, **kwargs):
_, info = choose(*args, **kwargs)
return (sign * 5.10, sign * .49), info
monkeypatch.setattr(p.joint.encoder, 'choose', near_bounds)
p.tick(1.01, sign * 120.)
assert p.joint.sent[:2] == pytest.approx((sign * 5.11, sign * .5))
def test_small_requests_and_small_errors_keep_full_history_identical():
base, trial = Pipeline(), assisted()
for i in range(1200):
target = 25 * np.sin(i * .02) if i < 600 else 100.
wheel = 0. if i < 600 else 90.
for p in (base, trial):
p.cs.steeringAngleDeg = wheel
p.tick(1. + i * .01, float(target))
assert trial.joint.sent == base.joint.sent
np.testing.assert_array_equal(state(trial.joint.request), state(base.joint.request))
assert trial.joint.angle_trim == base.joint.angle_trim
@pytest.mark.parametrize('sign', [-1., 1.])
def test_new_term_clears_on_relaxation_without_resetting_observed_state(sign):
p = assisted()
for i in range(100):
p.cs.steeringAngleDeg = sign * 20.
p.tick(1. + i * .01, sign * (100. + i))
assert p.joint.diagnostics['turn_entry_c0'] * sign > 0
p.tick(2., sign * 50.)
assert p.joint.diagnostics['turn_entry_c0'] == p.joint.diagnostics['turn_entry_c1'] == 0.
assert abs(p.joint.request.c0) + abs(p.joint.request.c1) > 0
assert p.joint.sent[2]
@pytest.mark.parametrize('gate', ['touch', 'limit', 'denied', 'fault', 'stale', 'inactive', 'accel'])
def test_extra_respects_intervention_health_and_target_limit_gates(gate):
p = assisted()
p.cs.vEgo = 30. if gate == 'accel' else 5.36
p.tick(1., 0.)
p.cs.steeringPressed = gate == 'touch'
p.cs.steerFaultTemporary = gate == 'fault'
status = SimpleNamespace(valid=True, canMonoTime=1_010_000_000, limit=2 if gate == 'limit' else 0, denied=gate == 'denied')
p.tick(1.01, 120., active=gate != 'inactive', fresh=gate != 'stale', pscm_status=status)
d = p.joint.diagnostics
assert d['turn_entry_c0'] == d['turn_entry_c1'] == 0.
if gate in ('touch', 'limit', 'accel'):
assert p.joint.sent[2] # Only the added term is suppressed.
else:
assert p.joint.sent == (0., 0., False)
if gate == 'accel':
assert d['accel_limited']
@@ -0,0 +1,297 @@
"""Signaled preview through real model messages, SP transport and Ford packing."""
import ast
import itertools
from pathlib import Path
from types import SimpleNamespace
import numpy as np
import pytest
from openpilot.cereal import custom, log
from openpilot.common.params import Params, ParamKeyFlag
from opendbc.car.vehicle_model import VehicleModel
from openpilot.selfdrive.car.ford_joint_control import FordJointControl, select_joint_control
from openpilot.selfdrive.car.helpers import convert_carControlSP
from openpilot.selfdrive.car.tests.test_ford_joint_control import Pipeline, cp
from openpilot.selfdrive.controls.lib.ford_joint.encoder import state
from openpilot.selfdrive.controls.lib.ford_turn_preview import FordTurnPreview, geometry_entry_lead, turn_preview_lead
from openpilot.selfdrive.controls.tests.test_ford_model_action_adapter import _method
from openpilot.selfdrive.controls.tests.test_ford_model_action_selection import car_params, startup
def model(sign=1., action=-.01):
m = log.ModelDataV2.new_message()
station = np.linspace(0, 32, 33)
m.position.x = station.tolist()
m.position.y = np.zeros(33).tolist()
m.orientation.z = (-sign * np.radians(station * 2.2)).tolist()
m.action.desiredCurvature = action
return m
def sample(sign=1., stamp=1_010_000_000):
cue = FordTurnPreview()
cue.update(model(sign, 0.), stamp - 50_000_000, True)
data = cue.update(model(sign, -sign * .01), stamp, True)
# Serialize exactly the additive production carrier; use its reader at card.
sp = custom.CarControlSP.new_message(fordTurnPreview=data)
with custom.CarControlSP.from_bytes(sp.to_bytes()) as parsed:
convert_carControlSP(parsed) # The cue is card metadata, not an opendbc field.
return parsed.as_builder().as_reader().fordTurnPreview
def pipeline(enabled=True):
p = Pipeline()
p.joint = FordJointControl(cp(), turn_entry_assist=True, turn_preview=enabled)
return p
def test_default_off_startup_and_master_fallback(tmp_path):
params = Params(str(tmp_path))
assert params.get_default_value('FordPscmTurnPreview') is False
assert b'FordPscmTurnPreview' in params.all_keys(ParamKeyFlag.PERSISTENT | ParamKeyFlag.BACKUP)
params.put_bool('FordPscmTurnPreview', True, block=True)
assert select_joint_control(cp(), params) is None
params.put_bool('FordModelActionController', True, block=True)
assert select_joint_control(cp(), params) is None
params.put_bool('FordPscmJointControl', True, block=True)
assert select_joint_control(cp(), params).turn_preview
for key in ('FordGeometryReference', 'JoystickDebugMode'):
params.put_bool(key, True, block=True)
assert select_joint_control(cp(), params) is None
params.put_bool(key, False, block=True)
params.put_bool('FordPscmTurnPreview', False, block=True)
assert not select_joint_control(cp(), params).turn_preview
@pytest.mark.parametrize('master,joint,geometry,joystick,preview', list(itertools.product((False, True), repeat=5)))
def test_both_processes_select_preview_with_the_same_gates(master, joint, geometry, joystick, preview):
flags = {'FordModelActionController': master, 'FordPscmJointControl': joint, 'FordGeometryReference': geometry,
'JoystickDebugMode': joystick, 'FordPscmTurnPreview': preview}
params = SimpleNamespace(get_bool=lambda key: flags.get(key, False))
selected = select_joint_control(cp(), params)
expected = master and joint and preview and not geometry and not joystick
assert bool(selected and selected.turn_preview) == expected
assert (startup(params=params).ford_turn_preview is not None) == expected
def publish_preview(tracker, message, stamp, *, healthy=True, maneuver=False):
root = Path(__file__).resolve().parents[3]
filename = root / 'sunnypilot/selfdrive/controls/controlsd_ext.py'
body = _method(filename, 'ControlsExt', 'state_control_ext').body
nodes = [n for n in body if (isinstance(n, ast.Assign) and ast.unparse(n.targets[0]) == 'turn_preview') or
(isinstance(n, ast.If) and ast.unparse(n.test) == 'turn_preview is not None')]
assert len(nodes) == 2
class Subscriptions(dict):
valid = {'modelV2': True, 'lateralManeuverPlan': maneuver}
logMonoTime = {'modelV2': stamp}
def all_checks(self, services):
return healthy
sp = custom.CarControlSP.new_message()
sp.fordLateralPath.enabled = sp.fordLateralPath.valid = True
exec(compile(ast.Module(body=nodes, type_ignores=[]), str(filename), 'exec'),
{'self': SimpleNamespace(ford_turn_preview=tracker, VM=VehicleModel(car_params())),
'sm': Subscriptions(modelV2=message, carState=SimpleNamespace(vEgo=5.),
vehicleParameters=SimpleNamespace(roll=0., angleOffsetDeg=0.)), 'CC_SP': sp})
return sp
@pytest.mark.parametrize('healthy,maneuver', [(True, False), (False, False), (True, True)])
def test_actual_publication_gates_and_original_model_timestamp(healthy, maneuver):
tracker = FordTurnPreview()
publish_preview(tracker, model(action=0.), 1_000_000_000)
sp = publish_preview(tracker, model(), 1_050_000_000, healthy=healthy, maneuver=maneuver)
assert sp.fordTurnPreview.valid == (healthy and not maneuver)
assert sp.fordTurnPreview.modelMonoTime == 1_050_000_000
assert sp.fordTurnPreview.geometryValid == healthy
assert sp.fordLateralPath.valid
convert_carControlSP(sp.as_reader())
def test_card_consumes_published_preview_and_preserves_reader_contract():
from opendbc.car import structs
from opendbc.car.interfaces import CarInterfaceBase
from openpilot.selfdrive.car.tests.test_ford_joint_control import controls
root = Path(__file__).resolve().parents[3]
filename = root / 'selfdrive/car/card.py'
method = _method(filename, 'Car', 'controls_update')
env = {'car': structs.car, 'custom': custom, 'REPLAY': True, 'convert_carControlSP': convert_carControlSP,
'can_list_to_can_capnp': lambda *args, **kwargs: None}
exec(compile(ast.Module(body=[method], type_ignores=[]), str(filename), 'exec'), env)
p = pipeline()
p.cs.leftBlinker = True
card = SimpleNamespace(initialized_prev=True, ford_joint_control=p.joint,
sm=SimpleNamespace(all_alive=lambda services: True, all_checks=lambda services: True, frame=1),
pm=SimpleNamespace(send=lambda *args: None),
CI=SimpleNamespace(apply=lambda *args: CarInterfaceBase.apply(p.interface, *args)))
tracker = FordTurnPreview()
for stamp, target, raw in ((1_000_000_000, 0., 0.), (1_050_000_000, 100., -.01)):
sp = publish_preview(tracker, model(action=raw), stamp)
with custom.CarControlSP.from_bytes(sp.to_bytes()) as reader:
card.can_log_mono_time = stamp
control, _ = controls(target)
env['controls_update'](card, p.cs, control.as_reader(), reader)
assert p.joint.sent[2]
assert p.joint.diagnostics['geometry_lead'] == 30.
assert card.CC_prev.actuators.as_builder().steeringAngleDeg == 100.
@pytest.mark.parametrize('sign', [-1., 1.])
def test_heading_distance_and_action_sign(sign):
p = sample(sign)
assert p.valid
assert p.heading7 == pytest.approx(sign * 15.4, abs=1e-5)
assert p.heading14 == pytest.approx(sign * 30.8, abs=1e-5)
assert turn_preview_lead(sign * 50., p, 1.01) == (sign * 10., 1.)
assert turn_preview_lead(sign * 1000., p, 1.01) == (sign * 30., 1.)
assert turn_preview_lead(-sign * 50., p, 1.01)[0] == 0.
def test_folded_path_uses_arc_distance_and_unwrapped_heading():
m = model()
angle = np.linspace(0., 3.5, 33)
m.position.x = (6 * np.sin(angle)).tolist()
m.position.y = (6 * (1 - np.cos(angle))).tolist()
m.orientation.z = ((angle + np.pi) % (2 * np.pi) - np.pi).tolist()
p = FordTurnPreview().update(m, 1_000_000_000, True)
assert p['valid']
assert p['heading14'] < p['heading7'] < -60. # x folds back, distance keeps increasing.
@pytest.mark.parametrize('bad', ['invalid', 'short', 'mismatch', 'nan', 'action_nan', 'lane_change'])
def test_invalid_geometry_disables_only_preview(bad):
m = model()
if bad == 'short':
m.position.x = np.linspace(0., 13., 33).tolist()
elif bad == 'mismatch':
m.orientation.z = [0.]
elif bad == 'nan':
m.position.y = [float('nan')] * 33
elif bad == 'action_nan':
m.action.desiredCurvature = float('nan')
elif bad == 'lane_change':
m.meta.laneChangeState = 'laneChangeStarting'
p = FordTurnPreview().update(m, 1_000_000_000, bad != 'invalid')
assert not p['valid']
def test_duplicate_stale_and_invalid_action_history():
c = FordTurnPreview()
c.update(model(action=0.), 1_000_000_000, True)
good = c.update(model(), 1_050_000_000, True).copy()
assert good['actionRate'] > 0
assert c.update(model(action=.1), 1_050_000_000, True) == good
p = sample(stamp=1_050_000_000)
for now in (1.049, 1.201):
assert turn_preview_lead(100., p, now) == (0., 0.)
c.update(model(), 1_100_000_000, False)
assert c.update(model(action=-.02), 1_150_000_000, True)['actionRate'] == 0.
assert c.update(model(action=-.03), 1_400_000_000, True)['actionRate'] == 0.
@pytest.mark.parametrize('signal', ['none', 'opposite', 'hazards'])
def test_no_matching_signal_retains_identical_packet_history(signal):
base, trial = pipeline(False), pipeline()
for i in range(500):
now = 1. + i * .01
target = 70. + 50. * np.sin(i * .02)
for p in (base, trial):
p.cs.leftBlinker = signal == 'hazards'
p.cs.rightBlinker = signal != 'none'
p.tick(now, float(target), turn_preview=sample(stamp=round(now * 1e9)))
assert trial.joint.sent == base.joint.sent
np.testing.assert_array_equal(state(trial.joint.request), state(base.joint.request))
assert trial.joint.angle_trim == base.joint.angle_trim
@pytest.mark.parametrize('sign', [-1., 1.])
def test_matching_signal_changes_inverse_only_and_retains_bounds(sign):
p = pipeline()
p.cs.leftBlinker, p.cs.rightBlinker = sign > 0, sign < 0
p.tick(1., 0.)
p.tick(1.01, sign * 100., turn_preview=sample(sign))
d = p.joint.diagnostics
assert d['geometry_lead'] == sign * 30.
assert d['inverse_target'] == d['trimmed_angle'] + sign * 30.
assert d['requested_angle'] == sign * 100.
assert d['requested_rate'] * sign > 0.
# Raw action relaxing suppresses the extra even while filtered request grows.
p.tick(1.02, sign * 105., turn_preview=sample(-sign, 1_020_000_000))
assert p.joint.diagnostics['geometry_lead'] == 0.
assert p.joint.sent[2]
@pytest.mark.parametrize('gate', ['touch', 'limit', 'denied', 'fault', 'stale', 'inactive', 'missing'])
def test_preview_respects_existing_health_and_override_gates(gate):
p = pipeline()
p.cs.leftBlinker = True
p.tick(1., 0.)
p.cs.steeringPressed = gate == 'touch'
p.cs.steerFaultTemporary = gate == 'fault'
status = SimpleNamespace(valid=True, canMonoTime=1_010_000_000, limit=2 if gate == 'limit' else 0, denied=gate == 'denied')
p.tick(1.01, 120., active=gate != 'inactive', fresh=gate != 'stale', pscm_status=status,
turn_preview=None if gate == 'missing' else sample())
assert p.joint.diagnostics.get('geometry_lead', 0.) == 0.
if gate in ('touch', 'limit', 'missing'):
assert p.joint.sent[2]
else:
assert p.joint.sent == (0., 0., False)
@pytest.mark.parametrize('sign', [-1., 1.])
def test_flat_action_geometry_reaches_wire_and_fades_when_action_takes_over(sign):
p, base = pipeline(), pipeline(False)
p.cs.leftBlinker, p.cs.rightBlinker = sign > 0, sign < 0
tracker = FordTurnPreview()
for i in range(30):
now = 1. + i*.01
sp = publish_preview(tracker, model(sign, action=0.), round((now-.001)*1e9))
with custom.CarControlSP.from_bytes(sp.to_bytes()) as parsed:
cue = parsed.fordTurnPreview
assert cue.valid and cue.geometryValid and cue.actionRate == 0.
p.tick(now, 0., turn_preview=cue)
base.tick(now, 0., turn_preview=cue)
d = p.joint.diagnostics
assert d['geometry_lead'] == sign*90.
assert d['inverse_target'] == sign*90. and d['requested_angle'] == 0.
assert geometry_entry_lead(sign*60., 0., cue, now) == pytest.approx(.5*(cue.geometryAngle-sign*60.))
assert geometry_entry_lead(sign*90., 0., cue, now) == 0.
assert geometry_entry_lead(0., -sign*30., cue, now) == 0.
assert p.joint.sent != base.joint.sent
@pytest.mark.parametrize('speed', [0., .3, 5., 20., 40.])
def test_geometry_angle_uses_local_heading_interval_and_vehicle_model(speed):
m = model()
station = np.asarray(m.position.x)
m.orientation.z = (-.001*station**2).tolist()
vm = VehicleModel(car_params())
data = FordTurnPreview().update(m, 1_000_000_000, True, speed=speed, VM=vm, roll=.01, angle_offset=1.2, geometry_valid=True)
lo, hi = max(0., speed*.8-1.), max(0., speed*.8-1.)+2.
if hi > station[-1]:
assert not data.get('geometryValid', False)
else:
curvature = -(np.interp(hi, station, m.orientation.z)-np.interp(lo, station, m.orientation.z))/2.
expected = np.degrees(vm.get_steer_from_curvature(curvature, speed, .01))+1.2
assert data['geometryValid'] and data['geometryAngle'] == pytest.approx(expected)
@pytest.mark.parametrize('gate', ['touch', 'limit', 'stale', 'missing', 'opposite', 'hazards', 'lane_change', 'unhealthy'])
def test_independent_geometry_obeys_gates_with_flat_action(gate):
base, p = pipeline(False), pipeline()
p.cs.leftBlinker, p.cs.rightBlinker = gate != 'opposite', gate in ('opposite', 'hazards')
p.cs.steeringPressed = gate == 'touch'
m = model(action=0.)
if gate == 'lane_change':
m.meta.laneChangeState = 'laneChangeStarting'
sp = publish_preview(FordTurnPreview(), m, 1_000_000_000, healthy=gate != 'unhealthy')
status = SimpleNamespace(valid=True, canMonoTime=1_000_000_000, limit=2 if gate == 'limit' else 0, denied=False)
cue = sp.as_reader().fordTurnPreview
for pipe in (base, p):
pipe.tick(1.2 if gate == 'stale' else 1., 0., turn_preview=None if gate == 'missing' else cue, pscm_status=status)
assert p.joint.diagnostics['geometry_lead'] == 0.
assert p.joint.sent == base.joint.sent
+60 -1
View File
@@ -1,5 +1,6 @@
#!/usr/bin/env python3
import math
import time
from numbers import Number
from openpilot.cereal import log
@@ -13,6 +14,8 @@ 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
@@ -44,7 +47,7 @@ class Controls(ControlsExt):
self.CI = interfaces[self.CP.carFingerprint](self.CP, self.CP_SP)
self.sm = messaging.SubMaster(['lateralDelay', 'vehicleParameters', 'lateralTorqueParameters', 'modelV2', 'selfdriveState',
'extrinsicsCalibration', 'deviceMotion', 'longitudinalPlan', 'lateralManeuverPlan', 'carState', 'carOutput',
'extrinsicsCalibration', 'deviceMotion', 'longitudinalPlan', 'lateralManeuverPlan', 'carState', 'carStateSP', 'carOutput',
'driverMonitoringState', 'onroadEvents', 'driverAssistance'] + self.sm_services_ext,
poll='selfdriveState')
self.pm = messaging.PubMaster(['carControl', 'controlsState'] + self.pm_services_ext)
@@ -52,6 +55,20 @@ 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"),
joint_control=self.params.get_bool("FordPscmJointControl") and
not self.params.get_bool("JoystickDebugMode"))
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.ford_turn_preview = None
if self.ford_model_action and self.ford_path_controller.joint_control and self.params.get_bool('FordPscmTurnPreview'):
from openpilot.selfdrive.controls.lib.ford_turn_preview import FordTurnPreview
self.ford_turn_preview = FordTurnPreview()
self.pose_calibrator = PoseCalibrator()
self.calibrated_pose: Pose | None = None
@@ -141,6 +158,8 @@ 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)
@@ -155,6 +174,46 @@ 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'
now = time.monotonic()
yaw_rate, motion_valid = -CS.yawRate, True
ford_speed = CS.vEgo
if self.ford_path_controller.joint_control:
from openpilot.selfdrive.controls.lib.ford_path import joint_control_speed
ford_speed = joint_control_speed(CS)
# card's inverse uses the calibrated yaw published in carControl.
# Never keep steering from a stale pose or fall back to raw CAN yaw.
motion = self.sm['deviceMotion']
motion_valid = (self.calibrated_pose is not None and self.pose_calibrator.calib_valid
and self.sm.all_checks(['deviceMotion', 'extrinsicsCalibration'])
and motion.inputsOK and motion.sensorsOK and motion.angularVelocityDevice.valid
and -0.005 <= now - self.sm.logMonoTime['deviceMotion'] * 1e-9 <= 0.15)
yaw_rate = float(self.calibrated_pose.angular_velocity.xyz[2]) if motion_valid else math.nan
self.ford_path = self.ford_path_controller.update(
ford_model, self.desired_curvature, current_curvature=self.curvature, yaw_rate=yaw_rate, speed=ford_speed, now=now,
# 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=motion_valid and 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)
@@ -0,0 +1,3 @@
Import('env')
env.SharedLibrary('encoder', 'encoder.cc', LIBS=[])
@@ -0,0 +1,31 @@
"""Measurement states of the recovered curvature-to-angle stage, before torque."""
import math
from openpilot.selfdrive.controls.lib.ford_joint.model import bracket, interp_int, clip
class AngleModel:
def __init__(self, cal):
self.cal = cal
self.speed_bp = cal.read(0xFEF25E40, '8H')
self.tables = {address: cal.read(address, '8H') for address in (0xFEF25CA0, 0xFEF25CB0)}
self.yaw_acc = self.bank_residual = self.angle_bias = 0.0
def step(self, speed_kmh, curvature, angle, yaw, accel, wheelbase, ratio):
c = self.cal
si, sf = bracket(int(speed_kmh * 256) & 0xFFFF, self.speed_bp)
v = speed_kmh / 3.6
self.yaw_acc += c.h(0xFEF25A90) / 65536 * (v * yaw - self.yaw_acc)
self.bank_residual += c.h(0xFEF25A88) / 65536 * (accel - v * yaw - self.bank_residual)
opposite = int(self.yaw_acc > 0) - int(self.yaw_acc < 0) != int(self.bank_residual > 0) - int(self.bank_residual < 0)
allowance = 3.0 + (abs(self.bank_residual) if opposite else 0.0)
requested_accel = clip(curvature * v * v, -allowance, allowance)
control_accel = requested_accel * c.b(0xFEF25AB1) / 128 + interp_int(self.tables[0xFEF25CA0], si, sf) / 2048 * (requested_accel - self.yaw_acc)
understeer = c.read(0xFEF25A9A, 'h')[0] / 16384
floor_v = max(v, c.b(0xFEF25AB7) / 32, 1e-6)
conversion = 180 / math.pi * ratio
target = ((self.bank_residual * c.read(0xFEF25AB0, 'b')[0] / 64 + control_accel) * understeer + control_accel / floor_v**2 * wheelbase) * conversion
error = (floor_v * understeer * yaw + yaw / floor_v * wheelbase) * conversion - angle - self.angle_bias
self.angle_bias += interp_int(self.tables[0xFEF25CB0], si, sf) / 65536 * error
return target + self.angle_bias * c.read(0xFEF25AB2, 'b')[0] / 64
@@ -0,0 +1,475 @@
{
"firmware": "ML3V-14D003-BD",
"calibration": "ML34-14D007-EDL",
"firmware_sha256": "8de3eb1f8191b13b57de07430c33f7b723f11b6ed2144991f91dc56476bbbf62",
"note": "Recovered numerical calibration only; RL38 equivalence unverified. No executable firmware.",
"entries": {
"0xfef25e40": {
"format": "8H",
"values": [
0,
3840,
7680,
15360,
25600,
33280,
40960,
56320
]
},
"0xfef25ca0": {
"format": "8H",
"values": [
0,
410,
410,
410,
614,
1024,
410,
410
]
},
"0xfef25cb0": {
"format": "8H",
"values": [
1311,
1311,
1147,
983,
819,
590,
393,
229
]
},
"0xfef25d98": {
"format": "10f",
"values": [
10.0,
20.0,
30.0,
40.0,
50.0,
60.0,
70.0,
80.0,
100.0,
150.0
]
},
"0xfef258fc": {
"format": "8B",
"values": [
3,
3,
4,
5,
7,
10,
11,
11
]
},
"0xfef25b78": {
"format": "8f",
"values": [
0.0,
0.019999999552965164,
0.05000000074505806,
0.10000000149011612,
0.11749999970197678,
0.125,
0.125,
0.125
]
},
"0xfef25b98": {
"format": "8f",
"values": [
15.0,
15.0,
25.0,
41.666664123535156,
55.55555725097656,
72.22222137451172,
88.88888549804688,
122.22222137451172
]
},
"0xfef25bb8": {
"format": "8f",
"values": [
15.0,
15.0,
25.0,
41.666664123535156,
50.0,
46.94444274902344,
57.77777862548828,
79.44444274902344
]
},
"0xfef25be8": {
"format": "8f",
"values": [
0.0,
0.0,
5.0,
5.0,
18.518518447875977,
24.074073791503906,
29.629629135131836,
29.0
]
},
"0xfef25c60": {
"format": "8H",
"values": [
512,
512,
512,
1024,
1536,
1536,
1536,
1536
]
},
"0xfef25d70": {
"format": "10f",
"values": [
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0
]
},
"0xfef25dc0": {
"format": "10f",
"values": [
0.19599999487400055,
0.09799999743700027,
0.06530000269412994,
0.04899999871850014,
0.03920000046491623,
0.03269999846816063,
0.02800000086426735,
0.02449999935925007,
0.019600000232458115,
0.013100000098347664
]
},
"0xfef25de8": {
"format": "10f",
"values": [
0.019999999552965164,
0.004999999888241291,
0.002222222276031971,
0.0012499999720603228,
0.0007999999797903001,
0.0005555555690079927,
0.0004081632650922984,
0.0003124999930150807,
0.00019999999494757503,
8.888888987712562e-05
]
},
"0xfef25e20": {
"format": "8H",
"values": [
24576,
24576,
24576,
24576,
24576,
24576,
16384,
819
]
},
"0xfef25e30": {
"format": "8H",
"values": [
24576,
24576,
24576,
24576,
20480,
18022,
16384,
819
]
},
"0xfef25a58": {
"format": "f",
"values": [
2.0
]
},
"0xfef259ec": {
"format": "f",
"values": [
1.0
]
},
"0xfef259f0": {
"format": "f",
"values": [
2.0
]
},
"0xfef259f4": {
"format": "f",
"values": [
0.6000000238418579
]
},
"0xfef25a14": {
"format": "f",
"values": [
30.0
]
},
"0xfef25a04": {
"format": "f",
"values": [
300.0
]
},
"0xfef2596c": {
"format": "f",
"values": [
1200.0
]
},
"0xfef259a0": {
"format": "f",
"values": [
0.20000000298023224
]
},
"0xfef2599c": {
"format": "f",
"values": [
1.350000023841858
]
},
"0xfef259a4": {
"format": "f",
"values": [
-10.0
]
},
"0xfef259a8": {
"format": "f",
"values": [
0.15000000596046448
]
},
"0xfef25a70": {
"format": "H",
"values": [
1229
]
},
"0xfef25a72": {
"format": "H",
"values": [
0
]
},
"0xfef25a60": {
"format": "H",
"values": [
256
]
},
"0xfef25aa5": {
"format": "B",
"values": [
0
]
},
"0xfef25aa6": {
"format": "B",
"values": [
38
]
},
"0xfef25aa1": {
"format": "B",
"values": [
0
]
},
"0xfef25aa2": {
"format": "B",
"values": [
160
]
},
"0xfef25a9e": {
"format": "B",
"values": [
0
]
},
"0xfef25a9f": {
"format": "B",
"values": [
32
]
},
"0xfef25aa4": {
"format": "B",
"values": [
255
]
},
"0xfef25aa7": {
"format": "B",
"values": [
26
]
},
"0xfef25aa3": {
"format": "B",
"values": [
9
]
},
"0xfef25aa0": {
"format": "B",
"values": [
38
]
},
"0xfef25a74": {
"format": "H",
"values": [
51
]
},
"0xfef25a00": {
"format": "f",
"values": [
0.30000001192092896
]
},
"0xfef25a10": {
"format": "f",
"values": [
0.014999999664723873
]
},
"0xfef259e8": {
"format": "f",
"values": [
0.0003000000142492354
]
},
"0xfef259e4": {
"format": "f",
"values": [
0.003000000026077032
]
},
"0xfef25a0c": {
"format": "f",
"values": [
0.30000001192092896
]
},
"0xfef259fc": {
"format": "f",
"values": [
2.799999952316284
]
},
"0xfef25968": {
"format": "f",
"values": [
-0.30000001192092896
]
},
"0xfef25960": {
"format": "f",
"values": [
0.30000001192092896
]
},
"0xfef25964": {
"format": "f",
"values": [
-0.4000000059604645
]
},
"0xfef2595c": {
"format": "f",
"values": [
0.4000000059604645
]
},
"0xfef25a08": {
"format": "f",
"values": [
0.10000000149011612
]
},
"0xfef259f8": {
"format": "f",
"values": [
1.5
]
},
"0xfef25a90": {
"format": "H",
"values": [
6554
]
},
"0xfef25a88": {
"format": "H",
"values": [
1627
]
},
"0xfef25ab7": {
"format": "B",
"values": [
32
]
},
"0xfef25a9a": {
"format": "h",
"values": [
33
]
},
"0xfef25ab1": {
"format": "B",
"values": [
128
]
},
"0xfef25ab0": {
"format": "b",
"values": [
0
]
},
"0xfef25ab2": {
"format": "b",
"values": [
-64
]
}
}
}
@@ -0,0 +1,206 @@
// Opt-in Ford joint encoder. Numerical request state is estimated, not ECU RAM.
#include <algorithm>
#include <array>
#include <cmath>
#include <cstring>
#include <map>
static double clip(double x, double lo, double hi) {
return std::min(std::max(x, lo), hi);
}
// State: held C0, held C1, internal I, filtered curvature, fast-slew latch.
// Parameters: g0/g1, base alpha, heading low/high/gain, alpha max, speed,
// interaction threshold/value, C0 slow/fast, C1 slow/fast, latch clear C0/C1,
// opposite C0/I thresholds, I gain, delta low/high, I low/high, release rate.
static void step(double *s, const double *p, double c0, double c1) {
if (p[9] < p[8]) {
s[4] = 1;
} else if (std::abs(c0) <= p[14] && std::abs(c1) <= p[15]) {
s[4] = 0;
}
double r0 = p[s[4] ? 11 : 10], r1 = p[s[4] ? 13 : 12];
s[0] += clip(c0 - s[0], -r0 * .008, r0 * .008);
s[1] += clip(c1 - s[1], -r1 * .008, r1 * .008);
if (std::abs(s[0]) > p[16] && std::abs(s[2]) > p[17] && s[0] * s[2] < 0) {
s[2] += clip(-s[2], -p[23], p[23]);
} else {
s[2] = clip(s[2] + clip(s[0], p[19], p[20]) * p[18], p[21], p[22]);
}
double raw = p[0] * (s[0] + s[2]) + p[1] * s[1];
double w = clip(std::abs(s[1] * p[7]) - p[3], 0, std::max(1e-6, p[4] - p[3])) / std::max(1e-6, p[4] - p[3]);
double alpha = std::min(p[6], p[2] + p[5] * w);
s[3] += alpha * (raw - s[3]);
}
static double command_cost(const double *initial, const double *p, double target,
const double *pref, int count, double c0, double c1, double *s) {
std::memcpy(s, initial, 5 * sizeof(double));
double steady_raw = p[0] * pref[0] + p[1] * pref[1];
double steady_error = (steady_raw - target) / .01, cost = 0;
for (int j = 0; j < count; j++) {
step(s, p, c0, c1);
double error = (s[3] - target) / .01;
cost += .008 * (error * error - steady_error * steady_error);
}
return cost;
}
static double return_cost(const double *first, const double *p, double target, const double *pref, double cost) {
double s[5];
std::memcpy(s, first, sizeof(s));
double steady_raw = p[0] * pref[0] + p[1] * pref[1];
double steady_error = (steady_raw - target) / .01;
auto tick = [&](double a, double b) {
step(s, p, a, b);
double error = (s[3] - target) / .01;
cost += .008 * (error * error - steady_error * steady_error);
};
// Include both channels' entire return, followed by the exact filter tail.
// There is no adjustable planning horizon or retained future command plan.
int n = 2 + std::ceil(std::max(std::abs(s[0] - pref[0]) / std::min(p[10], p[11]),
std::abs(s[1] - pref[1]) / std::min(p[12], p[13])) / .008);
for (int j = 0; j < n; j++) tick(pref[0], pref[1]);
double w = clip(std::abs(s[1] * p[7]) - p[3], 0, p[4] - p[3]) / (p[4] - p[3]);
double alpha = std::min(p[6], p[2] + p[5] * w), a = 1 - alpha;
double d = (s[3] - steady_raw) / .01;
return cost + .008 * (2 * steady_error * d * a / alpha + d * d * a * a / (1 - a * a));
}
extern "C" double paired_cost(const double *initial, const double *p, double target,
const double *pref, int count, double c0, double c1, double *first) {
double s[5];
double cost = command_cost(initial, p, target, pref, count, c0, c1, s);
if (first) std::memcpy(first, s, sizeof(s));
return return_cost(s, p, target, pref, cost);
}
// Preview uses the ordinary C1-anchored accuracy bound, but does not need the
// ordinary full C0/C1 search. Keep its scoring and tie breaks exactly the same.
extern "C" double paired_immediate_bound(const double *initial, const double *p, double target,
const double *pref, int count, const double *c0s, int n0) {
double max_error = 1e300, best = 1e300, best_move = 1e300, best_remaining = 1e300;
std::map<std::array<double, 6>, double> costs;
for (int i = 0; i < n0; i++) {
double first[5];
double cost = command_cost(initial, p, target, pref, count, c0s[i], pref[1], first);
std::array<double, 6> key = {first[0], first[1], first[2], first[3], first[4], cost};
auto entry = costs.emplace(key, 0.0);
if (entry.second) entry.first->second = return_cost(first, p, target, pref, cost);
double score = std::nearbyint(entry.first->second * 1e12) / 1e12;
double move = std::abs(c0s[i] - initial[0]), remaining = std::abs(c0s[i] - pref[0]);
if (score < best || (score == best && (move < best_move || (move == best_move && remaining < best_remaining)))) {
best = score;
best_move = move;
best_remaining = remaining;
max_error = std::abs(first[3] - target);
}
}
return max_error;
}
extern "C" void paired_select(const double *initial, const double *p, double target,
const double *pref, int count, const double *c0s, int n0,
const double *c1s, int n1, int preserve_now, double *result) {
double best = 1e300, best_move = 1e300, best_remaining = 1e300;
double first[5];
// Slew clipping makes different command fields reach identical states. Reuse
// their exact return cost within this selection only. Include the prefix cost
// so the original floating-point accumulation and tie breaks are preserved.
std::map<std::array<double, 6>, double> costs;
auto finish = [&](double cost) {
if (!std::isfinite(cost)) return return_cost(first, p, target, pref, cost);
std::array<double, 6> key = {first[0], first[1], first[2], first[3], first[4], cost};
auto entry = costs.emplace(key, 0.0);
if (entry.second) entry.first->second = return_cost(first, p, target, pref, cost);
return entry.first->second;
};
double max_error = 1e300, anchor_score = 1e300, anchor_move = 1e300, anchor_remaining = 1e300;
if (preserve_now) {
// Preserve the C1-anchored policy's immediate target accuracy.
// An inequality against a feasible reference, not a new gain/deadband.
for (int i = 0; i < n0; i++) {
double cost = command_cost(initial, p, target, pref, count, c0s[i], pref[1], first);
cost = finish(cost);
double score = std::nearbyint(cost * 1e12) / 1e12;
double move = std::abs(c0s[i] - initial[0]), remaining = std::abs(c0s[i] - pref[0]);
if (score < anchor_score || (score == anchor_score && (move < anchor_move || (move == anchor_move && remaining < anchor_remaining)))) {
anchor_score = score;
anchor_move = move;
anchor_remaining = remaining;
max_error = std::abs(first[3] - target);
}
}
}
for (int i = 0; i < n0; i++) {
for (int j = 0; j < n1; j++) {
double cost = command_cost(initial, p, target, pref, count, c0s[i], c1s[j], first);
if (std::abs(first[3] - target) > max_error + 1e-12) continue;
// Reject infeasible candidates before simulating their full return.
cost = finish(cost);
// Numerical equality only. Tie breaks cannot trade worse tracking for
// less channel motion; normalize them using the existing field spans.
double score = std::nearbyint(cost * 1e12) / 1e12;
double move = std::abs(c0s[i] - initial[0]) / 5.11 + std::abs(c1s[j] - initial[1]) / .5;
double remaining = std::abs(c0s[i] - pref[0]) / 5.11 + std::abs(c1s[j] - pref[1]) / .5;
if (score < best || (score == best && (move < best_move || (move == best_move && remaining < best_remaining)))) {
best = score;
best_move = move;
best_remaining = remaining;
result[0] = c0s[i];
result[1] = c1s[j];
result[2] = cost;
std::memcpy(result + 3, first, 5 * sizeof(double));
}
}
}
result[8] = max_error;
}
// Forecast the recent target trend briefly, then hold. The current-target
// accuracy bound remains the ordinary encoder's feasible reference.
extern "C" void paired_preview_select(const double *initial, const double *p,
const double *targets, const double *prefs, int n,
int count, double max_error, const double *c0s, int n0,
const double *c1s, int n1, double *result) {
double best = 1e300, best_move = 1e300, best_remaining = 1e300;
std::map<std::array<double, 6>, double> cache;
for (int i = 0; i < n0; i++) for (int j = 0; j < n1; j++) {
double first[5];
std::memcpy(first, initial, sizeof(first));
double cost = 0;
for (int k = 0; k < count; k++) {
step(first, p, c0s[i], c1s[j]);
double e = (first[3] - targets[std::min(k + 1, n)]) / .01;
cost += .008 * e * e;
}
// Preserve the ordinary C1-anchored bound against the current target.
if (std::abs(first[3] - targets[0]) > max_error + 1e-12) continue;
std::array<double, 6> key = {first[0], first[1], first[2], first[3], first[4], cost};
auto entry = cache.emplace(key, 0.);
if (entry.second) {
double s[5];
std::memcpy(s, first, sizeof(s));
for (int k = count; k < n; k++) {
step(s, p, prefs[2 * (k + 1)], prefs[2 * (k + 1) + 1]);
double e = (s[3] - targets[k + 1]) / .01;
cost += .008 * e * e;
}
entry.first->second = return_cost(s, p, targets[n], prefs + 2 * n, cost);
}
cost = entry.first->second;
double score = std::nearbyint(cost * 1e12) / 1e12;
double move = std::abs(c0s[i] - initial[0]) / 5.11 + std::abs(c1s[j] - initial[1]) / .5;
double remaining = std::abs(c0s[i] - prefs[2 * n]) / 5.11 + std::abs(c1s[j] - prefs[2 * n + 1]) / .5;
if (score < best || (score == best && (move < best_move || (move == best_move && remaining < best_remaining)))) {
best = score;
best_move = move;
best_remaining = remaining;
result[0] = c0s[i];
result[1] = c1s[j];
result[2] = cost;
std::memcpy(result + 3, first, sizeof(first));
}
}
result[8] = max_error;
}

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