From 4bd841eccd116f535a2ee99b920a631d1718f0cc Mon Sep 17 00:00:00 2001 From: Isaac Barham Date: Tue, 8 Sep 2026 13:53:15 -0400 Subject: [PATCH] Ford: verify unwind timing and reject incompatible offline replays Compare v7 and recorded v6 unwind instructions on a5 using both common request levels and own-peak thresholds. Preserve the later C0 release and earlier C1 release in the report without claiming a physical improvement. Exercise saturated release through the actual sender for both signs and every send phase. Require original model clocks for current replay extracts and reject current candidates in the historical unchanged-C1 damping replay. Record the complete 585-test suite and refreshed stress evidence. Assisted-by: OpenAI Codex --- docs/ford_model_points.md | 55 +- docs/ford_model_points_unwind.json | 1157 +++++++++++++++++ docs/ford_model_points_validation.json | 245 +++- .../tests/test_ford_model_action_cadence.py | 38 + tools/ford_pscm_lab/damping_replay.py | 10 +- tools/ford_pscm_lab/model_action_replay.py | 20 +- .../ford_pscm_lab/test_model_action_replay.py | 27 + 7 files changed, 1544 insertions(+), 8 deletions(-) create mode 100644 docs/ford_model_points_unwind.json diff --git a/docs/ford_model_points.md b/docs/ford_model_points.md index 6e10e6ed57..18c7fcab02 100644 --- a/docs/ford_model_points.md +++ b/docs/ford_model_points.md @@ -36,7 +36,7 @@ maneuver test reference is explicitly unsupported and disengages this candidate. ## Offline evidence -The broad run passed **572 tests and 9,146 subtests**, with 178 inherited safety +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, @@ -73,5 +73,58 @@ No root cause or physical fix is proven by frozen inputs. The latest a5 road logs used 100 Hz sends, whereas the immediately preceding code revision already changed to 20 Hz; a comparison against that drive also includes the cadence change. +## Unwind comparison + +The follow-up a5 comparison includes all seven clearly separated tight turns and +four completed ordinary bends identified in the full-route command plot. A fifth +bend runs into the next turn and is excluded from the summary. All scored windows +remain continuously paired-active, without control gaps over 30 ms. The tight +turns contain driver input; this compares instructions on frozen inputs, not +unassisted tracking or hypothetical truck motion. + +Measure the first time each instruction falls below the same fixed level on +exit and stays below for 100 ms. All seven tight turns exceed these levels: + +| Instruction | Candidate minus recorded clearance time | +| --- | --- | +| C0 below 0.5 m into the turn | 0.10 s later median; five later by 0.01–0.46 s, two unchanged | +| C1 below 0.1 rad into the turn | 0.10 s earlier median; all seven 0.04–0.17 s earlier | + +These are control-publication times. Actual packet timing also includes the +20 Hz send phase; shifts of only a few tens of milliseconds should not be +interpreted as equally precise changes at the PSCM. + +The report also compares half of each command's own peak: C1 reaches that level +0.25 s earlier in the median tight turn and C0 0.12 s later. Those normalized +crossings have different absolute thresholds when amplitudes differ. Comparing +half the smaller peak at an identical level instead gives C0 earlier in one +turn and later in six. No single threshold captures the whole release waveform. + +For the ordinary bend around 108 s, C0 reaches half of its own peak 0.20 s later; +C1 reaches half-peak 0.24 s earlier. The larger C0 takes 0.95 s longer to fall +below the same 0.05 m threshold. On the last tight turn, C0 clears 0.05 m 0.23 s +later. Near-zero thresholds are sensitive to small residual model offsets; the +complete report retains 90%, 50%, 10%, common-level and near-zero crossings +rather than treating any one threshold as a physical success criterion. + +The plotted candidate target and command largely coincide during these exits: +the longer C0 tail comes from the selected model point continuing to ask for +lateral offset, rather than a retained yaw correction. C1 can release sooner +because it now follows model orientation directly. These results do not show +that every command unwinds earlier, or that the vehicle will unwind earlier. + +Ten additional end-to-end sender cases cover both signs and all five CAN send +phases. After a full-cap turn, a zero model target starts reducing both states on +the first control update and appears on the next scheduled CAN message, 0–40 ms +later at nominal 100 Hz calculation. There is no additional release hold. The +unchanged slew itself takes 1.28 s of updates to clear 5.11 m C0 and 1.00 s to +clear 0.5 rad C1, plus scheduling to transmit zero. These are software timing +checks, not measured PSCM dynamics. The [unwind report](ford_model_points_unwind.json) +records the episode timings, method and source hashes. + +Older offline utilities now fail explicitly when their input lacks original +model clocks or their historical unchanged-C1 comparison cannot support v7. +This prevents missing model inputs from producing an all-invalid apparent match. + `calibration_approved=false` remains. Installation instructions and restore behavior are in the [drive-test guide](ford_model_action_drive_test.md). diff --git a/docs/ford_model_points_unwind.json b/docs/ford_model_points_unwind.json new file mode 100644 index 0000000000..c7c667564b --- /dev/null +++ b/docs/ford_model_points_unwind.json @@ -0,0 +1,1157 @@ +{ + "scope": "a5 recorded v6 vs v7 commands on identical recorded inputs; no counterfactual steering or plant response.", + "method": "Signed request into each turn. After each own peak, first below 90/50/10% held for 100 ms; near-zero is <=0.05 m C0 or <=0.01 rad C1 into the original turn. Common half is half the smaller peak; fixed clearance is <=0.5 m C0 or <=0.1 rad C1, scored only when both peaks exceed it. Negative timing delta means earlier. Windows must remain paired-active without >30 ms gaps. Driver inputs are retained and explicitly flagged; these are command comparisons, not driver-free tracking scores. Times are publication proxies; CAN scheduling is tested separately across all five send phases.", + "summary": { + "tight": { + "episodes": 7, + "c0": { + "below_90pct_s": { + "n": 7, + "median_s": 0.0, + "min_s": -0.12269046900000546, + "max_s": 0.04266988000000538, + "earlier": 2, + "later": 2 + }, + "below_50pct_s": { + "n": 7, + "median_s": 0.12129962300002717, + "min_s": 0.0848213179999675, + "max_s": 0.8246327600000001, + "earlier": 0, + "later": 7 + }, + "below_10pct_s": { + "n": 7, + "median_s": 0.1376267710000434, + "min_s": 0.0, + "max_s": 0.45046578099999124, + "earlier": 0, + "later": 6 + }, + "near_zero_s": { + "n": 5, + "median_s": 0.1399018539999588, + "min_s": -0.5224231490000193, + "max_s": 1.9510819790000085, + "earlier": 1, + "later": 4 + }, + "common_half_s": { + "n": 7, + "median_s": 0.07685077599995793, + "min_s": -0.2591174110000338, + "max_s": 0.5661551039999893, + "earlier": 1, + "later": 6 + }, + "fixed_clearance_s": { + "n": 7, + "median_s": 0.10080577899998389, + "min_s": 0.0, + "max_s": 0.4609723959999883, + "earlier": 0, + "later": 5 + } + }, + "c1": { + "below_90pct_s": { + "n": 7, + "median_s": -0.3856273459999784, + "min_s": -0.5246007750000103, + "max_s": 0.5545458859999997, + "earlier": 6, + "later": 1 + }, + "below_50pct_s": { + "n": 7, + "median_s": -0.2540558009999927, + "min_s": -0.46442294700000275, + "max_s": -0.21144918100003451, + "earlier": 7, + "later": 0 + }, + "below_10pct_s": { + "n": 6, + "median_s": -0.15774414349999688, + "min_s": -0.20159088100001554, + "max_s": 0.0, + "earlier": 5, + "later": 0 + }, + "near_zero_s": { + "n": 5, + "median_s": 0.0, + "min_s": -0.148710938000022, + "max_s": 1.5534367700000047, + "earlier": 2, + "later": 2 + }, + "common_half_s": { + "n": 7, + "median_s": -0.2528352759999848, + "min_s": -0.45502116000000115, + "max_s": -0.15168304499999863, + "earlier": 7, + "later": 0 + }, + "fixed_clearance_s": { + "n": 7, + "median_s": -0.10267424500000288, + "min_s": -0.1688626670000417, + "max_s": -0.040530361999969955, + "earlier": 7, + "later": 0 + } + } + }, + "ordinary": { + "episodes": 4, + "c0": { + "below_90pct_s": { + "n": 4, + "median_s": 0.49771416100000465, + "min_s": 0.2530698499999744, + "max_s": 3.195503137000003, + "earlier": 0, + "later": 4 + }, + "below_50pct_s": { + "n": 4, + "median_s": 0.3417062340000143, + "min_s": 0.19507780300000377, + "max_s": 0.4595077099999685, + "earlier": 0, + "later": 4 + }, + "below_10pct_s": { + "n": 4, + "median_s": 0.11352378200000146, + "min_s": 0.09310488600004874, + "max_s": 1.1537201169999776, + "earlier": 0, + "later": 4 + }, + "near_zero_s": { + "n": 4, + "median_s": 0.6988682280000234, + "min_s": 0.40513631699997177, + "max_s": 3.1885321799999815, + "earlier": 0, + "later": 4 + }, + "common_half_s": { + "n": 4, + "median_s": 1.0263607459999946, + "min_s": 0.7940629349999995, + "max_s": 2.846801904000017, + "earlier": 0, + "later": 4 + }, + "fixed_clearance_s": { + "n": 2, + "median_s": 1.97568246000003, + "min_s": 1.5486082510000188, + "max_s": 2.4027566690000413, + "earlier": 0, + "later": 2 + } + }, + "c1": { + "below_90pct_s": { + "n": 4, + "median_s": -0.0037014085000066643, + "min_s": -0.25255103900002496, + "max_s": 0.3999567310000316, + "earlier": 2, + "later": 1 + }, + "below_50pct_s": { + "n": 4, + "median_s": -0.17102558000001977, + "min_s": -0.2489269939999872, + "max_s": -0.046780566999984785, + "earlier": 4, + "later": 0 + }, + "below_10pct_s": { + "n": 4, + "median_s": -0.10130154349999998, + "min_s": -0.29764589699999533, + "max_s": 0.05121605200002932, + "earlier": 2, + "later": 1 + }, + "near_zero_s": { + "n": 4, + "median_s": -0.022888650000027155, + "min_s": -0.10325871399999187, + "max_s": 0.24679704999999785, + "earlier": 2, + "later": 1 + }, + "common_half_s": { + "n": 4, + "median_s": -0.22263683349999042, + "min_s": -0.2573995380000156, + "max_s": -0.09939024700003074, + "earlier": 4, + "later": 0 + }, + "fixed_clearance_s": { + "n": 2, + "median_s": -0.17009618150001415, + "min_s": -0.24998850800000127, + "max_s": -0.09020385500002703, + "earlier": 2, + "later": 0 + } + } + } + }, + "episodes": [ + { + "name": "tight_30s", + "window_s": [ + 29.0, + 37.0 + ], + "cohort": "tight", + "paired_continuously_active": true, + "overlaps_next_turn": false, + "turn_sign": -1.0, + "driver_input_present": true, + "fields": { + "c0": { + "recorded": { + "peak": 3.5899999141693115, + "peak_time_s": 30.240929207000008, + "below_90pct_s": 30.577019832000005, + "below_50pct_s": 32.081712382999996, + "below_10pct_s": 33.781046028000006, + "near_zero_s": 34.723653215, + "common_half_s": 32.34019003900001, + "fixed_clearance_s": 33.478672591000006 + }, + "candidate": { + "peak": 3.3899999999999997, + "peak_time_s": 30.219843945999997, + "below_90pct_s": 30.454329363, + "below_50pct_s": 32.906345142999996, + "below_10pct_s": 34.231511809, + "near_zero_s": 36.67473519400001, + "common_half_s": 32.906345142999996, + "fixed_clearance_s": 33.939644986999994 + }, + "common_half_threshold": 1.6949999999999998, + "fixed_clearance_threshold": 0.5, + "candidate_minus_recorded_seconds": { + "below_90pct_s": -0.12269046900000546, + "below_50pct_s": 0.8246327600000001, + "below_10pct_s": 0.45046578099999124, + "near_zero_s": 1.9510819790000085, + "common_half_s": 0.5661551039999893, + "fixed_clearance_s": 0.4609723959999883 + } + }, + "c1": { + "recorded": { + "peak": 0.4975000023841858, + "peak_time_s": 30.301567696000006, + "below_90pct_s": 30.512954623, + "below_50pct_s": 33.176987279, + "below_10pct_s": 34.231511809, + "near_zero_s": 34.879881131999994, + "common_half_s": 33.176987279, + "fixed_clearance_s": 33.781046028000006 + }, + "candidate": { + "peak": 0.5, + "peak_time_s": 30.374895092000003, + "below_90pct_s": 31.067500509, + "below_50pct_s": 32.925253477, + "below_10pct_s": 34.082231445000005, + "near_zero_s": 36.433317902, + "common_half_s": 32.925253477, + "fixed_clearance_s": 33.67845946600001 + }, + "common_half_threshold": 0.2487500011920929, + "fixed_clearance_threshold": 0.1, + "candidate_minus_recorded_seconds": { + "below_90pct_s": 0.5545458859999997, + "below_50pct_s": -0.25173380200000395, + "below_10pct_s": -0.149280363999992, + "near_zero_s": 1.5534367700000047, + "common_half_s": -0.25173380200000395, + "fixed_clearance_s": -0.10258656199999905 + } + } + } + }, + { + "name": "tight_101s", + "window_s": [ + 98.0, + 106.0 + ], + "cohort": "tight", + "paired_continuously_active": true, + "overlaps_next_turn": false, + "turn_sign": -1.0, + "driver_input_present": true, + "fields": { + "c0": { + "recorded": { + "peak": 2.7899999618530273, + "peak_time_s": 100.93407044199999, + "below_90pct_s": 101.180834951, + "below_50pct_s": 103.082122574, + "below_10pct_s": 104.038050498, + "near_zero_s": 104.48275043400001, + "common_half_s": 103.13559656400001, + "fixed_clearance_s": 103.7941135 + }, + "candidate": { + "peak": 2.5300000000000002, + "peak_time_s": 100.93407044199999, + "below_90pct_s": 101.180834951, + "below_50pct_s": 103.4372139, + "below_10pct_s": 104.38265232600001, + "near_zero_s": 104.535920273, + "common_half_s": 103.4372139, + "fixed_clearance_s": 104.038050498 + }, + "common_half_threshold": 1.2650000000000001, + "fixed_clearance_threshold": 0.5, + "candidate_minus_recorded_seconds": { + "below_90pct_s": 0.0, + "below_50pct_s": 0.35509132600000726, + "below_10pct_s": 0.34460182800000894, + "near_zero_s": 0.053169838999991725, + "common_half_s": 0.3016173359999925, + "fixed_clearance_s": 0.24393699800000945 + } + }, + "c1": { + "recorded": { + "peak": 0.35199999809265137, + "peak_time_s": 100.79037499800002, + "below_90pct_s": 102.07847973800001, + "below_50pct_s": 103.6570674, + "below_10pct_s": 104.434869094, + "near_zero_s": 104.58326638999999, + "common_half_s": 103.6570674, + "fixed_clearance_s": 103.94018452000002 + }, + "candidate": { + "peak": 0.495, + "peak_time_s": 100.93407044199999, + "below_90pct_s": 101.63742160299999, + "below_50pct_s": 103.192644453, + "below_10pct_s": 104.23327821299999, + "near_zero_s": 104.535920273, + "common_half_s": 103.505384355, + "fixed_clearance_s": 103.83751027500001 + }, + "common_half_threshold": 0.17599999904632568, + "fixed_clearance_threshold": 0.1, + "candidate_minus_recorded_seconds": { + "below_90pct_s": -0.44105813500002, + "below_50pct_s": -0.46442294700000275, + "below_10pct_s": -0.20159088100001554, + "near_zero_s": -0.04734611699998936, + "common_half_s": -0.15168304499999863, + "fixed_clearance_s": -0.10267424500000288 + } + } + } + }, + { + "name": "tight_204s", + "window_s": [ + 201.0, + 210.0 + ], + "cohort": "tight", + "paired_continuously_active": true, + "overlaps_next_turn": false, + "turn_sign": 1.0, + "driver_input_present": true, + "fields": { + "c0": { + "recorded": { + "peak": 3.25, + "peak_time_s": 204.452507826, + "below_90pct_s": 204.711612392, + "below_50pct_s": 205.71895763700002, + "below_10pct_s": 206.84365971900002, + "near_zero_s": 208.16542689100004, + "common_half_s": 205.72692817900003, + "fixed_clearance_s": 206.58940793700003 + }, + "candidate": { + "peak": 3.12, + "peak_time_s": 204.42139036100002, + "below_90pct_s": 204.754282272, + "below_50pct_s": 205.80377895499998, + "below_10pct_s": 206.893554451, + "near_zero_s": 207.64300374200002, + "common_half_s": 205.80377895499998, + "fixed_clearance_s": 206.60243090400002 + }, + "common_half_threshold": 1.56, + "fixed_clearance_threshold": 0.5, + "candidate_minus_recorded_seconds": { + "below_90pct_s": 0.04266988000000538, + "below_50pct_s": 0.0848213179999675, + "below_10pct_s": 0.04989473199998429, + "near_zero_s": -0.5224231490000193, + "common_half_s": 0.07685077599995793, + "fixed_clearance_s": 0.01302296699998351 + } + }, + "c1": { + "recorded": { + "peak": 0.5, + "peak_time_s": 203.71940647500003, + "below_90pct_s": 205.601266495, + "below_50pct_s": 206.08317727000002, + "below_10pct_s": 207.096331734, + "near_zero_s": 209.14135593100002, + "common_half_s": 206.08317727000002, + "fixed_clearance_s": 206.642961266 + }, + "candidate": { + "peak": 0.5, + "peak_time_s": 203.41543419100003, + "below_90pct_s": 205.21563914900003, + "below_50pct_s": 205.871728089, + "below_10pct_s": 207.096331734, + "near_zero_s": 209.14135593100002, + "common_half_s": 205.871728089, + "fixed_clearance_s": 206.60243090400002 + }, + "common_half_threshold": 0.25, + "fixed_clearance_threshold": 0.1, + "candidate_minus_recorded_seconds": { + "below_90pct_s": -0.3856273459999784, + "below_50pct_s": -0.21144918100003451, + "below_10pct_s": 0.0, + "near_zero_s": 0.0, + "common_half_s": -0.21144918100003451, + "fixed_clearance_s": -0.040530361999969955 + } + } + } + }, + { + "name": "tight_290s", + "window_s": [ + 287.0, + 304.0 + ], + "cohort": "tight", + "paired_continuously_active": true, + "overlaps_next_turn": false, + "turn_sign": 1.0, + "driver_input_present": true, + "fields": { + "c0": { + "recorded": { + "peak": 4.28000020980835, + "peak_time_s": 291.38023259600004, + "below_90pct_s": 291.58007872200005, + "below_50pct_s": 294.652892026, + "below_10pct_s": 297.505731785, + "near_zero_s": null, + "common_half_s": 295.01561511500006, + "fixed_clearance_s": 297.31398149600005 + }, + "candidate": { + "peak": 3.95, + "peak_time_s": 291.368099656, + "below_90pct_s": 291.58007872200005, + "below_50pct_s": 294.756497704, + "below_10pct_s": 297.505731785, + "near_zero_s": null, + "common_half_s": 294.756497704, + "fixed_clearance_s": 297.31398149600005 + }, + "common_half_threshold": 1.975, + "fixed_clearance_threshold": 0.5, + "candidate_minus_recorded_seconds": { + "below_90pct_s": 0.0, + "below_50pct_s": 0.10360567800000808, + "below_10pct_s": 0.0, + "near_zero_s": null, + "common_half_s": -0.2591174110000338, + "fixed_clearance_s": 0.0 + } + }, + "c1": { + "recorded": { + "peak": 0.5, + "peak_time_s": 291.208352487, + "below_90pct_s": 294.165487336, + "below_50pct_s": 296.11050717800003, + "below_10pct_s": null, + "near_zero_s": null, + "common_half_s": 296.11050717800003, + "fixed_clearance_s": 297.404249599 + }, + "candidate": { + "peak": 0.5, + "peak_time_s": 290.22304278, + "below_90pct_s": 293.80514934, + "below_50pct_s": 295.65548601800003, + "below_10pct_s": null, + "near_zero_s": null, + "common_half_s": 295.65548601800003, + "fixed_clearance_s": 297.35730984800006 + }, + "common_half_threshold": 0.25, + "fixed_clearance_threshold": 0.1, + "candidate_minus_recorded_seconds": { + "below_90pct_s": -0.3603379959999984, + "below_50pct_s": -0.45502116000000115, + "below_10pct_s": null, + "near_zero_s": null, + "common_half_s": -0.45502116000000115, + "fixed_clearance_s": -0.046939750999968055 + } + } + } + }, + { + "name": "tight_322s", + "window_s": [ + 319.0, + 328.0 + ], + "cohort": "tight", + "paired_continuously_active": true, + "overlaps_next_turn": false, + "turn_sign": -1.0, + "driver_input_present": true, + "fields": { + "c0": { + "recorded": { + "peak": 3.9700000286102295, + "peak_time_s": 322.63175601100005, + "below_90pct_s": 322.904643726, + "below_50pct_s": 324.25051608900003, + "below_10pct_s": 325.66393901500004, + "near_zero_s": null, + "common_half_s": 324.309763061, + "fixed_clearance_s": 325.50392031200005 + }, + "candidate": { + "peak": 3.71, + "peak_time_s": 322.63175601100005, + "below_90pct_s": 322.91897255900005, + "below_50pct_s": 324.34948010000005, + "below_10pct_s": 325.70485142900003, + "near_zero_s": null, + "common_half_s": 324.34948010000005, + "fixed_clearance_s": 325.50392031200005 + }, + "common_half_threshold": 1.855, + "fixed_clearance_threshold": 0.5, + "candidate_minus_recorded_seconds": { + "below_90pct_s": 0.014328833000035957, + "below_50pct_s": 0.09896401100002095, + "below_10pct_s": 0.04091241399999035, + "near_zero_s": null, + "common_half_s": 0.03971703900003831, + "fixed_clearance_s": 0.0 + } + }, + "c1": { + "recorded": { + "peak": 0.5, + "peak_time_s": 322.299259177, + "below_90pct_s": 324.33659532900003, + "below_50pct_s": 324.931697202, + "below_10pct_s": 326.251405185, + "near_zero_s": null, + "common_half_s": 324.931697202, + "fixed_clearance_s": 325.55373651300005 + }, + "candidate": { + "peak": 0.5, + "peak_time_s": 321.89953517600003, + "below_90pct_s": 323.894065735, + "below_50pct_s": 324.678861926, + "below_10pct_s": 326.19749975400003, + "near_zero_s": null, + "common_half_s": 324.678861926, + "fixed_clearance_s": 325.41309978600003 + }, + "common_half_threshold": 0.25, + "fixed_clearance_threshold": 0.1, + "candidate_minus_recorded_seconds": { + "below_90pct_s": -0.4425295940000069, + "below_50pct_s": -0.2528352759999848, + "below_10pct_s": -0.05390543099997558, + "near_zero_s": null, + "common_half_s": -0.2528352759999848, + "fixed_clearance_s": -0.14063672700001462 + } + } + } + }, + { + "name": "tight_340s", + "window_s": [ + 336.0, + 347.0 + ], + "cohort": "tight", + "paired_continuously_active": true, + "overlaps_next_turn": false, + "turn_sign": 1.0, + "driver_input_present": true, + "fields": { + "c0": { + "recorded": { + "peak": 4.519999980926514, + "peak_time_s": 340.598086634, + "below_90pct_s": 340.91626233600005, + "below_50pct_s": 342.40797476500006, + "below_10pct_s": 343.22053899800005, + "near_zero_s": 343.33921467400006, + "common_half_s": 342.44586709500004, + "fixed_clearance_s": 343.21147315800005 + }, + "candidate": { + "peak": 4.25, + "peak_time_s": 340.598086634, + "below_90pct_s": 340.91626233600005, + "below_50pct_s": 342.597023642, + "below_10pct_s": 343.362026375, + "near_zero_s": 343.479116528, + "common_half_s": 342.597023642, + "fixed_clearance_s": 343.329572508 + }, + "common_half_threshold": 2.125, + "fixed_clearance_threshold": 0.5, + "candidate_minus_recorded_seconds": { + "below_90pct_s": 0.0, + "below_50pct_s": 0.18904887699994788, + "below_10pct_s": 0.14148737699997582, + "near_zero_s": 0.1399018539999588, + "common_half_s": 0.15115654699997094, + "fixed_clearance_s": 0.11809934999996585 + } + }, + "c1": { + "recorded": { + "peak": 0.5, + "peak_time_s": 339.83505406300003, + "below_90pct_s": 342.75123392300003, + "below_50pct_s": 343.15934182, + "below_10pct_s": 343.56486573700005, + "near_zero_s": 343.652091847, + "common_half_s": 343.15934182, + "fixed_clearance_s": 343.45914287500005 + }, + "candidate": { + "peak": 0.5, + "peak_time_s": 338.10098615100003, + "below_90pct_s": 342.226633148, + "below_50pct_s": 342.89592961600005, + "below_10pct_s": 343.390365351, + "near_zero_s": 343.702920671, + "common_half_s": 342.89592961600005, + "fixed_clearance_s": 343.290280208 + }, + "common_half_threshold": 0.25, + "fixed_clearance_threshold": 0.1, + "candidate_minus_recorded_seconds": { + "below_90pct_s": -0.5246007750000103, + "below_50pct_s": -0.26341220399996246, + "below_10pct_s": -0.1745003860000338, + "near_zero_s": 0.05082882400000699, + "common_half_s": -0.26341220399996246, + "fixed_clearance_s": -0.1688626670000417 + } + } + } + }, + { + "name": "tight_374s", + "window_s": [ + 370.0, + 380.0 + ], + "cohort": "tight", + "paired_continuously_active": true, + "overlaps_next_turn": false, + "turn_sign": -1.0, + "driver_input_present": true, + "fields": { + "c0": { + "recorded": { + "peak": 4.440000057220459, + "peak_time_s": 374.03466016400006, + "below_90pct_s": 374.337971265, + "below_50pct_s": 376.446774042, + "below_10pct_s": 377.504629587, + "near_zero_s": 378.10658313600004, + "common_half_s": 376.54731860300006, + "fixed_clearance_s": 377.41278509800003 + }, + "candidate": { + "peak": 4.090000000000001, + "peak_time_s": 374.03466016400006, + "below_90pct_s": 374.27741194400005, + "below_50pct_s": 376.56807366500004, + "below_10pct_s": 377.64225635800005, + "near_zero_s": 378.33656807100004, + "common_half_s": 376.56807366500004, + "fixed_clearance_s": 377.513590877 + }, + "common_half_threshold": 2.0450000000000004, + "fixed_clearance_threshold": 0.5, + "candidate_minus_recorded_seconds": { + "below_90pct_s": -0.06055932099997108, + "below_50pct_s": 0.12129962300002717, + "below_10pct_s": 0.1376267710000434, + "near_zero_s": 0.2299849350000045, + "common_half_s": 0.020755061999977897, + "fixed_clearance_s": 0.10080577899998389 + } + }, + "c1": { + "recorded": { + "peak": 0.5, + "peak_time_s": 373.392786745, + "below_90pct_s": 376.58558943900005, + "below_50pct_s": 377.11858247900005, + "below_10pct_s": 377.856980516, + "near_zero_s": 378.38592790200005, + "common_half_s": 377.11858247900005, + "fixed_clearance_s": 377.550973906 + }, + "candidate": { + "peak": 0.5, + "peak_time_s": 372.73047255000006, + "below_90pct_s": 376.24770156600005, + "below_50pct_s": 376.86452667800006, + "below_10pct_s": 377.690772593, + "near_zero_s": 378.237216964, + "common_half_s": 376.86452667800006, + "fixed_clearance_s": 377.44232913900004 + }, + "common_half_threshold": 0.25, + "fixed_clearance_threshold": 0.1, + "candidate_minus_recorded_seconds": { + "below_90pct_s": -0.33788787299999967, + "below_50pct_s": -0.2540558009999927, + "below_10pct_s": -0.16620792300000176, + "near_zero_s": -0.148710938000022, + "common_half_s": -0.2540558009999927, + "fixed_clearance_s": -0.10864476699998704 + } + } + } + }, + { + "name": "bend_108s", + "window_s": [ + 105.0, + 119.0 + ], + "cohort": "ordinary", + "paired_continuously_active": true, + "overlaps_next_turn": false, + "turn_sign": 1.0, + "driver_input_present": false, + "fields": { + "c0": { + "recorded": { + "peak": 0.20000000298023224, + "peak_time_s": 106.548941925, + "below_90pct_s": 106.73395254500001, + "below_50pct_s": 112.338637133, + "below_10pct_s": 113.45055747600001, + "near_zero_s": 113.08906987499999, + "common_half_s": 112.338637133, + "fixed_clearance_s": null + }, + "candidate": { + "peak": 0.5999999999999996, + "peak_time_s": 109.08088833200001, + "below_90pct_s": 109.92945568200001, + "below_50pct_s": 112.53371493600001, + "below_10pct_s": 113.578768826, + "near_zero_s": 114.03448095400002, + "common_half_s": 113.39171969800002, + "fixed_clearance_s": 111.433984892 + }, + "common_half_threshold": 0.10000000149011612, + "fixed_clearance_threshold": 0.5, + "candidate_minus_recorded_seconds": { + "below_90pct_s": 3.195503137000003, + "below_50pct_s": 0.19507780300000377, + "below_10pct_s": 0.12821134999998662, + "near_zero_s": 0.9454110790000243, + "common_half_s": 1.053082565000011, + "fixed_clearance_s": null + } + }, + "c1": { + "recorded": { + "peak": 0.07150000333786011, + "peak_time_s": 108.389598889, + "below_90pct_s": 109.92945568200001, + "below_50pct_s": 112.58129804600001, + "below_10pct_s": 113.981820372, + "near_zero_s": 113.53178583500001, + "common_half_s": 112.58129804600001, + "fixed_clearance_s": null + }, + "candidate": { + "peak": 0.07150000000000001, + "peak_time_s": 108.389598889, + "below_90pct_s": 109.92945568200001, + "below_50pct_s": 112.338637133, + "below_10pct_s": 113.779217285, + "near_zero_s": 113.42852712100002, + "common_half_s": 112.338637133, + "fixed_clearance_s": null + }, + "common_half_threshold": 0.035750000000000004, + "fixed_clearance_threshold": 0.1, + "candidate_minus_recorded_seconds": { + "below_90pct_s": 0.0, + "below_50pct_s": -0.2426609130000088, + "below_10pct_s": -0.20260308699999996, + "near_zero_s": -0.10325871399999187, + "common_half_s": -0.2426609130000088, + "fixed_clearance_s": null + } + } + } + }, + { + "name": "bend_167s", + "window_s": [ + 161.0, + 177.0 + ], + "cohort": "ordinary", + "paired_continuously_active": true, + "overlaps_next_turn": false, + "turn_sign": -1.0, + "driver_input_present": true, + "fields": { + "c0": { + "recorded": { + "peak": 0.20999999344348907, + "peak_time_s": 167.902585901, + "below_90pct_s": 167.974404155, + "below_50pct_s": 168.98050512999998, + "below_10pct_s": 171.32639237400002, + "near_zero_s": 169.49194768, + "common_half_s": 168.98050512999998, + "fixed_clearance_s": null + }, + "candidate": { + "peak": 0.75, + "peak_time_s": 167.87432923699998, + "below_90pct_s": 168.2780269, + "below_50pct_s": 169.423584081, + "below_10pct_s": 172.480112491, + "near_zero_s": 172.68047986, + "common_half_s": 171.827307034, + "fixed_clearance_s": 168.98050512999998 + }, + "common_half_threshold": 0.10499999672174454, + "fixed_clearance_threshold": 0.5, + "candidate_minus_recorded_seconds": { + "below_90pct_s": 0.3036227449999842, + "below_50pct_s": 0.44307895100001815, + "below_10pct_s": 1.1537201169999776, + "near_zero_s": 3.1885321799999815, + "common_half_s": 2.846801904000017, + "fixed_clearance_s": null + } + }, + "c1": { + "recorded": { + "peak": 0.07100000232458115, + "peak_time_s": 167.723783749, + "below_90pct_s": 168.37932592200002, + "below_50pct_s": 169.52297432800003, + "below_10pct_s": 172.330038759, + "near_zero_s": 171.425926795, + "common_half_s": 169.52297432800003, + "fixed_clearance_s": null + }, + "candidate": { + "peak": 0.07200000000000006, + "peak_time_s": 167.272833955, + "below_90pct_s": 168.126774883, + "below_50pct_s": 169.423584081, + "below_10pct_s": 172.38125481100002, + "near_zero_s": 171.672723845, + "common_half_s": 169.423584081, + "fixed_clearance_s": null + }, + "common_half_threshold": 0.03550000116229057, + "fixed_clearance_threshold": 0.1, + "candidate_minus_recorded_seconds": { + "below_90pct_s": -0.25255103900002496, + "below_50pct_s": -0.09939024700003074, + "below_10pct_s": 0.05121605200002932, + "near_zero_s": 0.24679704999999785, + "common_half_s": -0.09939024700003074, + "fixed_clearance_s": null + } + } + } + }, + { + "name": "bend_213s", + "window_s": [ + 209.0, + 226.0 + ], + "cohort": "ordinary", + "paired_continuously_active": true, + "overlaps_next_turn": false, + "turn_sign": -1.0, + "driver_input_present": true, + "fields": { + "c0": { + "recorded": { + "peak": 0.6000000238418579, + "peak_time_s": 212.03470382600005, + "below_90pct_s": 212.97991787900003, + "below_50pct_s": 214.73159144800002, + "below_10pct_s": 217.028482467, + "near_zero_s": 217.12158735300005, + "common_half_s": 214.73159144800002, + "fixed_clearance_s": 213.083366259 + }, + "candidate": { + "peak": 0.8200000000000003, + "peak_time_s": 212.67528037000005, + "below_90pct_s": 213.232987729, + "below_50pct_s": 214.97192496500003, + "below_10pct_s": 217.12158735300005, + "near_zero_s": 217.52672367000002, + "common_half_s": 215.52565438300002, + "fixed_clearance_s": 214.63197451000002 + }, + "common_half_threshold": 0.30000001192092896, + "fixed_clearance_threshold": 0.5, + "candidate_minus_recorded_seconds": { + "below_90pct_s": 0.2530698499999744, + "below_50pct_s": 0.2403335170000105, + "below_10pct_s": 0.09310488600004874, + "near_zero_s": 0.40513631699997177, + "common_half_s": 0.7940629349999995, + "fixed_clearance_s": 1.5486082510000188 + } + }, + "c1": { + "recorded": { + "peak": 0.13600000739097595, + "peak_time_s": 212.745246436, + "below_90pct_s": 213.24039054600001, + "below_50pct_s": 215.018705532, + "below_10pct_s": 217.326128364, + "near_zero_s": 217.37190566400005, + "common_half_s": 215.174537719, + "fixed_clearance_s": 214.07720312900005 + }, + "candidate": { + "peak": 0.13250000000000006, + "peak_time_s": 212.435732395, + "below_90pct_s": 213.232987729, + "below_50pct_s": 214.97192496500003, + "below_10pct_s": 217.028482467, + "near_zero_s": 217.326128364, + "common_half_s": 214.97192496500003, + "fixed_clearance_s": 213.82721462100005 + }, + "common_half_threshold": 0.06625000000000003, + "fixed_clearance_threshold": 0.1, + "candidate_minus_recorded_seconds": { + "below_90pct_s": -0.007402817000013329, + "below_50pct_s": -0.046780566999984785, + "below_10pct_s": -0.29764589699999533, + "near_zero_s": -0.04577730000005431, + "common_half_s": -0.20261275399997203, + "fixed_clearance_s": -0.24998850800000127 + } + } + } + }, + { + "name": "bend_269s", + "window_s": [ + 265.0, + 280.0 + ], + "cohort": "ordinary", + "paired_continuously_active": true, + "overlaps_next_turn": false, + "turn_sign": -1.0, + "driver_input_present": true, + "fields": { + "c0": { + "recorded": { + "peak": 0.5899999737739563, + "peak_time_s": 269.0605254, + "below_90pct_s": 269.30781249, + "below_50pct_s": 272.29716238400005, + "below_10pct_s": 274.200510886, + "near_zero_s": 274.200510886, + "common_half_s": 272.29716238400005, + "fixed_clearance_s": 269.503329327 + }, + "candidate": { + "peak": 0.7000000000000002, + "peak_time_s": 268.90545461500005, + "below_90pct_s": 269.99961806700003, + "below_50pct_s": 272.756670094, + "below_10pct_s": 274.29934710000003, + "near_zero_s": 274.65283626300004, + "common_half_s": 273.296801311, + "fixed_clearance_s": 271.90608599600006 + }, + "common_half_threshold": 0.29499998688697815, + "fixed_clearance_threshold": 0.5, + "candidate_minus_recorded_seconds": { + "below_90pct_s": 0.6918055770000251, + "below_50pct_s": 0.4595077099999685, + "below_10pct_s": 0.0988362140000163, + "near_zero_s": 0.4523253770000224, + "common_half_s": 0.9996389269999781, + "fixed_clearance_s": 2.4027566690000413 + } + }, + "c1": { + "recorded": { + "peak": 0.12250000238418579, + "peak_time_s": 269.25779382, + "below_90pct_s": 269.503329327, + "below_50pct_s": 272.756670094, + "below_10pct_s": 274.200510886, + "near_zero_s": 274.29934710000003, + "common_half_s": 272.76514263800004, + "fixed_clearance_s": 270.09909269400003 + }, + "candidate": { + "peak": 0.118, + "peak_time_s": 269.100449764, + "below_90pct_s": 269.90328605800005, + "below_50pct_s": 272.5077431, + "below_10pct_s": 274.200510886, + "near_zero_s": 274.29934710000003, + "common_half_s": 272.5077431, + "fixed_clearance_s": 270.008888839 + }, + "common_half_threshold": 0.059, + "fixed_clearance_threshold": 0.1, + "candidate_minus_recorded_seconds": { + "below_90pct_s": 0.3999567310000316, + "below_50pct_s": -0.2489269939999872, + "below_10pct_s": 0.0, + "near_zero_s": 0.0, + "common_half_s": -0.2573995380000156, + "fixed_clearance_s": -0.09020385500002703 + } + } + } + }, + { + "name": "bend_365s", + "window_s": [ + 360.0, + 370.0 + ], + "cohort": "ordinary", + "paired_continuously_active": true, + "overlaps_next_turn": true, + "turn_sign": -1.0, + "driver_input_present": true, + "fields": { + "c0": { + "recorded": { + "peak": 0.5899999737739563, + "peak_time_s": 364.739029753, + "below_90pct_s": 365.945649611, + "below_50pct_s": 368.89615960500004, + "below_10pct_s": null, + "near_zero_s": null, + "common_half_s": 368.89615960500004, + "fixed_clearance_s": 366.246832137 + }, + "candidate": { + "peak": 0.8099999999999996, + "peak_time_s": 365.29007811900004, + "below_90pct_s": 365.993896069, + "below_50pct_s": 367.037059988, + "below_10pct_s": null, + "near_zero_s": null, + "common_half_s": 368.89615960500004, + "fixed_clearance_s": 366.59549220700006 + }, + "common_half_threshold": 0.29499998688697815, + "fixed_clearance_threshold": 0.5, + "candidate_minus_recorded_seconds": { + "below_90pct_s": 0.04824645799999416, + "below_50pct_s": -1.8590996170000267, + "below_10pct_s": null, + "near_zero_s": null, + "common_half_s": 0.0, + "fixed_clearance_s": 0.34866007000005084 + } + }, + "c1": { + "recorded": { + "peak": 0.13650000095367432, + "peak_time_s": 365.29007811900004, + "below_90pct_s": 366.234948614, + "below_50pct_s": 368.24164409, + "below_10pct_s": null, + "near_zero_s": null, + "common_half_s": 368.24164409, + "fixed_clearance_s": 366.507780704 + }, + "candidate": { + "peak": 0.13750000000000007, + "peak_time_s": 364.63958741600004, + "below_90pct_s": 366.043504152, + "below_50pct_s": 367.84505105000005, + "below_10pct_s": null, + "near_zero_s": null, + "common_half_s": 367.84505105000005, + "fixed_clearance_s": 366.49697023500005 + }, + "common_half_threshold": 0.06825000047683716, + "fixed_clearance_threshold": 0.1, + "candidate_minus_recorded_seconds": { + "below_90pct_s": -0.19144446199999265, + "below_50pct_s": -0.39659303999997064, + "below_10pct_s": null, + "near_zero_s": null, + "common_half_s": -0.39659303999997064, + "fixed_clearance_s": -0.01081046899997773 + } + } + } + } + ], + "source_sha256": { + "/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_model_points/unwind.py": "b1b9c02656353fac63eee92bb45434be2b123241347472e5dd960e61e00e7e1c", + "/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/openpilot/selfdrive/controls/lib/ford_model_action.py": "e7502ab68d04aef52edcffcef4b1d42c04e9008ab6892789aac3a865c100cb6d", + "/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_model_points/a5_commands.npz": "829a6fcb82a8df5e17310501cb17b090c58f604503731b739bbd7f12b783f71a", + "/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_model_points/a5_models.npz": "84cf4b6e0102d8d5a3f6c3e29bf332e929d7f28d0262eb46220fccc631b902aa", + ".cache/ford_routea5/route.npz": "815d1e248ff5c3e5e5cfc11dfbd0690d0d13ddafe1436890e239975a72dae9b8" + } +} diff --git a/docs/ford_model_points_validation.json b/docs/ford_model_points_validation.json index e98b1c1af5..29c8751eb9 100644 --- a/docs/ford_model_points_validation.json +++ b/docs/ford_model_points_validation.json @@ -4,7 +4,7 @@ "scope": "Model-point construction only; physical tracking not validated.", "calibration_approved": false, "tests": { - "passed": 572, + "passed": 585, "subtests_passed": 9146, "inapplicable_skips": 178 }, @@ -13,7 +13,11 @@ "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" + "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": { @@ -387,5 +391,242 @@ "/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_model_points/9e_models.npz": "acacf8f0d16fd986b6d74e4a02d704fa5b5b3327fe66dcbe149806f2a5bc3d65" } } + }, + "stress": { + "seed": 20260908, + "random_cycles": 200000, + "mirrored_core_updates": 200000, + "invalid_or_inactive_resets": 3537, + "field_boundary_cases": 18138, + "float32_can_round_trips": 218138, + "analytic_targets_scalar_slew_and_mirror_checks_pass": true, + "valid_yaw_does_not_affect_targets_checked": true, + "shared_model_point_checked": true, + "direct_raw_float32_packing_matches_host_output": true, + "max_continuous_step_c0_c1": [ + 0.40000000000000147, + 0.05000000000000002 + ], + "calibration_approved": false, + "scope": "Numerical construction only; no PSCM response or closed-loop performance claims.", + "opendbc_import_head": "87ca78e6e641eefb2d654f260a6ab08df3058bd5", + "source_sha256": { + "/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/stress_model_action.py": "0221f85ea2f5757d3b54d077ed8be1faaa6a5d2ed8371de1137f3b6f3d70bf47", + "/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/openpilot/selfdrive/controls/lib/ford_model_action.py": "e7502ab68d04aef52edcffcef4b1d42c04e9008ab6892789aac3a865c100cb6d", + "/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/model_action_replay.py": "95d546cc87c065fa7d581b41382e1ab78bacc4030931b81388cb69c61adf32f7" + } + }, + "unwind": { + "report": "docs/ford_model_points_unwind.json", + "sha256": "7395a4e66a275fd72d744f5a8244f1b9c8840bb7efff2eec225f15b9c033cd6e", + "full_cap_release_send_phase_cases": 10, + "summary": { + "tight": { + "episodes": 7, + "c0": { + "below_90pct_s": { + "n": 7, + "median_s": 0.0, + "min_s": -0.12269046900000546, + "max_s": 0.04266988000000538, + "earlier": 2, + "later": 2 + }, + "below_50pct_s": { + "n": 7, + "median_s": 0.12129962300002717, + "min_s": 0.0848213179999675, + "max_s": 0.8246327600000001, + "earlier": 0, + "later": 7 + }, + "below_10pct_s": { + "n": 7, + "median_s": 0.1376267710000434, + "min_s": 0.0, + "max_s": 0.45046578099999124, + "earlier": 0, + "later": 6 + }, + "near_zero_s": { + "n": 5, + "median_s": 0.1399018539999588, + "min_s": -0.5224231490000193, + "max_s": 1.9510819790000085, + "earlier": 1, + "later": 4 + }, + "common_half_s": { + "n": 7, + "median_s": 0.07685077599995793, + "min_s": -0.2591174110000338, + "max_s": 0.5661551039999893, + "earlier": 1, + "later": 6 + }, + "fixed_clearance_s": { + "n": 7, + "median_s": 0.10080577899998389, + "min_s": 0.0, + "max_s": 0.4609723959999883, + "earlier": 0, + "later": 5 + } + }, + "c1": { + "below_90pct_s": { + "n": 7, + "median_s": -0.3856273459999784, + "min_s": -0.5246007750000103, + "max_s": 0.5545458859999997, + "earlier": 6, + "later": 1 + }, + "below_50pct_s": { + "n": 7, + "median_s": -0.2540558009999927, + "min_s": -0.46442294700000275, + "max_s": -0.21144918100003451, + "earlier": 7, + "later": 0 + }, + "below_10pct_s": { + "n": 6, + "median_s": -0.15774414349999688, + "min_s": -0.20159088100001554, + "max_s": 0.0, + "earlier": 5, + "later": 0 + }, + "near_zero_s": { + "n": 5, + "median_s": 0.0, + "min_s": -0.148710938000022, + "max_s": 1.5534367700000047, + "earlier": 2, + "later": 2 + }, + "common_half_s": { + "n": 7, + "median_s": -0.2528352759999848, + "min_s": -0.45502116000000115, + "max_s": -0.15168304499999863, + "earlier": 7, + "later": 0 + }, + "fixed_clearance_s": { + "n": 7, + "median_s": -0.10267424500000288, + "min_s": -0.1688626670000417, + "max_s": -0.040530361999969955, + "earlier": 7, + "later": 0 + } + } + }, + "ordinary": { + "episodes": 4, + "c0": { + "below_90pct_s": { + "n": 4, + "median_s": 0.49771416100000465, + "min_s": 0.2530698499999744, + "max_s": 3.195503137000003, + "earlier": 0, + "later": 4 + }, + "below_50pct_s": { + "n": 4, + "median_s": 0.3417062340000143, + "min_s": 0.19507780300000377, + "max_s": 0.4595077099999685, + "earlier": 0, + "later": 4 + }, + "below_10pct_s": { + "n": 4, + "median_s": 0.11352378200000146, + "min_s": 0.09310488600004874, + "max_s": 1.1537201169999776, + "earlier": 0, + "later": 4 + }, + "near_zero_s": { + "n": 4, + "median_s": 0.6988682280000234, + "min_s": 0.40513631699997177, + "max_s": 3.1885321799999815, + "earlier": 0, + "later": 4 + }, + "common_half_s": { + "n": 4, + "median_s": 1.0263607459999946, + "min_s": 0.7940629349999995, + "max_s": 2.846801904000017, + "earlier": 0, + "later": 4 + }, + "fixed_clearance_s": { + "n": 2, + "median_s": 1.97568246000003, + "min_s": 1.5486082510000188, + "max_s": 2.4027566690000413, + "earlier": 0, + "later": 2 + } + }, + "c1": { + "below_90pct_s": { + "n": 4, + "median_s": -0.0037014085000066643, + "min_s": -0.25255103900002496, + "max_s": 0.3999567310000316, + "earlier": 2, + "later": 1 + }, + "below_50pct_s": { + "n": 4, + "median_s": -0.17102558000001977, + "min_s": -0.2489269939999872, + "max_s": -0.046780566999984785, + "earlier": 4, + "later": 0 + }, + "below_10pct_s": { + "n": 4, + "median_s": -0.10130154349999998, + "min_s": -0.29764589699999533, + "max_s": 0.05121605200002932, + "earlier": 2, + "later": 1 + }, + "near_zero_s": { + "n": 4, + "median_s": -0.022888650000027155, + "min_s": -0.10325871399999187, + "max_s": 0.24679704999999785, + "earlier": 2, + "later": 1 + }, + "common_half_s": { + "n": 4, + "median_s": -0.22263683349999042, + "min_s": -0.2573995380000156, + "max_s": -0.09939024700003074, + "earlier": 4, + "later": 0 + }, + "fixed_clearance_s": { + "n": 2, + "median_s": -0.17009618150001415, + "min_s": -0.24998850800000127, + "max_s": -0.09020385500002703, + "earlier": 2, + "later": 0 + } + } + } + } } } diff --git a/openpilot/selfdrive/controls/tests/test_ford_model_action_cadence.py b/openpilot/selfdrive/controls/tests/test_ford_model_action_cadence.py index 41d2be271b..77e57a1397 100644 --- a/openpilot/selfdrive/controls/tests/test_ford_model_action_cadence.py +++ b/openpilot/selfdrive/controls/tests/test_ford_model_action_cadence.py @@ -106,3 +106,41 @@ def test_core_slew_per_second_and_actual_panda_acceptance(): assert wire['LatCtlPath_An_Actl'] == pytest.approx(-command.path_angle) assert wire['LatCtlCurv_No_Actl'] == wire['LatCtlCrv_NoRate2_Actl'] == 0. assert frames == list(range(0, 100, 5)) + + +@pytest.mark.parametrize('sign', [-1., 1.]) +@pytest.mark.parametrize('phase', range(5)) +def test_saturated_turn_unwinds_on_first_update_and_next_scheduled_can_frame(sign, phase): + controller, cs = sender() + core = ModelActionController() + cc, sp = structs.CarControl(latActive=True), structs.CarControlSP() + parser = CANParser('ford_lincoln_base_pt', [('LateralMotionControl2', 0)], controller.CAN.main) + change_frame = 200 + phase + first_unwind = first_zero_c0 = first_zero_c1 = None + turn, released = straight(sign*10., sign), straight() + for frame in range(change_frame+135): + command = core.update(turn if frame < change_frame else released, sign*.1, speed=20., dt=.01) + if frame >= change_frame: + elapsed = (frame-change_frame+1)*.01 + assert sign*core.c0 == pytest.approx(max(0., 5.11-4.*elapsed), abs=1e-10) + assert sign*core.c1 == pytest.approx(max(0., .5-.5*elapsed), abs=1e-10) + sp.fordLateralPath.valid = command.valid + sp.fordLateralPath.pathOffset, sp.fordLateralPath.pathAngle = command.path_offset, command.path_angle + _, packets = controller.update(cc.as_reader(), sp, cs, frame*10_000_000) + lateral = [p for p in packets if p[0] == 0x3d6] + if not lateral or frame < change_frame: + continue + parser.update([frame*10_000_000, lateral]) + wire = parser.vl['LateralMotionControl2'] + c0, c1 = -sign*wire['LatCtlPathOffst_L_Actl'], -sign*wire['LatCtlPath_An_Actl'] + assert 0. <= c0 < 5.11 and 0. <= c1 < .5 + assert wire['LatCtlCurv_No_Actl'] == wire['LatCtlCrv_NoRate2_Actl'] == 0. + if first_unwind is None: + first_unwind = frame + if c0 == 0. and first_zero_c0 is None: + first_zero_c0 = frame + if c1 == 0. and first_zero_c1 is None: + first_zero_c1 = frame + assert first_unwind == ((change_frame+4)//5)*5 + assert first_zero_c0 == ((change_frame+127+4)//5)*5 + assert first_zero_c1 == ((change_frame+99+4)//5)*5 diff --git a/tools/ford_pscm_lab/damping_replay.py b/tools/ford_pscm_lab/damping_replay.py index beab494ccd..b372bbaf46 100644 --- a/tools/ford_pscm_lab/damping_replay.py +++ b/tools/ford_pscm_lab/damping_replay.py @@ -1,4 +1,4 @@ -"""Compare pinned selected-action controllers or current code on complete rlogs. +"""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. @@ -74,10 +74,13 @@ def run(directory, output, baseline_version='v1', candidate_version='v2', window 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 = ford_model_action if candidate_version == 'current' else load_controller(revisions[candidate_version]) + candidate_source = load_controller(revisions[candidate_version]) streams, models, sources, t0 = extract(directory) controls, model = streams['controls'], streams['model'] t = controls['t'] @@ -149,8 +152,7 @@ def run(directory, output, baseline_version='v1', candidate_version='v2', window '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': (hashlib.sha256(Path(ford_model_action.__file__).read_bytes()).hexdigest() - if candidate_version == 'current' else candidate_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) diff --git a/tools/ford_pscm_lab/model_action_replay.py b/tools/ford_pscm_lab/model_action_replay.py index 31583c8c01..ccc694f6af 100644 --- a/tools/ford_pscm_lab/model_action_replay.py +++ b/tools/ford_pscm_lab/model_action_replay.py @@ -5,6 +5,10 @@ command compatibility. The separate current adapter pass reconstructs input elig 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 @@ -62,6 +66,20 @@ 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') @@ -122,10 +140,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: diff --git a/tools/ford_pscm_lab/test_model_action_replay.py b/tools/ford_pscm_lab/test_model_action_replay.py index 70af7ff93b..e44e0167ae 100644 --- a/tools/ford_pscm_lab/test_model_action_replay.py +++ b/tools/ford_pscm_lab/test_model_action_replay.py @@ -5,6 +5,7 @@ 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(): @@ -70,6 +71,32 @@ 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))