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
This commit is contained in:
Isaac Barham
2026-09-07 19:19:03 -04:00
parent 01f8d51c82
commit 7e63449749
16 changed files with 955 additions and 52 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
# Experimental Ford offset damping, v2
This document and its validation counts describe the archived v2 source. The
[current v3 experiment](ford_model_action_prediction.md) adds bounded path prediction.
[current v4 experiment](ford_model_action_full_prediction.md) uses full path prediction.
Segment 10 of the supplied route9b recording shows measured turning persisting
as requested right curvature falls. At about 643.0 s, before strong driver
+7 -6
View File
@@ -1,8 +1,8 @@
# Ford selected-action drive-test branch
The candidate is selectable on the **Ford CAN FD F-150 Lightning** behind
its own persistent, default-off Sunnylink toggle. Version 3 adds
[bounded geometric path prediction](ford_model_action_prediction.md) while retaining
its own persistent, default-off Sunnylink toggle. Version 4 uses
[full geometric path prediction](ford_model_action_full_prediction.md) while retaining
[excess-yaw offset damping](ford_model_action_damping.md). Input gates are unchanged.
`calibration_approved=false`: offline checks do not establish physical tracking,
turn-exit behavior or closed-loop stability.
@@ -20,7 +20,7 @@ turn-exit behavior or closed-loop stability.
The startup log event `Ford path controller selected` should report
`FordModelActionController`. Periodic `Ford C2-free path tracking` events
identify `hypothesis=model-action-c0-c1-prediction-v3` and report host yaw and the command tuple.
identify `hypothesis=model-action-c0-c1-prediction-v4` and report host yaw and the command tuple.
Turning the new toggle off and completing another offroad-to-onroad cycle
restores **PSCM Coefficient Observer** if selected, otherwise the original
@@ -54,9 +54,10 @@ returns separate strings owned by the parameter handle. Regression tests
check distinct registered keys across flags, and toggle tests check its
persistence and backup registration using the rebuilt native library.
The current validation record is `ford_model_action_prediction_validation.json`;
the [prediction notes](ford_model_action_prediction.md) explain the command changes
and remaining physical uncertainty. `ford_model_action_damping_validation.json`
The current validation record is `ford_model_action_full_prediction_validation.json`;
the [full-prediction notes](ford_model_action_full_prediction.md) explain cap removal
and remaining physical uncertainty. `ford_model_action_prediction_validation.json`
archives the capped v3 evaluation. `ford_model_action_damping_validation.json`
archives the preceding v2 checks at their recorded source hashes.
`ford_model_action_drive_test_validation.json` archives v1 wiring validation
at the recorded source hashes, including 284 tests and 26 subtests. Its counts
+83
View File
@@ -0,0 +1,83 @@
# Experimental Ford full path prediction, v4
V4 removes the extra 15 cm / 25% limit on the geometric prediction introduced
in [v3](ford_model_action_prediction.md). Those numbers were hand-chosen tuning
bounds, not identified Ford response limits. The current user request is to
remove that restriction; the existing default-off Sunnylink toggle remains.
The controller now uses the full predicted offset from the same model path,
assuming 150 ms of motion along the selected, upstream-limited curvature. The
150 ms horizon remains an engineering assumption. Available model geometry
still limits the prediction distance, with endpoint hold for short paths and
fallback to the valid base offset if prediction arithmetic is nonfinite.
The existing total command limits (C0 ±5.11 m, C1 ±0.5 rad), independent slew
rates (4 m/s, 0.5 rad/s), quantization, yaw damping, input/service gates and
zero C2/C3 remain unchanged. Only two control states persist. No integrator,
model history, extra toggle or PSCM feedback loop is added.
Removing the adjustment cap also permits the predicted C0 to oppose the
original offset or become nonzero from a zero original offset. For example,
a straight path with a nonzero selected turn request can have an opposing
future-frame offset. Tests cover that behavior, mirrored turn releases,
return to zero, and unchanged slew; sign preservation of the original C0 is
no longer claimed. The yaw damper still cannot reverse its input target.
## Evidence and interpretation
The PSCM reports a generic `LimitReached` state. It does not tell us whether
an incoming target is geometrically correct or well timed. Its internal
limits cannot establish the tracking performance of this predictor. Removal
is an experiment supported by command comparisons, not by an assumption that
the PSCM will correct an excessive or mistimed request.
Compared with capped v3 on identical recorded inputs:
| Interval | Effect of removing the extra cap |
| --- | --- |
| Latest tight-left entry, 173175.4 s | Mean C0 magnitude +0.032 m, maximum change 0.07 m |
| Earlier right entry, 637640 s | Mean magnitude +0.028 m, maximum change 0.10 m |
| Earlier right exit, 642.7643.852 s | All 115 commands unchanged |
| Driver-clean low requests above 8 m/s, routes9b/9e | Mean absolute change 0.0018 / 0.0014 m; maximum 0.02 m |
| All eligible samples on either recent route | Maximum absolute command change 0.13 m |
C1 and command eligibility are exactly identical to v3 on both recent routes.
Some command signs change near zero: this is an intended consequence of using
the full transform, not proof those corrections improve driving. Entry windows
include driver input, reported in the validation record. The magnitude changes
above describe controller C0, not measured lateral vehicle displacement.
The earlier v1 archive is also reproduced exactly on routes90/95, separately
from the current controller pass. All replay uses original timestamps;
publication times proxy computation, exact consumed model frames and causal
carState are retained, and complete SubMaster health is unavailable. The
recorded model and vehicle motion remain fixed. There is no measured physical
improvement, stability result or new desired-versus-actual steering trajectory.
Final validation passes 374 tests and 26 subtests with 100% controller statement
and branch coverage, 299,604 original route cycles and 817,346 Float32/CAN
round trips. The controller is 190 total lines / 122 code lines excluding
blanks, comments and docstrings; two control states persist.
See `ford_model_action_full_prediction_validation.json` for final test counts,
coverage, dependency pins, source hashes and route/packing results. The initial
uncapped variant was evaluated in a separate lab file before editing production;
final route checks execute the production v4 source.
## Reproduce and select
Use the dependencies and combined suite command in the
[drive-test guide](ford_model_action_drive_test.md). For the recent routes:
```sh
python -m tools.ford_pscm_lab.damping_replay /path/to/route9e/rlogs --baseline v3 --candidate current --window left_entry 173 175.4 --window left_peak 175.4 178.3 --window left_exit 178.3 180.5 --window reversal 728 734 --output /path/to/separate/route9e-results
python -m tools.ford_pscm_lab.damping_replay /path/to/route9b/rlogs --baseline v3 --candidate current --window right_entry 637 640 --window right_exit 642.7 643.852 --output /path/to/separate/route9b-results
python -m tools.ford_pscm_lab.stress_model_action --cycles 200000 --seed 20260907 --opendbc-revision c21a9013700734dd20b09e05aa68329ad8cc20f9 --output /path/to/stress.json
```
The same **Selected-Action Path Tracking (Experimental)** toggle selects v4
on the CAN FD F-150 Lightning. An installation with the toggle already enabled
selects v4 after updating and restarting controlsd. Diagnostics identify
`model-action-c0-c1-prediction-v4`; `calibration_approved=false` remains explicit.
Deployment branch: `sunnypilot/sunnypilot`, `hiimisaac-dev`. This work does not
install software on the device or change its settings.
@@ -0,0 +1,799 @@
{
"date": "2026-09-07",
"baseline_commit": "01f8d51c82b3e863f1012d383b5994813ef01b81",
"hypothesis": "model-action-c0-c1-prediction-v4",
"scope": "Removal of only the extra prediction adjustment cap; no physical tracking or stability claim.",
"deployment_target": {
"repository": "sunnypilot/sunnypilot",
"branch": "hiimisaac-dev"
},
"calibration_approved": false,
"hardware_build_and_device_boot": "not performed",
"controller_size": {
"total_lines": 190,
"code_lines_excluding_blanks_comments_docstrings": 122,
"core_persistent_values": 2,
"adapter_timestamps": 3
},
"checks": {
"combined_ford_params_sunnylink_suite": "374 passed, 26 subtests passed; no skips",
"coverage": {
"covered_lines": 113,
"num_statements": 113,
"percent_covered": 100.0,
"percent_covered_display": "100",
"missing_lines": 0,
"excluded_lines": 0,
"percent_statements_covered": 100.0,
"percent_statements_covered_display": "100",
"num_branches": 32,
"num_partial_branches": 0,
"covered_branches": 32,
"missing_branches": 0,
"percent_branches_covered": 100.0,
"percent_branches_covered_display": "100"
},
"ruff": "pass",
"ty_controller_and_lab": "pass",
"settings_compiler_check": "pass",
"cap_removal_red_probe": "11 tests fail with the v3 cap present; all 40 prediction tests pass after removal.",
"standards_review_remaining_findings": 0,
"spec_review_remaining_findings": 0,
"independent_review_verification": "Each reviewer passed 172 focused tests and verified source/artifact hashes and route/packing totals."
},
"stress": {
"seed": 20260907,
"random_cycles": 200000,
"mirrored_core_updates": 200000,
"invalid_or_inactive_resets": 3537,
"field_boundary_cases": 18138,
"float32_can_round_trips": 218138,
"analytic_targets_scalar_slew_and_mirror_checks_pass": true,
"bounded_excess_yaw_damping_checked": true,
"full_geometric_prediction_checked": true,
"direct_raw_float32_packing_matches_host_output": true,
"max_continuous_step_c0_c1": [
0.400000000000329,
0.05000000000000002
],
"calibration_approved": false,
"scope": "Numerical construction only; no PSCM response or closed-loop performance claims.",
"opendbc_import_head": "c21a9013700734dd20b09e05aa68329ad8cc20f9",
"source_sha256": {
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/stress_model_action.py": "66adef6cba120a1a3d8e9730ea987ef49f8fe283023362dbb99608dee0e3229f",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/openpilot/selfdrive/controls/lib/ford_model_action.py": "7a226cf3cdf6dc8c3b15829078a7b93e486ba4722c4a1d93ceb55dd2e6c77b21",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/model_action_replay.py": "7cddac7ce9e88cc32bc7afbec7f9df79b66bb4dfa2fa5f9b36654891fae23a19"
}
},
"routes": {
"route90": {
"scope": "Command construction and adapter reconstruction only; no counterfactual closed-loop score.",
"calibration_approved": false,
"executes_live_selector": false,
"cycles": 78812,
"core_active_cycles": 73055,
"core_exact_archived_match": true,
"cohorts_reproduced": true,
"adapter_active_cycles": 73055,
"adapter_status_counts": {
"inactive": 5757,
"active": 73055
},
"adapter_matches_current_core_with_yaw_and_fresh_engagement_dt": true,
"core_active_path_shorter_than_7m_cycles": 0,
"adapter_validity_differs_from_archive_cycles": 0,
"adapter_command_differs_from_archive_cycles": 42170,
"adapter_max_absolute_command_difference_c0_c1": [
0.33999999999999986,
0.0020000000000000018
],
"field_slew_zero_c2_c3_pass": true,
"float32_can_round_trips": 157624,
"timing": "Original controls publication timestamps proxy computation time; repeated frames and gaps retained. No identified delay.",
"eligibility": "Adapter checks recorded services independently; full SubMaster health is unavailable. Core uses archived validity.",
"reference": "Recorded controlsState.desiredCurvature, already selected/limited. These two routes have no maneuver publications.",
"host_yaw": "Extract cs.yaw equals -carState.yawRate; current adapter uses it for bounded damping.",
"archived_core_revision": "5fc16abc7662020706e29f57d31a6d5e2bc1293a",
"archived_core_source_sha256": "8f3bc5d68e0051776f614a2ccffae84a88f7898dc95bdc12c23dcfe10dfe676a",
"cohorts": {
"small_request": {
"seconds": 475.24161910000026,
"core_c0_c1_rms": [
0.06125184365338734,
0.0057692154738982855
],
"recorded_v8_c0_c1_rms": [
0.057970374224682646,
0.007060795713496332
],
"adapter_eligible_seconds": 475.24161910000026,
"adapter_c0_c1_rms": [
0.0666350702799088,
0.0057692154738982855
]
},
"turn": {
"seconds": 47.85871389900012,
"core_c0_c1_rms": [
0.18725251016049418,
0.046449510236699354
],
"recorded_v8_c0_c1_rms": [
0.4773534845264775,
0.04315935101392594
],
"adapter_eligible_seconds": 47.85871389900012,
"adapter_c0_c1_rms": [
0.21255148222786066,
0.046449510236699354
]
},
"small_speed_2_8": {
"seconds": 38.696717235999785,
"core_c0_c1_rms": [
0.209264275431127,
0.014019080484620104
],
"recorded_v8_c0_c1_rms": [
0.1593558816527209,
0.01823849401244768
],
"adapter_eligible_seconds": 38.696717235999785,
"adapter_c0_c1_rms": [
0.22422047076469706,
0.014019080484620104
]
},
"turn_speed_2_8": {
"seconds": 0.30159887100000304,
"core_c0_c1_rms": [
2.090789090165622,
0.21878942570759013
],
"recorded_v8_c0_c1_rms": [
2.155728831937131,
0.38375302254248855
],
"adapter_eligible_seconds": 0.30159887100000304,
"adapter_c0_c1_rms": [
2.2977717861671105,
0.21878942570759013
]
},
"small_speed_8_15": {
"seconds": 79.2599167590001,
"core_c0_c1_rms": [
0.02250905681378238,
0.005843904113223639
],
"recorded_v8_c0_c1_rms": [
0.03477379324171293,
0.006545440153133947
],
"adapter_eligible_seconds": 79.2599167590001,
"adapter_c0_c1_rms": [
0.027461818062006878,
0.005843904113223639
]
},
"turn_speed_8_15": {
"seconds": 0.022264622000001566,
"core_c0_c1_rms": [
0.20999999999999996,
0.04349999999999998
],
"recorded_v8_c0_c1_rms": [
0.25,
0.04050000011920929
],
"adapter_eligible_seconds": 0.022264622000001566,
"adapter_c0_c1_rms": [
0.20999999999999996,
0.04349999999999998
]
},
"small_speed_15_55": {
"seconds": 357.2849851050004,
"core_c0_c1_rms": [
0.011622097035118838,
0.003925571829874466
],
"recorded_v8_c0_c1_rms": [
0.03809717934501699,
0.0045587590980714285
],
"adapter_eligible_seconds": 357.2849851050004,
"adapter_c0_c1_rms": [
0.017138026451004092,
0.003925571829874466
]
},
"turn_speed_15_55": {
"seconds": 47.53485040600012,
"core_c0_c1_rms": [
0.08686835454709245,
0.043216347944464766
],
"recorded_v8_c0_c1_rms": [
0.4471065308273131,
0.030663943784303503
],
"adapter_eligible_seconds": 47.53485040600012,
"adapter_c0_c1_rms": [
0.10939067791228956,
0.043216347944464766
]
},
"pose_quiet": {
"seconds": 427.99642020700026,
"core_c0_c1_rms": [
0.01261133542366393,
0.00422389167351435
],
"recorded_v8_c0_c1_rms": [
0.03724956081568198,
0.0046880581807907966
],
"adapter_eligible_seconds": 427.99642020700026,
"adapter_c0_c1_rms": [
0.017590935519277658,
0.00422389167351435
]
}
},
"workspace_head": "01f8d51c82b3e863f1012d383b5994813ef01b81",
"opendbc_import_head": "72a775d35e54c21ff5c5798acef22016eedcc0a7",
"opendbc_import_path": "/Users/ibpersonal/.codex/worktrees/b926/sunnypilot/opendbc_repo",
"source_sha256": {
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/model_action_replay.py": "7cddac7ce9e88cc32bc7afbec7f9df79b66bb4dfa2fa5f9b36654891fae23a19",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/openpilot/selfdrive/controls/lib/ford_model_action.py": "7a226cf3cdf6dc8c3b15829078a7b93e486ba4722c4a1d93ceb55dd2e6c77b21",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/openpilot/selfdrive/controls/lib/ford_path.py": "383538fc7cdae3bc28dffb71fe12ac5f3f9866ffbe6adfb7457f3593e9fc903a",
"/Users/ibpersonal/.codex/worktrees/3548/sunnypilot/analysis/controller_search_20260904/route90/route.npz": "51e8c26eedde253e171af47d704c1967ba45ae6825d883393bec1fb9e00251c1",
"/Users/ibpersonal/.codex/worktrees/3548/sunnypilot/analysis/controller_search_20260904/route90/metadata.json": "742afec55ed629155b22d387f376f878f7b2765221fe94f515a829052bdd916f",
"/Users/ibpersonal/.codex/worktrees/3548/sunnypilot/analysis/controller_search_20260904/route90/encoder_comparison.npz": "7a625d3ed5cbd8013d1028aa3bc421740551dcae5c3d60981208bd047af9794c",
"/Users/ibpersonal/.codex/worktrees/3548/sunnypilot/analysis/controller_search_20260904/route90/encoder_comparison.json": "64e1cc4be84394ac7ec408d383b99b48ada9b3fd129cdef2fd846e5fac620d66",
"/Users/ibpersonal/.codex/worktrees/3548/sunnypilot/analysis/controller_search_20260904/route90/pose_candidate/pose_replay.npz": "4457ccc0868354749da5b72c1dea0faf750f783dfdc87038101288fdcba1e707"
}
},
"route95": {
"scope": "Command construction and adapter reconstruction only; no counterfactual closed-loop score.",
"calibration_approved": false,
"executes_live_selector": false,
"cycles": 54738,
"core_active_cycles": 37614,
"core_exact_archived_match": true,
"cohorts_reproduced": true,
"adapter_active_cycles": 37614,
"adapter_status_counts": {
"inactive": 17124,
"active": 37614
},
"adapter_matches_current_core_with_yaw_and_fresh_engagement_dt": true,
"core_active_path_shorter_than_7m_cycles": 44,
"adapter_validity_differs_from_archive_cycles": 0,
"adapter_command_differs_from_archive_cycles": 25912,
"adapter_max_absolute_command_difference_c0_c1": [
0.2999999999999998,
0.0010000000000000009
],
"field_slew_zero_c2_c3_pass": true,
"float32_can_round_trips": 109476,
"timing": "Original controls publication timestamps proxy computation time; repeated frames and gaps retained. No identified delay.",
"eligibility": "Adapter checks recorded services independently; full SubMaster health is unavailable. Core uses archived validity.",
"reference": "Recorded controlsState.desiredCurvature, already selected/limited. These two routes have no maneuver publications.",
"host_yaw": "Extract cs.yaw equals -carState.yawRate; current adapter uses it for bounded damping.",
"archived_core_revision": "5fc16abc7662020706e29f57d31a6d5e2bc1293a",
"archived_core_source_sha256": "8f3bc5d68e0051776f614a2ccffae84a88f7898dc95bdc12c23dcfe10dfe676a",
"cohorts": {
"small_request": {
"seconds": 160.93427628100014,
"core_c0_c1_rms": [
0.027283284164810895,
0.006054573235641941
],
"recorded_v8_c0_c1_rms": [
0.04189890174428264,
0.0075342351227490395
],
"adapter_eligible_seconds": 160.93427628100014,
"adapter_c0_c1_rms": [
0.03232621215993653,
0.006054573235641941
]
},
"turn": {
"seconds": 58.39837079799993,
"core_c0_c1_rms": [
0.5425346915921071,
0.09102985199238507
],
"recorded_v8_c0_c1_rms": [
0.63700479987207,
0.09691236120507696
],
"adapter_eligible_seconds": 58.39837079799993,
"adapter_c0_c1_rms": [
0.5674989495695087,
0.09102985199238507
]
},
"small_speed_2_8": {
"seconds": 8.083969085000064,
"core_c0_c1_rms": [
0.09941192804175551,
0.016572084086884566
],
"recorded_v8_c0_c1_rms": [
0.07597383189748476,
0.016403469943482765
],
"adapter_eligible_seconds": 8.083969085000064,
"adapter_c0_c1_rms": [
0.11170387210996009,
0.016572084086884566
]
},
"turn_speed_2_8": {
"seconds": 2.9056519089999995,
"core_c0_c1_rms": [
2.286598281178436,
0.3420570411209625
],
"recorded_v8_c0_c1_rms": [
2.4371443775668484,
0.37360610108051345
],
"adapter_eligible_seconds": 2.9056519089999995,
"adapter_c0_c1_rms": [
2.3669481787447944,
0.3420570411209625
]
},
"small_speed_8_15": {
"seconds": 32.021085868,
"core_c0_c1_rms": [
0.028387860150467858,
0.0067602044219821
],
"recorded_v8_c0_c1_rms": [
0.036941169841952046,
0.007349671742584858
],
"adapter_eligible_seconds": 32.021085868,
"adapter_c0_c1_rms": [
0.033317964465740625,
0.0067602044219821
]
},
"turn_speed_8_15": {
"seconds": 22.160645306000006,
"core_c0_c1_rms": [
0.28668316323581683,
0.0671915746138579
],
"recorded_v8_c0_c1_rms": [
0.3391022553367454,
0.07297678510653233
],
"adapter_eligible_seconds": 22.160645306000006,
"adapter_c0_c1_rms": [
0.32040564199313326,
0.0671915746138579
]
},
"small_speed_15_55": {
"seconds": 120.82922132800007,
"core_c0_c1_rms": [
0.010802222799973712,
0.004282512931613862
],
"recorded_v8_c0_c1_rms": [
0.03987958989535526,
0.0065793876512313535
],
"adapter_eligible_seconds": 120.82922132800007,
"adapter_c0_c1_rms": [
0.016212091011218406,
0.004282512931613862
]
},
"turn_speed_15_55": {
"seconds": 33.332073582999925,
"core_c0_c1_rms": [
0.07257996778378971,
0.03628926246224237
],
"recorded_v8_c0_c1_rms": [
0.3416081588451539,
0.027322786376822172
],
"adapter_eligible_seconds": 33.332073582999925,
"adapter_c0_c1_rms": [
0.08724830487921002,
0.03628926246224237
]
},
"pose_quiet": {
"seconds": 145.24136911700015,
"core_c0_c1_rms": [
0.012566204405760934,
0.004614194153565963
],
"recorded_v8_c0_c1_rms": [
0.03855743841849719,
0.006621274926345545
],
"adapter_eligible_seconds": 145.24136911700015,
"adapter_c0_c1_rms": [
0.01710948709543248,
0.004614194153565963
]
}
},
"workspace_head": "01f8d51c82b3e863f1012d383b5994813ef01b81",
"opendbc_import_head": "72a775d35e54c21ff5c5798acef22016eedcc0a7",
"opendbc_import_path": "/Users/ibpersonal/.codex/worktrees/b926/sunnypilot/opendbc_repo",
"source_sha256": {
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/model_action_replay.py": "7cddac7ce9e88cc32bc7afbec7f9df79b66bb4dfa2fa5f9b36654891fae23a19",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/openpilot/selfdrive/controls/lib/ford_model_action.py": "7a226cf3cdf6dc8c3b15829078a7b93e486ba4722c4a1d93ceb55dd2e6c77b21",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/openpilot/selfdrive/controls/lib/ford_path.py": "383538fc7cdae3bc28dffb71fe12ac5f3f9866ffbe6adfb7457f3593e9fc903a",
"/Users/ibpersonal/.codex/worktrees/3548/sunnypilot/analysis/controller_search_20260904/route95/route.npz": "6e5438867ea618e53ca395b60ff4b9b146256f1bf6f07dc8e92d663286074154",
"/Users/ibpersonal/.codex/worktrees/3548/sunnypilot/analysis/controller_search_20260904/route95/metadata.json": "f7c95a102e0e084bbcc9938399219080c1d3b26274b4ea3c3a9514ff7dc8ea8d",
"/Users/ibpersonal/.codex/worktrees/3548/sunnypilot/analysis/controller_search_20260904/route95/encoder_comparison.npz": "23bd05c4b6400299844acaba1d97051c96c23c682bf17b46e89e7f2cca5fce38",
"/Users/ibpersonal/.codex/worktrees/3548/sunnypilot/analysis/controller_search_20260904/route95/encoder_comparison.json": "1ab24a796d14817b8672ea6dfd39711d65cb06744eaaef7dcc02a840c392c4d3",
"/Users/ibpersonal/.codex/worktrees/3548/sunnypilot/analysis/controller_search_20260904/route95/pose_candidate/pose_replay.npz": "9cfbb3c6f0b1fdb7e3d38e6b64b8c94cffbcc9fabe41f6344d84a9b657b6af9b"
}
},
"route9b": {
"scope": "Fixed-input command replay only; no physical improvement or stability claim.",
"calibration_approved": false,
"cycles": 82386,
"eligible_cycles": 70703,
"status_counts": {
"inactive": 11683,
"active": 70703
},
"c1_exactly_unchanged": true,
"same_validity": true,
"field_slew_zero_c2_c3_pass": true,
"float32_can_round_trips": 164772,
"cohorts": {
"eligible": {
"cycles": 70703,
"seconds": 711.571703289,
"changed_c0_cycles": 10475,
"mean_absolute_c0_change_m": 0.00219777369954635,
"max_absolute_c0_change_m": 0.13000000000000078,
"increased_absolute_c0_cycles": 9822,
"decreased_absolute_c0_cycles": 633,
"driver_input_percent": 3.0297623863274845,
"baseline_peak_absolute_c0_m": 3.96,
"candidate_peak_absolute_c0_m": 3.99
},
"driver_clean": {
"cycles": 63267,
"seconds": 636.7469998759999,
"changed_c0_cycles": 8901,
"mean_absolute_c0_change_m": 0.0014713899370902674,
"max_absolute_c0_change_m": 0.05999999999999961,
"increased_absolute_c0_cycles": 8314,
"decreased_absolute_c0_cycles": 568,
"driver_input_percent": 0.0,
"baseline_peak_absolute_c0_m": 2.24,
"candidate_peak_absolute_c0_m": 2.24
},
"driver_clean_low_request_above_8mps": {
"cycles": 40656,
"seconds": 409.13067679800054,
"changed_c0_cycles": 7132,
"mean_absolute_c0_change_m": 0.0017587822497730527,
"max_absolute_c0_change_m": 0.020000000000000462,
"increased_absolute_c0_cycles": 6888,
"decreased_absolute_c0_cycles": 244,
"driver_input_percent": 0.0,
"baseline_peak_absolute_c0_m": 0.11000000000000032,
"candidate_peak_absolute_c0_m": 0.11000000000000032
},
"turn": {
"cycles": 5459,
"seconds": 54.939139939000256,
"changed_c0_cycles": 119,
"mean_absolute_c0_change_m": 0.00021719119817404867,
"max_absolute_c0_change_m": 0.010000000000000675,
"increased_absolute_c0_cycles": 119,
"decreased_absolute_c0_cycles": 0,
"driver_input_percent": 0.0,
"baseline_peak_absolute_c0_m": 2.24,
"candidate_peak_absolute_c0_m": 2.24
},
"right_entry": {
"cycles": 298,
"seconds": 2.9932484459999387,
"changed_c0_cycles": 216,
"mean_absolute_c0_change_m": 0.028050765542768245,
"max_absolute_c0_change_m": 0.10000000000000009,
"increased_absolute_c0_cycles": 216,
"decreased_absolute_c0_cycles": 0,
"driver_input_percent": 20.395198628296086,
"baseline_peak_absolute_c0_m": 2.95,
"candidate_peak_absolute_c0_m": 3.0300000000000002
},
"right_exit": {
"cycles": 115,
"seconds": 1.156770048999988,
"changed_c0_cycles": 0,
"mean_absolute_c0_change_m": 0.0,
"max_absolute_c0_change_m": 0.0,
"increased_absolute_c0_cycles": 0,
"decreased_absolute_c0_cycles": 0,
"driver_input_percent": 0.0,
"baseline_peak_absolute_c0_m": 0.7199999999999998,
"candidate_peak_absolute_c0_m": 0.7199999999999998
}
},
"baseline_commands_vs_recorded_publications": {
"paired_cycles": 70703,
"within_one_quantum_cycles": 65890,
"maximum_absolute_error_c0_c1": [
0.25000004768371564,
0.0005000143051147043
]
},
"timing": "Publication-time proxy, causal carState, exact consumed model; full SubMaster health unavailable.",
"baseline_version": "v3",
"baseline_revision": "01f8d51c82b3e863f1012d383b5994813ef01b81",
"candidate_version": "current",
"candidate_revision": "working_tree",
"focus_windows": [
[
"right_entry",
"637",
"640"
],
[
"right_exit",
"642.7",
"643.852"
]
],
"baseline_source_sha256": "5b9f7946f29b5fcd94ca93e53be14a12b18ae74d7ca27380e92b90c8c599dce0",
"candidate_source_sha256": "7a226cf3cdf6dc8c3b15829078a7b93e486ba4722c4a1d93ceb55dd2e6c77b21",
"source_rlog_sha256": {
"84865544361f55cb_0000009b--e4616dddaa--0--rlog.zst": "22746f7119109b73ed7f2c26ce8c99f87136e9124fb7fc14c9554409a28a7c3f",
"84865544361f55cb_0000009b--e4616dddaa--1--rlog.zst": "4c2e1d7083c31a2b37d0f8dd3be4d330898511b7e02c26f7d40ca9bc2779397d",
"84865544361f55cb_0000009b--e4616dddaa--2--rlog.zst": "62f3e049e220cd3681fadf386f2969537bd571998ae2f6ba2d08479428b5a28f",
"84865544361f55cb_0000009b--e4616dddaa--3--rlog.zst": "83bf0131b2d36b2ba7e5ba050bbc13c0a3350feb5c9b89dc9c87d3a37abebfb3",
"84865544361f55cb_0000009b--e4616dddaa--4--rlog.zst": "430985a80dd6e10f7abeb89457a17022e6bb6978617f415c905f584b1647603e",
"84865544361f55cb_0000009b--e4616dddaa--5--rlog.zst": "8c0c5ae6323ec33b3e14f84ca834f70cb56f6b29f471a350f1e3efc06b6ba553",
"84865544361f55cb_0000009b--e4616dddaa--6--rlog.zst": "db53dfa8156b9d66792c3eff0b2ce5d31b71ad41cc580dec85f528845593c184",
"84865544361f55cb_0000009b--e4616dddaa--7--rlog.zst": "91b0b3be10cb7d7d7f7dd2024d8f9ee99d1e9fd2204203a3a9a2f2f1c6e3fa03",
"84865544361f55cb_0000009b--e4616dddaa--8--rlog.zst": "687dbbfc49837efbfe8fa6bc091e40f7fad2908832234d7884f4616d1bc9ccff",
"84865544361f55cb_0000009b--e4616dddaa--9--rlog.zst": "a88ec4d25b04cdbf5844686fc77f6b28dca920c9b164e37ebf69844a3ae398fc",
"84865544361f55cb_0000009b--e4616dddaa--10--rlog.zst": "fe6b29580a6c94e1c236d13e18db4cd9f31cc1b25d52e1e6e19a5021125c9932",
"84865544361f55cb_0000009b--e4616dddaa--11--rlog.zst": "150d31b1944d7a1b8c562f3aee20b66cefa6c4e8d02660ec889907d835142f45",
"84865544361f55cb_0000009b--e4616dddaa--12--rlog.zst": "e105e5f703a70f36f1989c66fb46c35a65ff60a265b4332e10fa2e6875a2ced4",
"84865544361f55cb_0000009b--e4616dddaa--13--rlog.zst": "69304cf0c81401374f047c5022ca47d257ff9e59e30ec171325a2ee1e7ed13d4"
},
"opendbc_head": "c21a9013700734dd20b09e05aa68329ad8cc20f9",
"source_sha256": {
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/damping_replay.py": "8c4f1f987b65e2be146c473a6e529168f08b1f7ce3c9514e22a9874450fa430e",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/openpilot/selfdrive/controls/lib/ford_model_action.py": "7a226cf3cdf6dc8c3b15829078a7b93e486ba4722c4a1d93ceb55dd2e6c77b21"
}
},
"route9e": {
"scope": "Fixed-input command replay only; no physical improvement or stability claim.",
"calibration_approved": false,
"cycles": 83668,
"eligible_cycles": 74669,
"status_counts": {
"inactive": 8999,
"active": 74669
},
"c1_exactly_unchanged": true,
"same_validity": true,
"field_slew_zero_c2_c3_pass": true,
"float32_can_round_trips": 167336,
"cohorts": {
"eligible": {
"cycles": 74669,
"seconds": 751.5479497329998,
"changed_c0_cycles": 8108,
"mean_absolute_c0_change_m": 0.00147612892002983,
"max_absolute_c0_change_m": 0.13000000000000034,
"increased_absolute_c0_cycles": 7665,
"decreased_absolute_c0_cycles": 423,
"driver_input_percent": 2.7324748055386094,
"baseline_peak_absolute_c0_m": 4.38,
"candidate_peak_absolute_c0_m": 4.38
},
"driver_clean": {
"cycles": 66321,
"seconds": 667.5291971840106,
"changed_c0_cycles": 7257,
"mean_absolute_c0_change_m": 0.0012096637080993352,
"max_absolute_c0_change_m": 0.08000000000000007,
"increased_absolute_c0_cycles": 6883,
"decreased_absolute_c0_cycles": 374,
"driver_input_percent": 0.0,
"baseline_peak_absolute_c0_m": 2.4699999999999998,
"candidate_peak_absolute_c0_m": 2.4699999999999998
},
"driver_clean_low_request_above_8mps": {
"cycles": 39635,
"seconds": 398.9613925449994,
"changed_c0_cycles": 5427,
"mean_absolute_c0_change_m": 0.0013748276784907216,
"max_absolute_c0_change_m": 0.020000000000000462,
"increased_absolute_c0_cycles": 5341,
"decreased_absolute_c0_cycles": 86,
"driver_input_percent": 0.0,
"baseline_peak_absolute_c0_m": 0.07000000000000028,
"candidate_peak_absolute_c0_m": 0.07000000000000028
},
"turn": {
"cycles": 6219,
"seconds": 62.59491144700405,
"changed_c0_cycles": 244,
"mean_absolute_c0_change_m": 0.000391833417652352,
"max_absolute_c0_change_m": 0.010000000000000675,
"increased_absolute_c0_cycles": 244,
"decreased_absolute_c0_cycles": 0,
"driver_input_percent": 0.0,
"baseline_peak_absolute_c0_m": 2.4699999999999998,
"candidate_peak_absolute_c0_m": 2.4699999999999998
},
"left_entry": {
"cycles": 238,
"seconds": 2.4048367620016506,
"changed_c0_cycles": 204,
"mean_absolute_c0_change_m": 0.03167584138917884,
"max_absolute_c0_change_m": 0.07000000000000028,
"increased_absolute_c0_cycles": 204,
"decreased_absolute_c0_cycles": 0,
"driver_input_percent": 2.403816920672437,
"baseline_peak_absolute_c0_m": 2.6100000000000003,
"candidate_peak_absolute_c0_m": 2.6399999999999997
},
"left_peak": {
"cycles": 288,
"seconds": 2.891819394000777,
"changed_c0_cycles": 6,
"mean_absolute_c0_change_m": 0.00022373297976799967,
"max_absolute_c0_change_m": 0.019999999999999574,
"increased_absolute_c0_cycles": 6,
"decreased_absolute_c0_cycles": 0,
"driver_input_percent": 6.41034524444729,
"baseline_peak_absolute_c0_m": 2.9400000000000004,
"candidate_peak_absolute_c0_m": 2.95
},
"left_exit": {
"cycles": 219,
"seconds": 2.203320298998733,
"changed_c0_cycles": 11,
"mean_absolute_c0_change_m": 0.0008062973780191794,
"max_absolute_c0_change_m": 0.019999999999999574,
"increased_absolute_c0_cycles": 11,
"decreased_absolute_c0_cycles": 0,
"driver_input_percent": 9.936619705158035,
"baseline_peak_absolute_c0_m": 2.51,
"candidate_peak_absolute_c0_m": 2.51
},
"reversal": {
"cycles": 554,
"seconds": 5.570295188001182,
"changed_c0_cycles": 54,
"mean_absolute_c0_change_m": 0.002577813237060219,
"max_absolute_c0_change_m": 0.04999999999999982,
"increased_absolute_c0_cycles": 29,
"decreased_absolute_c0_cycles": 25,
"driver_input_percent": 0.0,
"baseline_peak_absolute_c0_m": 0.21999999999999975,
"candidate_peak_absolute_c0_m": 0.21999999999999975
}
},
"baseline_commands_vs_recorded_publications": {
"paired_cycles": 74669,
"within_one_quantum_cycles": 69690,
"maximum_absolute_error_c0_c1": [
0.369999988079071,
0.0010000016689301061
]
},
"timing": "Publication-time proxy, causal carState, exact consumed model; full SubMaster health unavailable.",
"baseline_version": "v3",
"baseline_revision": "01f8d51c82b3e863f1012d383b5994813ef01b81",
"candidate_version": "current",
"candidate_revision": "working_tree",
"focus_windows": [
[
"left_entry",
"173",
"175.4"
],
[
"left_peak",
"175.4",
"178.3"
],
[
"left_exit",
"178.3",
"180.5"
],
[
"reversal",
"728",
"734"
]
],
"baseline_source_sha256": "5b9f7946f29b5fcd94ca93e53be14a12b18ae74d7ca27380e92b90c8c599dce0",
"candidate_source_sha256": "7a226cf3cdf6dc8c3b15829078a7b93e486ba4722c4a1d93ceb55dd2e6c77b21",
"source_rlog_sha256": {
"84865544361f55cb_0000009e--592f7dc149--0--rlog.zst": "de63532ae6aedf5dc7fd3ac8e47a2d96a8f065614f4aca79ae2f120ea00390c7",
"84865544361f55cb_0000009e--592f7dc149--1--rlog.zst": "67b58197158b3e8f0581643f6657d2a85c47b0bb75dd3e00306d9ffae790008b",
"84865544361f55cb_0000009e--592f7dc149--2--rlog.zst": "a4840f338f51b5f1864c79ce3a4f2b11dc13d58d52b8459961c9da57237f2cc2",
"84865544361f55cb_0000009e--592f7dc149--3--rlog.zst": "ff3ea8d948006ab19c4dbfeeff59a509de3f193da47b91c88faa27ecd0b3f1fc",
"84865544361f55cb_0000009e--592f7dc149--4--rlog.zst": "5ff1996c2336299128a11e657c32bc21921716b7e3bd401de69a2d49484aa223",
"84865544361f55cb_0000009e--592f7dc149--5--rlog.zst": "238763457e89896933afaf9a5df325469ee8c02ddc53550df252531bc94fc540",
"84865544361f55cb_0000009e--592f7dc149--6--rlog.zst": "2ee86de80cfd762be10cd2dfb2895ddbee6b813706e9c7261460203e09b9bc4d",
"84865544361f55cb_0000009e--592f7dc149--7--rlog.zst": "017801f080861c63d799f87aebe30ab57cdf82f078ac882e6187d3870c403538",
"84865544361f55cb_0000009e--592f7dc149--8--rlog.zst": "3729016bd1f00bb1077613b63fe25b21ba7112822b994e0e0aa2b4cd93bdb940",
"84865544361f55cb_0000009e--592f7dc149--9--rlog.zst": "c40b3c1f6eb9252b85f176fee32cae16c23eaad3db830f6bbf37a730034a5ccf",
"84865544361f55cb_0000009e--592f7dc149--10--rlog.zst": "e0ce8f231798073e5bbc34551fbd9493f169a06a068adc1957ef8cd914b71fbb",
"84865544361f55cb_0000009e--592f7dc149--11--rlog.zst": "90863353f7c862a05618b4d0761dda3fcedd3cf6b4234c21c6dcc2eee3ec20b2",
"84865544361f55cb_0000009e--592f7dc149--12--rlog.zst": "e064f8abde9615b2daf00f469ef36b438cd4b3a1637069467c84de91bee3bdf1",
"84865544361f55cb_0000009e--592f7dc149--13--rlog.zst": "790fc438ce2675e0690fdce3b07bf54fdf6d06b17bbe7f5ff447adfa71189754",
"84865544361f55cb_0000009e--592f7dc149--14--rlog.zst": "b98f5a5be660013b1fe11250fdc24ee4fe1d9dd1301b787ddfdff1f2bcd3e27f"
},
"opendbc_head": "c21a9013700734dd20b09e05aa68329ad8cc20f9",
"source_sha256": {
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/damping_replay.py": "8c4f1f987b65e2be146c473a6e529168f08b1f7ce3c9514e22a9874450fa430e",
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/openpilot/selfdrive/controls/lib/ford_model_action.py": "7a226cf3cdf6dc8c3b15829078a7b93e486ba4722c4a1d93ceb55dd2e6c77b21"
}
}
},
"ablation_comparison": {
"route9b": {
"lab_variant_commands_exactly_match_production": true,
"opposed_nonzero_c0_commands": 24,
"max_absolute_difference_on_opposed_commands_m": 0.05999999999999961
},
"route9e": {
"lab_variant_commands_exactly_match_production": true,
"opposed_nonzero_c0_commands": 30,
"max_absolute_difference_on_opposed_commands_m": 0.02999999999999936
}
},
"total_original_route_cycles": 299604,
"total_float32_can_round_trips": 817346,
"source_sha256": {
"openpilot/selfdrive/controls/lib/ford_model_action.py": "7a226cf3cdf6dc8c3b15829078a7b93e486ba4722c4a1d93ceb55dd2e6c77b21",
"openpilot/selfdrive/controls/tests/test_ford_model_action.py": "30cfffb86fcd3830320e9b2ec1f65cdfd219684a55d0bc0bb13f77966cc18b53",
"openpilot/selfdrive/controls/tests/test_ford_model_action_adapter.py": "8ab5dd52958f13a2426e07f8713b7d8d56c52b8a05d173ffba87ffccc0087796",
"openpilot/selfdrive/controls/tests/test_ford_model_action_damping.py": "a7062d2c3c46b3abcd4c3facd97970644ce609fb55285daa0359681950948250",
"openpilot/selfdrive/controls/tests/test_ford_model_action_prediction.py": "eb50e1d6acdb5e7332fadc3dcfcfa1b3809545e607c47c54b0ba3de80b2df208",
"openpilot/selfdrive/controls/tests/test_ford_model_action_selection.py": "f826c6328f0abac2a61f1a0a6f8d119fdbab858e363cb466d84ba9a7783059cd",
"openpilot/selfdrive/controls/tests/test_ford_controlsd_logging.py": "175a1969ac83b14b88798bbde37371f740cd05cf3f8f6e5d21bbf9afe857bb57",
"tools/ford_pscm_lab/__init__.py": "db4b8b7d2e317ed34ca0ec220bf9d53e7b80a23e766f4e1cb2c8224dada45f2e",
"tools/ford_pscm_lab/damping_replay.py": "8c4f1f987b65e2be146c473a6e529168f08b1f7ce3c9514e22a9874450fa430e",
"tools/ford_pscm_lab/model_action_replay.py": "7cddac7ce9e88cc32bc7afbec7f9df79b66bb4dfa2fa5f9b36654891fae23a19",
"tools/ford_pscm_lab/stress_model_action.py": "66adef6cba120a1a3d8e9730ea987ef49f8fe283023362dbb99608dee0e3229f",
"tools/ford_pscm_lab/test_model_action_replay.py": "1f7d90c06ec09b405571565632653dc7d984b106b955877916d4a0836c11e3b5",
"openpilot/sunnypilot/sunnylink/settings_ui_src/pages/vehicle.yaml": "41ac31726b941e470f5be34e2523dc1fc2b25fb6a12705ab267a57ed4f3c27c8",
"openpilot/sunnypilot/sunnylink/settings_ui.json": "c44c75e02042a0485b5bd7896e0b794761d768bbe513b6dc9c2c986ddd22b44c"
},
"artifact_sha256": {
".cache/ford_prediction_uncapped/uncapped_red.txt": "09160dbc36320be886561cfdbdd21eef0c8bc420a11bae9bb1a8dd43f7b93a3c",
".cache/ford_prediction_uncapped/suite_run.txt": "99b3bd2af256c9a4a083cb015a51a458a26ac02a8cad73c28c47470f9a5eb910",
".cache/ford_prediction_uncapped/coverage.json": "50a282c90fa86147ed6925fdf37918471f8e5204a4c1e1ef9adb39335e220f42",
".cache/ford_prediction_uncapped/stress.json": "e4226f403a3a914b9f3a7868b14a2b763215ec1f77e7d45ac06604859acc9fb6",
".cache/ford_prediction_uncapped/route90/report.json": "c5cee3c56f9f7f292c8069be22dd1ffb1649055fccaf829b28d96bdec6558541",
".cache/ford_prediction_uncapped/route95/report.json": "6fcc8a3bd7abb5b513888cb64426670e7e05f8b2100b1876be196a800cec2c90",
".cache/ford_prediction_uncapped/final9b/report.json": "79faca3ff89e2015dad4823d4d988ea26ead55c7e51e4a594258102d32701b07",
".cache/ford_prediction_uncapped/final9e/report.json": "9b003ce97225e65e03e351c5e133f3fed08dc79e026f73308098d51a98dd67ae",
".cache/ford_prediction_uncapped/route90/commands.npz": "8787aabbe7007bcd7c68976c272085761aec6a4f360fa6cc8c8553bd1421c92b",
".cache/ford_prediction_uncapped/route95/commands.npz": "349b716bd910cd030716b9ef519ee24f620fae5280776ffa871bc43318939400",
".cache/ford_prediction_uncapped/final9b/commands.npz": "ff4a14fc74b144800f459c250be961ebb05a46a20e6ad1fc0394a659c9854394",
".cache/ford_prediction_uncapped/final9e/commands.npz": "59f6046acb10b00ef00e7b9feb5b8547139a67a2f8767d7cbfe3603a59e6c77d"
}
}
+10 -4
View File
@@ -1,5 +1,9 @@
# Experimental Ford path prediction, v3
This document and its counts describe archived v3. The
[current v4 experiment](ford_model_action_full_prediction.md) removes the extra
15 cm / 25% prediction adjustment cap.
The latest driven route9e used v1 (`5fc16abc7`), before the v2 yaw damping.
It often follows the requested steering angle closely, but some tight turns
fall behind after a reasonable initial turn-in. The requested angle is replanned
@@ -97,13 +101,15 @@ See `ford_model_action_prediction_validation.json` for provenance and counts.
## Reproduce and select
Use the native dependencies and suite command in the
[drive-test guide](ford_model_action_drive_test.md). New-route comparisons use
To reproduce the archived suite and stress results, use v3 commit
`01f8d51c82b3e863f1012d383b5994813ef01b81` with the native dependencies and
suite command in the [drive-test guide](ford_model_action_drive_test.md).
Current replay tooling can select the immutable v3 source explicitly. New-route comparisons use
the deployment opendbc pin `c21a9013700734dd20b09e05aa68329ad8cc20f9`:
```sh
python -m tools.ford_pscm_lab.damping_replay /path/to/route9e/rlogs --baseline v2 --candidate current --window left_entry 173 175.4 --window left_peak 175.4 178.3 --window left_exit 178.3 180.5 --window reversal 728 734 --window final_entry 822 825.5 --output /path/to/separate/route9e-results
python -m tools.ford_pscm_lab.damping_replay /path/to/route9b/rlogs --baseline v2 --candidate current --window right_entry 637 640 --window right_exit_before_strong_input 642.7 643.852 --output /path/to/separate/route9b-results
python -m tools.ford_pscm_lab.damping_replay /path/to/route9e/rlogs --baseline v2 --candidate v3 --window left_entry 173 175.4 --window left_peak 175.4 178.3 --window left_exit 178.3 180.5 --window reversal 728 734 --window final_entry 822 825.5 --output /path/to/separate/route9e-results
python -m tools.ford_pscm_lab.damping_replay /path/to/route9b/rlogs --baseline v2 --candidate v3 --window right_entry 637 640 --window right_exit_before_strong_input 642.7 643.852 --output /path/to/separate/route9b-results
python -m tools.ford_pscm_lab.stress_model_action --cycles 200000 --seed 20260907 --opendbc-revision c21a9013700734dd20b09e05aa68329ad8cc20f9 --output /path/to/stress.json
```
@@ -17,16 +17,14 @@ HEADING_TIME_S = 1.0
EXCESS_YAW_DEADBAND = .02 # rad/s; above the observed approximately .008 rad/s Ford yaw offset
EXCESS_YAW_LOOKAHEAD_S = .2 # engineering choice, not an identified PSCM delay
CALIBRATION_APPROVED = False
PREDICTION_TIME_S = .15 # bounded geometric preview, not an identified actuator delay
PREDICTION_LIMIT_M = .15
PREDICTION_FRACTION = .25
PREDICTION_TIME_S = .15 # geometric preview, not an identified actuator delay
def _predict_offset(path, c0, desired_curvature, speed):
"""Read the same path from a predicted pose along the selected curvature.
A matched constant-radius path retains its offset. Developing/flattening
bends can move the target earlier, bounded to 15 cm and 25% of current C0.
bends can move the target earlier. The core retains field limits and slew.
Use only available geometry; shortened horizons taper prediction to zero.
"""
station, longitudinal, lateral, _ = path
@@ -41,8 +39,7 @@ def _predict_offset(path, c0, desired_curvature, speed):
predicted = math.cos(rotation)*y-math.sin(rotation)*x+translation
if not _finite(predicted):
return c0
limit = min(PREDICTION_LIMIT_M, PREDICTION_FRACTION*abs(c0))
return c0+float(np.clip(predicted-c0, -limit, limit))
return predicted
def _packed(value, resolution, offset):
@@ -59,7 +56,7 @@ def _finite(*values):
def encode_model_action(model, desired_curvature, speed):
"""Encode bounded predicted y(7) and max(7, v*1s)*selected limited curvature.
"""Encode predicted y(7) and max(7, v*1s)*selected limited curvature.
Preserve the reviewed core's endpoint hold when the path ends before 7 m.
This samples the available geometry; it does not extrapolate an unseen path.
@@ -145,7 +142,7 @@ class FordModelActionController:
def reset(self, status='inactive'):
self.core.reset()
self.last_time = self.last_measurement_time = self.last_model_time = None
self.diagnostics = {'status': status, 'hypothesis': 'model-action-c0-c1-prediction-v3',
self.diagnostics = {'status': status, 'hypothesis': 'model-action-c0-c1-prediction-v4',
'calibration_approved': CALIBRATION_APPROVED, 'command': (0., 0., 0., 0.)}
def update(self, model, desired_curvature, *, yaw_rate, speed, now, measurement_time, model_time, reference_time,
@@ -176,7 +173,7 @@ class FordModelActionController:
self.reset('invalid_path')
return command
self.last_time, self.last_measurement_time, self.last_model_time = now, measurement_time, model_time
self.diagnostics = {'status': 'active', 'hypothesis': 'model-action-c0-c1-prediction-v3',
self.diagnostics = {'status': 'active', 'hypothesis': 'model-action-c0-c1-prediction-v4',
'calibration_approved': CALIBRATION_APPROVED, 'desired_curvature': desired_curvature,
'yaw_rate': yaw_rate,
'model_age': now - model_time, 'measurement_age': now - measurement_time, 'reference_age': now - reference_time,
@@ -53,7 +53,7 @@ class TestFordControlsLogging(unittest.TestCase):
controls = SimpleNamespace(ford_path_controller=controller, desired_curvature=.005, curvature=.0025,
sm=SimpleNamespace(logMonoTime={'modelV2': 123456789, 'carState': 123450000}))
record = self.emit_controls_event('Ford C2-free path tracking', controls)
self.assertEqual(record['hypothesis'], 'model-action-c0-c1-prediction-v3')
self.assertEqual(record['hypothesis'], 'model-action-c0-c1-prediction-v4')
self.assertIs(record['calibration_approved'], False)
self.assertEqual(record['command'][2:], [0., 0.])
self.assertEqual(record['status'], controller.diagnostics['status'])
@@ -54,19 +54,24 @@ def test_two_actuator_positions_are_sufficient_for_every_next_output():
assert controller.update(model, desired, **kwargs) == copied.update(model, desired, **kwargs)
def test_held_turn_releases_without_a_bias_tail_or_sign_reversal():
def test_held_turn_releases_with_geometric_countersteering_and_no_retained_bias():
for sign in (-1., 1.):
controller = ModelActionController()
for _ in range(400):
out = controller.update(circle(sign*.01), sign*.01, speed=20., dt=.01)
assert out.path_angle == pytest.approx(sign*.2)
previous = np.array([out.path_offset, out.path_angle])
previous = np.array([controller.c0, controller.c1])
opposed = False
for desired in sign*np.linspace(.01, 0., 101):
out = controller.update(straight(), desired, speed=20., dt=.01)
values = np.array([out.path_offset, out.path_angle])
assert (abs(values) <= abs(previous)+1e-8).all()
assert (sign*values >= -1e-8).all()
rotation = desired*3.
predicted = (1.-math.cos(rotation))/desired-10.*math.sin(rotation) if desired else 0.
expected = previous+np.clip([predicted, 20.*desired]-previous, [-.04, -.005], [.04, .005])
values = np.array([controller.c0, controller.c1])
np.testing.assert_allclose(values, expected, atol=1e-10)
opposed |= sign*out.path_offset < 0.
previous = values
assert opposed
assert out == FordPath(True, 0., 0., 0., 0.)
@@ -180,8 +185,9 @@ def test_arc_station_not_forward_x_or_model_heading_determines_offset():
x = np.array([0., 6., 12.])
y = .4+x*.75
target = encode_model_action(make_model(x, y, [2., -2., 1.]), -.01, 20.)
# Arc length is 1.25*x, so base y(7)=4.6. Prediction reaches its +.15m bound.
assert target.path_offset == pytest.approx(4.75)
# Arc length is 1.25*x. At station 10, x=8 and y=6.4; use the full predicted pose.
expected = math.cos(-.03)*6.4-math.sin(-.03)*8.+(1.-math.cos(-.03))/-.01
assert target.path_offset == pytest.approx(expected)
assert target.path_angle == pytest.approx(-.2)
@@ -81,7 +81,7 @@ def test_repeated_measurements_do_not_freeze_slew_or_cache_invalid_model_geometr
controller = FordModelActionController()
for i in range(10):
result = update(controller, 1.+i*.01, measurement_time=1., model_time=1., reference_time=1.)
assert result.path_offset == pytest.approx(.3) # Preview accounts for selected turning toward the offset path.
assert result.path_offset == pytest.approx(.14) # Full preview accounts for selected turning toward the offset path.
assert result.path_angle == pytest.approx(.05)
broken = straight(.4)
broken.position.y[5] = math.nan
@@ -115,7 +115,7 @@ def test_release_keeps_current_geometry_and_may_grow_c0_while_c1_decreases():
assert abs(after.path_angle) < abs(before.path_angle)
for i in range(100):
released = update(controller, 3.+i*.01, model=circle(sign*.02), desired_curvature=0.)
assert released.path_offset == after.path_offset
assert abs(released.path_offset) > abs(after.path_offset) # Less expected turning raises the future-frame offset.
assert released.path_angle == pytest.approx(0.)
@@ -14,13 +14,15 @@ def geometric_offset(model):
@pytest.mark.parametrize('sign', [-1., 1.])
def test_developing_bend_advances_offset_with_bounded_request(sign):
def test_developing_bend_uses_full_prediction_beyond_former_cap(sign):
x = np.linspace(0., 30., 3001)
model = make_model(x, sign*.001*x**3, np.zeros_like(x))
base = geometric_offset(model)
target = encode_model_action(model, 0., 10.)
assert sign*target.path_offset > sign*base+.05
assert target.path_offset == pytest.approx(base*1.25)
station = np.r_[0., np.cumsum(np.hypot(np.diff(x), np.diff(model.position.y)))]
assert target.path_offset == pytest.approx(np.interp(8.5, station, model.position.y))
assert abs(target.path_offset-base) > .25*abs(base)
assert target.path_angle == target.curvature == target.curvature_rate == 0.
@@ -53,11 +55,23 @@ def test_straight_centering_and_near_zero_curvature_remain_well_conditioned(offs
@pytest.mark.parametrize('offset', [-4., -.4, -.001, 0., .001, .4, 4.])
def test_prediction_cannot_reverse_or_swamp_existing_centering(offset):
for curvature in (-1., -.1, .1, 1.):
value = encode_model_action(straight(offset), curvature, 55.).path_offset
assert abs(value-offset) <= min(.15, .25*abs(offset))+1e-12
assert value*offset >= 0.
def test_full_rotated_path_prediction_is_independent_of_base_offset_magnitude(offset):
station = np.linspace(0., 30., 301)
for heading in (-.3, .3):
model = make_model(station*np.cos(heading), offset+station*np.sin(heading), np.full_like(station, heading))
value = encode_model_action(model, 0., 20.).path_offset
assert value == pytest.approx(offset+10.*math.sin(heading))
assert abs(value-geometric_offset(model)) > .15
@pytest.mark.parametrize('sign', [-1., 1.])
def test_zero_near_offset_can_request_opposing_centering_from_the_predicted_pose(sign):
curvature = sign*.01
target = encode_model_action(straight(), curvature, 20.)
expected = (1.-math.cos(curvature*3.))/curvature-10.*math.sin(curvature*3.)
assert target.path_offset == pytest.approx(expected)
assert sign*target.path_offset < -.25
assert target.path_angle == pytest.approx(sign*.2)
def test_short_horizon_holds_endpoint_and_available_prediction_tapers_to_zero():
@@ -2184,7 +2184,7 @@
"needs_onroad_cycle": true,
"title": "Selected-Action Path Tracking (Experimental)",
"description": "Follow the selected steering plan with nearby model-path centering on the Ford CAN FD F-150 Lightning.",
"details": "Uses a small, bounded prediction of the nearby model path to respond as bends develop, plus a heading request based on selected planned curvature. Reduces same-direction offset demand when measured turning exceeds the requested turn. Default off; this revised turn-entry and exit behavior is not road-validated. Enable only for controlled testing. On the Ford CAN FD F-150 Lightning this takes priority over PSCM Coefficient Observer; other vehicles retain their existing controller. Turning it off restores PSCM Coefficient Observer if selected, otherwise the original Ford path controller. Changes apply after a real offroad-to-onroad cycle, not immediately or on disengagement alone.",
"details": "Uses a short prediction of the nearby model path to respond as bends develop, plus a heading request based on selected planned curvature. The predicted offset uses the full geometric request within the existing command limits and rate limits. Reduces same-direction offset demand when measured turning exceeds the requested turn. Default off; this revised turn-entry and exit behavior is not road-validated. Enable only for controlled testing. On the Ford CAN FD F-150 Lightning this takes priority over PSCM Coefficient Observer; other vehicles retain their existing controller. Turning it off restores PSCM Coefficient Observer if selected, otherwise the original Ford path controller. Changes apply after a real offroad-to-onroad cycle, not immediately or on disengagement alone.",
"enablement": [
{
"type": "offroad_only"
@@ -15,7 +15,7 @@ sections:
needs_onroad_cycle: true
title: Selected-Action Path Tracking (Experimental)
description: Follow the selected steering plan with nearby model-path centering on the Ford CAN FD F-150 Lightning.
details: Uses a small, bounded prediction of the nearby model path to respond as bends develop, plus a heading request based on selected planned curvature. Reduces same-direction offset demand when measured turning exceeds the requested turn. Default off; this revised turn-entry and exit behavior is not road-validated. Enable only for controlled testing. On the Ford CAN FD F-150 Lightning this takes priority over PSCM Coefficient Observer; other vehicles retain their existing controller. Turning it off restores PSCM Coefficient Observer if selected, otherwise the original Ford path controller. Changes apply after a real offroad-to-onroad cycle, not immediately or on disengagement alone.
details: Uses a short prediction of the nearby model path to respond as bends develop, plus a heading request based on selected planned curvature. The predicted offset uses the full geometric request within the existing command limits and rate limits. Reduces same-direction offset demand when measured turning exceeds the requested turn. Default off; this revised turn-entry and exit behavior is not road-validated. Enable only for controlled testing. On the Ford CAN FD F-150 Lightning this takes priority over PSCM Coefficient Observer; other vehicles retain their existing controller. Turning it off restores PSCM Coefficient Observer if selected, otherwise the original Ford path controller. Changes apply after a real offroad-to-onroad cycle, not immediately or on disengagement alone.
enablement:
- $ref: '#/macros/offroad'
- key: FordPscmObserver
+4 -4
View File
@@ -16,7 +16,7 @@ import zstandard
from openpilot.cereal import log
from openpilot.selfdrive.controls.lib import ford_model_action
from tools.ford_pscm_lab.model_action_replay import V1_REVISION, V2_REVISION, WireCheck, field_checks, load_controller, sample, verify_dependency
from tools.ford_pscm_lab.model_action_replay import V1_REVISION, V2_REVISION, V3_REVISION, WireCheck, field_checks, load_controller, sample, verify_dependency
DEPLOYMENT_OPENDBC = 'c21a9013700734dd20b09e05aa68329ad8cc20f9'
@@ -73,7 +73,7 @@ def run(directory, output, baseline_version='v1', candidate_version='v2', window
if output == directory or directory in output.parents:
raise ValueError('Output must be outside the source route directory')
verify_dependency(DEPLOYMENT_OPENDBC)
revisions = {'v1': V1_REVISION, 'v2': V2_REVISION}
revisions = {'v1': V1_REVISION, 'v2': V2_REVISION, 'v3': V3_REVISION}
baseline_source = load_controller(revisions[baseline_version])
candidate_source = ford_model_action if candidate_version == 'current' else load_controller(revisions[candidate_version])
streams, models, sources, t0 = extract(directory)
@@ -162,8 +162,8 @@ if __name__ == '__main__':
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('rlog_directory', type=Path)
parser.add_argument('--output', type=Path, required=True)
parser.add_argument('--baseline', choices=['v1', 'v2'], default='v1')
parser.add_argument('--candidate', choices=['v2', 'current'], default='v2')
parser.add_argument('--baseline', choices=['v1', 'v2', 'v3'], default='v1')
parser.add_argument('--candidate', choices=['v2', 'v3', 'current'], default='v2')
parser.add_argument('--window', action='append', nargs=3, metavar=('LABEL', 'START_SECONDS', 'END_SECONDS'), default=[])
args = parser.parse_args()
run(args.rlog_directory, args.output, args.baseline, args.candidate, args.window)
+2 -1
View File
@@ -28,9 +28,10 @@ from openpilot.selfdrive.controls.lib.ford_path import _model_path
PINNED_OPENDBC = '72a775d35e54c21ff5c5798acef22016eedcc0a7'
V1_REVISION = '5fc16abc7662020706e29f57d31a6d5e2bc1293a'
V2_REVISION = '744a97d9bc08d8743b250eceff7c88585b5480de'
V3_REVISION = '01f8d51c82b3e863f1012d383b5994813ef01b81'
@lru_cache(maxsize=2)
@lru_cache(maxsize=3)
def load_controller(commit):
"""Load exact archived Python source for offline comparisons, never production."""
if len(commit) != 40 or any(c not in '0123456789abcdef' for c in commit):
+2 -6
View File
@@ -71,7 +71,6 @@ def run(cycles, seed, output, opendbc_revision=PINNED_OPENDBC):
assert out.valid == other.valid == expected_valid
previous = np.array([c0, c1])
if expected_valid:
base = offset+7.*math.sin(heading)
distance, rotation = speed*.15, desired*speed*.15
# Independent full pose transform, including stable small-angle series.
if abs(rotation) < 1e-5:
@@ -81,10 +80,7 @@ def run(cycles, seed, output, opendbc_revision=PINNED_OPENDBC):
ego_x, ego_y = math.sin(rotation)/desired, (1.-math.cos(rotation))/desired
future_x, future_y = (7.+distance)*math.cos(heading), offset+(7.+distance)*math.sin(heading)
predicted = math.cos(rotation)*(future_y-ego_y)-math.sin(rotation)*(future_x-ego_x)
bound = min(.15, .25*abs(base))
advanced = base+max(-bound, min(bound, predicted-base))
assert abs(advanced-base) <= bound+1e-12 and advanced*base >= 0.
target = (max(-5.11, min(5.11, advanced)), max(-.5, min(.5, max(7., speed)*desired)))
target = (max(-5.11, min(5.11, predicted)), max(-.5, min(.5, max(7., speed)*desired)))
# Independent piecewise scalar oracle; do not call the production helper.
offset_target = target[0]
if offset_target > 0. and yaw > 0.:
@@ -127,7 +123,7 @@ def run(cycles, seed, output, opendbc_revision=PINNED_OPENDBC):
'invalid_or_inactive_resets': resets, 'field_boundary_cases': boundary_cases,
'float32_can_round_trips': wire.count, 'analytic_targets_scalar_slew_and_mirror_checks_pass': True,
'bounded_excess_yaw_damping_checked': True,
'bounded_geometric_prediction_checked': True,
'full_geometric_prediction_checked': True,
'direct_raw_float32_packing_matches_host_output': True, 'max_continuous_step_c0_c1': max_continuous_step.tolist(),
'calibration_approved': False, 'scope': 'Numerical construction only; no PSCM response or closed-loop performance claims.',
'opendbc_import_head': revision(dependency),
@@ -45,13 +45,13 @@ def test_archived_loader_uses_exact_source_and_records_its_hash(monkeypatch):
monkeypatch.setattr(replay.subprocess, 'check_output', read_source)
replay.load_controller.cache_clear()
try:
for commit in (replay.V1_REVISION, replay.V2_REVISION):
for commit in (replay.V1_REVISION, replay.V2_REVISION, replay.V3_REVISION):
module = replay.load_controller(commit)
assert module.archived_value == 42
assert module.source_sha256 == hashlib.sha256(source).hexdigest()
assert calls[-1][-2:] == ['show', f'{commit}:openpilot/selfdrive/controls/lib/ford_model_action.py']
assert replay.load_controller(commit) is module
assert len(calls) == 2
assert len(calls) == 3
finally:
replay.load_controller.cache_clear()