From 7e63449749d112f096c56cb848dd289054e5f85b Mon Sep 17 00:00:00 2001 From: Isaac Barham Date: Mon, 7 Sep 2026 19:19:03 -0400 Subject: [PATCH] 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 --- docs/ford_model_action_damping.md | 2 +- docs/ford_model_action_drive_test.md | 13 +- docs/ford_model_action_full_prediction.md | 83 ++ ...del_action_full_prediction_validation.json | 799 ++++++++++++++++++ docs/ford_model_action_prediction.md | 14 +- .../controls/lib/ford_model_action.py | 15 +- .../tests/test_ford_controlsd_logging.py | 2 +- .../controls/tests/test_ford_model_action.py | 20 +- .../tests/test_ford_model_action_adapter.py | 4 +- .../test_ford_model_action_prediction.py | 28 +- .../sunnypilot/sunnylink/settings_ui.json | 2 +- .../settings_ui_src/pages/vehicle.yaml | 2 +- tools/ford_pscm_lab/damping_replay.py | 8 +- tools/ford_pscm_lab/model_action_replay.py | 3 +- tools/ford_pscm_lab/stress_model_action.py | 8 +- .../ford_pscm_lab/test_model_action_replay.py | 4 +- 16 files changed, 955 insertions(+), 52 deletions(-) create mode 100644 docs/ford_model_action_full_prediction.md create mode 100644 docs/ford_model_action_full_prediction_validation.json diff --git a/docs/ford_model_action_damping.md b/docs/ford_model_action_damping.md index e2c40c35fb..3e871c5736 100644 --- a/docs/ford_model_action_damping.md +++ b/docs/ford_model_action_damping.md @@ -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 diff --git a/docs/ford_model_action_drive_test.md b/docs/ford_model_action_drive_test.md index 84df2c746e..71677048c9 100644 --- a/docs/ford_model_action_drive_test.md +++ b/docs/ford_model_action_drive_test.md @@ -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 diff --git a/docs/ford_model_action_full_prediction.md b/docs/ford_model_action_full_prediction.md new file mode 100644 index 0000000000..f5d87bfb64 --- /dev/null +++ b/docs/ford_model_action_full_prediction.md @@ -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, 173–175.4 s | Mean C0 magnitude +0.032 m, maximum change 0.07 m | +| Earlier right entry, 637–640 s | Mean magnitude +0.028 m, maximum change 0.10 m | +| Earlier right exit, 642.7–643.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. diff --git a/docs/ford_model_action_full_prediction_validation.json b/docs/ford_model_action_full_prediction_validation.json new file mode 100644 index 0000000000..0d0d82e38c --- /dev/null +++ b/docs/ford_model_action_full_prediction_validation.json @@ -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" + } +} diff --git a/docs/ford_model_action_prediction.md b/docs/ford_model_action_prediction.md index 884b39f2fc..2cad0d6a33 100644 --- a/docs/ford_model_action_prediction.md +++ b/docs/ford_model_action_prediction.md @@ -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 ``` diff --git a/openpilot/selfdrive/controls/lib/ford_model_action.py b/openpilot/selfdrive/controls/lib/ford_model_action.py index 6bb126f6ea..78782d0735 100644 --- a/openpilot/selfdrive/controls/lib/ford_model_action.py +++ b/openpilot/selfdrive/controls/lib/ford_model_action.py @@ -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, diff --git a/openpilot/selfdrive/controls/tests/test_ford_controlsd_logging.py b/openpilot/selfdrive/controls/tests/test_ford_controlsd_logging.py index 3aff26f59f..a6d2933abd 100644 --- a/openpilot/selfdrive/controls/tests/test_ford_controlsd_logging.py +++ b/openpilot/selfdrive/controls/tests/test_ford_controlsd_logging.py @@ -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']) diff --git a/openpilot/selfdrive/controls/tests/test_ford_model_action.py b/openpilot/selfdrive/controls/tests/test_ford_model_action.py index d59b08a79c..d9b68c7764 100644 --- a/openpilot/selfdrive/controls/tests/test_ford_model_action.py +++ b/openpilot/selfdrive/controls/tests/test_ford_model_action.py @@ -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) diff --git a/openpilot/selfdrive/controls/tests/test_ford_model_action_adapter.py b/openpilot/selfdrive/controls/tests/test_ford_model_action_adapter.py index 66b3ce3823..e2eaba3e56 100644 --- a/openpilot/selfdrive/controls/tests/test_ford_model_action_adapter.py +++ b/openpilot/selfdrive/controls/tests/test_ford_model_action_adapter.py @@ -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.) diff --git a/openpilot/selfdrive/controls/tests/test_ford_model_action_prediction.py b/openpilot/selfdrive/controls/tests/test_ford_model_action_prediction.py index d7d25d786c..4260bc2425 100644 --- a/openpilot/selfdrive/controls/tests/test_ford_model_action_prediction.py +++ b/openpilot/selfdrive/controls/tests/test_ford_model_action_prediction.py @@ -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(): diff --git a/openpilot/sunnypilot/sunnylink/settings_ui.json b/openpilot/sunnypilot/sunnylink/settings_ui.json index 5bdb739e96..09866c698e 100644 --- a/openpilot/sunnypilot/sunnylink/settings_ui.json +++ b/openpilot/sunnypilot/sunnylink/settings_ui.json @@ -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" diff --git a/openpilot/sunnypilot/sunnylink/settings_ui_src/pages/vehicle.yaml b/openpilot/sunnypilot/sunnylink/settings_ui_src/pages/vehicle.yaml index 57115265f9..93df92910d 100644 --- a/openpilot/sunnypilot/sunnylink/settings_ui_src/pages/vehicle.yaml +++ b/openpilot/sunnypilot/sunnylink/settings_ui_src/pages/vehicle.yaml @@ -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 diff --git a/tools/ford_pscm_lab/damping_replay.py b/tools/ford_pscm_lab/damping_replay.py index bc1aa3ba11..6f237753df 100644 --- a/tools/ford_pscm_lab/damping_replay.py +++ b/tools/ford_pscm_lab/damping_replay.py @@ -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) diff --git a/tools/ford_pscm_lab/model_action_replay.py b/tools/ford_pscm_lab/model_action_replay.py index fcc47168f6..ada8cde662 100644 --- a/tools/ford_pscm_lab/model_action_replay.py +++ b/tools/ford_pscm_lab/model_action_replay.py @@ -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): diff --git a/tools/ford_pscm_lab/stress_model_action.py b/tools/ford_pscm_lab/stress_model_action.py index 265befb6f3..29f23a4d55 100644 --- a/tools/ford_pscm_lab/stress_model_action.py +++ b/tools/ford_pscm_lab/stress_model_action.py @@ -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), diff --git a/tools/ford_pscm_lab/test_model_action_replay.py b/tools/ford_pscm_lab/test_model_action_replay.py index 455b6003f6..507f14dc50 100644 --- a/tools/ford_pscm_lab/test_model_action_replay.py +++ b/tools/ford_pscm_lab/test_model_action_replay.py @@ -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()