Ford: restore original selected-curvature v1 controller

Restore the exact tracked tree from 5fc16abc7, identified in the
9b and 9e strong-tracking routes, including opendbc c21a9013 and
the original 100 Hz CAN FD cadence. C0 samples model y at 7 m;
C1 is max(7 m, speed times 1 s) times selected desired curvature.

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

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

Assisted-by: OpenAI Codex
This commit is contained in:
Isaac Barham
2026-09-08 21:44:44 -04:00
parent 00de176331
commit a7d70e2b08
32 changed files with 183 additions and 5377 deletions
-60
View File
@@ -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 v1v6 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
```
@@ -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"
}
}
-88
View File
@@ -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.7643.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.
@@ -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"
}
}
+65 -62
View File
@@ -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 2100 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 v1v6 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.
-86
View File
@@ -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, 173175.4 s | Mean C0 magnitude +0.032 m, maximum change 0.07 m |
| Earlier right entry, 637640 s | Mean magnitude +0.028 m, maximum change 0.10 m |
| Earlier right exit, 642.7643.852 s | All 115 commands unchanged |
| Driver-clean low requests above 8 m/s, routes9b/9e | Mean absolute change 0.0018 / 0.0014 m; maximum 0.02 m |
| All eligible samples on either recent route | Maximum absolute command change 0.13 m |
C1 and command eligibility are exactly identical to v3 on both recent routes.
Some command signs change near zero: this is an intended consequence of using
the full transform, not proof those corrections improve driving. Entry windows
include driver input, reported in the validation record. The magnitude changes
above describe controller C0, not measured lateral vehicle displacement.
The earlier v1 archive is also reproduced exactly on routes90/95, separately
from the current controller pass. All replay uses original timestamps;
publication times proxy computation, exact consumed model frames and causal
carState are retained, and complete SubMaster health is unavailable. The
recorded model and vehicle motion remain fixed. There is no measured physical
improvement, stability result or new desired-versus-actual steering trajectory.
Final validation passes 374 tests and 26 subtests with 100% controller statement
and branch coverage, 299,604 original route cycles and 817,346 Float32/CAN
round trips. The controller is 190 total lines / 122 code lines excluding
blanks, comments and docstrings; two control states persist.
See `ford_model_action_full_prediction_validation.json` for final test counts,
coverage, dependency pins, source hashes and route/packing results. The initial
uncapped variant was evaluated in a separate lab file before editing production;
final route checks execute the production v4 source.
## Reproduce and select
Use the dependencies and combined suite command in the
[drive-test guide](ford_model_action_drive_test.md). For the recent routes:
```sh
python -m tools.ford_pscm_lab.damping_replay /path/to/route9e/rlogs --baseline v3 --candidate current --window left_entry 173 175.4 --window left_peak 175.4 178.3 --window left_exit 178.3 180.5 --window reversal 728 734 --output /path/to/separate/route9e-results
python -m tools.ford_pscm_lab.damping_replay /path/to/route9b/rlogs --baseline v3 --candidate current --window right_entry 637 640 --window right_exit 642.7 643.852 --output /path/to/separate/route9b-results
python -m tools.ford_pscm_lab.stress_model_action --cycles 200000 --seed 20260907 --opendbc-revision c21a9013700734dd20b09e05aa68329ad8cc20f9 --output /path/to/stress.json
```
The same **Selected-Action Path Tracking (Experimental)** toggle selects v4
on the CAN FD F-150 Lightning. An installation with the toggle already enabled
selects v4 after updating and restarting controlsd. Diagnostics identify
`model-action-c0-c1-prediction-v4`; `calibration_approved=false` remains explicit.
Deployment branch: `sunnypilot/sunnypilot`, `hiimisaac-dev`. This work does not
install software on the device or change its settings.
@@ -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"
}
}
@@ -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."
]
}
-67
View File
@@ -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.
@@ -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"
}
}
-126
View File
@@ -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, 173175.4 s | Mean C0 magnitude +0.143 m; same 2.0 m level reached 0.203 s earlier |
| route9e left peak, 175.4178.3 s | Mean magnitude +0.095 m; prediction also increases some late demand |
| route9e reversal, 728734 s | Peak C0 magnitude 0.24 → 0.22 m |
| route9b right exit, 642.7643.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.
@@ -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"
}
}
-130
View File
@@ -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.010.46 s, two unchanged |
| C1 below 0.1 rad into the turn | 0.10 s earlier median; all seven 0.040.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, 040 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).
File diff suppressed because it is too large Load Diff
-632
View File
@@ -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
}
}
}
}
}
}
+3 -7
View File
@@ -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)
@@ -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.)}
@@ -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.)
@@ -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)
@@ -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)
@@ -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
@@ -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):
@@ -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.
@@ -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)
-6
View File
@@ -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
@@ -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"
@@ -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
-173
View File
@@ -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)
+10 -48
View File
@@ -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}}
+11 -16
View File
@@ -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),
@@ -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))