diff --git a/docs/ford_model_action_cadence.md b/docs/ford_model_action_cadence.md deleted file mode 100644 index f2c0ffafba..0000000000 --- a/docs/ford_model_action_cadence.md +++ /dev/null @@ -1,60 +0,0 @@ -# Selected-action 20 Hz cadence experiment - -The selected-action controller now sends LateralMotionControl2 every fifth -100 Hz update (20 Hz, nominally 50 ms). Each send uses the latest published -C0/C1. The v6 controller module is byte-identical to `c70a9ee84`: its geometry, -150 ms forecast, caps, two states, and 4 m/s / 0.5 rad/s slew are unchanged. -C2 and C3 remain zero. This tests transport cadence, not a new strength gain. - -The existing default-off `FordModelActionController` Sunnylink setting is -snapshotted by `card` into `CarParamsSP.flags` on the supported CAN FD Lightning. -Both controller selection and send cadence use that snapshot. An onroad setting -write cannot switch either one. Complete an offroad-to-onroad cycle after updating. -The diagnostic hypothesis remains `model-action-measured-pose-v6`; the build -commit, CarParamsSP flag, and measured send cadence distinguish this experiment. - -With the toggle off, existing CAN FD controllers retain 100 Hz transmission. -Legacy CAN stays at 20 Hz. Panda safety is byte-identical to `c21a9013`. -The sender retains Panda's existing per-message C2 slew bound even on the -20 Hz path; this controller does not use C2. No safety limit is relaxed. -Counters advance once per transmitted request, including wrap from 15 to 0. -Invalid paths zero the next scheduled request; disengagement sends mode 0 on -that request, without a new ramp-out sequence. Relative to a 100 Hz sender, -a change can wait up to four more control ticks (nominally 40 ms). - -## Evidence and limits - -On route `84865544361f55cb_000000a5--d0f935d323`, the camera's observed inactive -LMC2 stream ran at 19.993 Hz (7,952 messages, median 50.051 ms). Our sender ran -at 99.321 Hz (38,496 messages, median 9.938 ms). The camera data does not establish -the factory's active-mode cadence. All supplied v1–v6 drives already used -100 Hz; cadence has not been established as the cause of weak tracking. - -The [transport replay record](ford_model_action_cadence_validation.json) covers -38,496 recorded send cycles at each of five possible scheduling phases: -192,480 sender updates and 38,496 transmitted requests in total. Every emitted -C0/C1 exactly matched the corresponding recorded request; C2/C3 stayed zero; -mode, counter and checksum checks passed. Every request passed the unchanged, -compiled Panda TX hook with controls eligibility set from the recorded mode. -This tests TX bounds, not a full Panda RX watchdog, vehicle response, or device boot. - -Targeted tests additionally cover exact send intervals, latest-sample delivery, -counter wrap, every disengagement/invalid-input phase, unchanged fallback cadence, -100 Hz core slew, and a shared selection snapshot surviving serialization and -subsequent stored-toggle changes. The broader offline run passed 689 tests and -9,146 subtests; 178 inherited safety cases were skipped as inapplicable. -Ruff and typechecking of the touched production modules passed. - -No physical tracking improvement is claimed. The next drive must establish -whether the lower cadence helps ordinary bends and turn exits, while checking -for added turn-in delay. Offline replay cannot predict that closed-loop response. - -## Reproduce - -Initialize the pinned submodule and use the project's built Python/native environment: - -```sh -export PYTHONDONTWRITEBYTECODE=1 -export PYTHONPATH=.:opendbc_repo -python -m pytest -q openpilot/selfdrive/controls/tests/test_ford*.py tools/ford_pscm_lab opendbc_repo/opendbc/car/ford/tests/test_ford.py openpilot/sunnypilot/sunnylink/tests openpilot/sunnypilot/mads/tests openpilot/selfdrive/car/tests/test_ford_pscm_status.py openpilot/common/tests/test_params.py opendbc_repo/opendbc/safety/tests/test_ford.py -``` diff --git a/docs/ford_model_action_cadence_validation.json b/docs/ford_model_action_cadence_validation.json deleted file mode 100644 index 0766fd97c2..0000000000 --- a/docs/ford_model_action_cadence_validation.json +++ /dev/null @@ -1,45 +0,0 @@ -{ - "scope": "Frozen a5 publications through actual 20Hz CarController and unchanged compiled Panda TX hook, all five scheduling phases. No physical response simulation.", - "route": "84865544361f55cb_000000a5--d0f935d323", - "baseline_root": "c70a9ee84bbf6db9a687d63ef674ed4db9c13e3a", - "baseline_opendbc": "c21a9013700734dd20b09e05aa68329ad8cc20f9", - "input_sha256": "815d1e248ff5c3e5e5cfc11dfbd0690d0d13ddafe1436890e239975a72dae9b8", - "phases": [ - { - "phase": 0, - "input_cycles": 38496, - "sent_and_accepted": 7700 - }, - { - "phase": 1, - "input_cycles": 38496, - "sent_and_accepted": 7699 - }, - { - "phase": 2, - "input_cycles": 38496, - "sent_and_accepted": 7699 - }, - { - "phase": 3, - "input_cycles": 38496, - "sent_and_accepted": 7699 - }, - { - "phase": 4, - "input_cycles": 38496, - "sent_and_accepted": 7699 - } - ], - "core_byte_identical": true, - "ford_safety_byte_identical": true, - "source_sha256": { - "openpilot/selfdrive/controls/lib/ford_model_action.py": "326539389b04034932db41ca2c67794779403b2c0e97f408a81a9f5734114899", - "openpilot/selfdrive/controls/controlsd.py": "002d57a0b5b6e4e3a04789bee20b9b175a71d6893dd06074cebca2738f3986da", - "openpilot/sunnypilot/mads/helpers.py": "24970993d37242fe8a0457bac118e2c265c4c32427dc930c0001de12e5f204dd", - "opendbc_repo/opendbc/car/ford/carcontroller.py": "591b0d8455d256f7504cbd2a6a11fe1a54ba6ad3e12ac16a6952dbd92274f303", - "opendbc_repo/opendbc/car/ford/values.py": "edadaacc13581642917d1fba473ef3efc90d7145dbe801915f860747a4294046", - "opendbc_repo/opendbc/safety/modes/ford.h": "1d9d996292d6697ab4f02d55fae348d6aca1df94a07f7bdae48b68971b91afe7", - ".cache/ford_cadence/replay.py": "7227f0b4f130be0e5b28f517416ffbf281f0b33085b8391f66814aa531c3e514" - } -} diff --git a/docs/ford_model_action_damping.md b/docs/ford_model_action_damping.md deleted file mode 100644 index 3e871c5736..0000000000 --- a/docs/ford_model_action_damping.md +++ /dev/null @@ -1,88 +0,0 @@ -# Experimental Ford offset damping, v2 - -This document and its validation counts describe the archived v2 source. The -[current v4 experiment](ford_model_action_full_prediction.md) uses full path prediction. - -Segment 10 of the supplied route9b recording shows measured turning persisting -as requested right curvature falls. At about 643.0 s, before strong driver -intervention, device-gyro curvature is approximately 0.01786/m against a -0.01172/m request. Around 643.9 s, heading demand has reversed slightly but -C0 still requests approximately +0.12 m into the turn. Strong column input -starts around 643.852 s; later motion cannot establish autonomous recovery. -Earlier light driver input also exists. - -The outgoing CAN commands match preceding publications. All 70,937 decoded -frames have zero C2/C3 and valid checksums. Focused exit diagnostics have fresh -model/carState inputs and targets within normal quantization of the outputs. -This supports trying less residual C0; it does not identify PSCM dynamics or -prove C0 alone caused the physical oversteer. - -## Change - -C0 starts from the clipped current model offset at 7 m. When C0 and measured -host yaw point in the same direction, compute: - -``` -requested_yaw = max(0, sign(C0) * speed * desiredCurvature) -excess = max(0, sign(C0) * yaw - requested_yaw - 0.02 rad/s) -reduction = 7 m * 0.2 s * excess -target = sign(C0) * max(0, abs(C0) - reduction) -``` - -Opposing centering demand is unchanged. The correction cannot increase the -target's magnitude or reverse its sign. Opposite-direction planned curvature -cannot amplify a small yaw bias into a correction. Existing 4 m/s C0 slew still -applies; this target bound is not a claim that every stateful output is smaller than a -separate v1 controller after arbitrary direction reversals. C1 construction, -clipping and slew are unchanged; C2=C3=0. Only C0/C1 slew states persist. -There is no integral, model-history filter, turn state machine or fitted plant. - -Host yaw is `-carState.yawRate`, as in the existing Ford call path. The -0.02 rad/s deadband exceeds the approximately 0.008 rad/s offset measured -against the device gyro on quiet straights. The 0.2 s scale is an initial -engineering choice, not an identified delay or gain. Both remain physically -unvalidated. Large biased or noisy yaw within the existing sanity gate can -still attenuate useful centering; fixed-input replay cannot establish stability. - -## Offline evidence - -The complete 12-rlog route is replayed at original controls publication times, -with exact consumed model geometry, causal carState, and carControl matched -within 5 ms. These times proxy computation; full SubMaster health is unavailable. -V1 reconstruction is within one field quantum of all 64,701 paired active -publications. V2 has identical eligibility and exactly identical C1. - -On 381.78 seconds of driver-clean low requests above 8 m/s, only 3 of 37,937 cycles -change C0, each by one 0.01 m quantum. Across the 642.7–643.852 s exit window, -C0 changes on all 115 cycles, averaging 0.080 m reduction. Entry/peak C0 -maximum stays 2.80 m; some entry-window samples decrease by up to 0.05 m. -These are command comparisons on recorded inputs, not predicted tracking. -Low request means requested lateral acceleration below 0.15 m/s²; it is a -proxy for straight driving and does not establish a physically straight path. - -The original routes90/95 also run through v2. Their zero-yaw baseline pass -checks archived v1 compatibility; the measured-yaw adapter pass checks current -construction, eligibility, field limits and packing. It is not an exact match -to v1 or v8. Randomized testing checks the damping against an independent -piecewise oracle, mirror symmetry, resets and slew, with real Float32/CAN -round trips. See `ford_model_action_damping_validation.json` for counts and hashes. - -Final validation passes 325 tests and 26 subtests, with 100% controller -statement/branch coverage, 204,946 original route cycles and 628,030 CAN round -trips. The module is 166 total lines, including 107 code lines excluding -comments, blanks and docstrings. Standards and Spec reviews have no remaining findings. - -## Reproduce - -Use the dependency setup and suite command in the [drive-test guide](ford_model_action_drive_test.md). -The new route replay requires the deployment opendbc pin recorded there: - -```sh -python -m tools.ford_pscm_lab.damping_replay /path/to/complete/rlogs --baseline v1 --candidate v2 --window segment10_entry_peak 637 640 --window segment10_exit_before_strong_input 642.7 643.852 --output /path/to/separate/results -python -m tools.ford_pscm_lab.stress_model_action --cycles 200000 --seed 20260907 --opendbc-revision c21a9013700734dd20b09e05aa68329ad8cc20f9 --output /path/to/stress.json -``` - -At the v2 revision, the same default-off Sunnylink toggle selects v2; no additional setting is -introduced. Updating an installation with the toggle already enabled selects -v2 at the next controlsd startup. `calibration_approved=false` remains explicit. -No physical fix, hardware build or device boot is established by these checks. diff --git a/docs/ford_model_action_damping_validation.json b/docs/ford_model_action_damping_validation.json deleted file mode 100644 index 4354f2c5ac..0000000000 --- a/docs/ford_model_action_damping_validation.json +++ /dev/null @@ -1,164 +0,0 @@ -{ - "date": "2026-09-07", - "baseline_commit": "5fc16abc7662020706e29f57d31a6d5e2bc1293a", - "deployment_target": { - "repository": "sunnypilot/sunnypilot", - "branch": "hiimisaac-dev" - }, - "hypothesis": "model-action-c0-c1-yaw-damping-v2", - "scope": "Experimental bounded offset damping; fixed-input offline evidence only.", - "calibration_approved": false, - "hardware_build_and_device_boot": "not performed", - "controller_size": { - "total_lines": 166, - "code_lines_excluding_blanks_comments_docstrings": 107, - "core_persistent_values": 2, - "adapter_timestamps": 3 - }, - "checks": { - "combined_ford_params_sunnylink_suite": "325 passed, 26 subtests passed; no skips", - "suite_log_sha256": "69cb8ab40e93e00a9ff7b6ea1933e4c3554336746860efb53dad3b29f94fc9d3", - "coverage": { - "statements": 98, - "branches": 28, - "percent": 100.0 - }, - "ruff": "pass", - "ty_controller_and_lab": "pass", - "settings_compiler_check": "pass", - "standards_review_remaining_findings": 0, - "spec_review_remaining_findings": 0, - "review_resolutions": [ - "Prevent opposite-direction planned curvature amplifying small yaw bias; eight new cases failed before the fix and passed after it.", - "Relabel requested-acceleration cohort as low request; it does not establish physically straight driving." - ], - "mutation_probe": "Disabling damping fails all four mirrored recorded-exit cases." - }, - "stress": { - "random_cycles": 200000, - "mirrored_core_updates": 200000, - "field_boundary_cases": 18138, - "float32_can_round_trips": 218138, - "bounded_excess_yaw_damping_checked": true, - "opendbc_import_head": "c21a9013700734dd20b09e05aa68329ad8cc20f9" - }, - "routes": { - "route90": { - "cycles": 78812, - "core_exact_archived_match": true, - "adapter_active_cycles": 73055, - "adapter_matches_current_core_with_yaw_and_fresh_engagement_dt": true, - "adapter_validity_differs_from_archive_cycles": 0, - "adapter_command_differs_from_archive_cycles": 2280, - "adapter_max_absolute_command_difference_c0_c1": [ - 0.1900000000000004, - 0.0020000000000000018 - ], - "float32_can_round_trips": 157624, - "opendbc_import_head": "72a775d35e54c21ff5c5798acef22016eedcc0a7" - }, - "route95": { - "cycles": 54738, - "core_exact_archived_match": true, - "adapter_active_cycles": 37614, - "adapter_matches_current_core_with_yaw_and_fresh_engagement_dt": true, - "adapter_validity_differs_from_archive_cycles": 0, - "adapter_command_differs_from_archive_cycles": 2051, - "adapter_max_absolute_command_difference_c0_c1": [ - 0.22999999999999998, - 0.0010000000000000009 - ], - "float32_can_round_trips": 109476, - "opendbc_import_head": "72a775d35e54c21ff5c5798acef22016eedcc0a7" - }, - "route9b": { - "cycles": 71396, - "eligible_cycles": 64701, - "same_validity": true, - "c1_exactly_unchanged": true, - "field_slew_zero_c2_c3_pass": true, - "float32_can_round_trips": 142792, - "v1_reconstruction_vs_recorded": { - "paired_cycles": 64701, - "within_one_quantum_cycles": 64701, - "maximum_absolute_error_c0_c1": [ - 0.010000114440917862, - 0.0005000143051147043 - ] - }, - "timing": "Publication-time proxy, causal carState, exact consumed model; full SubMaster health unavailable.", - "baseline": "Current adapter with zero yaw retains v1 targets and actual-yaw sanity gate.", - "opendbc_head": "c21a9013700734dd20b09e05aa68329ad8cc20f9", - "cohorts": { - "driver_clean_low_request_above_8mps": { - "cycles": 37937, - "seconds": 381.77672216900055, - "changed_c0_cycles": 3, - "mean_absolute_c0_change_m": 8.419774473786704e-07, - "max_absolute_c0_change_m": 0.009999999999999787, - "v1_peak_absolute_c0_m": 0.08999999999999986, - "v2_peak_absolute_c0_m": 0.08999999999999986 - }, - "segment10_entry_peak": { - "cycles": 298, - "seconds": 2.9932484459999387, - "changed_c0_cycles": 100, - "mean_absolute_c0_change_m": 0.007564164795694466, - "max_absolute_c0_change_m": 0.04999999999999982, - "v1_peak_absolute_c0_m": 2.8000000000000003, - "v2_peak_absolute_c0_m": 2.8000000000000003 - }, - "segment10_exit_before_strong_input": { - "cycles": 115, - "seconds": 1.156770048999988, - "changed_c0_cycles": 115, - "mean_absolute_c0_change_m": 0.0799313220721162, - "max_absolute_c0_change_m": 0.1200000000000001, - "v1_peak_absolute_c0_m": 0.75, - "v2_peak_absolute_c0_m": 0.7000000000000002 - } - }, - "exit_c0_strictly_lower_on_all_115_cycles": true, - "rlog_sha256_by_segment": { - "0": "22746f7119109b73ed7f2c26ce8c99f87136e9124fb7fc14c9554409a28a7c3f", - "1": "4c2e1d7083c31a2b37d0f8dd3be4d330898511b7e02c26f7d40ca9bc2779397d", - "2": "62f3e049e220cd3681fadf386f2969537bd571998ae2f6ba2d08479428b5a28f", - "3": "83bf0131b2d36b2ba7e5ba050bbc13c0a3350feb5c9b89dc9c87d3a37abebfb3", - "4": "430985a80dd6e10f7abeb89457a17022e6bb6978617f415c905f584b1647603e", - "5": "8c0c5ae6323ec33b3e14f84ca834f70cb56f6b29f471a350f1e3efc06b6ba553", - "6": "db53dfa8156b9d66792c3eff0b2ce5d31b71ad41cc580dec85f528845593c184", - "7": "91b0b3be10cb7d7d7f7dd2024d8f9ee99d1e9fd2204203a3a9a2f2f1c6e3fa03", - "8": "687dbbfc49837efbfe8fa6bc091e40f7fad2908832234d7884f4616d1bc9ccff", - "9": "a88ec4d25b04cdbf5844686fc77f6b28dca920c9b164e37ebf69844a3ae398fc", - "10": "fe6b29580a6c94e1c236d13e18db4cd9f31cc1b25d52e1e6e19a5021125c9932", - "11": "150d31b1944d7a1b8c562f3aee20b66cefa6c4e8d02660ec889907d835142f45" - } - } - }, - "total_original_route_cycles": 204946, - "total_float32_can_round_trips": 628030, - "source_sha256": { - "docs/ford_model_action_damping.md": "1aca8ca8e78d953beeda5b0c9803161a1d7c58556b1966040c71f809c3960bf8", - "docs/ford_model_action_drive_test.md": "3ce8bf6cb511a4461fa7abf194f47020af7ef5a6c90fcae7ed09571d1f750846", - "openpilot/selfdrive/controls/lib/ford_model_action.py": "59d66297a017557f3d4f28b115be3f6220b800566c11935e2284b3814783fb7e", - "openpilot/selfdrive/controls/tests/test_ford_controlsd_logging.py": "318742bd707ae526d0f5181bf7f081660c2c55de4bdf28518ccdf50d60e88080", - "openpilot/selfdrive/controls/tests/test_ford_model_action_adapter.py": "de6f8524347f7c4a339941bc8565ccaa131cb93aa0418c75006ce08ab7edeb99", - "openpilot/selfdrive/controls/tests/test_ford_model_action_damping.py": "76466997ab4fef435f44339a6cb2d06303d5f0ab8717d487f27655297bd429d1", - "openpilot/sunnypilot/sunnylink/settings_ui.json": "dbb78c98f57eef532f0dff0cb0b38396442882876e6d115f0d3c36159f64baf5", - "openpilot/sunnypilot/sunnylink/settings_ui_src/pages/vehicle.yaml": "b92b55e23467e74227988fb39fff13a4ebba3c960fb90e34a6b341411699fc95", - "tools/ford_pscm_lab/damping_replay.py": "2f52fef12116ce87c0a1f465cf13d6af5e9fd76ce05d4318d72e34b70bc6a11f", - "tools/ford_pscm_lab/model_action_replay.py": "05a658dfcaf81693bf0d92184c0edff0172802f351a61a9e866a7967e74ae46d", - "tools/ford_pscm_lab/stress_model_action.py": "3e308733f4af0101ad0c414fbd724f6a99f56269ed8e791f0e15526d8bfd8f17" - }, - "artifact_sha256": { - "route9b/report.json": "1b9317850c1724f433269c6a58349d0a0ee4eb6c9a03d6ec5858fa17713796c5", - "route9b/commands.npz": "d9f553a5384c84416a27c52b5dda0e751dda25edc5d6620fc311977f34a7a946", - "route90/report.json": "e37f71dc032e375b1c9b0beb4d6e0c915257bf72e59b0785ac0ca49c2472d2c2", - "route95/report.json": "d42d5a080fef8a1f0b3c7ae2cabcad20c88ce01d91be07ab33770ff5948c06b6", - "stress.json": "06e69a23340e3f5ed174e8e0b2e5791b320686dce7df963233d50a9982dca17b", - "coverage.json": "8f7915b9bd884abedfdbc2e0c18ef4474737e27225a714414384242535cc396f", - "mutation.txt": "67a76549fd7bb71e7092a155d4d0c3459be7b04dc2ca36eef9b54fbb574ad83d", - "bias_regression_red.txt": "8903c5a967e8c376050db85f7cf5f73abf71ee972f6025b25eb874479f94c62c", - "segment10_damping.png": "7d60cf9aabdcca9000fcf49bd14bd6130418aa6ab1b57af2678b171498ee9505" - } -} diff --git a/docs/ford_model_action_drive_test.md b/docs/ford_model_action_drive_test.md index 9e5d453261..70cf6875a3 100644 --- a/docs/ford_model_action_drive_test.md +++ b/docs/ford_model_action_drive_test.md @@ -1,80 +1,83 @@ -# Ford model-point drive-test branch +# Ford selected-action drive-test branch -The default-off **Selected-Action Path Tracking (Experimental)** Sunnylink toggle -now selects the v7 model-point candidate on the **Ford CAN FD F-150 Lightning**. -The stored key remains `FordModelActionController`; an already enabled setting -selects this revision after updating and completing an offroad-to-onroad cycle. - -The controller reads **model lateral position and model heading at the same -point**. Start with the model's predicted distance at one second, enforce the -existing seven-metre minimum, and hold the available endpoint when necessary. -C0 is that point's lateral position in metres; C1 is its unwrapped heading in -radians. C2 and C3 stay zero. The 150 ms yaw-based forecast and the reconstruction -of heading from selected curvature are removed. - -This point choice is an engineering guess, not an identified Ford reference or -PSCM calibration. `calibration_approved=false`: offline tests do not establish -physical tracking, turn-exit behavior, or stability across different PSCMs. -See [the model-point decision and validation](ford_model_points.md). +The candidate is selectable on the **Ford CAN FD F-150 Lightning** behind +its own persistent, default-off Sunnylink toggle. The command law and input +gates from the [offline candidate](ford_model_action_candidate.md) are unchanged. +`calibration_approved=false`: offline checks do not establish physical tracking, +turn-exit behavior or closed-loop stability. ## Select and restore -1. Install branch `hiimisaac-dev` from `sunnypilot/sunnypilot` and allow the build - to finish. -2. While offroad, open Sunnylink device settings → Vehicle → Ford. Keep or enable - **Selected-Action Path Tracking (Experimental)**. -3. Complete a real offroad-to-onroad cycle. `card` snapshots the toggle into - `CarParamsSP`; the sender and `controlsd` share that selection. Changing a - stored toggle or disengaging alone cannot swap an active controller. +1. Install branch `hiimisaac-dev` from + `sunnypilot/sunnypilot` on the device using your normal branch-switch process. + Allow its build to finish before changing the setting. +2. While offroad, open Sunnylink device settings → Vehicle → Ford and enable + **Selected-Action Path Tracking (Experimental)** (`FordModelActionController`). +3. Complete a real offroad-to-onroad cycle. Selection occurs when `controlsd` + starts; changing a stored toggle or disengaging alone cannot swap an active + controller. Initial physical evaluation remains controlled testing. -The startup event `Ford path controller selected` reports -`FordModelActionController`. Periodic `Ford C2-free path tracking` events report -`hypothesis=model-pose-one-second-v7`, `pose_source=model`, `preview_time_s=1.0` -and `minimum_station_m=7.0`, plus input ages, slew state and the command tuple. -Selected desired curvature is still logged, but no longer constructs C0/C1. +The startup log event `Ford path controller selected` should report +`FordModelActionController`. Periodic `Ford C2-free path tracking` events +identify `hypothesis=model-action-c0-c1-v1` and report the command tuple. -Turning the toggle off and completing another offroad-to-onroad cycle restores -**PSCM Coefficient Observer** if selected, otherwise the original Ford path -controller. Other vehicles keep their previous selection. The retired v8 toggle -cannot select this candidate. +Turning the new toggle off and completing another offroad-to-onroad cycle +restores **PSCM Coefficient Observer** if selected, otherwise the original +Ford path controller. The stored observer selection is preserved. The candidate +takes priority on the supported vehicle, independently of EPS firmware query +results. Other vehicles retain their existing selection. -## Wiring and limits +The v8 implementation, its Sunnylink toggle and its dedicated tests are removed. +A leftover `FordVirtualAngleController=1` file cannot enable the new controller. +The shared Float32/CAN rounding helper now lives in `ford_model_action.py`; +unused v8 PSCM-feedback plumbing is removed. Historical v8 route evidence remains +in Git history and the archived validation documents. -Both model fields must have matching, finite, strictly increasing time arrays -starting at zero. Malformed geometry, stale required services, invalid timing, -or disengagement resets both actuator states. Freshness still requires model, -car-state and reference publications no older than 150 ms, with at most 5 ms -future skew. The valid control timestep remains 2–100 ms. +## Wiring and validation -Only the two unquantized C0/C1 slew positions persist in the core. Field caps are -±5.11 m / ±0.5 rad and slew rates are 4 m/s / 0.5 rad/s. Calculation stays at -100 Hz; the existing [cadence experiment](ford_model_action_cadence.md) sends this -candidate at 20 Hz. Float32 publication, host-to-wire negation, packing and -Panda safety are unchanged. The opendbc pin remains -`87ca78e6e641eefb2d654f260a6ab08df3058bd5`. +`Controls.__init__` selects the candidate once at startup. It shares the +existing Ford call path, selected upstream-limited curvature, service gates, +invalid-output disengagement, Float32 publication and downstream CAN builder. +C2 and C3 stay zero. No opendbc pointer or Panda safety change is included. -Normal operation uses the model's point directly. The upstream scalar curvature -and its clipping still exist for logging/other controllers, but no longer bound -this candidate's heading target. Its C0/C1 field caps and slew still apply; -passing Panda TX checks does not establish an actual vehicle acceleration bound. -Lateral maneuver test mode supplies only a scalar curvature, not a model pose. -It explicitly invalidates/disengages this candidate as `unsupported_reference`. -Optional measured motion is no longer a command input. +Sunnylink publishes the toggle through its generated settings schema and +writes the registered Boolean through the existing parameter endpoint. The +offroad UI rule and `needs_onroad_cycle` metadata describe when it can be +changed and when it takes effect. An onroad backend write changes storage +only; the controller continues using its startup selection. -## Reproduce offline checks +Native validation also exposed a pre-existing `params_keys_by_flag` bug: +every returned buffer referenced the same reusable string. Sunnylink backup +key enumeration could therefore return corrupted names. The bridge now +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. -Initialize the pinned submodule and build the project's native Python dependencies: +The current validation record is `ford_model_action_drive_test_validation.json`. +The final combined Ford, Params and Sunnylink suite passes **284 tests and +26 subtests**, with no skips. The candidate has **100% statement and branch +coverage** (87 statements, 26 branches). Ruff, Ty, settings compilation and +both review axes pass. The fresh route/stress runs check **485,238 Float32/CAN +round trips**, including 200,000 randomized and 200,000 mirrored core updates. +The controller is 145 total lines, including 95 code lines excluding comments, +blanks and docstrings; v8's 469-line module is removed. + +The previous 133,550-cycle route reconstruction, 485,238 packing round trips +and mutation probes remain recorded separately in +`ford_model_action_validation.json` at the offline-stage source hashes. + +## Reproduce deployment checks + +Initialize the branch's exact opendbc submodule (`c21a9013700734dd20b09e05aa68329ad8cc20f9`) +and build the native Params library from this branch before testing. ```sh -git submodule update --init opendbc_repo export PYTHONDONTWRITEBYTECODE=1 export PYTHONPATH=.:opendbc_repo -python -m pytest -q openpilot/selfdrive/controls/tests/test_ford*.py tools/ford_pscm_lab opendbc_repo/opendbc/car/ford/tests/test_ford.py openpilot/sunnypilot/sunnylink/tests openpilot/sunnypilot/mads/tests openpilot/selfdrive/car/tests/test_ford_pscm_status.py openpilot/common/tests/test_params.py opendbc_repo/opendbc/safety/tests/test_ford.py -python -m tools.ford_pscm_lab.stress_model_action --cycles 200000 --seed 20260908 --opendbc-revision 87ca78e6e641eefb2d654f260a6ab08df3058bd5 --output .cache/ford_model_points/stress.json +python -m pytest -q -p no:cacheprovider openpilot/selfdrive/controls/tests/test_ford_*.py tools/ford_pscm_lab openpilot/selfdrive/car/tests/test_ford_pscm_status.py openpilot/sunnypilot/sunnylink/tests openpilot/common/tests/test_params.py opendbc_repo/opendbc/car/ford/tests/test_ford.py +python -m tools.ford_pscm_lab.stress_model_action --cycles 200000 --seed 20260907 --opendbc-revision c21a9013700734dd20b09e05aa68329ad8cc20f9 --output .cache/ford_model_action_drive_test/stress.json ``` -Historical v1–v6 validation files retain their original source hashes and apply -to those revisions. In particular, `ford_model_action_measured_pose_validation.json` -describes v6, not the current model-point mapping. The hardware build and device -boot are not performed by these offline checks. Pushing a branch does not update -a device or change its stored settings. +The full hardware build and device boot are not performed by these offline +tests. Installing the branch and enabling the toggle are separate actions; +pushing the branch does not change a device's selected software or settings. diff --git a/docs/ford_model_action_full_prediction.md b/docs/ford_model_action_full_prediction.md deleted file mode 100644 index c402714688..0000000000 --- a/docs/ford_model_action_full_prediction.md +++ /dev/null @@ -1,86 +0,0 @@ -# Experimental Ford full path prediction, v4 - -This document archives v4. The [current v5 controller](ford_model_action_no_yaw_damping.md) -retains this prediction and removes yaw damping. - -V4 removes the extra 15 cm / 25% limit on the geometric prediction introduced -in [v3](ford_model_action_prediction.md). Those numbers were hand-chosen tuning -bounds, not identified Ford response limits. The current user request is to -remove that restriction; the existing default-off Sunnylink toggle remains. - -The controller now uses the full predicted offset from the same model path, -assuming 150 ms of motion along the selected, upstream-limited curvature. The -150 ms horizon remains an engineering assumption. Available model geometry -still limits the prediction distance, with endpoint hold for short paths and -fallback to the valid base offset if prediction arithmetic is nonfinite. - -The existing total command limits (C0 ±5.11 m, C1 ±0.5 rad), independent slew -rates (4 m/s, 0.5 rad/s), quantization, yaw damping, input/service gates and -zero C2/C3 remain unchanged. Only two control states persist. No integrator, -model history, extra toggle or PSCM feedback loop is added. - -Removing the adjustment cap also permits the predicted C0 to oppose the -original offset or become nonzero from a zero original offset. For example, -a straight path with a nonzero selected turn request can have an opposing -future-frame offset. Tests cover that behavior, mirrored turn releases, -return to zero, and unchanged slew; sign preservation of the original C0 is -no longer claimed. The yaw damper still cannot reverse its input target. - -## Evidence and interpretation - -The PSCM reports a generic `LimitReached` state. It does not tell us whether -an incoming target is geometrically correct or well timed. Its internal -limits cannot establish the tracking performance of this predictor. Removal -is an experiment supported by command comparisons, not by an assumption that -the PSCM will correct an excessive or mistimed request. - -Compared with capped v3 on identical recorded inputs: - -| Interval | Effect of removing the extra cap | -| --- | --- | -| Latest tight-left entry, 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 deleted file mode 100644 index 0d0d82e38c..0000000000 --- a/docs/ford_model_action_full_prediction_validation.json +++ /dev/null @@ -1,799 +0,0 @@ -{ - "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_measured_pose_validation.json b/docs/ford_model_action_measured_pose_validation.json deleted file mode 100644 index b42944fd5f..0000000000 --- a/docs/ford_model_action_measured_pose_validation.json +++ /dev/null @@ -1,94 +0,0 @@ -{ - "version": "model-action-measured-pose-v6", - "base_commit": "72e9d94f62487d07e9b7c5cb17d5988d5ddfbedb", - "opendbc_revision": "c21a9013700734dd20b09e05aa68329ad8cc20f9", - "scope": "Offline runtime command replay on frozen recorded calibrated yaw, geometry and vehicle inputs; separate controlsd integration tests exercise health/freshness, calibration updates, publication and CAN. No counterfactual vehicle simulation or road-validation claim.", - "calibration_approved": false, - "source_sha256": { - "openpilot/selfdrive/controls/controlsd.py": "c12e8d9be37e21f27a2132974b0efe98f3e681fd07e12c263e897ad67a44871b", - "openpilot/selfdrive/controls/lib/ford_model_action.py": "326539389b04034932db41ca2c67794779403b2c0e97f408a81a9f5734114899", - "openpilot/selfdrive/controls/tests/test_ford_controlsd_logging.py": "2a55779b1a32f4dc3317b568365efbadc1f6aab06a0b3398514be891025d5e2d", - "openpilot/selfdrive/controls/tests/test_ford_model_action_adapter.py": "0e131ca17380a5a4fe37754659f77aeda550f5e3c113edb27b576673e57bf6b4", - "openpilot/selfdrive/controls/tests/test_ford_model_action_measured_pose.py": "4d81efa0f09350559ff4290fd2ec11e744be221623fd414de85adfe2fecb4289", - "openpilot/selfdrive/controls/tests/test_ford_model_action_selection.py": "4e87ff347046d8de7248b9272a0393dd5d12e10df5430f00540b675331d7fe58", - "openpilot/selfdrive/controls/tests/test_ford_model_action_yaw.py": "063b066aeddf0410dc208fa0017a58cda78effb1ca33c2af8b53422dbdad06f3", - "openpilot/sunnypilot/sunnylink/settings_ui.json": "6f8900aa642d7f3883b003fd9244f29997343808fc22119c5ca953c3e1946fde", - "openpilot/sunnypilot/sunnylink/settings_ui_src/pages/vehicle.yaml": "c8f7cc4c44e39027fecb52efc61dd5f3902e030193b10ac2f2c36c51cc6d4010" - }, - "validation": { - "pytest": { - "passed": 427, - "subtests_passed": 26, - "skipped": 0, - "command": "python -m pytest -q openpilot/selfdrive/controls/tests/test_ford*.py tools/ford_pscm_lab opendbc_repo/opendbc/car/ford/tests/test_ford.py openpilot/sunnypilot/sunnylink/tests", - "output_sha256": "ed2ecdf280dadc52a834b2dfaa777e64505f7763d1a271849dadc729a86abb5d" - }, - "type_check": "ty check openpilot/selfdrive/controls/lib/ford_model_action.py openpilot/selfdrive/controls/controlsd.py: pass", - "ruff_changed_python": "pass", - "settings_compiler_check": "pass", - "diff_check": "pass", - "regression_red_green": "49 measured-pose tests and 4 state-freshness/calibration tests failed before their implementation and passed afterward" - }, - "replay": { - "cycles": 340757, - "wire_round_trips": 681514, - "command_matches_reviewed_candidate": true, - "baseline_matches_archived_v5": true, - "c1_eligibility_unchanged": true, - "checks": [ - "finite values", - "caps", - "independent slew", - "inactive reset", - "Float32/CAN packing", - "zero C2 and C3", - "publication and filter-state freshness" - ], - "routes": { - "a2": { - "cycles": 71111, - "wire_round_trips": 142222, - "runtime_matches_reviewed_lab_candidate": true, - "baseline_matches_archive": true, - "identical_c1_eligibility": true - }, - "a0": { - "cycles": 114582, - "wire_round_trips": 229164, - "runtime_matches_reviewed_lab_candidate": true, - "baseline_matches_archive": true, - "identical_c1_eligibility": true - }, - "9b": { - "cycles": 71396, - "wire_round_trips": 142792, - "runtime_matches_reviewed_lab_candidate": true, - "baseline_matches_archive": true, - "identical_c1_eligibility": true - }, - "9e": { - "cycles": 83668, - "wire_round_trips": 167336, - "runtime_matches_reviewed_lab_candidate": true, - "baseline_matches_archive": true, - "identical_c1_eligibility": true - } - }, - "report_sha256": { - "a2_pose_report.json": "35d709991507ac7c10fd3148fd61dcf33a9606f97cd134d2bd94cee999f9cbe1", - "a0_pose_report.json": "1e34fbcdb14d04b23ce755ddd2e66fa6ab12377ab925693474e9f719b6339530", - "9b_pose_report.json": "e601662de44a354247f3943c233ca14ce5c449dbb4289ab9564ced21d524f02b", - "9e_pose_report.json": "fb0ee49e7aab65d45b61251aa83ad2247cd90e91f6c077f86d2f8474af7013e4" - }, - "reproduce": "PYTHONDONTWRITEBYTECODE=1 PYTHONPATH=.:opendbc_repo python .cache/ford_v6/replay.py a2 a0 9b 9e", - "reproduction_requirements": "Original route extracts in the local .cache directories and the matching scripts/reports in the ford-v6 artifact. Replay scripts and private recordings are not shipped on the device." - }, - "limitations": [ - "All route extracts came from one truck; cross-PSCM behavior is unverified.", - "C0 command changes are not percentages of physical steering improvement.", - "The older overshoot example has nearby driver influence.", - "The same C1 cap remains active during tight-turn peak misses.", - "Quiet-path per-cycle C0-change RMS increases about 28%; physical centering is unverified.", - "Hardware build, device boot and physical driving were not performed." - ] -} diff --git a/docs/ford_model_action_no_yaw_damping.md b/docs/ford_model_action_no_yaw_damping.md deleted file mode 100644 index cd42dc5881..0000000000 --- a/docs/ford_model_action_no_yaw_damping.md +++ /dev/null @@ -1,67 +0,0 @@ -# Experimental Ford selected-action controller, v5 - -V5 removes the excess-yaw C0 attenuation at the user's request. The damping -function, 0.02 rad/s deadband and 0.2 s reduction scale are deleted. Valid measured -yaw no longer changes either command target. Existing yaw input-health checks -and logging remain. No replacement gain, increment gate or controller state is added. - -The full 150 ms geometric prediction introduced in v4 remains, along with the -7 m offset station and one-second heading scale. This is not a return to v1: -v1 did not predict the offset. C0/C1 bounds, slew, quantization, service gates, -engagement, downstream driver arbitration and zero C2/C3 are unchanged. - -The latest supplied route a0 ran v2, not v4. Prior same-input comparisons found -identical v1/v2 commands during its driver-clean minor-bend warning intervals -and the preceding five seconds. That evidence does not identify damping as the -cause of those misses. Removing damping can restore C0 demand where the damper -was active, including turn exits; it is not evidence of improved tracking or -reduced oversteer. - -## Offline validation - -The regression suite checks yaw-independent commands through mirrored turn -entry, release and reversal, including valid yaw extremes and small yaw offsets. -Six cases fail with v4 damping present and pass after removal. Invalid yaw still -resets the controller. Actual controlsd selection, upstream limiting, Float32 -publication and downstream CAN tests cover both model and maneuver references. - -Full-rlog comparisons run pinned v4 against production v5 on routes9b, 9e and a0. -They preserve original clocks, exact consumed model frames and causal carState; -publication times proxy computation time, and complete SubMaster health is -unavailable. The numerical stress run checks independent geometric targets, -scalar slew, mirrored turns and Float32/CAN packing. Recorded vehicle motion -stays fixed: none of these checks establishes counterfactual steering response, -closed-loop stability or a physical tracking improvement. - -Results and source hashes are recorded in -`ford_model_action_no_yaw_damping_validation.json`. Earlier validation documents -remain archives of their specified controller versions. - -Validation passes 356 tests and 26 subtests with 100% controller statement and -branch coverage, 280,636 recorded route cycles and 779,410 Float32/CAN round trips, -including 200,000 random stress cycles. C1 and input eligibility match v4 exactly -on all three routes. Commands during all 1,578 driver-clean ordinary-bend warning -cycles on route a0 also remain identical to v4. At the earlier right-turn exit, -removing damping increases C0 magnitude by a mean 0.079 m, maximum 0.13 m; these -are command offsets, not measured vehicle displacement. - -The module is 171 total lines, or 111 code lines excluding blanks, comments and -docstrings, with two control states. No hardware build or device boot was performed. - -## Reproduce and select - -Use the dependency setup and combined suite in the -[drive-test guide](ford_model_action_drive_test.md). Replay and stress commands: - -```sh -python -m tools.ford_pscm_lab.damping_replay /path/to/route9b/rlogs --baseline v4 --candidate current --window right_entry 637 640 --window right_exit 642.7 643.852 --output /path/to/separate/route9b-results -python -m tools.ford_pscm_lab.damping_replay /path/to/route9e/rlogs --baseline v4 --candidate current --window left_entry 173 175.4 --window left_peak 175.4 178.3 --window left_exit 178.3 180.5 --output /path/to/separate/route9e-results -python -m tools.ford_pscm_lab.damping_replay /path/to/routea0/rlogs --baseline v4 --candidate current --window bends_5min 298 338 --window bend_7min 449 458 --window bend_9min 579 588 --output /path/to/separate/routea0-results -python -m tools.ford_pscm_lab.stress_model_action --cycles 200000 --seed 20260908 --opendbc-revision c21a9013700734dd20b09e05aa68329ad8cc20f9 --output /path/to/stress.json -``` - -The same default-off **Selected-Action Path Tracking (Experimental)** Sunnylink -toggle selects v5 on the CAN FD F-150 Lightning. Deployment remains -`sunnypilot/sunnypilot`, branch `hiimisaac-dev`. After updating, restart controlsd -through a real offroad-to-onroad cycle. Diagnostics identify -`model-action-c0-c1-prediction-v5`; `calibration_approved=false` remains explicit. diff --git a/docs/ford_model_action_no_yaw_damping_validation.json b/docs/ford_model_action_no_yaw_damping_validation.json deleted file mode 100644 index 1d53666480..0000000000 --- a/docs/ford_model_action_no_yaw_damping_validation.json +++ /dev/null @@ -1,304 +0,0 @@ -{ - "date": "2026-09-08", - "baseline_commit": "7e63449749d112f096c56cb848dd289054e5f85b", - "hypothesis": "model-action-c0-c1-prediction-v5", - "scope": "Remove yaw damping only; retain full path prediction and existing input-health gates. Fixed-input command checks, not physical tracking or stability evidence.", - "deployment_target": { - "repository": "sunnypilot/sunnypilot", - "branch": "hiimisaac-dev" - }, - "calibration_approved": false, - "hardware_build_and_device_boot": "not performed", - "opendbc_revision": "c21a9013700734dd20b09e05aa68329ad8cc20f9", - "controller_size": { - "total_lines": 171, - "code_lines_excluding_blanks_comments_docstrings": 111, - "core_persistent_values": 2, - "adapter_timestamps": 3 - }, - "checks": { - "combined_ford_params_sunnylink_suite": "356 passed, 26 subtests passed; no skips", - "coverage": { - "covered_lines": 102, - "num_statements": 102, - "percent_covered": 100.0, - "percent_covered_display": "100", - "missing_lines": 0, - "excluded_lines": 0, - "percent_statements_covered": 100.0, - "percent_statements_covered_display": "100", - "num_branches": 30, - "num_partial_branches": 0, - "covered_branches": 30, - "missing_branches": 0, - "percent_branches_covered": 100.0, - "percent_branches_covered_display": "100" - }, - "ruff": "pass", - "ty_controller_and_lab": "pass", - "settings_compiler_check": "pass", - "removal_regression_probe": "6 cases fail with v4 damping present; all 21 yaw tests pass after removal.", - "reviews": { - "standards": { - "remaining_findings": 0 - }, - "spec": { - "remaining_findings": 0, - "independent_focused_tests_passed": 107 - }, - "corrected_findings": [ - "Removed stale damping claim from Sunnylink YAML and regenerated JSON.", - "Corrected replay yaw-use metadata to input-health checks and diagnostics." - ] - } - }, - "total_original_route_cycles": 280636, - "total_float32_can_round_trips": 779410, - "stress": { - "seed": 20260908, - "random_cycles": 200000, - "mirrored_core_updates": 200000, - "invalid_or_inactive_resets": 3537, - "field_boundary_cases": 18138, - "float32_can_round_trips": 218138, - "analytic_targets_scalar_slew_and_mirror_checks_pass": true, - "valid_yaw_does_not_affect_targets_checked": true, - "full_geometric_prediction_checked": true, - "direct_raw_float32_packing_matches_host_output": true, - "max_continuous_step_c0_c1": [ - 0.4000000000003041, - 0.05000000000000002 - ], - "calibration_approved": false, - "scope": "Numerical construction only; no PSCM response or closed-loop performance claims.", - "opendbc_import_head": "c21a9013700734dd20b09e05aa68329ad8cc20f9" - }, - "routes": { - "route9b": { - "cycles": 82386, - "eligible_cycles": 70703, - "same_validity": true, - "c1_exactly_unchanged": true, - "field_slew_zero_c2_c3_pass": true, - "float32_can_round_trips": 164772, - "baseline_revision": "7e63449749d112f096c56cb848dd289054e5f85b", - "baseline_source_sha256": "7a226cf3cdf6dc8c3b15829078a7b93e486ba4722c4a1d93ceb55dd2e6c77b21", - "candidate_source_sha256": "213a4dfa586092c28d8e6e2c27a7dd3622c5862615db9afd5a4e2d38144b1af8", - "source_rlog_sha256": { - "84865544361f55cb_0000009b--e4616dddaa--0--rlog.zst": "22746f7119109b73ed7f2c26ce8c99f87136e9124fb7fc14c9554409a28a7c3f", - "84865544361f55cb_0000009b--e4616dddaa--1--rlog.zst": "4c2e1d7083c31a2b37d0f8dd3be4d330898511b7e02c26f7d40ca9bc2779397d", - "84865544361f55cb_0000009b--e4616dddaa--2--rlog.zst": "62f3e049e220cd3681fadf386f2969537bd571998ae2f6ba2d08479428b5a28f", - "84865544361f55cb_0000009b--e4616dddaa--3--rlog.zst": "83bf0131b2d36b2ba7e5ba050bbc13c0a3350feb5c9b89dc9c87d3a37abebfb3", - "84865544361f55cb_0000009b--e4616dddaa--4--rlog.zst": "430985a80dd6e10f7abeb89457a17022e6bb6978617f415c905f584b1647603e", - "84865544361f55cb_0000009b--e4616dddaa--5--rlog.zst": "8c0c5ae6323ec33b3e14f84ca834f70cb56f6b29f471a350f1e3efc06b6ba553", - "84865544361f55cb_0000009b--e4616dddaa--6--rlog.zst": "db53dfa8156b9d66792c3eff0b2ce5d31b71ad41cc580dec85f528845593c184", - "84865544361f55cb_0000009b--e4616dddaa--7--rlog.zst": "91b0b3be10cb7d7d7f7dd2024d8f9ee99d1e9fd2204203a3a9a2f2f1c6e3fa03", - "84865544361f55cb_0000009b--e4616dddaa--8--rlog.zst": "687dbbfc49837efbfe8fa6bc091e40f7fad2908832234d7884f4616d1bc9ccff", - "84865544361f55cb_0000009b--e4616dddaa--9--rlog.zst": "a88ec4d25b04cdbf5844686fc77f6b28dca920c9b164e37ebf69844a3ae398fc", - "84865544361f55cb_0000009b--e4616dddaa--10--rlog.zst": "fe6b29580a6c94e1c236d13e18db4cd9f31cc1b25d52e1e6e19a5021125c9932", - "84865544361f55cb_0000009b--e4616dddaa--11--rlog.zst": "150d31b1944d7a1b8c562f3aee20b66cefa6c4e8d02660ec889907d835142f45", - "84865544361f55cb_0000009b--e4616dddaa--12--rlog.zst": "e105e5f703a70f36f1989c66fb46c35a65ff60a265b4332e10fa2e6875a2ced4", - "84865544361f55cb_0000009b--e4616dddaa--13--rlog.zst": "69304cf0c81401374f047c5022ca47d257ff9e59e30ec171325a2ee1e7ed13d4" - }, - "focus_cohorts": { - "right_entry": { - "cycles": 298, - "seconds": 2.9932484459999387, - "changed_c0_cycles": 100, - "mean_absolute_c0_change_m": 0.007683282515599457, - "max_absolute_c0_change_m": 0.04999999999999982, - "increased_absolute_c0_cycles": 100, - "decreased_absolute_c0_cycles": 0, - "driver_input_percent": 20.395198628296086, - "baseline_peak_absolute_c0_m": 3.0300000000000002, - "candidate_peak_absolute_c0_m": 3.0300000000000002 - }, - "right_exit": { - "cycles": 115, - "seconds": 1.156770048999988, - "changed_c0_cycles": 115, - "mean_absolute_c0_change_m": 0.07934493768172877, - "max_absolute_c0_change_m": 0.13000000000000078, - "increased_absolute_c0_cycles": 115, - "decreased_absolute_c0_cycles": 0, - "driver_input_percent": 0.0, - "baseline_peak_absolute_c0_m": 0.7199999999999998, - "candidate_peak_absolute_c0_m": 0.7800000000000002 - } - } - }, - "route9e": { - "cycles": 83668, - "eligible_cycles": 74669, - "same_validity": true, - "c1_exactly_unchanged": true, - "field_slew_zero_c2_c3_pass": true, - "float32_can_round_trips": 167336, - "baseline_revision": "7e63449749d112f096c56cb848dd289054e5f85b", - "baseline_source_sha256": "7a226cf3cdf6dc8c3b15829078a7b93e486ba4722c4a1d93ceb55dd2e6c77b21", - "candidate_source_sha256": "213a4dfa586092c28d8e6e2c27a7dd3622c5862615db9afd5a4e2d38144b1af8", - "source_rlog_sha256": { - "84865544361f55cb_0000009e--592f7dc149--0--rlog.zst": "de63532ae6aedf5dc7fd3ac8e47a2d96a8f065614f4aca79ae2f120ea00390c7", - "84865544361f55cb_0000009e--592f7dc149--1--rlog.zst": "67b58197158b3e8f0581643f6657d2a85c47b0bb75dd3e00306d9ffae790008b", - "84865544361f55cb_0000009e--592f7dc149--2--rlog.zst": "a4840f338f51b5f1864c79ce3a4f2b11dc13d58d52b8459961c9da57237f2cc2", - "84865544361f55cb_0000009e--592f7dc149--3--rlog.zst": "ff3ea8d948006ab19c4dbfeeff59a509de3f193da47b91c88faa27ecd0b3f1fc", - "84865544361f55cb_0000009e--592f7dc149--4--rlog.zst": "5ff1996c2336299128a11e657c32bc21921716b7e3bd401de69a2d49484aa223", - "84865544361f55cb_0000009e--592f7dc149--5--rlog.zst": "238763457e89896933afaf9a5df325469ee8c02ddc53550df252531bc94fc540", - "84865544361f55cb_0000009e--592f7dc149--6--rlog.zst": "2ee86de80cfd762be10cd2dfb2895ddbee6b813706e9c7261460203e09b9bc4d", - "84865544361f55cb_0000009e--592f7dc149--7--rlog.zst": "017801f080861c63d799f87aebe30ab57cdf82f078ac882e6187d3870c403538", - "84865544361f55cb_0000009e--592f7dc149--8--rlog.zst": "3729016bd1f00bb1077613b63fe25b21ba7112822b994e0e0aa2b4cd93bdb940", - "84865544361f55cb_0000009e--592f7dc149--9--rlog.zst": "c40b3c1f6eb9252b85f176fee32cae16c23eaad3db830f6bbf37a730034a5ccf", - "84865544361f55cb_0000009e--592f7dc149--10--rlog.zst": "e0ce8f231798073e5bbc34551fbd9493f169a06a068adc1957ef8cd914b71fbb", - "84865544361f55cb_0000009e--592f7dc149--11--rlog.zst": "90863353f7c862a05618b4d0761dda3fcedd3cf6b4234c21c6dcc2eee3ec20b2", - "84865544361f55cb_0000009e--592f7dc149--12--rlog.zst": "e064f8abde9615b2daf00f469ef36b438cd4b3a1637069467c84de91bee3bdf1", - "84865544361f55cb_0000009e--592f7dc149--13--rlog.zst": "790fc438ce2675e0690fdce3b07bf54fdf6d06b17bbe7f5ff447adfa71189754", - "84865544361f55cb_0000009e--592f7dc149--14--rlog.zst": "b98f5a5be660013b1fe11250fdc24ee4fe1d9dd1301b787ddfdff1f2bcd3e27f" - }, - "focus_cohorts": { - "left_entry": { - "cycles": 238, - "seconds": 2.4048367620016506, - "changed_c0_cycles": 0, - "mean_absolute_c0_change_m": 0.0, - "max_absolute_c0_change_m": 0.0, - "increased_absolute_c0_cycles": 0, - "decreased_absolute_c0_cycles": 0, - "driver_input_percent": 2.403816920672437, - "baseline_peak_absolute_c0_m": 2.6399999999999997, - "candidate_peak_absolute_c0_m": 2.6399999999999997 - }, - "left_peak": { - "cycles": 288, - "seconds": 2.891819394000777, - "changed_c0_cycles": 0, - "mean_absolute_c0_change_m": 0.0, - "max_absolute_c0_change_m": 0.0, - "increased_absolute_c0_cycles": 0, - "decreased_absolute_c0_cycles": 0, - "driver_input_percent": 6.41034524444729, - "baseline_peak_absolute_c0_m": 2.95, - "candidate_peak_absolute_c0_m": 2.95 - }, - "left_exit": { - "cycles": 219, - "seconds": 2.203320298998733, - "changed_c0_cycles": 129, - "mean_absolute_c0_change_m": 0.037433836245882854, - "max_absolute_c0_change_m": 0.11000000000000032, - "increased_absolute_c0_cycles": 128, - "decreased_absolute_c0_cycles": 1, - "driver_input_percent": 9.936619705158035, - "baseline_peak_absolute_c0_m": 2.51, - "candidate_peak_absolute_c0_m": 2.51 - } - } - }, - "routea0": { - "cycles": 114582, - "eligible_cycles": 105382, - "same_validity": true, - "c1_exactly_unchanged": true, - "field_slew_zero_c2_c3_pass": true, - "float32_can_round_trips": 229164, - "baseline_revision": "7e63449749d112f096c56cb848dd289054e5f85b", - "baseline_source_sha256": "7a226cf3cdf6dc8c3b15829078a7b93e486ba4722c4a1d93ceb55dd2e6c77b21", - "candidate_source_sha256": "213a4dfa586092c28d8e6e2c27a7dd3622c5862615db9afd5a4e2d38144b1af8", - "source_rlog_sha256": { - "84865544361f55cb_000000a0--5e86c30dae--0--rlog.zst": "57b8113783f70f9176e1f2703e70185f64df07b3283f739a6fd7b182a2c92417", - "84865544361f55cb_000000a0--5e86c30dae--1--rlog.zst": "a821ce7df80c6466110a6f6433b2c9482f5f3a5b4227e8459182c3ee9fb8c7a7", - "84865544361f55cb_000000a0--5e86c30dae--2--rlog.zst": "984cca8e311be8a61444ca2bc23bbf3937c6f00264303faefb7efb8f0a3e8aa2", - "84865544361f55cb_000000a0--5e86c30dae--3--rlog.zst": "c6ea25b226aa875a15a8f8356522ab3b12501d280f63ebe0438a9d50b2116279", - "84865544361f55cb_000000a0--5e86c30dae--4--rlog.zst": "99b129e2965674fb3406855d1b022c0876083984adf2f2ef51c07f1e699974ea", - "84865544361f55cb_000000a0--5e86c30dae--5--rlog.zst": "428e2ce9a1ac0ca7135c00221590c286ab32b4ce5cf24d376de1519b39b25fec", - "84865544361f55cb_000000a0--5e86c30dae--6--rlog.zst": "efca7fedd0aaed4be4500fd1a0c67f01d8f805cfdd681a0a9c0439061ec2f36f", - "84865544361f55cb_000000a0--5e86c30dae--7--rlog.zst": "1da238f57b63efbbf7dfd2b147637c1b846736e198999741817fc0646d4f60b7", - "84865544361f55cb_000000a0--5e86c30dae--8--rlog.zst": "b61209325da26751b98da85fe166dab65ab9eaaef129b859a59fe99e4de5ab65", - "84865544361f55cb_000000a0--5e86c30dae--9--rlog.zst": "cf555f1c57a9afdbed702b5934892acb5b2174ef48f1fcf494422aba9bd01321", - "84865544361f55cb_000000a0--5e86c30dae--10--rlog.zst": "13ff83c236cca64b57c11ffa05cb74511af1258cde3cf0554395922769a39683", - "84865544361f55cb_000000a0--5e86c30dae--11--rlog.zst": "848a1d26fd72bbe4e119339bd405aedb0b9d7e6b96329d0b868a27bd01b6b128", - "84865544361f55cb_000000a0--5e86c30dae--12--rlog.zst": "9e8bf8597942cd74d8d04c783148b8201f63f56e55b766f7887f27465de276f2", - "84865544361f55cb_000000a0--5e86c30dae--13--rlog.zst": "e37575ed1424d574015c20a3066754bcca6fdacbb104db2bedce782428fd48ef", - "84865544361f55cb_000000a0--5e86c30dae--14--rlog.zst": "b67c7ed7ddd957bf5dcbcdb0604ddc9c52cd7af9d6aa70c1eae3e7622cee160d", - "84865544361f55cb_000000a0--5e86c30dae--15--rlog.zst": "6baaf9fdf64c72a9be70d261580914e2489f363230d95a1291b8479bcc249ba2", - "84865544361f55cb_000000a0--5e86c30dae--16--rlog.zst": "f84803fa2a615caf59a6bd4e9b2848059dd9f4a7f0c01ac4ec7e2347e317b5e7", - "84865544361f55cb_000000a0--5e86c30dae--17--rlog.zst": "f0f5f36cb155c18f8f9f90f700e1e91e169ef0442fd70670323be4a1c658040e", - "84865544361f55cb_000000a0--5e86c30dae--18--rlog.zst": "8674d5822949bafaefc0d0f30b0e91603354b9981de7566419a2e5b780054e64", - "84865544361f55cb_000000a0--5e86c30dae--19--rlog.zst": "b5df55701d5bcfbfd750608a1dfbcba9b931a39fba7a82e87bc7915b8ca2333f" - }, - "focus_cohorts": { - "bends_5min": { - "cycles": 3966, - "seconds": 39.99197329400005, - "changed_c0_cycles": 8, - "mean_absolute_c0_change_m": 1.950586094527418e-05, - "max_absolute_c0_change_m": 0.009999999999999787, - "increased_absolute_c0_cycles": 8, - "decreased_absolute_c0_cycles": 0, - "driver_input_percent": 14.825647085260368, - "baseline_peak_absolute_c0_m": 0.3099999999999996, - "candidate_peak_absolute_c0_m": 0.3099999999999996 - }, - "bend_7min": { - "cycles": 893, - "seconds": 8.995078784999919, - "changed_c0_cycles": 0, - "mean_absolute_c0_change_m": 0.0, - "max_absolute_c0_change_m": 0.0, - "increased_absolute_c0_cycles": 0, - "decreased_absolute_c0_cycles": 0, - "driver_input_percent": 0.0, - "baseline_peak_absolute_c0_m": 0.20000000000000018, - "candidate_peak_absolute_c0_m": 0.20000000000000018 - }, - "bend_9min": { - "cycles": 895, - "seconds": 8.998970718999999, - "changed_c0_cycles": 0, - "mean_absolute_c0_change_m": 0.0, - "max_absolute_c0_change_m": 0.0, - "increased_absolute_c0_cycles": 0, - "decreased_absolute_c0_cycles": 0, - "driver_input_percent": 0.22147398432951632, - "baseline_peak_absolute_c0_m": 0.2400000000000002, - "candidate_peak_absolute_c0_m": 0.2400000000000002 - } - } - } - }, - "routea0_ordinary_bend_warnings_v4_vs_v5": { - "definition": "Driver-clean +/-1s, speed>=8m/s, absolute desired wheel angle 3 to30deg. Proxy for ordinary bends, not map geometry.", - "cycles": 1578, - "seconds": 15.92979767199978, - "changed_c0_cycles": 0, - "changed_c1_cycles": 0, - "max_c0_change_m": 0.0 - }, - "source_sha256": { - "openpilot/selfdrive/controls/lib/ford_model_action.py": "213a4dfa586092c28d8e6e2c27a7dd3622c5862615db9afd5a4e2d38144b1af8", - "openpilot/sunnypilot/sunnylink/settings_ui_src/pages/vehicle.yaml": "be610c09e4bbd93e84d1920cfbb3e9609b3fbb7dca8eb229b8e67befb4b2ca84", - "openpilot/sunnypilot/sunnylink/settings_ui.json": "4d2dffdfd81190a871bd41e28e5832502aa95d8aacf91daa7e68382085588aca", - "openpilot/selfdrive/controls/tests/test_ford_model_action.py": "30cfffb86fcd3830320e9b2ec1f65cdfd219684a55d0bc0bb13f77966cc18b53", - "openpilot/selfdrive/controls/tests/test_ford_model_action_adapter.py": "7f8b13c4d85217cbb6bd32ec193ef18bdb30bdf1f2b59fa7fa9d40988cb20bea", - "openpilot/selfdrive/controls/tests/test_ford_model_action_prediction.py": "eb50e1d6acdb5e7332fadc3dcfcfa1b3809545e607c47c54b0ba3de80b2df208", - "openpilot/selfdrive/controls/tests/test_ford_model_action_selection.py": "f826c6328f0abac2a61f1a0a6f8d119fdbab858e363cb466d84ba9a7783059cd", - "openpilot/selfdrive/controls/tests/test_ford_model_action_yaw.py": "d646f89e2c4d4e112d03e77c5c34b7541b0bdba800419d472a4a69753637c515", - "openpilot/selfdrive/controls/tests/test_ford_controlsd_logging.py": "9697db696d5a01838ea5cdcf4f1771d813a647f5038cabe9f0460778a2936929", - "tools/ford_pscm_lab/__init__.py": "db4b8b7d2e317ed34ca0ec220bf9d53e7b80a23e766f4e1cb2c8224dada45f2e", - "tools/ford_pscm_lab/damping_replay.py": "d21565a29b9c2b4668fafe07f995c15e113888a1f4787f37d72d02d36998f9cf", - "tools/ford_pscm_lab/model_action_replay.py": "a90caed1c46c6f964fecb50bd0f531ae355505f1e84be9a1e37243d910b292b4", - "tools/ford_pscm_lab/stress_model_action.py": "123c6c0c9f53a5af2ab50bb568e47c0f8960b7d49fad2fb6ea74de5c63e03ad5", - "tools/ford_pscm_lab/test_model_action_replay.py": "bf1a1612474cb7308b3640cba535ee534999fdecdf055ae6d7bc83b9a8e66f63" - }, - "artifact_sha256": { - ".cache/ford_no_yaw_damping/removal_red.txt": "4aa79e5af1066d66db65853faad3923f8645f5dff8383fa3106489a3e53cae18", - ".cache/ford_no_yaw_damping/suite_run.txt": "434d80389bc7d2859070c9f9df0cac1ac0f854151b5888dcedc2ce38df23d519", - ".cache/ford_no_yaw_damping/coverage.json": "d99baeb8729a1394b41d9a4739b538372aad27b56d379f4b9cefe682ed99d662", - ".cache/ford_no_yaw_damping/stress.json": "3e2c49dcc473d90ea03c95dc12905c2216ee573a691891b8a1f56ba82fd78251", - ".cache/ford_no_yaw_damping/route9b/report.json": "446371173fed1ff632ece4286e53d0d941491be590d467af960d1931431212ca", - ".cache/ford_no_yaw_damping/route9b/commands.npz": "d543a92991f06b56e20d0ed58daf6e4ef6a0021b79c027570b8d997658336b9d", - ".cache/ford_no_yaw_damping/route9e/report.json": "fb8ddd8d2c242a14a63993593c96604e7306dcd97219d073bbcfcbeba68af45b", - ".cache/ford_no_yaw_damping/route9e/commands.npz": "375a94227f3cb1a161a1295496908baa428d5ef0e0b79134a728f876477dbd6b", - ".cache/ford_no_yaw_damping/routea0/report.json": "7959b77af715016b4da45b9bc8f095872702f662950324f64a0f4d332c814eb0", - ".cache/ford_no_yaw_damping/routea0/commands.npz": "4f9dcde39ad9e20958ad04e53dbb32830c6a933dc7f743fdffe5a612252cb16b" - } -} diff --git a/docs/ford_model_action_prediction.md b/docs/ford_model_action_prediction.md deleted file mode 100644 index 2cad0d6a33..0000000000 --- a/docs/ford_model_action_prediction.md +++ /dev/null @@ -1,126 +0,0 @@ -# Experimental Ford path prediction, v3 - -This document and its counts describe archived v3. The -[current v4 experiment](ford_model_action_full_prediction.md) removes the extra -15 cm / 25% prediction adjustment cap. - -The latest driven route9e used v1 (`5fc16abc7`), before the v2 yaw damping. -It often follows the requested steering angle closely, but some tight turns -fall behind after a reasonable initial turn-in. The requested angle is replanned -from the car's changing position; a large late request may partly be a recovery -request after arriving wide. It is not proof that the original turn required -that much steering. The generic PSCM limit flag does not identify a torque, -rate or mechanical limit, and the miss starts before our C1 cap in the clearest -left turn. - -Short measured-motion integrations against earlier frozen model paths are -consistent with a growing miss, but model uncertainty, reference timing and -some driver input prevent a conclusive causal attribution. V3 tests a bounded -change to initial path demand. No counterfactual physical tracking score is -claimed from replaying fixed logs. - -## Change and bounds - -Start with the current model's lateral offset at 7 m of path arc length, as in -v1/v2. Advance the reference pose by speed × 0.15 s along the **selected, -upstream-limited curvature**, and read the same model path 7 m beyond that -advance, expressed in the predicted ego frame. Bound the change from the -original offset to both ±0.15 m and ±25% of its magnitude. Prediction cannot -reverse that target or create C0 from a zero offset. - -For advance `d`, selected curvature `k`, rotation `theta = k*d`, and model -point `(x, y)` at arc station `7+d`, the predicted lateral coordinate is: - -``` -y_predicted = cos(theta)*y - sin(theta)*x + (1-cos(theta))/k -``` - -The code evaluates the last term continuously at zero curvature without -cancellation. Available path horizon limits `d`; prediction tapers to zero as -the horizon approaches 7 m. Nonfinite prediction falls back to the validated -current offset. The existing endpoint hold remains for paths shorter than 7 m. - -A matched constant-radius path retains essentially the same C0, subject to -sample interpolation. Developing and flattening bends can move the target -earlier. This is a geometric hypothesis assuming motion along selected -curvature, not an identified 150 ms actuator delay or a calibrated plant model. -Errors in that assumption can increase or reduce useful steering demand. - -The predicted offset passes through the existing ±5.11 m clip, v2 excess-yaw -damping, independent 4 m/s slew and 0.01 m quantization. C1 construction, clip, -slew and quantization are unchanged. C2=C3=0. Input sanity, freshness, service -health and startup selection gates are unchanged. There are still only two -control states (C0 and C1 slew positions), plus three adapter timestamps. -The module is 193 total lines, including 125 code lines excluding blanks, -comments and docstrings (18 more code lines than v2). No model history, integral -or turn state machine is added. The bounds above -apply to the prediction target, not arbitrary differences between separately -slewed controllers after different histories. - -## Offline results and tradeoff - -All four supplied routes run at their original controls timestamps. Routes90/95 -also reproduce the archived v1 command construction exactly. The newer routes -compare immutable v2 code with v3 using identical measured yaw, selected -curvature, exact consumed model and causal carState. Publication times proxy -computation time; complete SubMaster health is unavailable. Neither v2 nor v3 -was driven on these recordings. V2-versus-recorded error is therefore not a -reconstruction accuracy measurement. - -| Recorded interval | Command change versus v2 | -| --- | --- | -| route9e left entry, 173–175.4 s | Mean C0 magnitude +0.143 m; same 2.0 m level reached 0.203 s earlier | -| route9e left peak, 175.4–178.3 s | Mean magnitude +0.095 m; prediction also increases some late demand | -| route9e reversal, 728–734 s | Peak C0 magnitude 0.24 → 0.22 m | -| route9b right exit, 642.7–643.852 s | Mean C0 +0.024 m, partially offsetting v2 damping | -| Driver-clean low requests above 8 m/s, routes9b/9e | Mean absolute C0 change ≈0.0015 m; maximum 0.02 m | - -The entry C0 crossings at 0.5, 1.0, 1.5, 2.0 and 2.4 m move earlier by 61, 64, -367, 203 and 90 ms respectively. These are command-level crossing times, -not measured improvements in wheel response. Several entry/peak windows -contain driver input, quantified in the validation record. - -On all 115 earlier right-exit cycles before strong intervention, v3 remains -below the driven v1 reconstruction: mean C0 is 0.370 m for v1, 0.290 m for v2, -and 0.314 m for v3. This tradeoff is retained explicitly; v3 does not improve -every exit command relative to v2. There is no evidence yet that it reduces -the late model request or the physical miss. - -Route9b now includes full rlogs 12/13, added after the archived v2 evaluation. -Its 14-rlog totals therefore differ from the historical 12-rlog report. The -focused segment-10 comparison uses identical timestamps and data. - -Validation passes 372 tests and 26 subtests, including the real extracted -controlsd selection/limiter/publication path and downstream CAN builder, with -100% controller statement and branch coverage. Four routes cover 299,604 -original cycles. Randomized testing adds 200,000 core updates plus their -mirrors against an independent analytic geometry/damping/slew oracle; boundary -and route checks total 817,346 Float32/CAN round trips. These checks establish -command construction and retained gates, not closed-loop vehicle behavior. -See `ford_model_action_prediction_validation.json` for provenance and counts. - -## Reproduce and select - -To reproduce the archived suite and stress results, use v3 commit -`01f8d51c82b3e863f1012d383b5994813ef01b81` with the native dependencies and -suite command in the [drive-test guide](ford_model_action_drive_test.md). -Current replay tooling can select the immutable v3 source explicitly. New-route comparisons use -the deployment opendbc pin `c21a9013700734dd20b09e05aa68329ad8cc20f9`: - -```sh -python -m tools.ford_pscm_lab.damping_replay /path/to/route9e/rlogs --baseline v2 --candidate v3 --window left_entry 173 175.4 --window left_peak 175.4 178.3 --window left_exit 178.3 180.5 --window reversal 728 734 --window final_entry 822 825.5 --output /path/to/separate/route9e-results -python -m tools.ford_pscm_lab.damping_replay /path/to/route9b/rlogs --baseline v2 --candidate v3 --window right_entry 637 640 --window right_exit_before_strong_input 642.7 643.852 --output /path/to/separate/route9b-results -python -m tools.ford_pscm_lab.stress_model_action --cycles 200000 --seed 20260907 --opendbc-revision c21a9013700734dd20b09e05aa68329ad8cc20f9 --output /path/to/stress.json -``` - -Historical replay loads trusted controller source from immutable local Git -commits; those objects must exist in the checkout. The original replay tool -still requires its explicit historical opendbc pin. Neither tool downloads -code or drives the car. - -The existing default-off Sunnylink **Selected-Action Path Tracking -(Experimental)** toggle selects v3 on the CAN FD F-150 Lightning. Updating -with that toggle already enabled selects v3 at the next controlsd startup. -Diagnostics identify `model-action-c0-c1-prediction-v3` and keep -`calibration_approved=false`. No new device installation, hardware build, -physical calibration or device boot is part of this offline validation. diff --git a/docs/ford_model_action_prediction_validation.json b/docs/ford_model_action_prediction_validation.json deleted file mode 100644 index 0077761f91..0000000000 --- a/docs/ford_model_action_prediction_validation.json +++ /dev/null @@ -1,849 +0,0 @@ -{ - "date": "2026-09-07", - "baseline_commit": "744a97d9bc08d8743b250eceff7c88585b5480de", - "deployment_target": { - "repository": "sunnypilot/sunnypilot", - "branch": "hiimisaac-dev" - }, - "hypothesis": "model-action-c0-c1-prediction-v3", - "scope": "Bounded geometric prediction; fixed-input offline evidence only.", - "calibration_approved": false, - "hardware_build_and_device_boot": "not performed", - "controller_size": { - "total_lines": 193, - "code_lines_excluding_blanks_comments_docstrings": 125, - "core_persistent_values": 2, - "adapter_timestamps": 3 - }, - "checks": { - "combined_ford_params_sunnylink_suite": "372 passed, 26 subtests passed; no skips", - "suite_log_sha256": "b9546bd4cac8349ab9a699f175ba569ed41fc8a9e85e79a0018c79d3af24c11d", - "coverage": { - "covered_lines": 116, - "num_statements": 116, - "percent_covered": 100.0, - "percent_covered_display": "100", - "missing_lines": 0, - "excluded_lines": 0, - "percent_statements_covered": 100.0, - "percent_statements_covered_display": "100", - "num_branches": 32, - "num_partial_branches": 0, - "covered_branches": 32, - "missing_branches": 0, - "percent_branches_covered": 100.0, - "percent_branches_covered_display": "100" - }, - "ruff": "pass", - "ty_controller_and_lab": "pass", - "settings_compiler_check": "pass", - "prediction_red_probe": "Disabling prediction fails all four mirrored developing/flattening-bend cases (4 failed, 34 passed); all 38 pass with prediction.", - "standards_review_remaining_findings": 0, - "spec_review_remaining_findings": 0, - "review_scope": "Tracked and untracked v3 changes since 744a97d9bc08d8743b250eceff7c88585b5480de; final loader unit test checked separately.", - "test_portability": "Archive loader unit test mocks Git source retrieval; full offline route runs use the actual immutable commit sources." - }, - "stress": { - "seed": 20260907, - "random_cycles": 200000, - "mirrored_core_updates": 200000, - "invalid_or_inactive_resets": 3537, - "field_boundary_cases": 18138, - "float32_can_round_trips": 218138, - "analytic_targets_scalar_slew_and_mirror_checks_pass": true, - "bounded_excess_yaw_damping_checked": true, - "bounded_geometric_prediction_checked": true, - "direct_raw_float32_packing_matches_host_output": true, - "max_continuous_step_c0_c1": [ - 0.40000000000015523, - 0.05000000000000002 - ], - "calibration_approved": false, - "scope": "Numerical construction only; no PSCM response or closed-loop performance claims.", - "opendbc_import_head": "c21a9013700734dd20b09e05aa68329ad8cc20f9", - "source_sha256": { - "/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/stress_model_action.py": "be77689e5327a13b7b4d0e18a3fc1863b57cab76be714339efc609124400489f", - "/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/openpilot/selfdrive/controls/lib/ford_model_action.py": "5b9f7946f29b5fcd94ca93e53be14a12b18ae74d7ca27380e92b90c8c599dce0", - "/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/model_action_replay.py": "a872cf4594e364988796394b09f531d18cc3d6732e0c92f45eed9fbf6f5cbee6" - } - }, - "routes": { - "route90": { - "scope": "Command construction and adapter reconstruction only; no counterfactual closed-loop score.", - "calibration_approved": false, - "executes_live_selector": false, - "cycles": 78812, - "core_active_cycles": 73055, - "core_exact_archived_match": true, - "cohorts_reproduced": true, - "adapter_active_cycles": 73055, - "adapter_status_counts": { - "inactive": 5757, - "active": 73055 - }, - "adapter_matches_current_core_with_yaw_and_fresh_engagement_dt": true, - "core_active_path_shorter_than_7m_cycles": 0, - "adapter_validity_differs_from_archive_cycles": 0, - "adapter_command_differs_from_archive_cycles": 27264, - "adapter_max_absolute_command_difference_c0_c1": [ - 0.16000000000000014, - 0.0020000000000000018 - ], - "field_slew_zero_c2_c3_pass": true, - "float32_can_round_trips": 157624, - "timing": "Original controls publication timestamps proxy computation time; repeated frames and gaps retained. No identified delay.", - "eligibility": "Adapter checks recorded services independently; full SubMaster health is unavailable. Core uses archived validity.", - "reference": "Recorded controlsState.desiredCurvature, already selected/limited. These two routes have no maneuver publications.", - "host_yaw": "Extract cs.yaw equals -carState.yawRate; current adapter uses it for bounded damping.", - "archived_core_revision": "5fc16abc7662020706e29f57d31a6d5e2bc1293a", - "archived_core_source_sha256": "8f3bc5d68e0051776f614a2ccffae84a88f7898dc95bdc12c23dcfe10dfe676a", - "cohorts": { - "small_request": { - "seconds": 475.24161910000026, - "core_c0_c1_rms": [ - 0.06125184365338734, - 0.0057692154738982855 - ], - "recorded_v8_c0_c1_rms": [ - 0.057970374224682646, - 0.007060795713496332 - ], - "adapter_eligible_seconds": 475.24161910000026, - "adapter_c0_c1_rms": [ - 0.06384544973437024, - 0.0057692154738982855 - ] - }, - "turn": { - "seconds": 47.85871389900012, - "core_c0_c1_rms": [ - 0.18725251016049418, - 0.046449510236699354 - ], - "recorded_v8_c0_c1_rms": [ - 0.4773534845264775, - 0.04315935101392594 - ], - "adapter_eligible_seconds": 47.85871389900012, - "adapter_c0_c1_rms": [ - 0.20375259142277982, - 0.046449510236699354 - ] - }, - "small_speed_2_8": { - "seconds": 38.696717235999785, - "core_c0_c1_rms": [ - 0.209264275431127, - 0.014019080484620104 - ], - "recorded_v8_c0_c1_rms": [ - 0.1593558816527209, - 0.01823849401244768 - ], - "adapter_eligible_seconds": 38.696717235999785, - "adapter_c0_c1_rms": [ - 0.21664870054721863, - 0.014019080484620104 - ] - }, - "turn_speed_2_8": { - "seconds": 0.30159887100000304, - "core_c0_c1_rms": [ - 2.090789090165622, - 0.21878942570759013 - ], - "recorded_v8_c0_c1_rms": [ - 2.155728831937131, - 0.38375302254248855 - ], - "adapter_eligible_seconds": 0.30159887100000304, - "adapter_c0_c1_rms": [ - 2.2043400076988204, - 0.21878942570759013 - ] - }, - "small_speed_8_15": { - "seconds": 79.2599167590001, - "core_c0_c1_rms": [ - 0.02250905681378238, - 0.005843904113223639 - ], - "recorded_v8_c0_c1_rms": [ - 0.03477379324171293, - 0.006545440153133947 - ], - "adapter_eligible_seconds": 79.2599167590001, - "adapter_c0_c1_rms": [ - 0.026080258136122546, - 0.005843904113223639 - ] - }, - "turn_speed_8_15": { - "seconds": 0.022264622000001566, - "core_c0_c1_rms": [ - 0.20999999999999996, - 0.04349999999999998 - ], - "recorded_v8_c0_c1_rms": [ - 0.25, - 0.04050000011920929 - ], - "adapter_eligible_seconds": 0.022264622000001566, - "adapter_c0_c1_rms": [ - 0.20999999999999996, - 0.04349999999999998 - ] - }, - "small_speed_15_55": { - "seconds": 357.2849851050004, - "core_c0_c1_rms": [ - 0.011622097035118838, - 0.003925571829874466 - ], - "recorded_v8_c0_c1_rms": [ - 0.03809717934501699, - 0.0045587590980714285 - ], - "adapter_eligible_seconds": 357.2849851050004, - "adapter_c0_c1_rms": [ - 0.013693372023947319, - 0.003925571829874466 - ] - }, - "turn_speed_15_55": { - "seconds": 47.53485040600012, - "core_c0_c1_rms": [ - 0.08686835454709245, - 0.043216347944464766 - ], - "recorded_v8_c0_c1_rms": [ - 0.4471065308273131, - 0.030663943784303503 - ], - "adapter_eligible_seconds": 47.53485040600012, - "adapter_c0_c1_rms": [ - 0.10462899220936993, - 0.043216347944464766 - ] - }, - "pose_quiet": { - "seconds": 427.99642020700026, - "core_c0_c1_rms": [ - 0.01261133542366393, - 0.00422389167351435 - ], - "recorded_v8_c0_c1_rms": [ - 0.03724956081568198, - 0.0046880581807907966 - ], - "adapter_eligible_seconds": 427.99642020700026, - "adapter_c0_c1_rms": [ - 0.014639447114479471, - 0.00422389167351435 - ] - } - }, - "workspace_head": "744a97d9bc08d8743b250eceff7c88585b5480de", - "opendbc_import_head": "72a775d35e54c21ff5c5798acef22016eedcc0a7", - "opendbc_import_path": "/Users/ibpersonal/.codex/worktrees/b926/sunnypilot/opendbc_repo", - "source_sha256": { - "/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/model_action_replay.py": "a872cf4594e364988796394b09f531d18cc3d6732e0c92f45eed9fbf6f5cbee6", - "/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/openpilot/selfdrive/controls/lib/ford_model_action.py": "5b9f7946f29b5fcd94ca93e53be14a12b18ae74d7ca27380e92b90c8c599dce0", - "/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/openpilot/selfdrive/controls/lib/ford_path.py": "383538fc7cdae3bc28dffb71fe12ac5f3f9866ffbe6adfb7457f3593e9fc903a", - "/Users/ibpersonal/.codex/worktrees/3548/sunnypilot/analysis/controller_search_20260904/route90/route.npz": "51e8c26eedde253e171af47d704c1967ba45ae6825d883393bec1fb9e00251c1", - "/Users/ibpersonal/.codex/worktrees/3548/sunnypilot/analysis/controller_search_20260904/route90/metadata.json": "742afec55ed629155b22d387f376f878f7b2765221fe94f515a829052bdd916f", - "/Users/ibpersonal/.codex/worktrees/3548/sunnypilot/analysis/controller_search_20260904/route90/encoder_comparison.npz": "7a625d3ed5cbd8013d1028aa3bc421740551dcae5c3d60981208bd047af9794c", - "/Users/ibpersonal/.codex/worktrees/3548/sunnypilot/analysis/controller_search_20260904/route90/encoder_comparison.json": "64e1cc4be84394ac7ec408d383b99b48ada9b3fd129cdef2fd846e5fac620d66", - "/Users/ibpersonal/.codex/worktrees/3548/sunnypilot/analysis/controller_search_20260904/route90/pose_candidate/pose_replay.npz": "4457ccc0868354749da5b72c1dea0faf750f783dfdc87038101288fdcba1e707" - } - }, - "route95": { - "scope": "Command construction and adapter reconstruction only; no counterfactual closed-loop score.", - "calibration_approved": false, - "executes_live_selector": false, - "cycles": 54738, - "core_active_cycles": 37614, - "core_exact_archived_match": true, - "cohorts_reproduced": true, - "adapter_active_cycles": 37614, - "adapter_status_counts": { - "inactive": 17124, - "active": 37614 - }, - "adapter_matches_current_core_with_yaw_and_fresh_engagement_dt": true, - "core_active_path_shorter_than_7m_cycles": 44, - "adapter_validity_differs_from_archive_cycles": 0, - "adapter_command_differs_from_archive_cycles": 20110, - "adapter_max_absolute_command_difference_c0_c1": [ - 0.16999999999999993, - 0.0010000000000000009 - ], - "field_slew_zero_c2_c3_pass": true, - "float32_can_round_trips": 109476, - "timing": "Original controls publication timestamps proxy computation time; repeated frames and gaps retained. No identified delay.", - "eligibility": "Adapter checks recorded services independently; full SubMaster health is unavailable. Core uses archived validity.", - "reference": "Recorded controlsState.desiredCurvature, already selected/limited. These two routes have no maneuver publications.", - "host_yaw": "Extract cs.yaw equals -carState.yawRate; current adapter uses it for bounded damping.", - "archived_core_revision": "5fc16abc7662020706e29f57d31a6d5e2bc1293a", - "archived_core_source_sha256": "8f3bc5d68e0051776f614a2ccffae84a88f7898dc95bdc12c23dcfe10dfe676a", - "cohorts": { - "small_request": { - "seconds": 160.93427628100014, - "core_c0_c1_rms": [ - 0.027283284164810895, - 0.006054573235641941 - ], - "recorded_v8_c0_c1_rms": [ - 0.04189890174428264, - 0.0075342351227490395 - ], - "adapter_eligible_seconds": 160.93427628100014, - "adapter_c0_c1_rms": [ - 0.02970169836480558, - 0.006054573235641941 - ] - }, - "turn": { - "seconds": 58.39837079799993, - "core_c0_c1_rms": [ - 0.5425346915921071, - 0.09102985199238507 - ], - "recorded_v8_c0_c1_rms": [ - 0.63700479987207, - 0.09691236120507696 - ], - "adapter_eligible_seconds": 58.39837079799993, - "adapter_c0_c1_rms": [ - 0.5622074838217578, - 0.09102985199238507 - ] - }, - "small_speed_2_8": { - "seconds": 8.083969085000064, - "core_c0_c1_rms": [ - 0.09941192804175551, - 0.016572084086884566 - ], - "recorded_v8_c0_c1_rms": [ - 0.07597383189748476, - 0.016403469943482765 - ], - "adapter_eligible_seconds": 8.083969085000064, - "adapter_c0_c1_rms": [ - 0.1039594708778471, - 0.016572084086884566 - ] - }, - "turn_speed_2_8": { - "seconds": 2.9056519089999995, - "core_c0_c1_rms": [ - 2.286598281178436, - 0.3420570411209625 - ], - "recorded_v8_c0_c1_rms": [ - 2.4371443775668484, - 0.37360610108051345 - ], - "adapter_eligible_seconds": 2.9056519089999995, - "adapter_c0_c1_rms": [ - 2.342145227147664, - 0.3420570411209625 - ] - }, - "small_speed_8_15": { - "seconds": 32.021085868, - "core_c0_c1_rms": [ - 0.028387860150467858, - 0.0067602044219821 - ], - "recorded_v8_c0_c1_rms": [ - 0.036941169841952046, - 0.007349671742584858 - ], - "adapter_eligible_seconds": 32.021085868, - "adapter_c0_c1_rms": [ - 0.032473217223426755, - 0.0067602044219821 - ] - }, - "turn_speed_8_15": { - "seconds": 22.160645306000006, - "core_c0_c1_rms": [ - 0.28668316323581683, - 0.0671915746138579 - ], - "recorded_v8_c0_c1_rms": [ - 0.3391022553367454, - 0.07297678510653233 - ], - "adapter_eligible_seconds": 22.160645306000006, - "adapter_c0_c1_rms": [ - 0.32040564199313326, - 0.0671915746138579 - ] - }, - "small_speed_15_55": { - "seconds": 120.82922132800007, - "core_c0_c1_rms": [ - 0.010802222799973712, - 0.004282512931613862 - ], - "recorded_v8_c0_c1_rms": [ - 0.03987958989535526, - 0.0065793876512313535 - ], - "adapter_eligible_seconds": 120.82922132800007, - "adapter_c0_c1_rms": [ - 0.013133007539405142, - 0.004282512931613862 - ] - }, - "turn_speed_15_55": { - "seconds": 33.332073582999925, - "core_c0_c1_rms": [ - 0.07257996778378971, - 0.03628926246224237 - ], - "recorded_v8_c0_c1_rms": [ - 0.3416081588451539, - 0.027322786376822172 - ], - "adapter_eligible_seconds": 33.332073582999925, - "adapter_c0_c1_rms": [ - 0.08556161843867907, - 0.03628926246224237 - ] - }, - "pose_quiet": { - "seconds": 145.24136911700015, - "core_c0_c1_rms": [ - 0.012566204405760934, - 0.004614194153565963 - ], - "recorded_v8_c0_c1_rms": [ - 0.03855743841849719, - 0.006621274926345545 - ], - "adapter_eligible_seconds": 145.24136911700015, - "adapter_c0_c1_rms": [ - 0.014652655970248427, - 0.004614194153565963 - ] - } - }, - "workspace_head": "744a97d9bc08d8743b250eceff7c88585b5480de", - "opendbc_import_head": "72a775d35e54c21ff5c5798acef22016eedcc0a7", - "opendbc_import_path": "/Users/ibpersonal/.codex/worktrees/b926/sunnypilot/opendbc_repo", - "source_sha256": { - "/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/model_action_replay.py": "a872cf4594e364988796394b09f531d18cc3d6732e0c92f45eed9fbf6f5cbee6", - "/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/openpilot/selfdrive/controls/lib/ford_model_action.py": "5b9f7946f29b5fcd94ca93e53be14a12b18ae74d7ca27380e92b90c8c599dce0", - "/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/openpilot/selfdrive/controls/lib/ford_path.py": "383538fc7cdae3bc28dffb71fe12ac5f3f9866ffbe6adfb7457f3593e9fc903a", - "/Users/ibpersonal/.codex/worktrees/3548/sunnypilot/analysis/controller_search_20260904/route95/route.npz": "6e5438867ea618e53ca395b60ff4b9b146256f1bf6f07dc8e92d663286074154", - "/Users/ibpersonal/.codex/worktrees/3548/sunnypilot/analysis/controller_search_20260904/route95/metadata.json": "f7c95a102e0e084bbcc9938399219080c1d3b26274b4ea3c3a9514ff7dc8ea8d", - "/Users/ibpersonal/.codex/worktrees/3548/sunnypilot/analysis/controller_search_20260904/route95/encoder_comparison.npz": "23bd05c4b6400299844acaba1d97051c96c23c682bf17b46e89e7f2cca5fce38", - "/Users/ibpersonal/.codex/worktrees/3548/sunnypilot/analysis/controller_search_20260904/route95/encoder_comparison.json": "1ab24a796d14817b8672ea6dfd39711d65cb06744eaaef7dcc02a840c392c4d3", - "/Users/ibpersonal/.codex/worktrees/3548/sunnypilot/analysis/controller_search_20260904/route95/pose_candidate/pose_replay.npz": "9cfbb3c6f0b1fdb7e3d38e6b64b8c94cffbcc9fabe41f6344d84a9b657b6af9b" - } - }, - "route9b": { - "scope": "Fixed-input command replay only; no physical improvement or stability claim.", - "calibration_approved": false, - "cycles": 82386, - "eligible_cycles": 70703, - "status_counts": { - "inactive": 11683, - "active": 70703 - }, - "c1_exactly_unchanged": true, - "same_validity": true, - "field_slew_zero_c2_c3_pass": true, - "float32_can_round_trips": 164772, - "cohorts": { - "eligible": { - "cycles": 70703, - "seconds": 711.571703289, - "changed_c0_cycles": 20858, - "mean_absolute_c0_change_m": 0.006916798541216091, - "max_absolute_c0_change_m": 0.15000000000000124, - "increased_absolute_c0_cycles": 17700, - "decreased_absolute_c0_cycles": 3157, - "driver_input_percent": 3.0297623863274845, - "baseline_peak_absolute_c0_m": 3.89, - "candidate_peak_absolute_c0_m": 3.96 - }, - "driver_clean": { - "cycles": 63267, - "seconds": 636.7469998759999, - "changed_c0_cycles": 16024, - "mean_absolute_c0_change_m": 0.002923399405403562, - "max_absolute_c0_change_m": 0.15000000000000036, - "increased_absolute_c0_cycles": 13587, - "decreased_absolute_c0_cycles": 2437, - "driver_input_percent": 0.0, - "baseline_peak_absolute_c0_m": 2.16, - "candidate_peak_absolute_c0_m": 2.24 - }, - "driver_clean_low_request_above_8mps": { - "cycles": 40656, - "seconds": 409.13067679800054, - "changed_c0_cycles": 6007, - "mean_absolute_c0_change_m": 0.0014813245503935623, - "max_absolute_c0_change_m": 0.020000000000000462, - "increased_absolute_c0_cycles": 5099, - "decreased_absolute_c0_cycles": 908, - "driver_input_percent": 0.0, - "baseline_peak_absolute_c0_m": 0.08999999999999986, - "candidate_peak_absolute_c0_m": 0.11000000000000032 - }, - "turn": { - "cycles": 5459, - "seconds": 54.939139939000256, - "changed_c0_cycles": 3156, - "mean_absolute_c0_change_m": 0.007655026845650452, - "max_absolute_c0_change_m": 0.08999999999999986, - "increased_absolute_c0_cycles": 2938, - "decreased_absolute_c0_cycles": 218, - "driver_input_percent": 0.0, - "baseline_peak_absolute_c0_m": 2.16, - "candidate_peak_absolute_c0_m": 2.24 - }, - "right_entry": { - "cycles": 298, - "seconds": 2.9932484459999387, - "changed_c0_cycles": 298, - "mean_absolute_c0_change_m": 0.14871908015683724, - "max_absolute_c0_change_m": 0.15000000000000036, - "increased_absolute_c0_cycles": 298, - "decreased_absolute_c0_cycles": 0, - "driver_input_percent": 20.395198628296086, - "baseline_peak_absolute_c0_m": 2.8000000000000003, - "candidate_peak_absolute_c0_m": 2.95 - }, - "right_exit_before_strong_input": { - "cycles": 115, - "seconds": 1.156770048999988, - "changed_c0_cycles": 115, - "mean_absolute_c0_change_m": 0.02362929150320746, - "max_absolute_c0_change_m": 0.040000000000000036, - "increased_absolute_c0_cycles": 115, - "decreased_absolute_c0_cycles": 0, - "driver_input_percent": 0.0, - "baseline_peak_absolute_c0_m": 0.7000000000000002, - "candidate_peak_absolute_c0_m": 0.7199999999999998 - } - }, - "baseline_commands_vs_recorded_publications": { - "paired_cycles": 70703, - "within_one_quantum_cycles": 69862, - "maximum_absolute_error_c0_c1": [ - 0.20999998569488554, - 0.0005000143051147043 - ] - }, - "timing": "Publication-time proxy, causal carState, exact consumed model; full SubMaster health unavailable.", - "baseline_version": "v2", - "baseline_revision": "744a97d9bc08d8743b250eceff7c88585b5480de", - "candidate_version": "current", - "candidate_revision": "working_tree", - "focus_windows": [ - [ - "right_entry", - "637", - "640" - ], - [ - "right_exit_before_strong_input", - "642.7", - "643.852" - ] - ], - "baseline_source_sha256": "59d66297a017557f3d4f28b115be3f6220b800566c11935e2284b3814783fb7e", - "candidate_source_sha256": "5b9f7946f29b5fcd94ca93e53be14a12b18ae74d7ca27380e92b90c8c599dce0", - "source_rlog_sha256": { - "84865544361f55cb_0000009b--e4616dddaa--0--rlog.zst": "22746f7119109b73ed7f2c26ce8c99f87136e9124fb7fc14c9554409a28a7c3f", - "84865544361f55cb_0000009b--e4616dddaa--1--rlog.zst": "4c2e1d7083c31a2b37d0f8dd3be4d330898511b7e02c26f7d40ca9bc2779397d", - "84865544361f55cb_0000009b--e4616dddaa--2--rlog.zst": "62f3e049e220cd3681fadf386f2969537bd571998ae2f6ba2d08479428b5a28f", - "84865544361f55cb_0000009b--e4616dddaa--3--rlog.zst": "83bf0131b2d36b2ba7e5ba050bbc13c0a3350feb5c9b89dc9c87d3a37abebfb3", - "84865544361f55cb_0000009b--e4616dddaa--4--rlog.zst": "430985a80dd6e10f7abeb89457a17022e6bb6978617f415c905f584b1647603e", - "84865544361f55cb_0000009b--e4616dddaa--5--rlog.zst": "8c0c5ae6323ec33b3e14f84ca834f70cb56f6b29f471a350f1e3efc06b6ba553", - "84865544361f55cb_0000009b--e4616dddaa--6--rlog.zst": "db53dfa8156b9d66792c3eff0b2ce5d31b71ad41cc580dec85f528845593c184", - "84865544361f55cb_0000009b--e4616dddaa--7--rlog.zst": "91b0b3be10cb7d7d7f7dd2024d8f9ee99d1e9fd2204203a3a9a2f2f1c6e3fa03", - "84865544361f55cb_0000009b--e4616dddaa--8--rlog.zst": "687dbbfc49837efbfe8fa6bc091e40f7fad2908832234d7884f4616d1bc9ccff", - "84865544361f55cb_0000009b--e4616dddaa--9--rlog.zst": "a88ec4d25b04cdbf5844686fc77f6b28dca920c9b164e37ebf69844a3ae398fc", - "84865544361f55cb_0000009b--e4616dddaa--10--rlog.zst": "fe6b29580a6c94e1c236d13e18db4cd9f31cc1b25d52e1e6e19a5021125c9932", - "84865544361f55cb_0000009b--e4616dddaa--11--rlog.zst": "150d31b1944d7a1b8c562f3aee20b66cefa6c4e8d02660ec889907d835142f45", - "84865544361f55cb_0000009b--e4616dddaa--12--rlog.zst": "e105e5f703a70f36f1989c66fb46c35a65ff60a265b4332e10fa2e6875a2ced4", - "84865544361f55cb_0000009b--e4616dddaa--13--rlog.zst": "69304cf0c81401374f047c5022ca47d257ff9e59e30ec171325a2ee1e7ed13d4" - }, - "opendbc_head": "c21a9013700734dd20b09e05aa68329ad8cc20f9", - "source_sha256": { - "/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/damping_replay.py": "663674fa9686291228766c4fd2990dcfc620b155f6d74e2f957bbb33e5593a5d", - "/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/openpilot/selfdrive/controls/lib/ford_model_action.py": "5b9f7946f29b5fcd94ca93e53be14a12b18ae74d7ca27380e92b90c8c599dce0" - } - }, - "route9e": { - "scope": "Fixed-input command replay only; no physical improvement or stability claim.", - "calibration_approved": false, - "cycles": 83668, - "eligible_cycles": 74669, - "status_counts": { - "inactive": 8999, - "active": 74669 - }, - "c1_exactly_unchanged": true, - "same_validity": true, - "field_slew_zero_c2_c3_pass": true, - "float32_can_round_trips": 167336, - "cohorts": { - "eligible": { - "cycles": 74669, - "seconds": 751.5479497329998, - "changed_c0_cycles": 23282, - "mean_absolute_c0_change_m": 0.00617904439256301, - "max_absolute_c0_change_m": 0.15000000000000036, - "increased_absolute_c0_cycles": 19880, - "decreased_absolute_c0_cycles": 3402, - "driver_input_percent": 2.7324748055386094, - "baseline_peak_absolute_c0_m": 4.33, - "candidate_peak_absolute_c0_m": 4.38 - }, - "driver_clean": { - "cycles": 66321, - "seconds": 667.5291971840106, - "changed_c0_cycles": 18123, - "mean_absolute_c0_change_m": 0.003409158953346422, - "max_absolute_c0_change_m": 0.15000000000000036, - "increased_absolute_c0_cycles": 16136, - "decreased_absolute_c0_cycles": 1987, - "driver_input_percent": 0.0, - "baseline_peak_absolute_c0_m": 2.3600000000000003, - "candidate_peak_absolute_c0_m": 2.4699999999999998 - }, - "driver_clean_low_request_above_8mps": { - "cycles": 39635, - "seconds": 398.9613925449994, - "changed_c0_cycles": 5866, - "mean_absolute_c0_change_m": 0.0014778774629256156, - "max_absolute_c0_change_m": 0.020000000000000462, - "increased_absolute_c0_cycles": 5377, - "decreased_absolute_c0_cycles": 489, - "driver_input_percent": 0.0, - "baseline_peak_absolute_c0_m": 0.05999999999999961, - "candidate_peak_absolute_c0_m": 0.07000000000000028 - }, - "turn": { - "cycles": 6219, - "seconds": 62.59491144700405, - "changed_c0_cycles": 5129, - "mean_absolute_c0_change_m": 0.011069748090899225, - "max_absolute_c0_change_m": 0.10999999999999943, - "increased_absolute_c0_cycles": 5098, - "decreased_absolute_c0_cycles": 31, - "driver_input_percent": 0.0, - "baseline_peak_absolute_c0_m": 2.3600000000000003, - "candidate_peak_absolute_c0_m": 2.4699999999999998 - }, - "left_entry": { - "cycles": 238, - "seconds": 2.4048367620016506, - "changed_c0_cycles": 238, - "mean_absolute_c0_change_m": 0.14261645650107144, - "max_absolute_c0_change_m": 0.15000000000000036, - "increased_absolute_c0_cycles": 238, - "decreased_absolute_c0_cycles": 0, - "driver_input_percent": 2.403816920672437, - "baseline_peak_absolute_c0_m": 2.46, - "candidate_peak_absolute_c0_m": 2.6100000000000003 - }, - "left_peak": { - "cycles": 288, - "seconds": 2.891819394000777, - "changed_c0_cycles": 288, - "mean_absolute_c0_change_m": 0.095420590363382, - "max_absolute_c0_change_m": 0.15000000000000036, - "increased_absolute_c0_cycles": 288, - "decreased_absolute_c0_cycles": 0, - "driver_input_percent": 6.41034524444729, - "baseline_peak_absolute_c0_m": 2.79, - "candidate_peak_absolute_c0_m": 2.9400000000000004 - }, - "left_exit": { - "cycles": 219, - "seconds": 2.203320298998733, - "changed_c0_cycles": 185, - "mean_absolute_c0_change_m": 0.0422943841629376, - "max_absolute_c0_change_m": 0.09000000000000075, - "increased_absolute_c0_cycles": 56, - "decreased_absolute_c0_cycles": 129, - "driver_input_percent": 9.936619705158035, - "baseline_peak_absolute_c0_m": 2.4299999999999997, - "candidate_peak_absolute_c0_m": 2.51 - }, - "reversal": { - "cycles": 554, - "seconds": 5.570295188001182, - "changed_c0_cycles": 167, - "mean_absolute_c0_change_m": 0.007060409634059627, - "max_absolute_c0_change_m": 0.040000000000000036, - "increased_absolute_c0_cycles": 20, - "decreased_absolute_c0_cycles": 147, - "driver_input_percent": 0.0, - "baseline_peak_absolute_c0_m": 0.2400000000000002, - "candidate_peak_absolute_c0_m": 0.21999999999999975 - }, - "final_entry": { - "cycles": 348, - "seconds": 3.4958847329980927, - "changed_c0_cycles": 348, - "mean_absolute_c0_change_m": 0.11950442609746584, - "max_absolute_c0_change_m": 0.15000000000000036, - "increased_absolute_c0_cycles": 348, - "decreased_absolute_c0_cycles": 0, - "driver_input_percent": 3.556656282920533, - "baseline_peak_absolute_c0_m": 3.31, - "candidate_peak_absolute_c0_m": 3.4400000000000004 - } - }, - "baseline_commands_vs_recorded_publications": { - "paired_cycles": 74669, - "within_one_quantum_cycles": 74156, - "maximum_absolute_error_c0_c1": [ - 0.29000000834465034, - 0.0010000016689301061 - ] - }, - "timing": "Publication-time proxy, causal carState, exact consumed model; full SubMaster health unavailable.", - "baseline_version": "v2", - "baseline_revision": "744a97d9bc08d8743b250eceff7c88585b5480de", - "candidate_version": "current", - "candidate_revision": "working_tree", - "focus_windows": [ - [ - "left_entry", - "173", - "175.4" - ], - [ - "left_peak", - "175.4", - "178.3" - ], - [ - "left_exit", - "178.3", - "180.5" - ], - [ - "reversal", - "728", - "734" - ], - [ - "final_entry", - "822", - "825.5" - ] - ], - "baseline_source_sha256": "59d66297a017557f3d4f28b115be3f6220b800566c11935e2284b3814783fb7e", - "candidate_source_sha256": "5b9f7946f29b5fcd94ca93e53be14a12b18ae74d7ca27380e92b90c8c599dce0", - "source_rlog_sha256": { - "84865544361f55cb_0000009e--592f7dc149--0--rlog.zst": "de63532ae6aedf5dc7fd3ac8e47a2d96a8f065614f4aca79ae2f120ea00390c7", - "84865544361f55cb_0000009e--592f7dc149--1--rlog.zst": "67b58197158b3e8f0581643f6657d2a85c47b0bb75dd3e00306d9ffae790008b", - "84865544361f55cb_0000009e--592f7dc149--2--rlog.zst": "a4840f338f51b5f1864c79ce3a4f2b11dc13d58d52b8459961c9da57237f2cc2", - "84865544361f55cb_0000009e--592f7dc149--3--rlog.zst": "ff3ea8d948006ab19c4dbfeeff59a509de3f193da47b91c88faa27ecd0b3f1fc", - "84865544361f55cb_0000009e--592f7dc149--4--rlog.zst": "5ff1996c2336299128a11e657c32bc21921716b7e3bd401de69a2d49484aa223", - "84865544361f55cb_0000009e--592f7dc149--5--rlog.zst": "238763457e89896933afaf9a5df325469ee8c02ddc53550df252531bc94fc540", - "84865544361f55cb_0000009e--592f7dc149--6--rlog.zst": "2ee86de80cfd762be10cd2dfb2895ddbee6b813706e9c7261460203e09b9bc4d", - "84865544361f55cb_0000009e--592f7dc149--7--rlog.zst": "017801f080861c63d799f87aebe30ab57cdf82f078ac882e6187d3870c403538", - "84865544361f55cb_0000009e--592f7dc149--8--rlog.zst": "3729016bd1f00bb1077613b63fe25b21ba7112822b994e0e0aa2b4cd93bdb940", - "84865544361f55cb_0000009e--592f7dc149--9--rlog.zst": "c40b3c1f6eb9252b85f176fee32cae16c23eaad3db830f6bbf37a730034a5ccf", - "84865544361f55cb_0000009e--592f7dc149--10--rlog.zst": "e0ce8f231798073e5bbc34551fbd9493f169a06a068adc1957ef8cd914b71fbb", - "84865544361f55cb_0000009e--592f7dc149--11--rlog.zst": "90863353f7c862a05618b4d0761dda3fcedd3cf6b4234c21c6dcc2eee3ec20b2", - "84865544361f55cb_0000009e--592f7dc149--12--rlog.zst": "e064f8abde9615b2daf00f469ef36b438cd4b3a1637069467c84de91bee3bdf1", - "84865544361f55cb_0000009e--592f7dc149--13--rlog.zst": "790fc438ce2675e0690fdce3b07bf54fdf6d06b17bbe7f5ff447adfa71189754", - "84865544361f55cb_0000009e--592f7dc149--14--rlog.zst": "b98f5a5be660013b1fe11250fdc24ee4fe1d9dd1301b787ddfdff1f2bcd3e27f" - }, - "opendbc_head": "c21a9013700734dd20b09e05aa68329ad8cc20f9", - "source_sha256": { - "/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/damping_replay.py": "663674fa9686291228766c4fd2990dcfc620b155f6d74e2f957bbb33e5593a5d", - "/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/openpilot/selfdrive/controls/lib/ford_model_action.py": "5b9f7946f29b5fcd94ca93e53be14a12b18ae74d7ca27380e92b90c8c599dce0" - } - } - }, - "focus_comparison": { - "scope": "Same recorded inputs; crossing a C0 level earlier does not identify vehicle response time.", - "left_entry_crossings": [ - { - "c0_magnitude_m": 0.5, - "v2_time_s": 173.04409075799958, - "v3_time_s": 172.98298211199835, - "earlier_ms": 61.10864600123023 - }, - { - "c0_magnitude_m": 1.0, - "v2_time_s": 173.41054691999852, - "v3_time_s": 173.3469171409979, - "earlier_ms": 63.629779000621056 - }, - { - "c0_magnitude_m": 1.5, - "v2_time_s": 174.09016323599826, - "v3_time_s": 173.7234842099988, - "earlier_ms": 366.6790259994741 - }, - { - "c0_magnitude_m": 2.0, - "v2_time_s": 174.69427409499804, - "v3_time_s": 174.49167145299907, - "earlier_ms": 202.60264199896483 - }, - { - "c0_magnitude_m": 2.4, - "v2_time_s": 175.08411706099832, - "v3_time_s": 174.99415714599854, - "earlier_ms": 89.95991499978118 - } - ], - "right_exit": { - "mean_c0_v1_m": 0.3704272817752263, - "mean_c0_v2_m": 0.29049595970311604, - "mean_c0_v3_m": 0.31412525120632345, - "v3_below_driven_v1_every_cycle": true, - "cycles": 115 - } - }, - "total_original_route_cycles": 299604, - "total_float32_can_round_trips": 817346, - "source_sha256": { - "openpilot/selfdrive/controls/lib/ford_model_action.py": "5b9f7946f29b5fcd94ca93e53be14a12b18ae74d7ca27380e92b90c8c599dce0", - "openpilot/selfdrive/controls/tests/test_ford_model_action.py": "8851104f811757f6a31b5e23ec5933c7bfc18c17012af9a0a78f851217eca525", - "openpilot/selfdrive/controls/tests/test_ford_model_action_adapter.py": "04daed84093330c05ae1669c155ed9c197f95ebb02fd4e7ca1c7a10ce70d03da", - "openpilot/selfdrive/controls/tests/test_ford_model_action_damping.py": "a7062d2c3c46b3abcd4c3facd97970644ce609fb55285daa0359681950948250", - "openpilot/selfdrive/controls/tests/test_ford_model_action_prediction.py": "c05ae173c809228a9f4782af32a174b1b928f506a76c454aa89a51ef0ed17bdd", - "openpilot/selfdrive/controls/tests/test_ford_model_action_selection.py": "f826c6328f0abac2a61f1a0a6f8d119fdbab858e363cb466d84ba9a7783059cd", - "openpilot/selfdrive/controls/tests/test_ford_controlsd_logging.py": "fd4095bc4f1363fd014c43a9e0776a2245105e8fb2703d87da367d0f33dc22dc", - "tools/ford_pscm_lab/__init__.py": "db4b8b7d2e317ed34ca0ec220bf9d53e7b80a23e766f4e1cb2c8224dada45f2e", - "tools/ford_pscm_lab/damping_replay.py": "663674fa9686291228766c4fd2990dcfc620b155f6d74e2f957bbb33e5593a5d", - "tools/ford_pscm_lab/model_action_replay.py": "a872cf4594e364988796394b09f531d18cc3d6732e0c92f45eed9fbf6f5cbee6", - "tools/ford_pscm_lab/stress_model_action.py": "be77689e5327a13b7b4d0e18a3fc1863b57cab76be714339efc609124400489f", - "tools/ford_pscm_lab/test_model_action_replay.py": "d2a3602e145db4f969ecf1402fe66684c3b1f884e3ed429e95f5fb5dcaddefb3", - "openpilot/sunnypilot/sunnylink/settings_ui_src/pages/vehicle.yaml": "b5951a90cb01bfcd79b82705600ad2d165b3cc5f7e33c7404536eb989bc3ce39", - "openpilot/sunnypilot/sunnylink/settings_ui.json": "6badb0080d8a999d63d063430c0a320f1a58884dfe0c390774f90a39cbf0e0e2" - }, - "artifact_sha256": { - ".cache/ford_model_action_prediction/prediction_red.txt": "81e765edd1fc73783855ef30aa3c4aa8f111c001d7e9e0951d5791e18a860f4e", - ".cache/ford_model_action_prediction/stress.json": "e358a7caafe457a83637857cee7f0b1b2b60e0e4020f64b46630b8462e5edff5", - ".cache/ford_model_action_prediction/coverage.json": "333ed4414632adbb12a8a1f3ef2f9dc7a62dd9fde08bbc6b8a3d372cf9a6869b", - ".cache/ford_model_action_prediction/focus_comparison.json": "b0af320c0eb7e535420702ed78df30c43134f8f9e2a977e5f88f765ba0626c69", - ".cache/ford_model_action_prediction/route90/report.json": "b58f738269d9fe176672670f5bf4116f3226d319f43b5b407ac221c288aba174", - ".cache/ford_model_action_prediction/route95/report.json": "379a6f5bc9c98d8d1bccf687d61d46c54eda439284508e321d49b5312c1ca9f2", - ".cache/ford_model_action_prediction/route9b/report.json": "650ce4056b663124e2f72fae94cd1b66423ff74de42611a99501271c2c846bc2", - ".cache/ford_model_action_prediction/route9e/report.json": "196b3b242e3e056ed531cd6e657a235cc9bf8ed4357ce696b1bec1e5f0a49a39", - ".cache/ford_model_action_prediction/route9b/commands.npz": "5d82318935253d4fa8a1b8c30cc7f2d465c151ae1cb9109fa09a622d5be49871", - ".cache/ford_model_action_prediction/route9e/commands.npz": "2917bbe801a78711340fa4eae63d5bf1cb4a4464c45d8538c9a46a77b5511e06", - ".cache/ford_model_action_prediction/plot_commands.py": "6a5bd97fedeb9bd951925dddc656e508082f6d457dbc1a6c9d4db769afcdd921", - "/Users/ibpersonal/.codex/visualizations/2026/09/07/01a07c33-1f6c-7292-8030-ad4b01923034/ford-route9e/prediction_command_comparison.png": "86e68a0497490352065997f73cd5a798ff93dcabc93152028669ccfb36a4fefe", - "/Users/ibpersonal/.codex/visualizations/2026/09/07/01a07c33-1f6c-7292-8030-ad4b01923034/ford-route9e/prediction_command_comparison.svg": "2310b2978b60b97619886c2e10ecca88c73b5fde3477452347e6db0a5bbcd5b3" - } -} diff --git a/docs/ford_model_points.md b/docs/ford_model_points.md deleted file mode 100644 index 18c7fcab02..0000000000 --- a/docs/ford_model_points.md +++ /dev/null @@ -1,130 +0,0 @@ -# Ford model-point candidate v7 - -## Decision - -Use the model's own position and orientation at one shared path point. This -implements the request to follow model geometry through C0/C1, with C2/C3 zero, -without a fitted PSCM plant, new strength gain, yaw integral, or release mode. - -Let `s(t)` be cumulative planar arc distance along model position. Choose: - -``` -station = min(path_end, max(7 metres, s(1 second))) -C0_target = model.position.y at station -C1_target = unwrapped model.orientation.z at station -C2 = C3 = 0 -``` - -Use the published model timestamps, not `speed × 1 second`, so the point also -follows the model's predicted acceleration/braking. The seven-metre floor keeps -the existing low-speed preview distance; it can select a time beyond one second. -If the entire path is shorter, both fields hold the same endpoint. Interpolation -uses the same arc segment and weights for position and heading. - -One second keeps C1 near the old one-second heading scale in ordinary driving. -Using that same point for C0 represents a meaningful change in faster bends. -This is a chosen local approximation; Ford's expected reference point and -preview are unknown. Matching metres/radians does not prove PSCM equivalence. - -The former 150 ms vehicle-pose forecast is removed. C1 no longer uses -`max(7, speed) × selected desiredCurvature`. Measured yaw remains an input-health -check only; calibrated yaw has no command role. Freshness, independent slew, -packing, the shared startup toggle, 20 Hz sends, and Panda safety remain. -The upstream scalar curvature is still logged but does not limit this geometry; -C0/C1 retain their existing amplitude and slew limits. The scalar-only lateral -maneuver test reference is explicitly unsupported and disengages this candidate. - -## Offline evidence - -The broad run passed **585 tests and 9,146 subtests**, with 178 inherited safety -cases skipped as inapplicable. Tests of the removed yaw forecast were retired; -new tests cover actual model clocks, a nonconstant-speed trajectory, shared-point -sampling, the distance floor, endpoint holding, heading unwrap, source selection, -invalid geometry, reset, independent slew, and actual CAN delivery. - -The stress run covers 200,000 random cycles plus mirrored turns, 18,138 field -boundary cases, and 218,138 Float32/CAN round trips. An independent scalar oracle -checks analytic model points and slew. No vehicle plant is simulated. - -Five recorded routes (a5, a2, a0, 9b, 9e) supply **379,718 controller cycles** and -**76,294 model frames**. Original rlog hashes and position/orientation timestamps -were checked. An independently implemented segment-weight oracle matches every -sampled target and eligible slew state. Every cycle passed packing/bound checks; -all **75,947** scheduled 20 Hz requests passed the actual unchanged Panda TX hook. -This is TX acceptance with controlled eligibility, not a full Panda RX watchdog -or vehicle-response replay. The [validation manifest](ford_model_points_validation.json) -binds the results to their source and input hashes. - -On a5's 61.24-second driver-clean ordinary-bend cohort, mean absolute C0 changes -from 0.075 m recorded to 0.167 m replayed; C1 changes from 0.02287 to 0.02224 rad. -Neither field target clips in that cohort. These are command differences, not -predicted changes in steering strength or tracking error. - -| a5 time | Recorded C0 / C1 | Candidate C0 / C1 | Selected station / time | -| --- | --- | --- | --- | -| 107.995 s, ordinary bend | +0.20 m / +0.068 rad | +0.55 m / +0.0685 rad | 13.71 m / 1.00 s | -| 390.681 s, sustained bend | −0.52 m / −0.1235 rad | −0.75 m / −0.1215 rad | 9.37 m / 1.00 s | -| 374.889 s, tight turn | −3.76 m / −0.50 rad | −3.46 m / −0.50 rad | 7.00 m / 1.49 s | -| 17.607 s, exit overshoot | −0.01 m / −0.015 rad | −0.05 m / −0.012 rad | 7.00 m / 1.85 s | - -The exit example retains more C0 into the turn than v6; physical unwind behavior -must be evaluated. Tight-turn C1 clipping remains (14.92 eligible seconds on a5). -No root cause or physical fix is proven by frozen inputs. The latest a5 road -logs used 100 Hz sends, whereas the immediately preceding code revision already -changed to 20 Hz; a comparison against that drive also includes the cadence change. - -## Unwind comparison - -The follow-up a5 comparison includes all seven clearly separated tight turns and -four completed ordinary bends identified in the full-route command plot. A fifth -bend runs into the next turn and is excluded from the summary. All scored windows -remain continuously paired-active, without control gaps over 30 ms. The tight -turns contain driver input; this compares instructions on frozen inputs, not -unassisted tracking or hypothetical truck motion. - -Measure the first time each instruction falls below the same fixed level on -exit and stays below for 100 ms. All seven tight turns exceed these levels: - -| Instruction | Candidate minus recorded clearance time | -| --- | --- | -| C0 below 0.5 m into the turn | 0.10 s later median; five later by 0.01–0.46 s, two unchanged | -| C1 below 0.1 rad into the turn | 0.10 s earlier median; all seven 0.04–0.17 s earlier | - -These are control-publication times. Actual packet timing also includes the -20 Hz send phase; shifts of only a few tens of milliseconds should not be -interpreted as equally precise changes at the PSCM. - -The report also compares half of each command's own peak: C1 reaches that level -0.25 s earlier in the median tight turn and C0 0.12 s later. Those normalized -crossings have different absolute thresholds when amplitudes differ. Comparing -half the smaller peak at an identical level instead gives C0 earlier in one -turn and later in six. No single threshold captures the whole release waveform. - -For the ordinary bend around 108 s, C0 reaches half of its own peak 0.20 s later; -C1 reaches half-peak 0.24 s earlier. The larger C0 takes 0.95 s longer to fall -below the same 0.05 m threshold. On the last tight turn, C0 clears 0.05 m 0.23 s -later. Near-zero thresholds are sensitive to small residual model offsets; the -complete report retains 90%, 50%, 10%, common-level and near-zero crossings -rather than treating any one threshold as a physical success criterion. - -The plotted candidate target and command largely coincide during these exits: -the longer C0 tail comes from the selected model point continuing to ask for -lateral offset, rather than a retained yaw correction. C1 can release sooner -because it now follows model orientation directly. These results do not show -that every command unwinds earlier, or that the vehicle will unwind earlier. - -Ten additional end-to-end sender cases cover both signs and all five CAN send -phases. After a full-cap turn, a zero model target starts reducing both states on -the first control update and appears on the next scheduled CAN message, 0–40 ms -later at nominal 100 Hz calculation. There is no additional release hold. The -unchanged slew itself takes 1.28 s of updates to clear 5.11 m C0 and 1.00 s to -clear 0.5 rad C1, plus scheduling to transmit zero. These are software timing -checks, not measured PSCM dynamics. The [unwind report](ford_model_points_unwind.json) -records the episode timings, method and source hashes. - -Older offline utilities now fail explicitly when their input lacks original -model clocks or their historical unchanged-C1 comparison cannot support v7. -This prevents missing model inputs from producing an all-invalid apparent match. - -`calibration_approved=false` remains. Installation instructions and restore -behavior are in the [drive-test guide](ford_model_action_drive_test.md). diff --git a/docs/ford_model_points_unwind.json b/docs/ford_model_points_unwind.json deleted file mode 100644 index c7c667564b..0000000000 --- a/docs/ford_model_points_unwind.json +++ /dev/null @@ -1,1157 +0,0 @@ -{ - "scope": "a5 recorded v6 vs v7 commands on identical recorded inputs; no counterfactual steering or plant response.", - "method": "Signed request into each turn. After each own peak, first below 90/50/10% held for 100 ms; near-zero is <=0.05 m C0 or <=0.01 rad C1 into the original turn. Common half is half the smaller peak; fixed clearance is <=0.5 m C0 or <=0.1 rad C1, scored only when both peaks exceed it. Negative timing delta means earlier. Windows must remain paired-active without >30 ms gaps. Driver inputs are retained and explicitly flagged; these are command comparisons, not driver-free tracking scores. Times are publication proxies; CAN scheduling is tested separately across all five send phases.", - "summary": { - "tight": { - "episodes": 7, - "c0": { - "below_90pct_s": { - "n": 7, - "median_s": 0.0, - "min_s": -0.12269046900000546, - "max_s": 0.04266988000000538, - "earlier": 2, - "later": 2 - }, - "below_50pct_s": { - "n": 7, - "median_s": 0.12129962300002717, - "min_s": 0.0848213179999675, - "max_s": 0.8246327600000001, - "earlier": 0, - "later": 7 - }, - "below_10pct_s": { - "n": 7, - "median_s": 0.1376267710000434, - "min_s": 0.0, - "max_s": 0.45046578099999124, - "earlier": 0, - "later": 6 - }, - "near_zero_s": { - "n": 5, - "median_s": 0.1399018539999588, - "min_s": -0.5224231490000193, - "max_s": 1.9510819790000085, - "earlier": 1, - "later": 4 - }, - "common_half_s": { - "n": 7, - "median_s": 0.07685077599995793, - "min_s": -0.2591174110000338, - "max_s": 0.5661551039999893, - "earlier": 1, - "later": 6 - }, - "fixed_clearance_s": { - "n": 7, - "median_s": 0.10080577899998389, - "min_s": 0.0, - "max_s": 0.4609723959999883, - "earlier": 0, - "later": 5 - } - }, - "c1": { - "below_90pct_s": { - "n": 7, - "median_s": -0.3856273459999784, - "min_s": -0.5246007750000103, - "max_s": 0.5545458859999997, - "earlier": 6, - "later": 1 - }, - "below_50pct_s": { - "n": 7, - "median_s": -0.2540558009999927, - "min_s": -0.46442294700000275, - "max_s": -0.21144918100003451, - "earlier": 7, - "later": 0 - }, - "below_10pct_s": { - "n": 6, - "median_s": -0.15774414349999688, - "min_s": -0.20159088100001554, - "max_s": 0.0, - "earlier": 5, - "later": 0 - }, - "near_zero_s": { - "n": 5, - "median_s": 0.0, - "min_s": -0.148710938000022, - "max_s": 1.5534367700000047, - "earlier": 2, - "later": 2 - }, - "common_half_s": { - "n": 7, - "median_s": -0.2528352759999848, - "min_s": -0.45502116000000115, - "max_s": -0.15168304499999863, - "earlier": 7, - "later": 0 - }, - "fixed_clearance_s": { - "n": 7, - "median_s": -0.10267424500000288, - "min_s": -0.1688626670000417, - "max_s": -0.040530361999969955, - "earlier": 7, - "later": 0 - } - } - }, - "ordinary": { - "episodes": 4, - "c0": { - "below_90pct_s": { - "n": 4, - "median_s": 0.49771416100000465, - "min_s": 0.2530698499999744, - "max_s": 3.195503137000003, - "earlier": 0, - "later": 4 - }, - "below_50pct_s": { - "n": 4, - "median_s": 0.3417062340000143, - "min_s": 0.19507780300000377, - "max_s": 0.4595077099999685, - "earlier": 0, - "later": 4 - }, - "below_10pct_s": { - "n": 4, - "median_s": 0.11352378200000146, - "min_s": 0.09310488600004874, - "max_s": 1.1537201169999776, - "earlier": 0, - "later": 4 - }, - "near_zero_s": { - "n": 4, - "median_s": 0.6988682280000234, - "min_s": 0.40513631699997177, - "max_s": 3.1885321799999815, - "earlier": 0, - "later": 4 - }, - "common_half_s": { - "n": 4, - "median_s": 1.0263607459999946, - "min_s": 0.7940629349999995, - "max_s": 2.846801904000017, - "earlier": 0, - "later": 4 - }, - "fixed_clearance_s": { - "n": 2, - "median_s": 1.97568246000003, - "min_s": 1.5486082510000188, - "max_s": 2.4027566690000413, - "earlier": 0, - "later": 2 - } - }, - "c1": { - "below_90pct_s": { - "n": 4, - "median_s": -0.0037014085000066643, - "min_s": -0.25255103900002496, - "max_s": 0.3999567310000316, - "earlier": 2, - "later": 1 - }, - "below_50pct_s": { - "n": 4, - "median_s": -0.17102558000001977, - "min_s": -0.2489269939999872, - "max_s": -0.046780566999984785, - "earlier": 4, - "later": 0 - }, - "below_10pct_s": { - "n": 4, - "median_s": -0.10130154349999998, - "min_s": -0.29764589699999533, - "max_s": 0.05121605200002932, - "earlier": 2, - "later": 1 - }, - "near_zero_s": { - "n": 4, - "median_s": -0.022888650000027155, - "min_s": -0.10325871399999187, - "max_s": 0.24679704999999785, - "earlier": 2, - "later": 1 - }, - "common_half_s": { - "n": 4, - "median_s": -0.22263683349999042, - "min_s": -0.2573995380000156, - "max_s": -0.09939024700003074, - "earlier": 4, - "later": 0 - }, - "fixed_clearance_s": { - "n": 2, - "median_s": -0.17009618150001415, - "min_s": -0.24998850800000127, - "max_s": -0.09020385500002703, - "earlier": 2, - "later": 0 - } - } - } - }, - "episodes": [ - { - "name": "tight_30s", - "window_s": [ - 29.0, - 37.0 - ], - "cohort": "tight", - "paired_continuously_active": true, - "overlaps_next_turn": false, - "turn_sign": -1.0, - "driver_input_present": true, - "fields": { - "c0": { - "recorded": { - "peak": 3.5899999141693115, - "peak_time_s": 30.240929207000008, - "below_90pct_s": 30.577019832000005, - "below_50pct_s": 32.081712382999996, - "below_10pct_s": 33.781046028000006, - "near_zero_s": 34.723653215, - "common_half_s": 32.34019003900001, - "fixed_clearance_s": 33.478672591000006 - }, - "candidate": { - "peak": 3.3899999999999997, - "peak_time_s": 30.219843945999997, - "below_90pct_s": 30.454329363, - "below_50pct_s": 32.906345142999996, - "below_10pct_s": 34.231511809, - "near_zero_s": 36.67473519400001, - "common_half_s": 32.906345142999996, - "fixed_clearance_s": 33.939644986999994 - }, - "common_half_threshold": 1.6949999999999998, - "fixed_clearance_threshold": 0.5, - "candidate_minus_recorded_seconds": { - "below_90pct_s": -0.12269046900000546, - "below_50pct_s": 0.8246327600000001, - "below_10pct_s": 0.45046578099999124, - "near_zero_s": 1.9510819790000085, - "common_half_s": 0.5661551039999893, - "fixed_clearance_s": 0.4609723959999883 - } - }, - "c1": { - "recorded": { - "peak": 0.4975000023841858, - "peak_time_s": 30.301567696000006, - "below_90pct_s": 30.512954623, - "below_50pct_s": 33.176987279, - "below_10pct_s": 34.231511809, - "near_zero_s": 34.879881131999994, - "common_half_s": 33.176987279, - "fixed_clearance_s": 33.781046028000006 - }, - "candidate": { - "peak": 0.5, - "peak_time_s": 30.374895092000003, - "below_90pct_s": 31.067500509, - "below_50pct_s": 32.925253477, - "below_10pct_s": 34.082231445000005, - "near_zero_s": 36.433317902, - "common_half_s": 32.925253477, - "fixed_clearance_s": 33.67845946600001 - }, - "common_half_threshold": 0.2487500011920929, - "fixed_clearance_threshold": 0.1, - "candidate_minus_recorded_seconds": { - "below_90pct_s": 0.5545458859999997, - "below_50pct_s": -0.25173380200000395, - "below_10pct_s": -0.149280363999992, - "near_zero_s": 1.5534367700000047, - "common_half_s": -0.25173380200000395, - "fixed_clearance_s": -0.10258656199999905 - } - } - } - }, - { - "name": "tight_101s", - "window_s": [ - 98.0, - 106.0 - ], - "cohort": "tight", - "paired_continuously_active": true, - "overlaps_next_turn": false, - "turn_sign": -1.0, - "driver_input_present": true, - "fields": { - "c0": { - "recorded": { - "peak": 2.7899999618530273, - "peak_time_s": 100.93407044199999, - "below_90pct_s": 101.180834951, - "below_50pct_s": 103.082122574, - "below_10pct_s": 104.038050498, - "near_zero_s": 104.48275043400001, - "common_half_s": 103.13559656400001, - "fixed_clearance_s": 103.7941135 - }, - "candidate": { - "peak": 2.5300000000000002, - "peak_time_s": 100.93407044199999, - "below_90pct_s": 101.180834951, - "below_50pct_s": 103.4372139, - "below_10pct_s": 104.38265232600001, - "near_zero_s": 104.535920273, - "common_half_s": 103.4372139, - "fixed_clearance_s": 104.038050498 - }, - "common_half_threshold": 1.2650000000000001, - "fixed_clearance_threshold": 0.5, - "candidate_minus_recorded_seconds": { - "below_90pct_s": 0.0, - "below_50pct_s": 0.35509132600000726, - "below_10pct_s": 0.34460182800000894, - "near_zero_s": 0.053169838999991725, - "common_half_s": 0.3016173359999925, - "fixed_clearance_s": 0.24393699800000945 - } - }, - "c1": { - "recorded": { - "peak": 0.35199999809265137, - "peak_time_s": 100.79037499800002, - "below_90pct_s": 102.07847973800001, - "below_50pct_s": 103.6570674, - "below_10pct_s": 104.434869094, - "near_zero_s": 104.58326638999999, - "common_half_s": 103.6570674, - "fixed_clearance_s": 103.94018452000002 - }, - "candidate": { - "peak": 0.495, - "peak_time_s": 100.93407044199999, - "below_90pct_s": 101.63742160299999, - "below_50pct_s": 103.192644453, - "below_10pct_s": 104.23327821299999, - "near_zero_s": 104.535920273, - "common_half_s": 103.505384355, - "fixed_clearance_s": 103.83751027500001 - }, - "common_half_threshold": 0.17599999904632568, - "fixed_clearance_threshold": 0.1, - "candidate_minus_recorded_seconds": { - "below_90pct_s": -0.44105813500002, - "below_50pct_s": -0.46442294700000275, - "below_10pct_s": -0.20159088100001554, - "near_zero_s": -0.04734611699998936, - "common_half_s": -0.15168304499999863, - "fixed_clearance_s": -0.10267424500000288 - } - } - } - }, - { - "name": "tight_204s", - "window_s": [ - 201.0, - 210.0 - ], - "cohort": "tight", - "paired_continuously_active": true, - "overlaps_next_turn": false, - "turn_sign": 1.0, - "driver_input_present": true, - "fields": { - "c0": { - "recorded": { - "peak": 3.25, - "peak_time_s": 204.452507826, - "below_90pct_s": 204.711612392, - "below_50pct_s": 205.71895763700002, - "below_10pct_s": 206.84365971900002, - "near_zero_s": 208.16542689100004, - "common_half_s": 205.72692817900003, - "fixed_clearance_s": 206.58940793700003 - }, - "candidate": { - "peak": 3.12, - "peak_time_s": 204.42139036100002, - "below_90pct_s": 204.754282272, - "below_50pct_s": 205.80377895499998, - "below_10pct_s": 206.893554451, - "near_zero_s": 207.64300374200002, - "common_half_s": 205.80377895499998, - "fixed_clearance_s": 206.60243090400002 - }, - "common_half_threshold": 1.56, - "fixed_clearance_threshold": 0.5, - "candidate_minus_recorded_seconds": { - "below_90pct_s": 0.04266988000000538, - "below_50pct_s": 0.0848213179999675, - "below_10pct_s": 0.04989473199998429, - "near_zero_s": -0.5224231490000193, - "common_half_s": 0.07685077599995793, - "fixed_clearance_s": 0.01302296699998351 - } - }, - "c1": { - "recorded": { - "peak": 0.5, - "peak_time_s": 203.71940647500003, - "below_90pct_s": 205.601266495, - "below_50pct_s": 206.08317727000002, - "below_10pct_s": 207.096331734, - "near_zero_s": 209.14135593100002, - "common_half_s": 206.08317727000002, - "fixed_clearance_s": 206.642961266 - }, - "candidate": { - "peak": 0.5, - "peak_time_s": 203.41543419100003, - "below_90pct_s": 205.21563914900003, - "below_50pct_s": 205.871728089, - "below_10pct_s": 207.096331734, - "near_zero_s": 209.14135593100002, - "common_half_s": 205.871728089, - "fixed_clearance_s": 206.60243090400002 - }, - "common_half_threshold": 0.25, - "fixed_clearance_threshold": 0.1, - "candidate_minus_recorded_seconds": { - "below_90pct_s": -0.3856273459999784, - "below_50pct_s": -0.21144918100003451, - "below_10pct_s": 0.0, - "near_zero_s": 0.0, - "common_half_s": -0.21144918100003451, - "fixed_clearance_s": -0.040530361999969955 - } - } - } - }, - { - "name": "tight_290s", - "window_s": [ - 287.0, - 304.0 - ], - "cohort": "tight", - "paired_continuously_active": true, - "overlaps_next_turn": false, - "turn_sign": 1.0, - "driver_input_present": true, - "fields": { - "c0": { - "recorded": { - "peak": 4.28000020980835, - "peak_time_s": 291.38023259600004, - "below_90pct_s": 291.58007872200005, - "below_50pct_s": 294.652892026, - "below_10pct_s": 297.505731785, - "near_zero_s": null, - "common_half_s": 295.01561511500006, - "fixed_clearance_s": 297.31398149600005 - }, - "candidate": { - "peak": 3.95, - "peak_time_s": 291.368099656, - "below_90pct_s": 291.58007872200005, - "below_50pct_s": 294.756497704, - "below_10pct_s": 297.505731785, - "near_zero_s": null, - "common_half_s": 294.756497704, - "fixed_clearance_s": 297.31398149600005 - }, - "common_half_threshold": 1.975, - "fixed_clearance_threshold": 0.5, - "candidate_minus_recorded_seconds": { - "below_90pct_s": 0.0, - "below_50pct_s": 0.10360567800000808, - "below_10pct_s": 0.0, - "near_zero_s": null, - "common_half_s": -0.2591174110000338, - "fixed_clearance_s": 0.0 - } - }, - "c1": { - "recorded": { - "peak": 0.5, - "peak_time_s": 291.208352487, - "below_90pct_s": 294.165487336, - "below_50pct_s": 296.11050717800003, - "below_10pct_s": null, - "near_zero_s": null, - "common_half_s": 296.11050717800003, - "fixed_clearance_s": 297.404249599 - }, - "candidate": { - "peak": 0.5, - "peak_time_s": 290.22304278, - "below_90pct_s": 293.80514934, - "below_50pct_s": 295.65548601800003, - "below_10pct_s": null, - "near_zero_s": null, - "common_half_s": 295.65548601800003, - "fixed_clearance_s": 297.35730984800006 - }, - "common_half_threshold": 0.25, - "fixed_clearance_threshold": 0.1, - "candidate_minus_recorded_seconds": { - "below_90pct_s": -0.3603379959999984, - "below_50pct_s": -0.45502116000000115, - "below_10pct_s": null, - "near_zero_s": null, - "common_half_s": -0.45502116000000115, - "fixed_clearance_s": -0.046939750999968055 - } - } - } - }, - { - "name": "tight_322s", - "window_s": [ - 319.0, - 328.0 - ], - "cohort": "tight", - "paired_continuously_active": true, - "overlaps_next_turn": false, - "turn_sign": -1.0, - "driver_input_present": true, - "fields": { - "c0": { - "recorded": { - "peak": 3.9700000286102295, - "peak_time_s": 322.63175601100005, - "below_90pct_s": 322.904643726, - "below_50pct_s": 324.25051608900003, - "below_10pct_s": 325.66393901500004, - "near_zero_s": null, - "common_half_s": 324.309763061, - "fixed_clearance_s": 325.50392031200005 - }, - "candidate": { - "peak": 3.71, - "peak_time_s": 322.63175601100005, - "below_90pct_s": 322.91897255900005, - "below_50pct_s": 324.34948010000005, - "below_10pct_s": 325.70485142900003, - "near_zero_s": null, - "common_half_s": 324.34948010000005, - "fixed_clearance_s": 325.50392031200005 - }, - "common_half_threshold": 1.855, - "fixed_clearance_threshold": 0.5, - "candidate_minus_recorded_seconds": { - "below_90pct_s": 0.014328833000035957, - "below_50pct_s": 0.09896401100002095, - "below_10pct_s": 0.04091241399999035, - "near_zero_s": null, - "common_half_s": 0.03971703900003831, - "fixed_clearance_s": 0.0 - } - }, - "c1": { - "recorded": { - "peak": 0.5, - "peak_time_s": 322.299259177, - "below_90pct_s": 324.33659532900003, - "below_50pct_s": 324.931697202, - "below_10pct_s": 326.251405185, - "near_zero_s": null, - "common_half_s": 324.931697202, - "fixed_clearance_s": 325.55373651300005 - }, - "candidate": { - "peak": 0.5, - "peak_time_s": 321.89953517600003, - "below_90pct_s": 323.894065735, - "below_50pct_s": 324.678861926, - "below_10pct_s": 326.19749975400003, - "near_zero_s": null, - "common_half_s": 324.678861926, - "fixed_clearance_s": 325.41309978600003 - }, - "common_half_threshold": 0.25, - "fixed_clearance_threshold": 0.1, - "candidate_minus_recorded_seconds": { - "below_90pct_s": -0.4425295940000069, - "below_50pct_s": -0.2528352759999848, - "below_10pct_s": -0.05390543099997558, - "near_zero_s": null, - "common_half_s": -0.2528352759999848, - "fixed_clearance_s": -0.14063672700001462 - } - } - } - }, - { - "name": "tight_340s", - "window_s": [ - 336.0, - 347.0 - ], - "cohort": "tight", - "paired_continuously_active": true, - "overlaps_next_turn": false, - "turn_sign": 1.0, - "driver_input_present": true, - "fields": { - "c0": { - "recorded": { - "peak": 4.519999980926514, - "peak_time_s": 340.598086634, - "below_90pct_s": 340.91626233600005, - "below_50pct_s": 342.40797476500006, - "below_10pct_s": 343.22053899800005, - "near_zero_s": 343.33921467400006, - "common_half_s": 342.44586709500004, - "fixed_clearance_s": 343.21147315800005 - }, - "candidate": { - "peak": 4.25, - "peak_time_s": 340.598086634, - "below_90pct_s": 340.91626233600005, - "below_50pct_s": 342.597023642, - "below_10pct_s": 343.362026375, - "near_zero_s": 343.479116528, - "common_half_s": 342.597023642, - "fixed_clearance_s": 343.329572508 - }, - "common_half_threshold": 2.125, - "fixed_clearance_threshold": 0.5, - "candidate_minus_recorded_seconds": { - "below_90pct_s": 0.0, - "below_50pct_s": 0.18904887699994788, - "below_10pct_s": 0.14148737699997582, - "near_zero_s": 0.1399018539999588, - "common_half_s": 0.15115654699997094, - "fixed_clearance_s": 0.11809934999996585 - } - }, - "c1": { - "recorded": { - "peak": 0.5, - "peak_time_s": 339.83505406300003, - "below_90pct_s": 342.75123392300003, - "below_50pct_s": 343.15934182, - "below_10pct_s": 343.56486573700005, - "near_zero_s": 343.652091847, - "common_half_s": 343.15934182, - "fixed_clearance_s": 343.45914287500005 - }, - "candidate": { - "peak": 0.5, - "peak_time_s": 338.10098615100003, - "below_90pct_s": 342.226633148, - "below_50pct_s": 342.89592961600005, - "below_10pct_s": 343.390365351, - "near_zero_s": 343.702920671, - "common_half_s": 342.89592961600005, - "fixed_clearance_s": 343.290280208 - }, - "common_half_threshold": 0.25, - "fixed_clearance_threshold": 0.1, - "candidate_minus_recorded_seconds": { - "below_90pct_s": -0.5246007750000103, - "below_50pct_s": -0.26341220399996246, - "below_10pct_s": -0.1745003860000338, - "near_zero_s": 0.05082882400000699, - "common_half_s": -0.26341220399996246, - "fixed_clearance_s": -0.1688626670000417 - } - } - } - }, - { - "name": "tight_374s", - "window_s": [ - 370.0, - 380.0 - ], - "cohort": "tight", - "paired_continuously_active": true, - "overlaps_next_turn": false, - "turn_sign": -1.0, - "driver_input_present": true, - "fields": { - "c0": { - "recorded": { - "peak": 4.440000057220459, - "peak_time_s": 374.03466016400006, - "below_90pct_s": 374.337971265, - "below_50pct_s": 376.446774042, - "below_10pct_s": 377.504629587, - "near_zero_s": 378.10658313600004, - "common_half_s": 376.54731860300006, - "fixed_clearance_s": 377.41278509800003 - }, - "candidate": { - "peak": 4.090000000000001, - "peak_time_s": 374.03466016400006, - "below_90pct_s": 374.27741194400005, - "below_50pct_s": 376.56807366500004, - "below_10pct_s": 377.64225635800005, - "near_zero_s": 378.33656807100004, - "common_half_s": 376.56807366500004, - "fixed_clearance_s": 377.513590877 - }, - "common_half_threshold": 2.0450000000000004, - "fixed_clearance_threshold": 0.5, - "candidate_minus_recorded_seconds": { - "below_90pct_s": -0.06055932099997108, - "below_50pct_s": 0.12129962300002717, - "below_10pct_s": 0.1376267710000434, - "near_zero_s": 0.2299849350000045, - "common_half_s": 0.020755061999977897, - "fixed_clearance_s": 0.10080577899998389 - } - }, - "c1": { - "recorded": { - "peak": 0.5, - "peak_time_s": 373.392786745, - "below_90pct_s": 376.58558943900005, - "below_50pct_s": 377.11858247900005, - "below_10pct_s": 377.856980516, - "near_zero_s": 378.38592790200005, - "common_half_s": 377.11858247900005, - "fixed_clearance_s": 377.550973906 - }, - "candidate": { - "peak": 0.5, - "peak_time_s": 372.73047255000006, - "below_90pct_s": 376.24770156600005, - "below_50pct_s": 376.86452667800006, - "below_10pct_s": 377.690772593, - "near_zero_s": 378.237216964, - "common_half_s": 376.86452667800006, - "fixed_clearance_s": 377.44232913900004 - }, - "common_half_threshold": 0.25, - "fixed_clearance_threshold": 0.1, - "candidate_minus_recorded_seconds": { - "below_90pct_s": -0.33788787299999967, - "below_50pct_s": -0.2540558009999927, - "below_10pct_s": -0.16620792300000176, - "near_zero_s": -0.148710938000022, - "common_half_s": -0.2540558009999927, - "fixed_clearance_s": -0.10864476699998704 - } - } - } - }, - { - "name": "bend_108s", - "window_s": [ - 105.0, - 119.0 - ], - "cohort": "ordinary", - "paired_continuously_active": true, - "overlaps_next_turn": false, - "turn_sign": 1.0, - "driver_input_present": false, - "fields": { - "c0": { - "recorded": { - "peak": 0.20000000298023224, - "peak_time_s": 106.548941925, - "below_90pct_s": 106.73395254500001, - "below_50pct_s": 112.338637133, - "below_10pct_s": 113.45055747600001, - "near_zero_s": 113.08906987499999, - "common_half_s": 112.338637133, - "fixed_clearance_s": null - }, - "candidate": { - "peak": 0.5999999999999996, - "peak_time_s": 109.08088833200001, - "below_90pct_s": 109.92945568200001, - "below_50pct_s": 112.53371493600001, - "below_10pct_s": 113.578768826, - "near_zero_s": 114.03448095400002, - "common_half_s": 113.39171969800002, - "fixed_clearance_s": 111.433984892 - }, - "common_half_threshold": 0.10000000149011612, - "fixed_clearance_threshold": 0.5, - "candidate_minus_recorded_seconds": { - "below_90pct_s": 3.195503137000003, - "below_50pct_s": 0.19507780300000377, - "below_10pct_s": 0.12821134999998662, - "near_zero_s": 0.9454110790000243, - "common_half_s": 1.053082565000011, - "fixed_clearance_s": null - } - }, - "c1": { - "recorded": { - "peak": 0.07150000333786011, - "peak_time_s": 108.389598889, - "below_90pct_s": 109.92945568200001, - "below_50pct_s": 112.58129804600001, - "below_10pct_s": 113.981820372, - "near_zero_s": 113.53178583500001, - "common_half_s": 112.58129804600001, - "fixed_clearance_s": null - }, - "candidate": { - "peak": 0.07150000000000001, - "peak_time_s": 108.389598889, - "below_90pct_s": 109.92945568200001, - "below_50pct_s": 112.338637133, - "below_10pct_s": 113.779217285, - "near_zero_s": 113.42852712100002, - "common_half_s": 112.338637133, - "fixed_clearance_s": null - }, - "common_half_threshold": 0.035750000000000004, - "fixed_clearance_threshold": 0.1, - "candidate_minus_recorded_seconds": { - "below_90pct_s": 0.0, - "below_50pct_s": -0.2426609130000088, - "below_10pct_s": -0.20260308699999996, - "near_zero_s": -0.10325871399999187, - "common_half_s": -0.2426609130000088, - "fixed_clearance_s": null - } - } - } - }, - { - "name": "bend_167s", - "window_s": [ - 161.0, - 177.0 - ], - "cohort": "ordinary", - "paired_continuously_active": true, - "overlaps_next_turn": false, - "turn_sign": -1.0, - "driver_input_present": true, - "fields": { - "c0": { - "recorded": { - "peak": 0.20999999344348907, - "peak_time_s": 167.902585901, - "below_90pct_s": 167.974404155, - "below_50pct_s": 168.98050512999998, - "below_10pct_s": 171.32639237400002, - "near_zero_s": 169.49194768, - "common_half_s": 168.98050512999998, - "fixed_clearance_s": null - }, - "candidate": { - "peak": 0.75, - "peak_time_s": 167.87432923699998, - "below_90pct_s": 168.2780269, - "below_50pct_s": 169.423584081, - "below_10pct_s": 172.480112491, - "near_zero_s": 172.68047986, - "common_half_s": 171.827307034, - "fixed_clearance_s": 168.98050512999998 - }, - "common_half_threshold": 0.10499999672174454, - "fixed_clearance_threshold": 0.5, - "candidate_minus_recorded_seconds": { - "below_90pct_s": 0.3036227449999842, - "below_50pct_s": 0.44307895100001815, - "below_10pct_s": 1.1537201169999776, - "near_zero_s": 3.1885321799999815, - "common_half_s": 2.846801904000017, - "fixed_clearance_s": null - } - }, - "c1": { - "recorded": { - "peak": 0.07100000232458115, - "peak_time_s": 167.723783749, - "below_90pct_s": 168.37932592200002, - "below_50pct_s": 169.52297432800003, - "below_10pct_s": 172.330038759, - "near_zero_s": 171.425926795, - "common_half_s": 169.52297432800003, - "fixed_clearance_s": null - }, - "candidate": { - "peak": 0.07200000000000006, - "peak_time_s": 167.272833955, - "below_90pct_s": 168.126774883, - "below_50pct_s": 169.423584081, - "below_10pct_s": 172.38125481100002, - "near_zero_s": 171.672723845, - "common_half_s": 169.423584081, - "fixed_clearance_s": null - }, - "common_half_threshold": 0.03550000116229057, - "fixed_clearance_threshold": 0.1, - "candidate_minus_recorded_seconds": { - "below_90pct_s": -0.25255103900002496, - "below_50pct_s": -0.09939024700003074, - "below_10pct_s": 0.05121605200002932, - "near_zero_s": 0.24679704999999785, - "common_half_s": -0.09939024700003074, - "fixed_clearance_s": null - } - } - } - }, - { - "name": "bend_213s", - "window_s": [ - 209.0, - 226.0 - ], - "cohort": "ordinary", - "paired_continuously_active": true, - "overlaps_next_turn": false, - "turn_sign": -1.0, - "driver_input_present": true, - "fields": { - "c0": { - "recorded": { - "peak": 0.6000000238418579, - "peak_time_s": 212.03470382600005, - "below_90pct_s": 212.97991787900003, - "below_50pct_s": 214.73159144800002, - "below_10pct_s": 217.028482467, - "near_zero_s": 217.12158735300005, - "common_half_s": 214.73159144800002, - "fixed_clearance_s": 213.083366259 - }, - "candidate": { - "peak": 0.8200000000000003, - "peak_time_s": 212.67528037000005, - "below_90pct_s": 213.232987729, - "below_50pct_s": 214.97192496500003, - "below_10pct_s": 217.12158735300005, - "near_zero_s": 217.52672367000002, - "common_half_s": 215.52565438300002, - "fixed_clearance_s": 214.63197451000002 - }, - "common_half_threshold": 0.30000001192092896, - "fixed_clearance_threshold": 0.5, - "candidate_minus_recorded_seconds": { - "below_90pct_s": 0.2530698499999744, - "below_50pct_s": 0.2403335170000105, - "below_10pct_s": 0.09310488600004874, - "near_zero_s": 0.40513631699997177, - "common_half_s": 0.7940629349999995, - "fixed_clearance_s": 1.5486082510000188 - } - }, - "c1": { - "recorded": { - "peak": 0.13600000739097595, - "peak_time_s": 212.745246436, - "below_90pct_s": 213.24039054600001, - "below_50pct_s": 215.018705532, - "below_10pct_s": 217.326128364, - "near_zero_s": 217.37190566400005, - "common_half_s": 215.174537719, - "fixed_clearance_s": 214.07720312900005 - }, - "candidate": { - "peak": 0.13250000000000006, - "peak_time_s": 212.435732395, - "below_90pct_s": 213.232987729, - "below_50pct_s": 214.97192496500003, - "below_10pct_s": 217.028482467, - "near_zero_s": 217.326128364, - "common_half_s": 214.97192496500003, - "fixed_clearance_s": 213.82721462100005 - }, - "common_half_threshold": 0.06625000000000003, - "fixed_clearance_threshold": 0.1, - "candidate_minus_recorded_seconds": { - "below_90pct_s": -0.007402817000013329, - "below_50pct_s": -0.046780566999984785, - "below_10pct_s": -0.29764589699999533, - "near_zero_s": -0.04577730000005431, - "common_half_s": -0.20261275399997203, - "fixed_clearance_s": -0.24998850800000127 - } - } - } - }, - { - "name": "bend_269s", - "window_s": [ - 265.0, - 280.0 - ], - "cohort": "ordinary", - "paired_continuously_active": true, - "overlaps_next_turn": false, - "turn_sign": -1.0, - "driver_input_present": true, - "fields": { - "c0": { - "recorded": { - "peak": 0.5899999737739563, - "peak_time_s": 269.0605254, - "below_90pct_s": 269.30781249, - "below_50pct_s": 272.29716238400005, - "below_10pct_s": 274.200510886, - "near_zero_s": 274.200510886, - "common_half_s": 272.29716238400005, - "fixed_clearance_s": 269.503329327 - }, - "candidate": { - "peak": 0.7000000000000002, - "peak_time_s": 268.90545461500005, - "below_90pct_s": 269.99961806700003, - "below_50pct_s": 272.756670094, - "below_10pct_s": 274.29934710000003, - "near_zero_s": 274.65283626300004, - "common_half_s": 273.296801311, - "fixed_clearance_s": 271.90608599600006 - }, - "common_half_threshold": 0.29499998688697815, - "fixed_clearance_threshold": 0.5, - "candidate_minus_recorded_seconds": { - "below_90pct_s": 0.6918055770000251, - "below_50pct_s": 0.4595077099999685, - "below_10pct_s": 0.0988362140000163, - "near_zero_s": 0.4523253770000224, - "common_half_s": 0.9996389269999781, - "fixed_clearance_s": 2.4027566690000413 - } - }, - "c1": { - "recorded": { - "peak": 0.12250000238418579, - "peak_time_s": 269.25779382, - "below_90pct_s": 269.503329327, - "below_50pct_s": 272.756670094, - "below_10pct_s": 274.200510886, - "near_zero_s": 274.29934710000003, - "common_half_s": 272.76514263800004, - "fixed_clearance_s": 270.09909269400003 - }, - "candidate": { - "peak": 0.118, - "peak_time_s": 269.100449764, - "below_90pct_s": 269.90328605800005, - "below_50pct_s": 272.5077431, - "below_10pct_s": 274.200510886, - "near_zero_s": 274.29934710000003, - "common_half_s": 272.5077431, - "fixed_clearance_s": 270.008888839 - }, - "common_half_threshold": 0.059, - "fixed_clearance_threshold": 0.1, - "candidate_minus_recorded_seconds": { - "below_90pct_s": 0.3999567310000316, - "below_50pct_s": -0.2489269939999872, - "below_10pct_s": 0.0, - "near_zero_s": 0.0, - "common_half_s": -0.2573995380000156, - "fixed_clearance_s": -0.09020385500002703 - } - } - } - }, - { - "name": "bend_365s", - "window_s": [ - 360.0, - 370.0 - ], - "cohort": "ordinary", - "paired_continuously_active": true, - "overlaps_next_turn": true, - "turn_sign": -1.0, - "driver_input_present": true, - "fields": { - "c0": { - "recorded": { - "peak": 0.5899999737739563, - "peak_time_s": 364.739029753, - "below_90pct_s": 365.945649611, - "below_50pct_s": 368.89615960500004, - "below_10pct_s": null, - "near_zero_s": null, - "common_half_s": 368.89615960500004, - "fixed_clearance_s": 366.246832137 - }, - "candidate": { - "peak": 0.8099999999999996, - "peak_time_s": 365.29007811900004, - "below_90pct_s": 365.993896069, - "below_50pct_s": 367.037059988, - "below_10pct_s": null, - "near_zero_s": null, - "common_half_s": 368.89615960500004, - "fixed_clearance_s": 366.59549220700006 - }, - "common_half_threshold": 0.29499998688697815, - "fixed_clearance_threshold": 0.5, - "candidate_minus_recorded_seconds": { - "below_90pct_s": 0.04824645799999416, - "below_50pct_s": -1.8590996170000267, - "below_10pct_s": null, - "near_zero_s": null, - "common_half_s": 0.0, - "fixed_clearance_s": 0.34866007000005084 - } - }, - "c1": { - "recorded": { - "peak": 0.13650000095367432, - "peak_time_s": 365.29007811900004, - "below_90pct_s": 366.234948614, - "below_50pct_s": 368.24164409, - "below_10pct_s": null, - "near_zero_s": null, - "common_half_s": 368.24164409, - "fixed_clearance_s": 366.507780704 - }, - "candidate": { - "peak": 0.13750000000000007, - "peak_time_s": 364.63958741600004, - "below_90pct_s": 366.043504152, - "below_50pct_s": 367.84505105000005, - "below_10pct_s": null, - "near_zero_s": null, - "common_half_s": 367.84505105000005, - "fixed_clearance_s": 366.49697023500005 - }, - "common_half_threshold": 0.06825000047683716, - "fixed_clearance_threshold": 0.1, - "candidate_minus_recorded_seconds": { - "below_90pct_s": -0.19144446199999265, - "below_50pct_s": -0.39659303999997064, - "below_10pct_s": null, - "near_zero_s": null, - "common_half_s": -0.39659303999997064, - "fixed_clearance_s": -0.01081046899997773 - } - } - } - } - ], - "source_sha256": { - "/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_model_points/unwind.py": "b1b9c02656353fac63eee92bb45434be2b123241347472e5dd960e61e00e7e1c", - "/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/openpilot/selfdrive/controls/lib/ford_model_action.py": "e7502ab68d04aef52edcffcef4b1d42c04e9008ab6892789aac3a865c100cb6d", - "/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_model_points/a5_commands.npz": "829a6fcb82a8df5e17310501cb17b090c58f604503731b739bbd7f12b783f71a", - "/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_model_points/a5_models.npz": "84cf4b6e0102d8d5a3f6c3e29bf332e929d7f28d0262eb46220fccc631b902aa", - ".cache/ford_routea5/route.npz": "815d1e248ff5c3e5e5cfc11dfbd0690d0d13ddafe1436890e239975a72dae9b8" - } -} diff --git a/docs/ford_model_points_validation.json b/docs/ford_model_points_validation.json deleted file mode 100644 index 29c8751eb9..0000000000 --- a/docs/ford_model_points_validation.json +++ /dev/null @@ -1,632 +0,0 @@ -{ - "baseline_root": "e1cd61166c529f39cb47c815db47e80de720a778", - "opendbc": "87ca78e6e641eefb2d654f260a6ab08df3058bd5", - "scope": "Model-point construction only; physical tracking not validated.", - "calibration_approved": false, - "tests": { - "passed": 585, - "subtests_passed": 9146, - "inapplicable_skips": 178 - }, - "source_sha256": { - "openpilot/selfdrive/controls/lib/ford_model_action.py": "e7502ab68d04aef52edcffcef4b1d42c04e9008ab6892789aac3a865c100cb6d", - "openpilot/selfdrive/controls/controlsd.py": "bea720b8ec68d6b8a6ad4376ae37324e2404833fb13097a8ab7710a1cedf37a5", - "tools/ford_pscm_lab/stress_model_action.py": "0221f85ea2f5757d3b54d077ed8be1faaa6a5d2ed8371de1137f3b6f3d70bf47", - "openpilot/sunnypilot/sunnylink/settings_ui_src/pages/vehicle.yaml": "ec5e0d9023f7260ae619a4cc6ec538d39053ac13e4515bd0de7d854b79d8abf6", - "openpilot/sunnypilot/sunnylink/settings_ui.json": "62e5178a8deee32ef5a88026094cedde359130741688e8a4bcfa82155ef4cca3", - "tools/ford_pscm_lab/model_action_replay.py": "95d546cc87c065fa7d581b41382e1ab78bacc4030931b81388cb69c61adf32f7", - "tools/ford_pscm_lab/damping_replay.py": "1c253b99342d8113739b0a0551ca66ec6ced9e0aa96338ce6cb42e09c04178a4", - "tools/ford_pscm_lab/test_model_action_replay.py": "7aa01cbf197155175215aa4494cfc909029526fbacad0a1e1a2a214583064c4b", - "openpilot/selfdrive/controls/tests/test_ford_model_action_cadence.py": "d647e33c83581257f2662c3fec725bb9cd245fb48d579c2e38388318592a2690" - }, - "routes": { - "a5": { - "route": "a5", - "scope": "Frozen inputs; command construction and Panda TX acceptance only. No counterfactual vehicle response.", - "cycles": 38961, - "models": 7817, - "round_trips": 38961, - "panda_accepted_sends": 7793, - "source_model_clocks_verified": true, - "independent_point_and_slew_oracle_passed": true, - "cohorts": { - "eligible": { - "seconds": 359.186785903, - "old_mean_abs_c0": 0.34434291232781916, - "new_mean_abs_c0": 0.3706073530276498, - "old_mean_abs_c1": 0.05784747586810226, - "new_mean_abs_c1": 0.06183462577629167, - "c0_target_capped_s": 0.0, - "c1_target_capped_s": 14.919213676999902 - }, - "road_speed": { - "seconds": 261.730126543, - "old_mean_abs_c0": 0.10481662146430282, - "new_mean_abs_c0": 0.16319455033344274, - "old_mean_abs_c1": 0.025151988851597765, - "new_mean_abs_c1": 0.02471229299756163, - "c0_target_capped_s": 0.0, - "c1_target_capped_s": 0.0 - }, - "low_speed": { - "seconds": 97.45665935999997, - "old_mean_abs_c0": 0.9876160024355487, - "new_mean_abs_c0": 0.9276362872461182, - "old_mean_abs_c1": 0.14565465097969882, - "new_mean_abs_c1": 0.16153056158564297, - "c0_target_capped_s": 0.0, - "c1_target_capped_s": 14.919213676999902 - }, - "clean": { - "seconds": 249.91415584299997, - "old_mean_abs_c0": 0.08470877473478762, - "new_mean_abs_c0": 0.1191542994782463, - "old_mean_abs_c1": 0.017726187650852487, - "new_mean_abs_c1": 0.018543416153046232, - "c0_target_capped_s": 0.0, - "c1_target_capped_s": 1.056498005999913 - }, - "clean_ordinary_bends": { - "seconds": 61.23773978999962, - "old_mean_abs_c0": 0.07537829679720884, - "new_mean_abs_c0": 0.16659385323910988, - "old_mean_abs_c1": 0.022868833214909796, - "new_mean_abs_c1": 0.022235380107927534, - "c0_target_capped_s": 0.0, - "c1_target_capped_s": 0.0 - } - }, - "points": [ - { - "t": 17.606574576, - "old_c0_c1": [ - -0.009999999776482582, - -0.014999999664723873 - ], - "new_c0_c1": [ - -0.04999999999999982, - -0.01200000000000001 - ], - "raw_target": [ - -0.04535145975205448, - -0.012038111718131041 - ], - "sample_station_m": 7.0, - "sample_time_s": 1.8525775632377612 - }, - { - "t": 107.99483939600002, - "old_c0_c1": [ - 0.20000000298023224, - 0.06800000369548798 - ], - "new_c0_c1": [ - 0.5499999999999998, - 0.0685 - ], - "raw_target": [ - 0.548033630847931, - 0.06853501158101219 - ], - "sample_station_m": 13.711669224561392, - "sample_time_s": 1.0 - }, - { - "t": 374.888921539, - "old_c0_c1": [ - -3.759999990463257, - -0.5 - ], - "new_c0_c1": [ - -3.46, - -0.5 - ], - "raw_target": [ - -3.497294441592979, - -0.6981158781127829 - ], - "sample_station_m": 7.0, - "sample_time_s": 1.490557194102553 - }, - { - "t": 390.68080671100006, - "old_c0_c1": [ - -0.5199999809265137, - -0.12349999696016312 - ], - "new_c0_c1": [ - -0.75, - -0.12150000000000005 - ], - "raw_target": [ - -0.7522029416901727, - -0.1214686983398029 - ], - "sample_station_m": 9.370794504969545, - "sample_time_s": 1.0 - } - ], - "hashes": { - "/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/openpilot/selfdrive/controls/lib/ford_model_action.py": "e7502ab68d04aef52edcffcef4b1d42c04e9008ab6892789aac3a865c100cb6d", - "/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_model_points/replay.py": "762ebaac3b31737ad609d5b9788514147c696e548fd1ee09458314a0eea30434", - ".cache/ford_routea5/route.npz": "815d1e248ff5c3e5e5cfc11dfbd0690d0d13ddafe1436890e239975a72dae9b8", - "/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_model_points/a5_models.npz": "84cf4b6e0102d8d5a3f6c3e29bf332e929d7f28d0262eb46220fccc631b902aa" - } - }, - "a2": { - "route": "a2", - "scope": "Frozen inputs; command construction and Panda TX acceptance only. No counterfactual vehicle response.", - "cycles": 71111, - "models": 14287, - "round_trips": 71111, - "panda_accepted_sends": 14223, - "source_model_clocks_verified": true, - "independent_point_and_slew_oracle_passed": true, - "cohorts": { - "eligible": { - "seconds": 437.0644468180001, - "old_mean_abs_c0": 0.2155311761983153, - "new_mean_abs_c0": 0.31863289785799315, - "old_mean_abs_c1": 0.04341179516514646, - "new_mean_abs_c1": 0.045887913019549746, - "c0_target_capped_s": 0.0, - "c1_target_capped_s": 11.38161301699995 - }, - "road_speed": { - "seconds": 328.02165902499996, - "old_mean_abs_c0": 0.06680861343584246, - "new_mean_abs_c0": 0.21392942512598495, - "old_mean_abs_c1": 0.025146318618058133, - "new_mean_abs_c1": 0.025146126320953914, - "c0_target_capped_s": 0.0, - "c1_target_capped_s": 0.0 - }, - "low_speed": { - "seconds": 109.04278779300012, - "old_mean_abs_c0": 0.6629172230811673, - "new_mean_abs_c0": 0.6336010633494195, - "old_mean_abs_c1": 0.09835785845941926, - "new_mean_abs_c1": 0.10828319309008405, - "c0_target_capped_s": 0.0, - "c1_target_capped_s": 11.38161301699995 - }, - "clean": { - "seconds": 332.6767952400001, - "old_mean_abs_c0": 0.07338523837198829, - "new_mean_abs_c0": 0.1946808147540538, - "old_mean_abs_c1": 0.023104103590461375, - "new_mean_abs_c1": 0.022903078905402382, - "c0_target_capped_s": 0.0, - "c1_target_capped_s": 2.902278352000053 - }, - "clean_ordinary_bends": { - "seconds": 111.90846122999926, - "old_mean_abs_c0": 0.07917757541079931, - "new_mean_abs_c0": 0.3589047266588909, - "old_mean_abs_c1": 0.03830142702025842, - "new_mean_abs_c1": 0.03807353528971872, - "c0_target_capped_s": 0.0, - "c1_target_capped_s": 0.0 - } - }, - "points": [], - "hashes": { - "/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/openpilot/selfdrive/controls/lib/ford_model_action.py": "e7502ab68d04aef52edcffcef4b1d42c04e9008ab6892789aac3a865c100cb6d", - "/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_model_points/replay.py": "762ebaac3b31737ad609d5b9788514147c696e548fd1ee09458314a0eea30434", - ".cache/ford_routea2/route.npz": "af4128d278be6be0042f72a02255ddbec96a8a5aa435b9852c8202e4a5cb39e5", - "/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_model_points/a2_models.npz": "c3a366796ca5fb27f8c0dc85732ccd6f0ec61b4c8646af3dfe55600e026c0810" - } - }, - "a0": { - "route": "a0", - "scope": "Frozen inputs; command construction and Panda TX acceptance only. No counterfactual vehicle response.", - "cycles": 114582, - "models": 23038, - "round_trips": 114582, - "panda_accepted_sends": 22917, - "source_model_clocks_verified": true, - "independent_point_and_slew_oracle_passed": true, - "cohorts": { - "eligible": { - "seconds": 1061.1945591, - "old_mean_abs_c0": 0.16681197800600925, - "new_mean_abs_c0": 0.25477442195222594, - "old_mean_abs_c1": 0.036905620742387873, - "new_mean_abs_c1": 0.0381744756199198, - "c0_target_capped_s": 0.0, - "c1_target_capped_s": 14.894412850999998 - }, - "road_speed": { - "seconds": 901.0129238640001, - "old_mean_abs_c0": 0.038282177648169546, - "new_mean_abs_c0": 0.13938686570168032, - "old_mean_abs_c1": 0.016746723820478274, - "new_mean_abs_c1": 0.01614716305741802, - "c0_target_capped_s": 0.0, - "c1_target_capped_s": 0.0 - }, - "low_speed": { - "seconds": 160.18163523600006, - "old_mean_abs_c0": 0.889785064486564, - "new_mean_abs_c0": 0.9038231052276234, - "old_mean_abs_c1": 0.15029831167554059, - "new_mean_abs_c1": 0.1620769021843944, - "c0_target_capped_s": 0.0, - "c1_target_capped_s": 14.894412850999998 - }, - "clean": { - "seconds": 884.2159145080002, - "old_mean_abs_c0": 0.05133685312289168, - "new_mean_abs_c0": 0.13569127462997554, - "old_mean_abs_c1": 0.017060990972111164, - "new_mean_abs_c1": 0.01680535356441334, - "c0_target_capped_s": 0.0, - "c1_target_capped_s": 4.094436286000047 - }, - "clean_ordinary_bends": { - "seconds": 229.50590253700045, - "old_mean_abs_c0": 0.06277285849992259, - "new_mean_abs_c0": 0.29272144832989233, - "old_mean_abs_c1": 0.032915757288861924, - "new_mean_abs_c1": 0.03226126900978203, - "c0_target_capped_s": 0.0, - "c1_target_capped_s": 0.0 - } - }, - "points": [], - "hashes": { - "/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/openpilot/selfdrive/controls/lib/ford_model_action.py": "e7502ab68d04aef52edcffcef4b1d42c04e9008ab6892789aac3a865c100cb6d", - "/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_model_points/replay.py": "762ebaac3b31737ad609d5b9788514147c696e548fd1ee09458314a0eea30434", - ".cache/ford_routea0/route.npz": "01ce8b0361c3154d07d5449525a650baa2cc261675095cb1f04ec9e37a9174d3", - "/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_model_points/a0_models.npz": "72a72c950d244f5afcffe8cb1cd09b9731297f7565c93814197c728d4b25e8cb" - } - }, - "9b": { - "route": "9b", - "scope": "Frozen inputs; command construction and Panda TX acceptance only. No counterfactual vehicle response.", - "cycles": 71396, - "models": 14346, - "round_trips": 71396, - "panda_accepted_sends": 14280, - "source_model_clocks_verified": true, - "independent_point_and_slew_oracle_passed": true, - "cohorts": { - "eligible": { - "seconds": 651.159063169, - "old_mean_abs_c0": 0.09686840630728118, - "new_mean_abs_c0": 0.16616909035237892, - "old_mean_abs_c1": 0.020769878239136775, - "new_mean_abs_c1": 0.02325818302804746, - "c0_target_capped_s": 0.0, - "c1_target_capped_s": 4.1034965929999885 - }, - "road_speed": { - "seconds": 549.860784016, - "old_mean_abs_c0": 0.025487998809066218, - "new_mean_abs_c0": 0.10707029577162376, - "old_mean_abs_c1": 0.011795500864550536, - "new_mean_abs_c1": 0.01141539602134342, - "c0_target_capped_s": 0.0, - "c1_target_capped_s": 0.0 - }, - "low_speed": { - "seconds": 101.29827915299992, - "old_mean_abs_c0": 0.484330929447169, - "new_mean_abs_c0": 0.48696535455636186, - "old_mean_abs_c1": 0.06948401455523531, - "new_mean_abs_c1": 0.08754243546441211, - "c0_target_capped_s": 0.0, - "c1_target_capped_s": 4.1034965929999885 - } - }, - "points": [], - "hashes": { - "/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/openpilot/selfdrive/controls/lib/ford_model_action.py": "e7502ab68d04aef52edcffcef4b1d42c04e9008ab6892789aac3a865c100cb6d", - "/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_model_points/replay.py": "762ebaac3b31737ad609d5b9788514147c696e548fd1ee09458314a0eea30434", - ".cache/ford_route9b/route.npz": "8942b281b76eac012ee660f8ae21bbcad0df95d515291d2497ce5897d5738647", - "/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_model_points/9b_models.npz": "fa444360a2775b0df38f274ab4c99568bbe47a3d2777c31ab0193705fce2b1fb" - } - }, - "9e": { - "route": "9e", - "scope": "Frozen inputs; command construction and Panda TX acceptance only. No counterfactual vehicle response.", - "cycles": 83668, - "models": 16806, - "round_trips": 83668, - "panda_accepted_sends": 16734, - "source_model_clocks_verified": true, - "independent_point_and_slew_oracle_passed": true, - "cohorts": { - "eligible": { - "seconds": 751.5479497329998, - "old_mean_abs_c0": 0.09489001052394747, - "new_mean_abs_c0": 0.16056913479509022, - "old_mean_abs_c1": 0.02256825583285058, - "new_mean_abs_c1": 0.022300154628921095, - "c0_target_capped_s": 0.0, - "c1_target_capped_s": 7.398116462998587 - }, - "road_speed": { - "seconds": 624.6602415809975, - "old_mean_abs_c0": 0.023757802428838078, - "new_mean_abs_c0": 0.10203005034223132, - "old_mean_abs_c1": 0.01124063950870769, - "new_mean_abs_c1": 0.010770200237869725, - "c0_target_capped_s": 0.0, - "c1_target_capped_s": 0.0 - }, - "low_speed": { - "seconds": 126.88770815200223, - "old_mean_abs_c0": 0.4450694167092398, - "new_mean_abs_c0": 0.4487533818653312, - "old_mean_abs_c1": 0.07833340166635812, - "new_mean_abs_c1": 0.07906139809550021, - "c0_target_capped_s": 0.0, - "c1_target_capped_s": 7.398116462998587 - }, - "clean": { - "seconds": 639.296275887018, - "old_mean_abs_c0": 0.02396620128468532, - "new_mean_abs_c0": 0.09600545527466989, - "old_mean_abs_c1": 0.01021397220255641, - "new_mean_abs_c1": 0.010198642694698857, - "c0_target_capped_s": 0.0, - "c1_target_capped_s": 0.0 - }, - "clean_ordinary_bends": { - "seconds": 168.17771330400137, - "old_mean_abs_c0": 0.05087679617819314, - "new_mean_abs_c0": 0.2527786285635614, - "old_mean_abs_c1": 0.02663914589176354, - "new_mean_abs_c1": 0.026386933889696897, - "c0_target_capped_s": 0.0, - "c1_target_capped_s": 0.0 - } - }, - "points": [], - "hashes": { - "/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/openpilot/selfdrive/controls/lib/ford_model_action.py": "e7502ab68d04aef52edcffcef4b1d42c04e9008ab6892789aac3a865c100cb6d", - "/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_model_points/replay.py": "762ebaac3b31737ad609d5b9788514147c696e548fd1ee09458314a0eea30434", - ".cache/ford_route9e/route.npz": "9f169338caf2b5a5377332d15914d1dde57be8b922e25c1bbe16264e0f1be6fb", - "/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_model_points/9e_models.npz": "acacf8f0d16fd986b6d74e4a02d704fa5b5b3327fe66dcbe149806f2a5bc3d65" - } - } - }, - "stress": { - "seed": 20260908, - "random_cycles": 200000, - "mirrored_core_updates": 200000, - "invalid_or_inactive_resets": 3537, - "field_boundary_cases": 18138, - "float32_can_round_trips": 218138, - "analytic_targets_scalar_slew_and_mirror_checks_pass": true, - "valid_yaw_does_not_affect_targets_checked": true, - "shared_model_point_checked": true, - "direct_raw_float32_packing_matches_host_output": true, - "max_continuous_step_c0_c1": [ - 0.40000000000000147, - 0.05000000000000002 - ], - "calibration_approved": false, - "scope": "Numerical construction only; no PSCM response or closed-loop performance claims.", - "opendbc_import_head": "87ca78e6e641eefb2d654f260a6ab08df3058bd5", - "source_sha256": { - "/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/stress_model_action.py": "0221f85ea2f5757d3b54d077ed8be1faaa6a5d2ed8371de1137f3b6f3d70bf47", - "/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/openpilot/selfdrive/controls/lib/ford_model_action.py": "e7502ab68d04aef52edcffcef4b1d42c04e9008ab6892789aac3a865c100cb6d", - "/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/model_action_replay.py": "95d546cc87c065fa7d581b41382e1ab78bacc4030931b81388cb69c61adf32f7" - } - }, - "unwind": { - "report": "docs/ford_model_points_unwind.json", - "sha256": "7395a4e66a275fd72d744f5a8244f1b9c8840bb7efff2eec225f15b9c033cd6e", - "full_cap_release_send_phase_cases": 10, - "summary": { - "tight": { - "episodes": 7, - "c0": { - "below_90pct_s": { - "n": 7, - "median_s": 0.0, - "min_s": -0.12269046900000546, - "max_s": 0.04266988000000538, - "earlier": 2, - "later": 2 - }, - "below_50pct_s": { - "n": 7, - "median_s": 0.12129962300002717, - "min_s": 0.0848213179999675, - "max_s": 0.8246327600000001, - "earlier": 0, - "later": 7 - }, - "below_10pct_s": { - "n": 7, - "median_s": 0.1376267710000434, - "min_s": 0.0, - "max_s": 0.45046578099999124, - "earlier": 0, - "later": 6 - }, - "near_zero_s": { - "n": 5, - "median_s": 0.1399018539999588, - "min_s": -0.5224231490000193, - "max_s": 1.9510819790000085, - "earlier": 1, - "later": 4 - }, - "common_half_s": { - "n": 7, - "median_s": 0.07685077599995793, - "min_s": -0.2591174110000338, - "max_s": 0.5661551039999893, - "earlier": 1, - "later": 6 - }, - "fixed_clearance_s": { - "n": 7, - "median_s": 0.10080577899998389, - "min_s": 0.0, - "max_s": 0.4609723959999883, - "earlier": 0, - "later": 5 - } - }, - "c1": { - "below_90pct_s": { - "n": 7, - "median_s": -0.3856273459999784, - "min_s": -0.5246007750000103, - "max_s": 0.5545458859999997, - "earlier": 6, - "later": 1 - }, - "below_50pct_s": { - "n": 7, - "median_s": -0.2540558009999927, - "min_s": -0.46442294700000275, - "max_s": -0.21144918100003451, - "earlier": 7, - "later": 0 - }, - "below_10pct_s": { - "n": 6, - "median_s": -0.15774414349999688, - "min_s": -0.20159088100001554, - "max_s": 0.0, - "earlier": 5, - "later": 0 - }, - "near_zero_s": { - "n": 5, - "median_s": 0.0, - "min_s": -0.148710938000022, - "max_s": 1.5534367700000047, - "earlier": 2, - "later": 2 - }, - "common_half_s": { - "n": 7, - "median_s": -0.2528352759999848, - "min_s": -0.45502116000000115, - "max_s": -0.15168304499999863, - "earlier": 7, - "later": 0 - }, - "fixed_clearance_s": { - "n": 7, - "median_s": -0.10267424500000288, - "min_s": -0.1688626670000417, - "max_s": -0.040530361999969955, - "earlier": 7, - "later": 0 - } - } - }, - "ordinary": { - "episodes": 4, - "c0": { - "below_90pct_s": { - "n": 4, - "median_s": 0.49771416100000465, - "min_s": 0.2530698499999744, - "max_s": 3.195503137000003, - "earlier": 0, - "later": 4 - }, - "below_50pct_s": { - "n": 4, - "median_s": 0.3417062340000143, - "min_s": 0.19507780300000377, - "max_s": 0.4595077099999685, - "earlier": 0, - "later": 4 - }, - "below_10pct_s": { - "n": 4, - "median_s": 0.11352378200000146, - "min_s": 0.09310488600004874, - "max_s": 1.1537201169999776, - "earlier": 0, - "later": 4 - }, - "near_zero_s": { - "n": 4, - "median_s": 0.6988682280000234, - "min_s": 0.40513631699997177, - "max_s": 3.1885321799999815, - "earlier": 0, - "later": 4 - }, - "common_half_s": { - "n": 4, - "median_s": 1.0263607459999946, - "min_s": 0.7940629349999995, - "max_s": 2.846801904000017, - "earlier": 0, - "later": 4 - }, - "fixed_clearance_s": { - "n": 2, - "median_s": 1.97568246000003, - "min_s": 1.5486082510000188, - "max_s": 2.4027566690000413, - "earlier": 0, - "later": 2 - } - }, - "c1": { - "below_90pct_s": { - "n": 4, - "median_s": -0.0037014085000066643, - "min_s": -0.25255103900002496, - "max_s": 0.3999567310000316, - "earlier": 2, - "later": 1 - }, - "below_50pct_s": { - "n": 4, - "median_s": -0.17102558000001977, - "min_s": -0.2489269939999872, - "max_s": -0.046780566999984785, - "earlier": 4, - "later": 0 - }, - "below_10pct_s": { - "n": 4, - "median_s": -0.10130154349999998, - "min_s": -0.29764589699999533, - "max_s": 0.05121605200002932, - "earlier": 2, - "later": 1 - }, - "near_zero_s": { - "n": 4, - "median_s": -0.022888650000027155, - "min_s": -0.10325871399999187, - "max_s": 0.24679704999999785, - "earlier": 2, - "later": 1 - }, - "common_half_s": { - "n": 4, - "median_s": -0.22263683349999042, - "min_s": -0.2573995380000156, - "max_s": -0.09939024700003074, - "earlier": 4, - "later": 0 - }, - "fixed_clearance_s": { - "n": 2, - "median_s": -0.17009618150001415, - "min_s": -0.24998850800000127, - "max_s": -0.09020385500002703, - "earlier": 2, - "later": 0 - } - } - } - } - } -} diff --git a/opendbc_repo b/opendbc_repo index 87ca78e6e6..c21a901370 160000 --- a/opendbc_repo +++ b/opendbc_repo @@ -1 +1 @@ -Subproject commit 87ca78e6e641eefb2d654f260a6ab08df3058bd5 +Subproject commit c21a9013700734dd20b09e05aa68329ad8cc20f9 diff --git a/openpilot/selfdrive/controls/controlsd.py b/openpilot/selfdrive/controls/controlsd.py index f1b91e03bd..5ab125d59a 100755 --- a/openpilot/selfdrive/controls/controlsd.py +++ b/openpilot/selfdrive/controls/controlsd.py @@ -12,7 +12,7 @@ from openpilot.common.realtime import config_realtime_process, DT_CTRL, Priority from openpilot.common.swaglog import cloudlog from opendbc.car.car_helpers import interfaces -from opendbc.car.ford.values import FordFlags, FordFlagsSP +from opendbc.car.ford.values import FordFlags from opendbc.car.vehicle_model import VehicleModel from openpilot.selfdrive.controls.lib.drive_helpers import clip_curvature from openpilot.selfdrive.controls.lib.ford_model_action import FordModelActionController, select_model_action_controller @@ -59,7 +59,7 @@ class Controls(ControlsExt): self.ford_pscm_observer = (self.CP.brand == "ford" and self.CP.flags & FordFlags.CANFD and self.params.get_bool("FordPscmObserver")) self.ford_path_controller = FordPscmObserverPathController() if self.ford_pscm_observer else FordPathController() - self.ford_path_controller = select_model_action_controller(self.CP, bool(self.CP_SP.flags & FordFlagsSP.MODEL_ACTION), + self.ford_path_controller = select_model_action_controller(self.CP, self.params.get_bool("FordModelActionController"), self.ford_path_controller) self.ford_model_action = isinstance(self.ford_path_controller, FordModelActionController) if self.CP.brand == "ford": @@ -171,15 +171,12 @@ class Controls(ControlsExt): if self.CP.brand == "ford": ford_model = model_v2 if self.sm.valid['modelV2'] else None if self.ford_model_action: - assert isinstance(self.ford_path_controller, FordModelActionController) reference_service = 'lateralManeuverPlan' if self.sm.valid['lateralManeuverPlan'] else 'modelV2' - now = time.monotonic() self.ford_path = self.ford_path_controller.update( - ford_model, self.desired_curvature, yaw_rate=-CS.yawRate, speed=CS.vEgo, now=now, + ford_model, self.desired_curvature, yaw_rate=-CS.yawRate, speed=CS.vEgo, now=time.monotonic(), measurement_time=self.sm.logMonoTime['carState'] * 1e-9, model_time=self.sm.logMonoTime['modelV2'] * 1e-9, reference_time=self.sm.logMonoTime[reference_service] * 1e-9, - reference_source=reference_service, active=CC.latActive, valid=CS.canValid and self.sm.all_checks(['carState', 'vehicleParameters', 'modelV2', reference_service]), ) if not self.ford_path.valid: @@ -191,7 +188,6 @@ class Controls(ControlsExt): measured_curvature=self.curvature, **self.ford_path_controller.diagnostics) elif self.ford_pscm_observer: - assert isinstance(self.ford_path_controller, FordPscmObserverPathController) self.ford_path = self.ford_path_controller.update(ford_model, self.desired_curvature, current_curvature=self.curvature, v_ego=CS.vEgo, v_ego_raw=CS.vEgoRaw, active=CC.latActive) diff --git a/openpilot/selfdrive/controls/lib/ford_model_action.py b/openpilot/selfdrive/controls/lib/ford_model_action.py index 56ca418620..9f01b43b3f 100644 --- a/openpilot/selfdrive/controls/lib/ford_model_action.py +++ b/openpilot/selfdrive/controls/lib/ford_model_action.py @@ -1,7 +1,7 @@ -"""Experimental Ford C2-free controller using one shared model-path point. +"""Experimental Ford C2-free controller: nearby offset and selected-action heading. -The one-second preview and seven-metre minimum are engineering choices, -not identified Ford reference points or PSCM calibration. +Selected only by its explicit toggle. The 7 m station and one-second scale are +engineering choices, not identified PSCM gains or physical calibration. """ import math import struct @@ -12,8 +12,8 @@ from opendbc.car.ford.values import FordFlags from openpilot.selfdrive.controls.lib.ford_path import FordPath, _model_path -MIN_STATION_M = 7.0 -PREVIEW_TIME_S = 1.0 +OFFSET_STATION_M = 7.0 +HEADING_TIME_S = 1.0 CALIBRATION_APPROVED = False @@ -31,34 +31,31 @@ def _finite(*values): def encode_model_action(model, desired_curvature, speed): - """Sample model lateral position and heading at the same arc station. + """Encode y(7) and max(7, v*1s)*selected limited curvature. - Use the model's distance at one second, with a seven-metre minimum and an - endpoint hold. Selected curvature remains a health/diagnostic input; it does - not reconstruct heading or rotate the model geometry in this experiment. + 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. """ if not _finite(desired_curvature, speed) or not .3 <= speed <= 55 or abs(desired_curvature) > 1: return FordPath() try: path = _model_path(model) - times = [float(t) for t in model.position.t] - heading_times = [float(t) for t in model.orientation.t] - except (AttributeError, TypeError, ValueError, OverflowError): + except OverflowError: return FordPath() if path is None or not all(_finite(*values) for values in path): return FordPath() - station, _, lateral, heading = path - if (len(times) != len(station) or not times or times[0] != 0. or times != heading_times or - not _finite(*times) or any(b <= a for a, b in zip(times, times[1:], strict=False))): - return FordPath() - sample_station = min(station[-1], max(MIN_STATION_M, float(np.interp(PREVIEW_TIME_S, times, station)))) - c0 = float(np.interp(sample_station, station, lateral)) - c1 = float(np.interp(sample_station, station, heading)) + station, _, lateral, _ = path + c0 = float(np.interp(min(OFFSET_STATION_M, station[-1]), station, lateral)) + c1 = max(OFFSET_STATION_M, speed*HEADING_TIME_S)*desired_curvature return FordPath(True, c0, c1, 0., 0.) if _finite(c0, c1) else FordPath() class ModelActionController: - """Only two states: independently slewed C0/C1 model-point requests.""" + """Only two control states: unquantized, independently slewed C0 and C1. + + Freshness and engagement belong to the caller. No measured yaw, model + history, heading integral, blending or release modes enter the law. + """ __slots__ = ('c0', 'c1') def __init__(self): @@ -67,9 +64,8 @@ class ModelActionController: def reset(self): self.c0 = self.c1 = 0. - def update(self, model, desired_curvature, *, speed, dt, yaw_rate=0., active=True, valid=True): - # Raw Ford yaw remains an input-health check, not a pose measurement. - if not active or not valid or not _finite(dt, yaw_rate) or not .002 <= dt <= .1 or abs(yaw_rate) > 3: + def update(self, model, desired_curvature, *, speed, dt, active=True, valid=True): + if not active or not valid or not _finite(dt) or not .002 <= dt <= .1: self.reset() return FordPath() target = encode_model_action(model, desired_curvature, speed) @@ -84,11 +80,16 @@ class ModelActionController: class FordModelActionController: - """Freshness, engagement and reference checks for model-point tracking. + """Input adapter for the opt-in selected-action controller. - Scalar-only maneuver references cannot supply this controller's model pose. - Reject them explicitly rather than silently following a different reference. - Measured yaw checks input health only; it never modifies valid geometry. + controlsd owns upstream selection/limiting and service health. This adapter + checks ages and clock order, then supplies elapsed time to the two-state + core. Its timestamps and diagnostics never affect the targets. Raw model + geometry is checked on every cycle, even at a repeated model timestamp. + + Yaw is checked only for the inherited finite/range input gate. Engagement + and downstream driver arbitration still apply. This controller does not use + PSCM status or driver torque as control-law inputs. """ def __init__(self): self.core = ModelActionController() @@ -97,16 +98,14 @@ 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-pose-one-second-v7', + self.diagnostics = {'status': status, 'hypothesis': 'model-action-c0-c1-v1', 'calibration_approved': CALIBRATION_APPROVED, 'command': (0., 0., 0., 0.)} def update(self, model, desired_curvature, *, yaw_rate, speed, now, measurement_time, model_time, reference_time, - active, valid=True, reference_source="modelV2"): + active, valid=True): reason = None if not active: reason = 'inactive' - elif reference_source != 'modelV2': - reason = 'unsupported_reference' elif not valid: reason = 'invalid_service' elif not _finite(desired_curvature, yaw_rate, speed, now, measurement_time, model_time, reference_time): @@ -125,15 +124,13 @@ class FordModelActionController: ): self.reset('timing_reset') return FordPath() - command = self.core.update(model, desired_curvature, speed=speed, dt=dt, yaw_rate=yaw_rate) + command = self.core.update(model, desired_curvature, speed=speed, dt=dt) if not command.valid: self.reset('invalid_path') return command self.last_time, self.last_measurement_time, self.last_model_time = now, measurement_time, model_time - self.diagnostics = {'status': 'active', 'hypothesis': 'model-pose-one-second-v7', + self.diagnostics = {'status': 'active', 'hypothesis': 'model-action-c0-c1-v1', 'calibration_approved': CALIBRATION_APPROVED, 'desired_curvature': desired_curvature, - 'yaw_rate': yaw_rate, 'pose_source': 'model', - 'preview_time_s': PREVIEW_TIME_S, 'minimum_station_m': MIN_STATION_M, 'model_age': now - model_time, 'measurement_age': now - measurement_time, 'reference_age': now - reference_time, 'dt': dt, 'offset_request': self.core.c0, 'heading_request': self.core.c1, 'command': (command.path_offset, command.path_angle, 0., 0.)} diff --git a/openpilot/selfdrive/controls/tests/test_ford_controlsd_logging.py b/openpilot/selfdrive/controls/tests/test_ford_controlsd_logging.py index 63518caeba..1e5fdfbf3c 100644 --- a/openpilot/selfdrive/controls/tests/test_ford_controlsd_logging.py +++ b/openpilot/selfdrive/controls/tests/test_ford_controlsd_logging.py @@ -53,11 +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-pose-one-second-v7') + self.assertEqual(record['hypothesis'], 'model-action-c0-c1-v1') self.assertIs(record['calibration_approved'], False) self.assertEqual(record['command'][2:], [0., 0.]) self.assertEqual(record['status'], controller.diagnostics['status']) - if active and valid: - self.assertEqual(record['pose_source'], 'model') - self.assertEqual(record['preview_time_s'], 1.) - self.assertEqual(record['minimum_station_m'], 7.) diff --git a/openpilot/selfdrive/controls/tests/test_ford_model_action.py b/openpilot/selfdrive/controls/tests/test_ford_model_action.py index f778541e8c..b5466b542a 100644 --- a/openpilot/selfdrive/controls/tests/test_ford_model_action.py +++ b/openpilot/selfdrive/controls/tests/test_ford_model_action.py @@ -12,8 +12,7 @@ from openpilot.selfdrive.controls.lib.ford_model_action import ModelActionContro def make_model(x, y, heading): - times = np.linspace(0., 3., len(x)) - return SimpleNamespace(position=SimpleNamespace(t=times, x=x, y=y), orientation=SimpleNamespace(t=times, z=heading)) + return SimpleNamespace(position=SimpleNamespace(x=x, y=y), orientation=SimpleNamespace(z=heading)) def circle(curvature): @@ -21,26 +20,26 @@ def circle(curvature): return make_model(np.sin(curvature*s)/curvature, (1-np.cos(curvature*s))/curvature, curvature*s) -def straight(offset=0., heading=0.): +def straight(offset=0.): x = np.linspace(0., 60., 121) - return make_model(x*np.cos(heading), offset+x*np.sin(heading), np.full_like(x, heading)) + return make_model(x, np.full_like(x, offset), np.zeros_like(x)) -def test_model_heading_is_used_even_when_scalar_action_differs(): +def test_selected_action_controls_heading_even_when_model_previews_another_turn(): model = circle(.02) - for desired in (0., -.004, .004): - target = encode_model_action(model, desired, 20.) - assert target.path_angle == pytest.approx(.4) - assert target.path_offset == pytest.approx((1-math.cos(.4))/.02) + assert encode_model_action(model, 0., 20.).path_angle == 0. + assert encode_model_action(model, -.004, 20.).path_angle == pytest.approx(-.08) + assert encode_model_action(model, 0., 20.).path_offset > 0. -def test_straight_centering_and_matched_model_circles(): +def test_centering_information_is_independent_of_action_and_not_scaled_with_speed(): for speed in (2., 7., 20., 35.): - assert encode_model_action(straight(.4), 0., speed) == FordPath(True, .4, 0., 0., 0.) + target = encode_model_action(straight(.4), 0., speed) + assert target == FordPath(True, .4, 0., 0., 0.) for sign in (-1, 1): target = encode_model_action(circle(sign*.01), sign*.01, 20.) - assert target.path_offset == pytest.approx(sign*(1-math.cos(.2))/.01) - assert target.path_angle == pytest.approx(sign*.2) + assert target.path_offset == pytest.approx(sign*(1-math.cos(.07))/.01, abs=1e-6) + assert target.path_angle == pytest.approx(sign*.2) # No 10 m cap at highway speed. def test_two_actuator_positions_are_sufficient_for_every_next_output(): @@ -55,18 +54,18 @@ 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_using_new_model_geometry_without_retained_bias(): +def test_held_turn_releases_without_a_bias_tail_or_sign_reversal(): for sign in (-1., 1.): controller = ModelActionController() for _ in range(400): out = controller.update(circle(sign*.01), sign*.01, speed=20., dt=.01) assert out.path_angle == pytest.approx(sign*.2) - previous = np.array([controller.c0, controller.c1]) - for _ in range(100): - out = controller.update(straight(), sign*.01, speed=20., dt=.01) - expected = previous+np.clip(-previous, [-.04, -.005], [.04, .005]) - values = np.array([controller.c0, controller.c1]) - np.testing.assert_allclose(values, expected, atol=1e-10) + previous = np.array([out.path_offset, out.path_angle]) + 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() previous = values assert out == FordPath(True, 0., 0., 0., 0.) @@ -74,7 +73,7 @@ def test_held_turn_releases_using_new_model_geometry_without_retained_bias(): def test_current_model_replacement_leaves_only_independent_actuator_slew(): controller = ModelActionController() for _ in range(150): - controller.update(straight(1.-20*math.sin(.4), .4), .04, speed=20., dt=.01) + controller.update(straight(1.), .04, speed=20., dt=.01) for _ in range(25): out = controller.update(straight(), 0., speed=20., dt=.01) assert out.path_offset == pytest.approx(0.) @@ -109,7 +108,7 @@ def test_selected_core_reversal_through_float32_and_wire_keeps_sign_and_zero_c2( previous = np.zeros(2) for i in range(600): sign = 1. if i < 300 else -1. - out = controller.update(straight(sign*8., sign*.8), sign*.1, speed=30., dt=.01) + out = controller.update(straight(sign*8.), sign*.1, speed=30., dt=.01) fields = np.array([out.path_offset, out.path_angle]) assert (abs(fields) <= [5.1100001, .5000001]).all() assert (abs(fields-previous) <= [.0500001, .0055001]).all() @@ -128,7 +127,7 @@ def test_selected_core_reversal_through_float32_and_wire_keeps_sign_and_zero_c2( def test_short_path_holds_available_endpoint_without_extrapolation(): model = make_model([0., 1.], [0., .1], [0., 0.]) - assert encode_model_action(model, .01, 20.) == FordPath(True, .1, 0., 0., 0.) + assert encode_model_action(model, .01, 20.) == FordPath(True, .1, .2, 0., 0.) def test_overflowing_arc_resets_instead_of_publishing_invalid_geometry(): @@ -177,18 +176,18 @@ def test_domain_and_elapsed_time_boundaries(field, value, valid): assert ModelActionController().update(straight(.4), **kwargs).valid == valid -def test_arc_station_floor_not_forward_x_determines_offset(): +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, [.4, .4, .4]), -.01, 20.) - # At one second arc station is 5 m; the 7 m minimum gives x=5.6, y=4.6. + target = encode_model_action(make_model(x, y, [2., -2., 1.]), -.01, 20.) + # Arc length is 1.25*x on this line, so y(arc=7)=.4+.75*(7/1.25). assert target.path_offset == pytest.approx(4.6) - assert target.path_angle == pytest.approx(.4) + assert target.path_angle == pytest.approx(-.2) def test_duplicate_stations_keep_valid_geometry_and_first_cycle_slew(): model = make_model([0., 0., 10.], [.4, .4, .4], [0., 0., 0.]) - assert encode_model_action(model, 0., 20.) == FordPath(True, .4, 0., 0., 0.) + assert encode_model_action(model, .01, 20.) == FordPath(True, .4, .2, 0., 0.) out = ModelActionController().update(model, .01, speed=20., dt=.002) assert out.path_offset == pytest.approx(.01) - assert out.path_angle == 0. + assert out.path_angle == pytest.approx(.001) 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 3a06c1197d..d705fb627c 100644 --- a/openpilot/selfdrive/controls/tests/test_ford_model_action_adapter.py +++ b/openpilot/selfdrive/controls/tests/test_ford_model_action_adapter.py @@ -26,7 +26,7 @@ from openpilot.selfdrive.controls.tests.test_ford_model_action_selection import def update(controller, now=1., **overrides): - kwargs = {'model': straight(.4, .1), 'desired_curvature': .01, 'speed': 20., 'yaw_rate': 0., 'now': now, + kwargs = {'model': straight(.4), 'desired_curvature': .01, 'speed': 20., 'yaw_rate': 0., 'now': now, 'model_time': now, 'measurement_time': now, 'reference_time': now, 'active': True} kwargs.update(overrides) return controller.update(**kwargs) @@ -95,7 +95,7 @@ def test_yaw_offset_does_not_change_the_base(): for i in range(100): outputs = [update(c, 1.+i*.01, **kwargs) for c, kwargs in zip(controllers, variants, strict=True)] assert all(out == outputs[0] for out in outputs) - assert outputs[0].path_angle == pytest.approx(.1) + assert outputs[0].path_angle == pytest.approx(.2) def test_reference_source_can_change_to_an_older_but_fresh_publication(): @@ -104,7 +104,7 @@ def test_reference_source_can_change_to_an_older_but_fresh_publication(): assert update(controller, 1.01, reference_time=.98).valid -def test_current_model_geometry_controls_both_fields_independently_of_scalar_action(): +def test_release_keeps_current_geometry_and_may_grow_c0_while_c1_decreases(): for sign in (-1., 1.): controller = FordModelActionController() for i in range(100): @@ -112,10 +112,11 @@ def test_current_model_geometry_controls_both_fields_independently_of_scalar_act for i in range(100): after = update(controller, 2.+i*.01, model=circle(sign*.02), desired_curvature=sign*.004) assert abs(after.path_offset) > abs(before.path_offset) - assert abs(after.path_angle) > abs(before.path_angle) + assert abs(after.path_angle) < abs(before.path_angle) for i in range(100): released = update(controller, 3.+i*.01, model=circle(sign*.02), desired_curvature=0.) - assert released == after # A scalar reference change does not fabricate a different model pose. + assert released.path_offset == after.path_offset + assert released.path_angle == pytest.approx(0.) def _method(filename, class_name, method): @@ -149,12 +150,9 @@ class Subscriptions: def __init__(self, maneuver): self.valid = {'lateralManeuverPlan': maneuver, 'modelV2': True} - self.logMonoTime = {'carState': 995_000_000, 'modelV2': 980_000_000, 'lateralManeuverPlan': 990_000_000, - 'deviceMotion': 980_000_000, 'extrinsicsCalibration': 750_000_000} + self.logMonoTime = {'carState': 995_000_000, 'modelV2': 980_000_000, 'lateralManeuverPlan': 990_000_000} self.failed = set() - self.messages = {'carStateSP': custom.CarStateSP.new_message(), 'lateralManeuverPlan': SimpleNamespace(desiredCurvature=-.1), - 'deviceMotion': SimpleNamespace(angularVelocityDevice=SimpleNamespace(valid=True), sensorsOK=True, inputsOK=True, - timestamp=970_000_000)} + self.messages = {'carStateSP': custom.CarStateSP.new_message(), 'lateralManeuverPlan': SimpleNamespace(desiredCurvature=-.1)} def __getitem__(self, service): return self.messages[service] @@ -164,44 +162,31 @@ class Subscriptions: @pytest.mark.parametrize('maneuver', [False, True]) -@pytest.mark.parametrize('host_yaw', [.0072, .3]) -@pytest.mark.parametrize('initial_curvature', [0., .005]) -def test_actual_controlsd_selection_limiting_publication_and_downstream_can(pipeline, maneuver, host_yaw, initial_curvature): +def test_actual_controlsd_selection_limiting_publication_and_downstream_can(pipeline, maneuver): call, publication = pipeline sm = Subscriptions(maneuver) controls = startup() controller = controls.ford_path_controller - initial_curvature *= -1 if maneuver else 1 - controls.sm, controls.desired_curvature, controls.curvature = sm, initial_curvature, 0. - if initial_curvature: - # Start at the old target so startup slew cannot hide prediction on the real call path. - controller.core.c0, controller.core.c1 = .4, .1 - model = straight(.4, .1) + controls.sm, controls.desired_curvature, controls.curvature = sm, 0., 0. + model = straight(.4) model.action = SimpleNamespace(desiredCurvature=.1) cc = structs.CarControl(latActive=True) - cs = SimpleNamespace(vEgo=20., yawRate=-host_yaw, canValid=True, steeringPressed=False, steeringTorque=0.) - environment = {'FordModelActionController': FordModelActionController, 'self': controls, 'CS': cs, 'CC': cc, - 'actuators': cc.actuators, 'model_v2': model, 'lp': SimpleNamespace(roll=0.), + cs = SimpleNamespace(vEgo=20., yawRate=-.0072, canValid=True, steeringPressed=False, steeringTorque=0.) + environment = {'self': controls, 'CS': cs, 'CC': cc, 'actuators': cc.actuators, 'model_v2': model, 'lp': SimpleNamespace(roll=0.), 'clip_curvature': clip_curvature, 'time': SimpleNamespace(monotonic=lambda: 1.)} exec(call, environment) - expected_curvature = initial_curvature+(-1 if maneuver else 1)*.000125 + expected_curvature = (-1 if maneuver else 1)*.000125 assert controls.desired_curvature == pytest.approx(expected_curvature) - if maneuver: - assert controls.ford_path == FordPath() and not cc.latActive - assert controller.diagnostics['status'] == 'unsupported_reference' - else: - assert controls.ford_path.path_offset == pytest.approx(.44 if initial_curvature else .04) - assert controls.ford_path.path_angle == pytest.approx(.1 if initial_curvature else .005) - assert controller.diagnostics['yaw_rate'] == host_yaw - assert controller.diagnostics['pose_source'] == 'model' - assert cc.latActive and cc.actuators.curvature == 0. - assert controller.diagnostics['reference_age'] == pytest.approx(.02) + assert controls.ford_path.path_angle == pytest.approx(20.*expected_curvature) + assert controls.ford_path.path_offset == pytest.approx(.04) + assert cc.latActive and cc.actuators.curvature == 0. + assert controller.diagnostics['reference_age'] == pytest.approx(.01 if maneuver else .02) cp = structs.CarParams(flags=int(FordFlags.CANFD), carFingerprint='FORD_F_150_LIGHTNING_MK1') - downstream = CarController({Bus.pt: 'ford_lincoln_base_pt'}, cp, controls.CP_SP) + downstream = CarController({Bus.pt: 'ford_lincoln_base_pt'}, cp, structs.CarParamsSP()) vehicle = SimpleNamespace(out=structs.CarState(vEgo=20., vEgoRaw=20.), acc_tja_status_stock_values=defaultdict(int), lkas_status_stock_values=defaultdict(int), buttons_stock_values=defaultdict(int)) - parser = CANParser('ford_lincoln_base_pt', [('LateralMotionControl2', 20)], downstream.CAN.main) + parser = CANParser('ford_lincoln_base_pt', [('LateralMotionControl2', 100)], downstream.CAN.main) for i, fail in enumerate((False, True)): if fail: sm.failed.add('modelV2') @@ -209,17 +194,13 @@ def test_actual_controlsd_selection_limiting_publication_and_downstream_can(pipe assert not cc.latActive and controls.ford_path == FordPath() msg = custom.CarControlSP.new_message() exec(publication, {'self': controls, 'CC_SP': msg}) - for tick in range(5 if fail else 1): - now_nanos = (i + tick + 1) * 10_000_000 - _, packets = downstream.update(cc.as_reader(), convert_carControlSP(msg.as_reader()), vehicle, now_nanos) - lateral = [p for p in packets if p[0] == 0x3d6] - assert len(lateral) == int(not fail or tick == 4) - parser.update([now_nanos, packets]) + _, packets = downstream.update(cc.as_reader(), convert_carControlSP(msg.as_reader()), vehicle, (i+1)*10_000_000) + parser.update([(i+1)*10_000_000, packets]) wire = parser.vl['LateralMotionControl2'] assert wire['LatCtlPathOffst_L_Actl'] == pytest.approx(-controls.ford_path.path_offset) assert wire['LatCtlPath_An_Actl'] == pytest.approx(-controls.ford_path.path_angle) assert wire['LatCtlCurv_No_Actl'] == wire['LatCtlCrv_NoRate2_Actl'] == 0. - assert wire['LatCtl_D2_Rq'] == (0 if fail or maneuver else 2) + assert wire['LatCtl_D2_Rq'] == (0 if fail else 2) @pytest.mark.parametrize('maneuver', [False, True]) @@ -233,37 +214,6 @@ def test_actual_controlsd_service_gates(pipeline, maneuver, failed): cs = SimpleNamespace(vEgo=20., yawRate=0., canValid=True, steeringPressed=False, steeringTorque=0.) model = straight() model.action = SimpleNamespace(desiredCurvature=.1) - exec(pipeline[0], {'FordModelActionController': FordModelActionController, 'self': controls, 'CS': cs, 'CC': cc, - 'actuators': cc.actuators, 'model_v2': model, 'lp': SimpleNamespace(roll=0.), + exec(pipeline[0], {'self': controls, 'CS': cs, 'CC': cc, 'actuators': cc.actuators, 'model_v2': model, 'lp': SimpleNamespace(roll=0.), 'clip_curvature': clip_curvature, 'time': SimpleNamespace(monotonic=lambda: 1.)}) assert controls.ford_path.valid == cc.latActive == (failed == 'lateralManeuverPlan' and not maneuver) - - -@pytest.mark.parametrize('service', ['deviceMotion', 'extrinsicsCalibration']) -def test_optional_pose_services_do_not_modify_model_point_requests(pipeline, service): - sm = Subscriptions(False) - sm.failed.add(service) - controls = startup() - controls.sm, controls.desired_curvature, controls.curvature = sm, .01, 0. - controls.calibrated_pose = None - controls.ford_path_controller.core.c0, controls.ford_path_controller.core.c1 = .4, .1 - cc = structs.CarControl(latActive=True) - cs = SimpleNamespace(vEgo=20., yawRate=-.3, canValid=True, steeringPressed=False, steeringTorque=0.) - model = straight(.4, .1) - model.action = SimpleNamespace(desiredCurvature=.01) - exec(pipeline[0], {'FordModelActionController': FordModelActionController, 'self': controls, 'CS': cs, 'CC': cc, - 'actuators': cc.actuators, 'model_v2': model, 'lp': SimpleNamespace(roll=0.), - 'clip_curvature': clip_curvature, 'time': SimpleNamespace(monotonic=lambda: 1.)}) - assert cc.latActive and controls.ford_path.valid - assert controls.ford_path.path_offset == pytest.approx(.44) - assert controls.ford_path.path_angle == pytest.approx(.1) - assert controls.ford_path_controller.diagnostics['pose_source'] == 'model' - - -def test_maneuver_reference_clears_existing_model_point_requests(): - controller = FordModelActionController() - update(controller) - assert update(controller, 1.01, reference_source='lateralManeuverPlan') == FordPath() - assert controller.diagnostics['status'] == 'unsupported_reference' - assert (controller.core.c0, controller.core.c1) == (0., 0.) - assert update(controller, 1.02).path_angle == pytest.approx(.005) diff --git a/openpilot/selfdrive/controls/tests/test_ford_model_action_cadence.py b/openpilot/selfdrive/controls/tests/test_ford_model_action_cadence.py deleted file mode 100644 index 77e57a1397..0000000000 --- a/openpilot/selfdrive/controls/tests/test_ford_model_action_cadence.py +++ /dev/null @@ -1,146 +0,0 @@ -"""Exercise 100Hz calculation and 20Hz transmission through the real CAN sender.""" -from collections import defaultdict -from types import SimpleNamespace - -import pytest - -from opendbc.can import CANParser -from opendbc.car import Bus, structs -from opendbc.car.ford.carcontroller import CarController -from opendbc.car.ford.fordcan import calculate_lat_ctl2_checksum -from opendbc.car.ford.values import FordFlags, FordFlagsSP, FordSafetyFlags -from opendbc.safety.tests.libsafety import libsafety_py -from openpilot.selfdrive.controls.lib.ford_model_action import ModelActionController -from openpilot.selfdrive.controls.tests.test_ford_model_action import straight - - -def sender(canfd=True, selected=True): - cp = structs.CarParams(flags=int(FordFlags.CANFD) if canfd else 0, carFingerprint='FORD_F_150_LIGHTNING_MK1', - safetyConfigs=[structs.CarParams.SafetyConfig()]) - cp_sp = structs.CarParamsSP(flags=int(FordFlagsSP.MODEL_ACTION) if selected else 0) - controller = CarController({Bus.pt: 'ford_lincoln_base_pt'}, cp, cp_sp) - cs = SimpleNamespace(out=structs.CarState(vEgo=20., vEgoRaw=20.), acc_tja_status_stock_values=defaultdict(int), - lkas_status_stock_values=defaultdict(int), buttons_stock_values=defaultdict(int)) - return controller, cs - - -@pytest.mark.parametrize('canfd,selected,step', [(True, True, 5), (True, False, 1), (False, True, 5), (False, False, 5)]) -def test_send_intervals_latest_sample_counter_and_checksum(canfd, selected, step): - controller, cs = sender(canfd, selected) - cc, sp = structs.CarControl(latActive=True), structs.CarControlSP() - sp.fordLateralPath.valid = True - name = 'LateralMotionControl2' if canfd else 'LateralMotionControl' - address = 0x3d6 if canfd else 0x3d3 - parser = CANParser('ford_lincoln_base_pt', [(name, 0)], controller.CAN.main) - sent = [] - for frame in range(1000): - sp.fordLateralPath.pathOffset = (frame % 101 - 50) * .01 - sp.fordLateralPath.pathAngle = (frame % 101 - 50) * .0005 - _, packets = controller.update(cc.as_reader(), sp, cs, frame * 10_000_000) - lateral = [p for p in packets if p[0] == address] - assert len(lateral) == int(frame % step == 0) - if not lateral: - continue - sent.append(frame) - parser.update([frame * 10_000_000, lateral]) - wire = parser.vl[name] - assert wire['LatCtlPathOffst_L_Actl'] == pytest.approx(-sp.fordLateralPath.pathOffset) - assert wire['LatCtlPath_An_Actl'] == pytest.approx(-sp.fordLateralPath.pathAngle) - if canfd: - counter = (len(sent) - 1) % 16 - assert wire['LatCtlPath_No_Cnt'] == counter - assert wire['LatCtlPath_No_Cs'] == calculate_lat_ctl2_checksum(2, counter, lateral[0][1]) - assert sent == list(range(0, 1000, step)) - - -@pytest.mark.parametrize('failure_frame', range(1, 6)) -@pytest.mark.parametrize('disengage', [False, True]) -def test_next_scheduled_frame_clears_invalid_or_inactive_path(failure_frame, disengage): - controller, cs = sender() - cc, sp = structs.CarControl(latActive=True), structs.CarControlSP() - sp.fordLateralPath.valid = True - sp.fordLateralPath.pathOffset, sp.fordLateralPath.pathAngle = .4, .1 - parser = CANParser('ford_lincoln_base_pt', [('LateralMotionControl2', 0)], controller.CAN.main) - for frame in range(11): - if frame == failure_frame: - if disengage: - cc.latActive = False - else: - sp.fordLateralPath.valid = False - _, packets = controller.update(cc.as_reader(), sp, cs, frame * 10_000_000) - lateral = [p for p in packets if p[0] == 0x3d6] - assert len(lateral) == int(frame % 5 == 0) - if lateral and frame >= failure_frame: - parser.update([frame * 10_000_000, lateral]) - wire = parser.vl['LateralMotionControl2'] - assert wire['LatCtl_D2_Rq'] == (0 if disengage else 2) - assert all(wire[k] == 0. for k in ('LatCtlPathOffst_L_Actl', 'LatCtlPath_An_Actl', 'LatCtlCurv_No_Actl', 'LatCtlCrv_NoRate2_Actl')) - - -def test_core_slew_per_second_and_actual_panda_acceptance(): - controller, cs = sender() - core = ModelActionController() - cc, sp = structs.CarControl(latActive=True), structs.CarControlSP() - parser = CANParser('ford_lincoln_base_pt', [('LateralMotionControl2', 0)], controller.CAN.main) - safety = libsafety_py.libsafety - assert safety.set_safety_hooks(structs.CarParams.SafetyModel.ford, FordSafetyFlags.CANFD) == 0 - safety.init_tests() - safety.set_controls_allowed(True) - frames = [] - for frame in range(100): - command = core.update(straight(10., 1.), .1, speed=20., dt=.01) - assert core.c0 == pytest.approx((frame + 1) * .04) - assert core.c1 == pytest.approx((frame + 1) * .005) - sp.fordLateralPath.valid = command.valid - sp.fordLateralPath.pathOffset, sp.fordLateralPath.pathAngle = command.path_offset, command.path_angle - _, packets = controller.update(cc.as_reader(), sp, cs, frame * 10_000_000) - for address, data, bus in packets: - if address != 0x3d6: - continue - frames.append(frame) - safety.set_timer(frame * 10_000) - assert safety.safety_tx_hook(libsafety_py.make_CANPacket(address, bus, data)) - parser.update([frame * 10_000_000, [(address, data, bus)]]) - wire = parser.vl['LateralMotionControl2'] - assert wire['LatCtlPathOffst_L_Actl'] == pytest.approx(-command.path_offset) - assert wire['LatCtlPath_An_Actl'] == pytest.approx(-command.path_angle) - assert wire['LatCtlCurv_No_Actl'] == wire['LatCtlCrv_NoRate2_Actl'] == 0. - assert frames == list(range(0, 100, 5)) - - -@pytest.mark.parametrize('sign', [-1., 1.]) -@pytest.mark.parametrize('phase', range(5)) -def test_saturated_turn_unwinds_on_first_update_and_next_scheduled_can_frame(sign, phase): - controller, cs = sender() - core = ModelActionController() - cc, sp = structs.CarControl(latActive=True), structs.CarControlSP() - parser = CANParser('ford_lincoln_base_pt', [('LateralMotionControl2', 0)], controller.CAN.main) - change_frame = 200 + phase - first_unwind = first_zero_c0 = first_zero_c1 = None - turn, released = straight(sign*10., sign), straight() - for frame in range(change_frame+135): - command = core.update(turn if frame < change_frame else released, sign*.1, speed=20., dt=.01) - if frame >= change_frame: - elapsed = (frame-change_frame+1)*.01 - assert sign*core.c0 == pytest.approx(max(0., 5.11-4.*elapsed), abs=1e-10) - assert sign*core.c1 == pytest.approx(max(0., .5-.5*elapsed), abs=1e-10) - sp.fordLateralPath.valid = command.valid - sp.fordLateralPath.pathOffset, sp.fordLateralPath.pathAngle = command.path_offset, command.path_angle - _, packets = controller.update(cc.as_reader(), sp, cs, frame*10_000_000) - lateral = [p for p in packets if p[0] == 0x3d6] - if not lateral or frame < change_frame: - continue - parser.update([frame*10_000_000, lateral]) - wire = parser.vl['LateralMotionControl2'] - c0, c1 = -sign*wire['LatCtlPathOffst_L_Actl'], -sign*wire['LatCtlPath_An_Actl'] - assert 0. <= c0 < 5.11 and 0. <= c1 < .5 - assert wire['LatCtlCurv_No_Actl'] == wire['LatCtlCrv_NoRate2_Actl'] == 0. - if first_unwind is None: - first_unwind = frame - if c0 == 0. and first_zero_c0 is None: - first_zero_c0 = frame - if c1 == 0. and first_zero_c1 is None: - first_zero_c1 = frame - assert first_unwind == ((change_frame+4)//5)*5 - assert first_zero_c0 == ((change_frame+127+4)//5)*5 - assert first_zero_c1 == ((change_frame+99+4)//5)*5 diff --git a/openpilot/selfdrive/controls/tests/test_ford_model_action_selection.py b/openpilot/selfdrive/controls/tests/test_ford_model_action_selection.py index 5990993338..2d0f681c48 100644 --- a/openpilot/selfdrive/controls/tests/test_ford_model_action_selection.py +++ b/openpilot/selfdrive/controls/tests/test_ford_model_action_selection.py @@ -7,14 +7,10 @@ from types import SimpleNamespace import pytest -from opendbc.car import structs -from opendbc.car.ford.values import FordFlags, FordFlagsSP -from openpilot.cereal import custom +from opendbc.car.ford.values import FordFlags from openpilot.common.params import Params, ParamKeyFlag, ParamKeyType -from openpilot.selfdrive.car.helpers import convert_to_capnp from openpilot.selfdrive.controls.lib.ford_model_action import FordModelActionController, select_model_action_controller from openpilot.selfdrive.controls.lib.ford_path import FordPath, FordPathController, FordPscmObserverPathController -from openpilot.sunnypilot.mads.helpers import set_car_specific_params def car_params(**overrides): @@ -22,7 +18,7 @@ def car_params(**overrides): 'carFw': []} | overrides)) -def startup(cp=None, params=None, cp_sp=None): +def startup(cp=None, params=None): filename = Path(__file__).resolve().parents[1]/'controlsd.py' tree = ast.parse(filename.read_text()) cls = next(n for n in tree.body if isinstance(n, ast.ClassDef) and n.name == 'Controls') @@ -31,15 +27,8 @@ def startup(cp=None, params=None, cp_sp=None): end = next(i for i, n in enumerate(body) if isinstance(n, ast.Assign) and ast.unparse(n.targets[0]) == 'self.ford_path') if params is None: params = SimpleNamespace(get_bool=lambda key: key == 'FordModelActionController') - cp = cp or car_params() - # card snapshots the toggle into CarParamsSP before controlsd starts. - if cp_sp is None: - cp_sp = structs.CarParamsSP() - if cp.brand == 'ford': - set_car_specific_params(cp, cp_sp, params) - controls = SimpleNamespace(CP=cp, CP_SP=cp_sp, params=params, calibrated_pose=None, - pose_calibrator=SimpleNamespace(calib_valid=False)) - environment = {'self': controls, 'FordFlags': FordFlags, 'FordFlagsSP': FordFlagsSP, 'FordPath': FordPath, + controls = SimpleNamespace(CP=cp or car_params(), params=params) + environment = {'self': controls, 'FordFlags': FordFlags, 'FordPath': FordPath, 'FordPathController': FordPathController, 'FordPscmObserverPathController': FordPscmObserverPathController, 'FordModelActionController': FordModelActionController, 'select_model_action_controller': select_model_action_controller, @@ -56,23 +45,9 @@ def test_actual_startup_priority(candidate, observer): expected = FordModelActionController if candidate else previous assert type(selected.ford_path_controller) is expected assert selected.ford_model_action == candidate - assert bool(selected.CP_SP.flags & FordFlagsSP.MODEL_ACTION) == candidate assert selected.ford_path == FordPath() -@pytest.mark.parametrize('selected', [False, True]) -def test_controller_and_sender_share_card_snapshot_when_stored_toggle_changes(selected): - cp, cp_sp = car_params(), structs.CarParamsSP(flags=128) - set_car_specific_params(cp, cp_sp, SimpleNamespace(get_bool=lambda key: selected)) - with custom.CarParamsSP.from_bytes(convert_to_capnp(cp_sp).to_bytes()) as snapshot: - controls = startup(cp, SimpleNamespace(get_bool=lambda key: not selected), snapshot) - assert controls.ford_model_action == selected - assert bool(controls.CP_SP.flags & FordFlagsSP.MODEL_ACTION) == selected - assert controls.CP_SP.flags & 128 - set_car_specific_params(cp, cp_sp, SimpleNamespace(get_bool=lambda key: False)) - assert cp_sp.flags == 128 - - @pytest.mark.parametrize('overrides', [{'brand': 'tesla'}, {'flags': 0}, {'carFingerprint': 'FORD_F_150_MK14'}]) @pytest.mark.parametrize('observer', [False, True]) def test_other_vehicles_keep_their_previous_selection(overrides, observer): diff --git a/openpilot/selfdrive/controls/tests/test_ford_model_action_yaw.py b/openpilot/selfdrive/controls/tests/test_ford_model_action_yaw.py deleted file mode 100644 index 5c544f1574..0000000000 --- a/openpilot/selfdrive/controls/tests/test_ford_model_action_yaw.py +++ /dev/null @@ -1,29 +0,0 @@ -"""Raw Ford yaw gates input health but cannot change path demand.""" -import math - -import pytest - -from openpilot.selfdrive.controls.lib.ford_model_action import ModelActionController -from openpilot.selfdrive.controls.lib.ford_path import FordPath -from openpilot.selfdrive.controls.tests.test_ford_model_action import straight - - -@pytest.mark.parametrize('sign', [-1., 1.]) -@pytest.mark.parametrize('yaw', [-3., -.2, -.008, 0., .008, .2, 3.]) -def test_valid_yaw_cannot_change_commands_during_entry_release_or_reversal(sign, yaw): - reference, measured = ModelActionController(), ModelActionController() - for i in range(400): - offset, desired = ((.4, .02), (.4, .001), (.12, -.0004078), (-.4, -.02))[i//100] - model = straight(sign*offset) - expected = reference.update(model, sign*desired, speed=10., dt=.01) - actual = measured.update(model, sign*desired, speed=10., dt=.01, yaw_rate=yaw) - assert actual == expected - assert actual.curvature == actual.curvature_rate == 0. - - -@pytest.mark.parametrize('yaw', [math.nan, math.inf, -math.inf, None, 'bad', 3.001, -3.001]) -def test_invalid_yaw_still_resets_core(yaw): - controller = ModelActionController() - controller.update(straight(.4), .01, speed=10., dt=.01) - assert controller.update(straight(.4), .01, speed=10., dt=.01, yaw_rate=yaw) == FordPath() - assert controller.c0 == controller.c1 == 0. diff --git a/openpilot/selfdrive/controls/tests/test_ford_model_points.py b/openpilot/selfdrive/controls/tests/test_ford_model_points.py deleted file mode 100644 index ea4a4e7b1e..0000000000 --- a/openpilot/selfdrive/controls/tests/test_ford_model_points.py +++ /dev/null @@ -1,54 +0,0 @@ -"""Model geometry contract, independent of any PSCM response model.""" -from types import SimpleNamespace - -import numpy as np -import pytest - -from openpilot.selfdrive.controls.lib.ford_model_action import encode_model_action - - -def model_points(t, x, y, heading): - return SimpleNamespace(position=SimpleNamespace(t=t, x=x, y=y), orientation=SimpleNamespace(t=t, z=heading)) - - -def test_both_fields_sample_the_same_model_time_without_constant_speed_assumption(): - # Accelerating plan: one second is 12 m along this straight inclined path. - s = np.array([0., 4., 12., 30.]) - heading = .1 - m = model_points([0., .5, 1., 2.], s*np.cos(heading), .3+s*np.sin(heading), np.full(4, heading)) - out = encode_model_action(m, -.01, 20.) - assert out.path_offset == pytest.approx(.3+12*np.sin(heading)) - assert out.path_angle == pytest.approx(.1) - assert out.curvature == out.curvature_rate == 0. - assert encode_model_action(m, .01, 30.) == out - - -def test_low_speed_floor_uses_one_shared_seven_metre_station(): - m = model_points([0., 1., 2.], [0., 3., 9.], [0., 0., 0.], [0., .03, .09]) - out = encode_model_action(m, .01, 3.) - assert out.path_offset == 0. - assert out.path_angle == pytest.approx(.07) - - -def test_short_plan_holds_both_endpoint_values_without_extrapolation(): - m = model_points([0., .5], [0., 2.], [0., .4], [0., .2]) - out = encode_model_action(m, -.01, 20.) - assert out.path_offset == .4 - assert out.path_angle == pytest.approx(.2) - - -@pytest.mark.parametrize('times', [[], [0.], [0., 0., 1.], [0., 1., .5], [0., float('nan'), 1.], [.1, .5, 1.]]) -def test_invalid_model_clock_cannot_publish_an_active_path(times): - m = model_points(times, [0., 10., 20.], [0., .1, .4], [0., .02, .04]) - assert not encode_model_action(m, .01, 20.).valid - - -def test_orientation_and_position_must_describe_the_same_times(): - m = model_points([0., .5, 1.], [0., 10., 20.], [0., .1, .4], [0., .02, .04]) - m.orientation.t = [0., .6, 1.] - assert not encode_model_action(m, .01, 20.).valid - - -def test_model_heading_unwraps_before_interpolation(): - m = model_points([0., .5, 1.5], [0., 10., 30.], [0., 0., 0.], [3., 3.1, -3.1]) - assert encode_model_action(m, 0., 20.).path_angle == pytest.approx(np.pi) diff --git a/openpilot/sunnypilot/mads/helpers.py b/openpilot/sunnypilot/mads/helpers.py index 46bf1457cf..655d7bc9a2 100644 --- a/openpilot/sunnypilot/mads/helpers.py +++ b/openpilot/sunnypilot/mads/helpers.py @@ -7,7 +7,6 @@ See the LICENSE.md file in the root directory for more details. from openpilot.common.params import Params from opendbc.car import structs -from opendbc.car.ford.values import FordFlags, FordFlagsSP from opendbc.safety import ALTERNATIVE_EXPERIENCE from opendbc.sunnypilot.car.hyundai.values import HyundaiFlagsSP, HyundaiSafetyFlagsSP from opendbc.sunnypilot.car.tesla.values import MadsScreenButtonType, TeslaFlagsSP @@ -55,11 +54,6 @@ def set_alternative_experience(CP: structs.CarParams, CP_SP: structs.CarParamsSP def set_car_specific_params(CP: structs.CarParams, CP_SP: structs.CarParamsSP, params: Params): - if CP.brand == "ford": - CP_SP.flags &= ~FordFlagsSP.MODEL_ACTION.value - if CP.flags & FordFlags.CANFD and CP.carFingerprint == "FORD_F_150_LIGHTNING_MK1" and params.get_bool("FordModelActionController"): - CP_SP.flags |= FordFlagsSP.MODEL_ACTION.value - if CP.brand == "hyundai": # TODO-SP: This should be separated from MADS module for future implementations # Use "HyundaiLongitudinalMainCruiseToggleable" param diff --git a/openpilot/sunnypilot/sunnylink/settings_ui.json b/openpilot/sunnypilot/sunnylink/settings_ui.json index 0f4c7fbafd..8903610d1b 100644 --- a/openpilot/sunnypilot/sunnylink/settings_ui.json +++ b/openpilot/sunnypilot/sunnylink/settings_ui.json @@ -2183,8 +2183,8 @@ "widget": "toggle", "needs_onroad_cycle": true, "title": "Selected-Action Path Tracking (Experimental)", - "description": "Follow model position and heading from one shared path point on the Ford CAN FD F-150 Lightning.", - "details": "Uses the model's lateral position and heading one second ahead, with a seven-metre minimum distance and no extrapolation beyond the available path. Sends requests at 20 Hz with the existing command limits and rate limits. Default off; this model-point experiment is not road-validated. Enable only for controlled testing. Lateral maneuver test mode is unsupported and disengages this controller. 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.", + "description": "Follow the selected steering plan with nearby model-path centering on the Ford CAN FD F-150 Lightning.", + "details": "Uses nearby model-path offset and a heading request based directly on selected planned curvature. There is no accumulated measured-turning correction. Default off; physical tracking and turn-exit behavior are 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 d361a75075..b4c1bf9e7d 100644 --- a/openpilot/sunnypilot/sunnylink/settings_ui_src/pages/vehicle.yaml +++ b/openpilot/sunnypilot/sunnylink/settings_ui_src/pages/vehicle.yaml @@ -14,8 +14,8 @@ sections: widget: toggle needs_onroad_cycle: true title: Selected-Action Path Tracking (Experimental) - description: Follow model position and heading from one shared path point on the Ford CAN FD F-150 Lightning. - details: Uses the model's lateral position and heading one second ahead, with a seven-metre minimum distance and no extrapolation beyond the available path. Sends requests at 20 Hz with the existing command limits and rate limits. Default off; this model-point experiment is not road-validated. Enable only for controlled testing. Lateral maneuver test mode is unsupported and disengages this controller. 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. + description: Follow the selected steering plan with nearby model-path centering on the Ford CAN FD F-150 Lightning. + details: Uses nearby model-path offset and a heading request based directly on selected planned curvature. There is no accumulated measured-turning correction. Default off; physical tracking and turn-exit behavior are 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 deleted file mode 100644 index b372bbaf46..0000000000 --- a/tools/ford_pscm_lab/damping_replay.py +++ /dev/null @@ -1,173 +0,0 @@ -"""Compare pinned historical selected-action controllers on complete rlogs. - -Original controls publication times proxy computation time. Consumed model -timestamps are exact; carState is causal and carControl is matched within 5 ms. -This compares commands on a fixed recording, never counterfactual vehicle motion. -""" -import argparse -from collections import Counter -import hashlib -import json -from pathlib import Path -from types import SimpleNamespace - -import numpy as np -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, V3_REVISION, V4_REVISION, WireCheck, field_checks, load_controller, sample, verify_dependency, -) - - -DEPLOYMENT_OPENDBC = 'c21a9013700734dd20b09e05aa68329ad8cc20f9' - - -def extract(directory): - columns = {'cs': 't valid can_valid speed yaw torque pressed', 'controls': 't valid desired model_ns', - 'cc': 't valid active', 'params': 't valid', 'path': 't valid active c0 c1', 'model': 't valid ns'} - rows = {name: [] for name in columns} - models, sources = [], {} - t0 = None - files = sorted(directory.glob('*--rlog.zst'), key=lambda p: int(p.name.split('--')[-2])) - if not files: - raise ValueError('No complete rlogs found') - for file in files: - compressed = file.read_bytes() - sources[file.name] = hashlib.sha256(compressed).hexdigest() - data = zstandard.ZstdDecompressor().stream_reader(compressed).read() - for event in log.Event.read_multiple_bytes(data): - kind, t, valid = event.which(), event.logMonoTime*1e-9, event.valid - if t0 is None: - t0 = t - if kind == 'carState': - cs = event.carState - rows['cs'].append((t, valid, cs.canValid, cs.vEgo, -cs.yawRate, cs.steeringTorque, cs.steeringPressed)) - elif kind == 'controlsState': - cs = event.controlsState - rows['controls'].append((t, valid, cs.desiredCurvature, cs.lateralPlanMonoTime)) - elif kind == 'carControl': - rows['cc'].append((t, valid, event.carControl.latActive)) - elif kind == 'vehicleParameters': - rows['params'].append((t, valid)) - elif kind == 'carControlSP': - path = event.carControlSP.fordLateralPath - rows['path'].append((t, valid, path.valid, path.pathOffset, path.pathAngle)) - elif kind == 'modelV2': - model = event.modelV2 - models.append(SimpleNamespace(position=SimpleNamespace(x=list(model.position.x), y=list(model.position.y)), - orientation=SimpleNamespace(z=list(model.orientation.z)))) - rows['model'].append((t, valid, event.logMonoTime)) - elif kind == 'lateralManeuverPlan': - raise ValueError('This replay requires routes without a separate maneuver reference') - streams = {} - for name, fields in columns.items(): - values = np.array(rows[name]) - if not len(values) or np.any(np.diff(values[:, 0]) < 0.): - raise ValueError(f'Missing or backward {name} stream') - streams[name] = dict(zip(fields.split(), values.T, strict=True)) - return streams, models, sources, t0 - - -def run(directory, output, baseline_version='v1', candidate_version='v2', windows=()): - directory, output = directory.resolve(), output.resolve() - if output == directory or directory in output.parents: - raise ValueError('Output must be outside the source route directory') - if candidate_version == 'current': - raise ValueError('This historical damping replay requires unchanged C1 and does not preserve model clocks; ' + - 'select a pinned v2/v3/v4 candidate, or use a model-point replay for current code') - verify_dependency(DEPLOYMENT_OPENDBC) - revisions = {'v1': V1_REVISION, 'v2': V2_REVISION, 'v3': V3_REVISION, 'v4': V4_REVISION} - baseline_source = load_controller(revisions[baseline_version]) - candidate_source = load_controller(revisions[candidate_version]) - streams, models, sources, t0 = extract(directory) - controls, model = streams['controls'], streams['model'] - t = controls['t'] - cs, params = (sample(streams[name], t) for name in ('cs', 'params')) - cc, recorded = (sample(streams[name], t, nearest=True) for name in ('cc', 'path')) - mi = np.clip(np.searchsorted(model['ns'], controls['model_ns']), 0, len(models)-1) - exact = model['ns'][mi] == controls['model_ns'] - services = ((controls['valid'] == 1) & (cc['valid'] == 1) & (abs(cc['t']-t) < .005) & - (cs['valid'] == 1) & (cs['can_valid'] == 1) & (params['valid'] == 1) & - (t-params['t'] >= 0.) & (t-params['t'] <= .15) & exact & (model['valid'][mi] == 1)) - baseline, candidate, wire = baseline_source.FordModelActionController(), candidate_source.FordModelActionController(), WireCheck() - before, after = np.zeros((len(t), 4)), np.zeros((len(t), 4)) - eligible = np.zeros(len(t), bool) - reasons = Counter() - for i, now in enumerate(t): - kwargs = {'speed': cs['speed'][i], 'now': now, 'measurement_time': cs['t'][i], 'model_time': model['t'][mi[i]], - 'reference_time': model['t'][mi[i]], 'active': bool(cc['active'][i]), 'valid': bool(services[i])} - geometry = models[mi[i]] if exact[i] else None - a = baseline.update(geometry, controls['desired'][i], yaw_rate=cs['yaw'][i], **kwargs) - b = candidate.update(geometry, controls['desired'][i], yaw_rate=cs['yaw'][i], **kwargs) - assert a.valid == b.valid and a.path_angle == b.path_angle - before[i] = a.path_offset, a.path_angle, a.curvature, a.curvature_rate - after[i] = b.path_offset, b.path_angle, b.curvature, b.curvature_rate - eligible[i] = b.valid - reasons[candidate.diagnostics['status']] += 1 - wire.check(a) - wire.check(b) - field_checks(before, eligible, t) - field_checks(after, eligible, t) - clean = eligible & (cs['pressed'] == 0) & (abs(cs['torque']) <= 1.) - # Erode driver eligibility by one second in each direction on original time. - bad = np.r_[0, np.cumsum(~clean)] - left, right = np.searchsorted(t, t-1.), np.searchsorted(t, t+1., side='right') - clean &= (bad[right] == bad[left]) & (t >= t[0]+1.) & (t <= t[-1]-1.) - relative = t-t0 - masks = {'eligible': eligible, 'driver_clean': clean, - 'driver_clean_low_request_above_8mps': clean & (cs['speed'] >= 8.) & (abs(controls['desired'])*cs['speed']**2 < .15), - 'turn': clean & (abs(controls['desired'])*cs['speed']**2 >= .5)} - for label, start, end in windows: - start, end = float(start), float(end) - if not np.isfinite([start, end]).all() or start >= end or label in masks: - raise ValueError('Focus windows need unique labels and finite increasing bounds') - masks[label] = eligible & (relative >= start) & (relative < end) - weight = np.minimum(np.diff(t, append=t[-1]+.01), .03) - difference = abs(before[:, 0]-after[:, 0]) - cohorts = {} - for name, mask in masks.items(): - if mask.any(): - cohorts[name] = {'cycles': int(mask.sum()), 'seconds': float(weight[mask].sum()), - 'changed_c0_cycles': int((difference[mask] > 1e-9).sum()), - 'mean_absolute_c0_change_m': float(np.average(difference[mask], weights=weight[mask])), - 'max_absolute_c0_change_m': float(difference[mask].max()), - 'increased_absolute_c0_cycles': int((abs(after[mask, 0])-abs(before[mask, 0]) > 1e-9).sum()), - 'decreased_absolute_c0_cycles': int((abs(before[mask, 0])-abs(after[mask, 0]) > 1e-9).sum()), - 'driver_input_percent': float(100*np.average((cs['pressed'][mask] == 1) | (abs(cs['torque'][mask]) > 1.), weights=weight[mask])), - 'baseline_peak_absolute_c0_m': float(abs(before[mask, 0]).max()), - 'candidate_peak_absolute_c0_m': float(abs(after[mask, 0]).max())} - paired = eligible & (recorded['valid'] == 1) & (recorded['active'] == 1) & (abs(recorded['t']-t) < .005) - actual = np.column_stack((recorded['c0'], recorded['c1'])) - error = abs(before[:, :2]-actual) - report = {'scope': 'Fixed-input command replay only; no physical improvement or stability claim.', 'calibration_approved': False, - 'cycles': len(t), 'eligible_cycles': int(eligible.sum()), 'status_counts': dict(reasons), - 'c1_exactly_unchanged': True, 'same_validity': True, 'field_slew_zero_c2_c3_pass': True, - 'float32_can_round_trips': wire.count, 'cohorts': cohorts, - 'baseline_commands_vs_recorded_publications': {'paired_cycles': int(paired.sum()), - 'within_one_quantum_cycles': int(np.all(error[paired] <= [.010001, .0005001], axis=1).sum()), - 'maximum_absolute_error_c0_c1': np.max(error[paired], axis=0).tolist()}, - 'timing': 'Publication-time proxy, causal carState, exact consumed model; full SubMaster health unavailable.', - 'baseline_version': baseline_version, 'baseline_revision': revisions[baseline_version], - 'candidate_version': candidate_version, 'candidate_revision': revisions.get(candidate_version, 'working_tree'), - 'focus_windows': windows, 'baseline_source_sha256': baseline_source.source_sha256, - 'candidate_source_sha256': candidate_source.source_sha256, - 'source_rlog_sha256': sources, 'opendbc_head': DEPLOYMENT_OPENDBC, - 'source_sha256': {str(p): hashlib.sha256(p.read_bytes()).hexdigest() for p in (Path(__file__), Path(ford_model_action.__file__))}} - output.mkdir(parents=True, exist_ok=True) - np.savez_compressed(output/'commands.npz', t=relative, before=before, after=after, eligible=eligible, - speed=cs['speed'], yaw=cs['yaw'], desired=controls['desired'], torque=cs['torque']) - (output/'report.json').write_text(json.dumps(report, indent=2, allow_nan=False)+'\n') - print(json.dumps({k: v for k, v in report.items() if k not in ('source_rlog_sha256', 'source_sha256')}, indent=2)) - - -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', 'v3', 'v4'], default='v1') - parser.add_argument('--candidate', choices=['v2', 'v3', 'v4', '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 ccc694f6af..4dca9d6759 100644 --- a/tools/ford_pscm_lab/model_action_replay.py +++ b/tools/ford_pscm_lab/model_action_replay.py @@ -1,23 +1,18 @@ """Replay the selected core and its adapter on route90/95 original-time extracts. -The historical pass uses pinned v1 source and the archived eligibility mask to check -command compatibility. The separate current adapter pass reconstructs input eligibility +The historical pass deliberately uses the archived eligibility mask to check +command compatibility. The separate adapter pass reconstructs input eligibility from service records, never from candidate/baseline output validity. Neither pass scores counterfactual motion. Source extracts and archived reports are read-only; --output selects a separate destination. - -Current comparisons require model_position_t and model_orientation_t arrays -preserved from the rlogs. Older extracts lack these clocks and must be replayed -using their archived tool revision; do not synthesize timestamps for them. """ import argparse from collections import Counter -from functools import lru_cache import hashlib import json from pathlib import Path import subprocess -from types import ModuleType, SimpleNamespace +from types import SimpleNamespace import numpy as np import opendbc @@ -30,24 +25,6 @@ from openpilot.selfdrive.controls.lib.ford_path import _model_path PINNED_OPENDBC = '72a775d35e54c21ff5c5798acef22016eedcc0a7' -V1_REVISION = '5fc16abc7662020706e29f57d31a6d5e2bc1293a' -V2_REVISION = '744a97d9bc08d8743b250eceff7c88585b5480de' -V3_REVISION = '01f8d51c82b3e863f1012d383b5994813ef01b81' -V4_REVISION = '7e63449749d112f096c56cb848dd289054e5f85b' - - -@lru_cache(maxsize=4) -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): - raise ValueError('A full immutable commit hash is required') - filename = 'openpilot/selfdrive/controls/lib/ford_model_action.py' - root = Path(__file__).resolve().parents[2] - source = subprocess.check_output(['git', '-C', str(root), 'show', f'{commit}:{filename}']) - module = ModuleType(f'ford_model_action_{commit}') - exec(compile(source, f'{commit}:{filename}', 'exec'), module.__dict__) - module.source_sha256 = hashlib.sha256(source).hexdigest() - return module def revision(directory): @@ -66,20 +43,6 @@ def table(raw, name): return dict(zip(raw[name+'_names'], raw[name].T, strict=True)) -def extract_models(raw): - clock_fields = ('model_position_t', 'model_orientation_t') - if any(name not in raw for name in clock_fields): - raise ValueError('Current replay requires original model_position_t and model_orientation_t; ' + - 'use the archived replay revision for older extracts, or re-extract the original rlogs with both clocks') - paths = raw['model_paths'] - if paths.ndim != 3 or paths.shape[1] != 4 or len(paths) == 0: - raise ValueError('Expected nonempty model_paths with four arrays per model') - if any(raw[name].shape != (len(paths), paths.shape[2]) for name in clock_fields): - raise ValueError('Model clocks must match the extracted model and point counts') - return [SimpleNamespace(position=SimpleNamespace(x=p[1], y=p[2], t=pt), orientation=SimpleNamespace(z=p[3], t=ht)) - for p, pt, ht in zip(paths, raw[clock_fields[0]], raw[clock_fields[1]], strict=True)] - - def sample(stream, query, *, nearest=False): if len(stream['t']) == 0 or np.any(np.diff(stream['t']) < 0): raise ValueError('Replay requires nonempty streams in original timestamp order') @@ -140,10 +103,10 @@ def run(directory, output): raise ValueError('Output must be outside the source route directory') dependency = verify_dependency() with np.load(directory/'route.npz', allow_pickle=False) as raw: - models = extract_models(raw) streams = {name: table(raw, name) for name in ('controls', 'cs', 'cc', 'model', 'params', 'path')} if len(raw['maneuver']): raise ValueError('This extract cannot identify the selected maneuver service per cycle; use the integration tests for that source') + models = [SimpleNamespace(position=SimpleNamespace(x=p[1], y=p[2]), orientation=SimpleNamespace(z=p[3])) for p in raw['model_paths']] with np.load(directory/'encoder_comparison.npz', allow_pickle=False) as archive: baseline = {key: archive[key] for key in ('t', 'valid', 'action_heading')} with np.load(directory/'pose_candidate/pose_replay.npz', allow_pickle=False) as pose: @@ -161,8 +124,7 @@ def run(directory, output): (cs['valid'] == 1) & (cs['can_valid'] == 1) & (params['valid'] == 1) & (t-params['t'] >= 0.) & (t-params['t'] <= .15) & exact & (model['valid'] == 1)) dt = np.r_[.01, np.diff(t)] - archived = load_controller(V1_REVISION) - core, entry_clock_core, adapter, wire = archived.ModelActionController(), ModelActionController(), FordModelActionController(), WireCheck() + core, entry_clock_core, adapter, wire = ModelActionController(), ModelActionController(), FordModelActionController(), WireCheck() commands = np.zeros((len(t), 4)) adapted = np.zeros_like(commands) valid = np.zeros(len(t), bool) @@ -181,10 +143,11 @@ def run(directory, output): adapter_valid[i] = new_gate.valid reasons[adapter.diagnostics['status']] += 1 wire.check(new_gate) - # Current core receives actual yaw and a fresh 10 ms engagement tick. + # Isolate the adapter's fresh 10 ms engagement tick from the archived + # harness, which used the preceding publication interval even on engage. entry_dt = dt[i] if i > 0 and baseline['valid'][i-1] else .01 expected_adapter = entry_clock_core.update(selected_model, controls['desired'][i], speed=cs['speed'][i], dt=entry_dt, - yaw_rate=cs['yaw'][i], active=bool(baseline['valid'][i])) + active=bool(baseline['valid'][i])) assert new_gate == expected_adapter, f'Unexplained adapter difference at cycle {i}' np.testing.assert_array_equal(valid, baseline['valid']) np.testing.assert_array_equal(commands[:, :2], baseline['action_heading']) @@ -227,7 +190,7 @@ def run(directory, output): 'calibration_approved': False, 'executes_live_selector': False, 'cycles': len(t), 'core_active_cycles': int(valid.sum()), 'core_exact_archived_match': True, 'cohorts_reproduced': True, 'adapter_active_cycles': int(adapter_valid.sum()), 'adapter_status_counts': dict(reasons), - 'adapter_matches_current_core_with_yaw_and_fresh_engagement_dt': True, + 'adapter_exact_match_with_fresh_engagement_dt': True, 'core_active_path_shorter_than_7m_cycles': int(np.sum(valid & (coverage < 7.))), 'adapter_validity_differs_from_archive_cycles': int(np.sum(adapter_valid != valid)), 'adapter_command_differs_from_archive_cycles': int(np.any(abs(adapted-commands) > 1e-9, axis=1).sum()), @@ -236,8 +199,7 @@ def run(directory, output): '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 input-health checks and diagnostics only.', - 'archived_core_revision': V1_REVISION, 'archived_core_source_sha256': archived.source_sha256, + 'host_yaw': 'Extract cs.yaw already equals -carState.yawRate. Used only for inherited finite/range gate, never feedback.', 'cohorts': cohorts, 'workspace_head': revision(root), 'opendbc_import_head': revision(dependency), 'opendbc_import_path': str(dependency), 'source_sha256': {str(p.resolve()): hashlib.sha256(p.read_bytes()).hexdigest() for p in sources}} diff --git a/tools/ford_pscm_lab/stress_model_action.py b/tools/ford_pscm_lab/stress_model_action.py index 06ba92b97f..4cce9cd6f2 100644 --- a/tools/ford_pscm_lab/stress_model_action.py +++ b/tools/ford_pscm_lab/stress_model_action.py @@ -1,8 +1,8 @@ """Deterministic numerical stress and exhaustive field-boundary CAN checks. -Analytic inclined lines check the shared model point; the slew oracle uses -scalar arithmetic. Packing is checked against direct Float32/CAN packing of the -continuous state, independently of host _packed. +Analytic straight/rotated paths supply an independent y(7) oracle. The +reference slew uses scalar arithmetic. Packing is checked against direct +Float32/CAN packing of the continuous state, independently of host _packed. No synthetic plant is fitted or used to claim vehicle tracking performance. """ import argparse @@ -21,10 +21,10 @@ from opendbc.car.ford.fordcan import create_lat_ctl2_msg from tools.ford_pscm_lab.model_action_replay import PINNED_OPENDBC, WireCheck, verify_dependency, revision -def line(offset, heading=0., speed=20.): +def line(offset, heading=0.): s = np.linspace(0., 30., 33) - return SimpleNamespace(position=SimpleNamespace(t=s/speed, x=s*math.cos(heading), y=offset+s*math.sin(heading)), - orientation=SimpleNamespace(t=s/speed, z=np.full_like(s, heading))) + return SimpleNamespace(position=SimpleNamespace(x=s*math.cos(heading), y=offset+s*math.sin(heading)), + orientation=SimpleNamespace(z=np.full_like(s, heading))) def check_raw_packing(wire, controller, path): @@ -56,24 +56,21 @@ def run(cycles, seed, output, opendbc_revision=PINNED_OPENDBC): offset, heading = float(rng.uniform(-8., 8.)), float(rng.uniform(-1.2, 1.2)) speed = float(rng.uniform(.3, 55.)) desired = float(rng.uniform(-.15, .15)) - yaw = float(rng.uniform(-3., 3.)) dt = dt_values[i % len(dt_values)] active = i % 137 != 0 valid = i % 211 != 0 if i % 307 == 0: dt = .101 - model, mirror = line(offset, heading, speed), line(-offset, -heading, speed) + model, mirror = line(offset, heading), line(-offset, -heading) if i % 401 == 0: model.position.y[4] = mirror.position.y[4] = math.nan - out = controller.update(model, desired, speed=speed, dt=dt, yaw_rate=yaw, active=active, valid=valid) - other = mirrored.update(mirror, -desired, speed=speed, dt=dt, yaw_rate=-yaw, active=active, valid=valid) + out = controller.update(model, desired, speed=speed, dt=dt, active=active, valid=valid) + other = mirrored.update(mirror, -desired, speed=speed, dt=dt, active=active, valid=valid) expected_valid = active and valid and dt <= .1 and i % 401 != 0 assert out.valid == other.valid == expected_valid previous = np.array([c0, c1]) if expected_valid: - station = min(30., max(7., speed)) - target = (max(-5.11, min(5.11, offset+station*math.sin(heading))), max(-.5, min(.5, heading))) - # Independent scalar slew oracle. Valid measured yaw cannot alter demand. + target = (max(-5.11, min(5.11, offset+7.*math.sin(heading))), max(-.5, min(.5, max(7., speed)*desired))) c0 += max(-4.*dt, min(4.*dt, target[0]-c0)) c1 += max(-.5*dt, min(.5*dt, target[1]-c1)) step = abs(np.array([controller.c0, controller.c1])-previous) @@ -102,14 +99,12 @@ def run(cycles, seed, output, opendbc_revision=PINNED_OPENDBC): selected = float(np.clip(scalar, low, high)) offset, heading = (selected, 0.) if field == 0 else (0., selected) controller.c0, controller.c1 = offset, heading - out = controller.update(line(offset-20.*math.sin(heading), heading), heading/20., speed=20., dt=.01) + out = controller.update(line(offset), heading/20., speed=20., dt=.01) check_raw_packing(wire, controller, out) boundary_cases += 1 report = {'seed': seed, 'random_cycles': cycles, 'mirrored_core_updates': cycles, '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, - 'valid_yaw_does_not_affect_targets_checked': True, - 'shared_model_point_checked': True, 'direct_raw_float32_packing_matches_host_output': True, 'max_continuous_step_c0_c1': 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 e44e0167ae..2f3ca47db9 100644 --- a/tools/ford_pscm_lab/test_model_action_replay.py +++ b/tools/ford_pscm_lab/test_model_action_replay.py @@ -1,11 +1,8 @@ -import hashlib - import numpy as np import pytest from openpilot.selfdrive.controls.lib.ford_path import FordPath from tools.ford_pscm_lab import model_action_replay as replay -from tools.ford_pscm_lab import damping_replay def test_service_sampling_keeps_original_gaps_and_never_pulls_future_inputs(): @@ -28,35 +25,6 @@ def test_dependency_mismatch_fails_before_replaying(monkeypatch): replay.verify_dependency() -@pytest.mark.parametrize('revision', ['HEAD', '744a97d9b', '--help', 'z'*40]) -def test_archived_controller_requires_an_immutable_commit(revision): - with pytest.raises(ValueError, match='immutable'): - replay.load_controller(revision) - - -def test_archived_loader_uses_exact_source_and_records_its_hash(monkeypatch): - # Unit tests must also work in shallow checkouts. Full replays verify real archived code. - calls = [] - source = b'archived_value = 42\n' - - def read_source(command): - calls.append(command) - return source - - monkeypatch.setattr(replay.subprocess, 'check_output', read_source) - replay.load_controller.cache_clear() - try: - for commit in (replay.V1_REVISION, replay.V2_REVISION, replay.V3_REVISION, replay.V4_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) == 4 - finally: - replay.load_controller.cache_clear() - - def test_explicit_stress_dependency_still_requires_an_exact_match(monkeypatch): monkeypatch.setattr(replay, 'revision', lambda _: 'deployment_commit') replay.verify_dependency('deployment_commit') @@ -71,32 +39,6 @@ def test_source_route_directory_cannot_be_overwritten(tmp_path): replay.run(tmp_path, tmp_path/'selected_controller') -def test_current_replay_rejects_missing_model_clocks_before_comparing_outputs(tmp_path, monkeypatch): - source = tmp_path/'source' - source.mkdir() - np.savez(source/'route.npz', model_paths=np.zeros((1, 4, 33))) - monkeypatch.setattr(replay, 'verify_dependency', lambda: tmp_path) - with pytest.raises(ValueError, match='requires original model_position_t and model_orientation_t'): - replay.run(source, tmp_path/'output') - assert not (tmp_path/'output').exists() - - -def test_extract_models_preserves_both_actual_clocks_and_rejects_count_mismatch(): - clocks = np.array([[0., .3, 1.7]]) - raw = {'model_paths': np.zeros((1, 4, 3)), 'model_position_t': clocks, 'model_orientation_t': clocks+.01} - model, = replay.extract_models(raw) - np.testing.assert_array_equal(model.position.t, clocks[0]) - np.testing.assert_array_equal(model.orientation.t, clocks[0]+.01) - raw['model_orientation_t'] = clocks[:, :2] - with pytest.raises(ValueError, match='must match'): - replay.extract_models(raw) - - -def test_historical_damping_replay_rejects_current_controller(tmp_path): - with pytest.raises(ValueError, match='historical damping replay requires unchanged C1'): - damping_replay.run(tmp_path/'source', tmp_path/'output', candidate_version='current') - - @pytest.mark.parametrize('field,value', [(0, 5.12), (1, .501), (2, .00002), (3, .000001), (0, np.nan)]) def test_field_validation_catches_range_and_zero_c2_c3_violations(field, value): command = np.zeros((2, 4))