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))