mirror of
https://github.com/sunnypilot/sunnypilot.git
synced 2026-10-03 14:33:43 +08:00
Compare commits
194 Commits
science
...
hiimisaac-dev
| Author | SHA1 | Date | |
|---|---|---|---|
| df5e2bbae2 | |||
| b30793bfb1 | |||
| d8c5639616 | |||
| 36fe2cf854 | |||
| 251e46e657 | |||
| 07affcf934 | |||
| d203a109a7 | |||
| ee3d0d33a0 | |||
| 2fe8559208 | |||
| 539fd716e5 | |||
| bd9c0a8b47 | |||
| 6aa35bc1f1 | |||
| a761165e31 | |||
| 99b3fb03e9 | |||
| 528ed36155 | |||
| 3b657f6de9 | |||
| 42ef386729 | |||
| 7bf3b9b428 | |||
| ed9c44f575 | |||
| baeabfaa80 | |||
| 4900c0a40c | |||
| 0020c0c419 | |||
| 9d34f77c86 | |||
| 01f5d54292 | |||
| 4f7d2b8d2f | |||
| 18ded03800 | |||
| 1336171a20 | |||
| d425b3260b | |||
| 9a1d06061b | |||
| 31cdfa12a0 | |||
| b720e9f1bb | |||
| 14bb8b0b1f | |||
| 94a460f672 | |||
| 16b2a3e996 | |||
| 506e9dd651 | |||
| b4a2b19aeb | |||
| 6b5661f1a1 | |||
| 0d0bb7eb33 | |||
| e697a5522e | |||
| a89c9c814f | |||
| a5f44653d7 | |||
| de23ac008e | |||
| 7750121675 | |||
| 5db3e3c9a0 | |||
| 26e5c9f532 | |||
| 57ae29f257 | |||
| 63a2a3868e | |||
| 5484f7f4a7 | |||
| 347238b307 | |||
| c57f9a7f4e | |||
| 3c24eeea25 | |||
| 20485134e1 | |||
| 08b3a14ad4 | |||
| 7430f245c7 | |||
| 5e6993aabb | |||
| bf00bc691d | |||
| 22d188776c | |||
| 6df5eabb7e | |||
| b67898fac4 | |||
| 17a86842f9 | |||
| b81c00f5b9 | |||
| 959ae3d6e7 | |||
| cf69210bb8 | |||
| 5fbb583e59 | |||
| a7d70e2b08 | |||
| 00de176331 | |||
| 7ca2df4819 | |||
| 4bd841eccd | |||
| 6f83b17457 | |||
| e1cd61166c | |||
| c70a9ee84b | |||
| d4f6403746 | |||
| 72e9d94f62 | |||
| 7e63449749 | |||
| 01f8d51c82 | |||
| 744a97d9bc | |||
| 5fc16abc76 | |||
| ea1ed70c71 | |||
| 7ca3c6e3b3 | |||
| c4b3c55c82 | |||
| b3bc05acd4 | |||
| 6135084c94 | |||
| dfcfddb91c | |||
| 61dac4977b | |||
| 09acf8ec2f | |||
| 79a4caa1f6 | |||
| 0ace0b0510 | |||
| 047ae41c0d | |||
| 7eb457f6c4 | |||
| 98662df401 | |||
| 302f3ad892 | |||
| 10e354d668 | |||
| 7d558c0650 | |||
| daeb966d05 | |||
| 727c26ce8c | |||
| 614022defb | |||
| 5e67122e64 | |||
| 25d095177e | |||
| 3eb7938aad | |||
| a525905708 | |||
| e298864a50 | |||
| 8639bdcca4 | |||
| 132b31f4cf | |||
| 752c07f9e4 | |||
| 458a3015cd | |||
| e87dbbaba7 | |||
| 15efdb392f | |||
| f5bb855477 | |||
| 336ce75f3d | |||
| 517c15f9c2 | |||
| 47db84ebfb | |||
| aa73207ab8 | |||
| 68be777395 | |||
| ab389498a8 | |||
| 184b73d8de | |||
| 6249f4d5b0 | |||
| 8b88f7dd6e | |||
| 6a1b697ed3 | |||
| 7eb7e93deb | |||
| 79658800ce | |||
| 36561258fa | |||
| 693daf9866 | |||
| cb85ac1f0e | |||
| afcc2b9455 | |||
| 3fdab7e8f0 | |||
| c9f1602040 | |||
| f488bfc806 | |||
| fd62fed669 | |||
| 3a665737c2 | |||
| 06af2abe67 | |||
| 7d5596d5c3 | |||
| b6a87b8958 | |||
| a2e422eee0 | |||
| 51987a62d0 | |||
| a1dcec490f | |||
| e10c0fd960 | |||
| 0729ce7c08 | |||
| 916fb1d522 | |||
| af5e7f5327 | |||
| bf2e9ca318 | |||
| 41b433c619 | |||
| da8ce858ec | |||
| 9fa7ef3d17 | |||
| ed56f3ff7c | |||
| 5865ad108c | |||
| 8ed82eae6f | |||
| 88f6f66032 | |||
| 33e70080ad | |||
| 4adbb85742 | |||
| b7f0e3fbdc | |||
| 70df7f227b | |||
| 7f371b8acd | |||
| 0e32059484 | |||
| 24c858e618 | |||
| 405407c252 | |||
| d49b56bff5 | |||
| f8d8b8ee56 | |||
| 8774a462ac | |||
| 1b41e9637f | |||
| 27a220677a | |||
| 7cf55c3b7a | |||
| 26e4889fcb | |||
| 682b6a20df | |||
| a67cdf9a51 | |||
| 70fa5d0fca | |||
| e571e21d14 | |||
| 839d3f5004 | |||
| 5645370f84 | |||
| 633d17cd12 | |||
| 5419f57b3a | |||
| 46f612224c | |||
| 6e0f4f4630 | |||
| 0f9c753e6e | |||
| 131e473f37 | |||
| 2d700cc0d0 | |||
| 30f358eb59 | |||
| 9b9e3ea604 | |||
| cc9ae66b22 | |||
| 505270420f | |||
| bb1a17d2a0 | |||
| 7cc48b5bc9 | |||
| cbf750de20 | |||
| 6db807b5a0 | |||
| 42e1414bc4 | |||
| 3e020e321f | |||
| 7e2000e909 | |||
| e96055846c | |||
| d47646b28f | |||
| e75bc83424 | |||
| 318257fa3b | |||
| 08e48958b6 | |||
| 4cdc16031f | |||
| 31ea1850f7 | |||
| 25d0d0f1ff |
@@ -9,6 +9,7 @@
|
||||
*.ttf filter=lfs diff=lfs merge=lfs -text
|
||||
*.otf filter=lfs diff=lfs merge=lfs -text
|
||||
*.wav filter=lfs diff=lfs merge=lfs -text
|
||||
openpilot/selfdrive/assets/sounds/milestone.wav -filter -diff -merge -text
|
||||
|
||||
openpilot/selfdrive/car/tests/test_models_segs.txt filter=lfs diff=lfs merge=lfs -text
|
||||
openpilot/common/hardware/comma/updater filter=lfs diff=lfs merge=lfs -text
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
* @sunnypilot/dev-internal
|
||||
/.github/ @devtekve @sunnyhaibin
|
||||
/release/ci/ @devtekve @sunnyhaibin
|
||||
/tinygrad_repo @devtekve @Discountchubbs
|
||||
/tinygrad/ @devtekve @Discountchubbs
|
||||
/selfdrive/controls/lib/longitudinal_planner.py @devtekve @Discountchubbs
|
||||
/selfdrive/controls/lib/longitudinal_mpc_lib/long_mpc.py @devtekve @Discountchubbs
|
||||
/selfdrive/modeld/ @devtekve @Discountchubbs
|
||||
/sunnypilot/model* @devtekve @Discountchubbs
|
||||
/sunnypilot/sunnylink/ @devtekve
|
||||
/system/athena/ @devtekve
|
||||
@@ -1,43 +0,0 @@
|
||||
exclude-labels:
|
||||
- 'no-changelog'
|
||||
categories:
|
||||
- title: '🚀 Features'
|
||||
labels:
|
||||
- 'feature'
|
||||
- 'enhancement'
|
||||
- title: '🐛 Bug Fixes'
|
||||
collapse-after: 5
|
||||
labels:
|
||||
- 'fix'
|
||||
- 'bugfix'
|
||||
- 'bug'
|
||||
- title: '🧰 Maintenance'
|
||||
collapse-after: 5
|
||||
label: 'chore'
|
||||
change-template: '- $TITLE @$AUTHOR (#$NUMBER)'
|
||||
change-title-escapes: '\<*_&'
|
||||
replacers:
|
||||
- search: '/[Ss][Uu][Nn][Nn][Yy][Pp][Ii][Ll][Oo][Tt]/g'
|
||||
replace: 'sunnypilot'
|
||||
- search: '/\b[Ss][Pp]\b/g'
|
||||
replace: 'SP'
|
||||
version-resolver:
|
||||
major:
|
||||
labels:
|
||||
- 'major'
|
||||
minor:
|
||||
labels:
|
||||
- 'minor'
|
||||
patch:
|
||||
labels:
|
||||
- 'patch'
|
||||
default: patch
|
||||
name-template: 'v$RESOLVED_VERSION 🚀'
|
||||
tag-template: 'v$RESOLVED_VERSION'
|
||||
version-template: "0.$MAJOR.$MINOR.$PATCH" # The day OP becomes v1, we need to bump this
|
||||
tag-prefix: "v0." # The day OP becomes v1, we need to bump this
|
||||
prerelease-identifier: "staging"
|
||||
template: |
|
||||
## Changes
|
||||
|
||||
$CHANGES
|
||||
@@ -20,6 +20,11 @@ on:
|
||||
required: false
|
||||
type: string
|
||||
default: 'sunnypilot/sunnypilot_models_v1'
|
||||
docs_repo:
|
||||
description: 'GitHub repo holding the driving_models JSON on its gh-pages branch'
|
||||
required: false
|
||||
type: string
|
||||
default: 'sunnypilot/sunnypilot-models'
|
||||
|
||||
jobs:
|
||||
setup:
|
||||
@@ -34,7 +39,6 @@ jobs:
|
||||
- name: Checkout sunnypilot repo
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: sunnypilot/sunnypilot
|
||||
path: sunnypilot
|
||||
submodules: recursive
|
||||
|
||||
@@ -47,10 +51,10 @@ jobs:
|
||||
echo "tinygrad_ref=$ref" >> $GITHUB_OUTPUT
|
||||
echo "tinygrad_ref is $ref"
|
||||
|
||||
- name: Checkout docs repo (sunnypilot-models, gh-pages)
|
||||
- name: Checkout docs repo (gh-pages)
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: sunnypilot/sunnypilot-models
|
||||
repository: ${{ inputs.docs_repo }}
|
||||
ref: gh-pages
|
||||
path: docs
|
||||
ssh-key: ${{ secrets.CI_SUNNYPILOT_DOCS_PRIVATE_KEY }}
|
||||
@@ -78,6 +82,7 @@ jobs:
|
||||
- name: Get next recompiled dir number
|
||||
id: create-recompiled-dir
|
||||
env:
|
||||
HF_TOKEN: ${{ secrets.HF_TOKEN }}
|
||||
HF_REPO: ${{ github.event.inputs.hf_repo }}
|
||||
run: |
|
||||
pip install huggingface_hub
|
||||
@@ -117,6 +122,7 @@ jobs:
|
||||
json_version: ${{ needs.setup.outputs.json_version }}
|
||||
target_hardware: ${{ github.event.inputs.target_hardware }}
|
||||
hf_repo: ${{ github.event.inputs.hf_repo }}
|
||||
docs_repo: ${{ inputs.docs_repo }}
|
||||
set_min_version: ${{ github.event.inputs.set_min_version }}
|
||||
tinygrad_ref: ${{ needs.setup.outputs.tinygrad_ref }}
|
||||
secrets: inherit
|
||||
@@ -161,6 +167,7 @@ jobs:
|
||||
target_hardware: ${{ github.event.inputs.target_hardware }}
|
||||
artifact_suffix: -retry
|
||||
hf_repo: ${{ github.event.inputs.hf_repo }}
|
||||
docs_repo: ${{ inputs.docs_repo }}
|
||||
set_min_version: ${{ github.event.inputs.set_min_version }}
|
||||
tinygrad_ref: ${{ needs.setup.outputs.tinygrad_ref }}
|
||||
secrets: inherit
|
||||
|
||||
@@ -30,6 +30,7 @@ jobs:
|
||||
runs-on: ubuntu-24.04
|
||||
outputs:
|
||||
model_name: ${{ steps.resolve.outputs.model_name }}
|
||||
safe_model_name: ${{ steps.resolve.outputs.safe_model_name }}
|
||||
onnx_ref: ${{ steps.resolve.outputs.onnx_ref }}
|
||||
onnx_path: ${{ steps.resolve.outputs.onnx_path }}
|
||||
hf_defaults_path: ${{ steps.resolve.outputs.hf_defaults_path }}
|
||||
@@ -64,7 +65,9 @@ jobs:
|
||||
exit 1
|
||||
fi
|
||||
|
||||
SAFE_NAME="${NAME// /-}"
|
||||
echo "model_name=${NAME}" >> $GITHUB_OUTPUT
|
||||
echo "safe_model_name=${SAFE_NAME}" >> $GITHUB_OUTPUT
|
||||
echo "onnx_ref=${ONNX_REF}" >> $GITHUB_OUTPUT
|
||||
echo "onnx_path=${ONNX_PATH}" >> $GITHUB_OUTPUT
|
||||
echo "hf_defaults_path=${HF_DEFAULTS_PATH}" >> $GITHUB_OUTPUT
|
||||
@@ -135,7 +138,7 @@ jobs:
|
||||
|
||||
- name: Prepare output
|
||||
env:
|
||||
MODEL_NAME: ${{ needs.resolve.outputs.model_name }}
|
||||
MODEL_NAME: ${{ needs.resolve.outputs.safe_model_name }}
|
||||
run: |
|
||||
source ${UV_PROJECT_ENVIRONMENT}/bin/activate
|
||||
export PYTHONPATH=${{ github.workspace }}
|
||||
@@ -158,13 +161,13 @@ jobs:
|
||||
- name: Upload small model artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: model-${{ needs.resolve.outputs.model_name }}-${{ github.run_number }}
|
||||
name: model-${{ needs.resolve.outputs.safe_model_name }}-${{ github.run_number }}
|
||||
path: ${{ github.workspace }}/small_output/
|
||||
|
||||
- name: Upload artifact name file
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: artifact-name-${{ needs.resolve.outputs.model_name }}
|
||||
name: artifact-name-${{ needs.resolve.outputs.safe_model_name }}
|
||||
path: ${{ github.workspace }}/small_output/artifact_name.txt
|
||||
|
||||
- name: Re-enable powersave
|
||||
@@ -254,7 +257,7 @@ jobs:
|
||||
|
||||
- name: Prepare output
|
||||
env:
|
||||
MODEL_NAME: ${{ needs.resolve.outputs.model_name }}
|
||||
MODEL_NAME: ${{ needs.resolve.outputs.safe_model_name }}
|
||||
run: |
|
||||
source ${UV_PROJECT_ENVIRONMENT}/bin/activate
|
||||
export PYTHONPATH=${{ github.workspace }}
|
||||
@@ -277,13 +280,13 @@ jobs:
|
||||
- name: Upload big model artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: model-${{ needs.resolve.outputs.model_name }}-${{ github.run_number }}
|
||||
name: model-${{ needs.resolve.outputs.safe_model_name }}-${{ github.run_number }}
|
||||
path: ${{ github.workspace }}/big_output/
|
||||
|
||||
- name: Upload artifact name file
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: artifact-name-${{ needs.resolve.outputs.model_name }}
|
||||
name: artifact-name-${{ needs.resolve.outputs.safe_model_name }}
|
||||
path: ${{ github.workspace }}/big_output/artifact_name.txt
|
||||
|
||||
- name: Re-enable powersave
|
||||
@@ -318,7 +321,7 @@ jobs:
|
||||
if: ${{ inputs.target == 'small' || inputs.target == 'big' }}
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: artifact-name-${{ needs.resolve.outputs.model_name }}
|
||||
name: artifact-name-${{ needs.resolve.outputs.safe_model_name }}
|
||||
path: artifact_name
|
||||
|
||||
- name: Read artifact name
|
||||
@@ -338,7 +341,7 @@ jobs:
|
||||
- name: Upload model to HF
|
||||
if: ${{ inputs.target == 'small' || inputs.target == 'big' }}
|
||||
env:
|
||||
HF_OIDC_RESOURCE: datasets/${{ env.HF_REPO }}
|
||||
HF_TOKEN: ${{ secrets.HF_TOKEN }}
|
||||
ARTIFACT_NAME: ${{ steps.artifact.outputs.artifact_name }}
|
||||
run: |
|
||||
rm -f output/artifact_name.txt
|
||||
@@ -364,7 +367,7 @@ jobs:
|
||||
- name: Generate DM metadata and upload to HF
|
||||
if: ${{ inputs.target == 'dm' }}
|
||||
env:
|
||||
HF_OIDC_RESOURCE: datasets/${{ env.HF_REPO }}
|
||||
HF_TOKEN: ${{ secrets.HF_TOKEN }}
|
||||
run: |
|
||||
export PYTHONPATH=$(pwd)
|
||||
python3 -c "
|
||||
@@ -481,11 +484,29 @@ jobs:
|
||||
print(f'Chunked {pkl} into {len(targets)} chunks')
|
||||
"
|
||||
|
||||
- name: Compile DM warp
|
||||
run: |
|
||||
source ${UV_PROJECT_ENVIRONMENT}/bin/activate
|
||||
export PYTHONPATH="${PYTHONPATH}:${{ github.workspace }}/tinygrad_repo:${{ github.workspace }}"
|
||||
|
||||
TG_FLAGS="DEV=QCOM IMAGE=1 FLOAT16=1 NOLOCALS=1 JIT_BATCH_SIZE=0 OPENPILOT_HACKS=1"
|
||||
MODEL_DIR="${{ github.workspace }}/openpilot/selfdrive/modeld"
|
||||
DM_SIZE=$(python3 -c "from openpilot.common.transformations.model import DM_INPUT_SIZE as s; print(f'{s[0]}x{s[1]}')")
|
||||
|
||||
for res in $(python3 -c "from openpilot.common.transformations.camera import _ar_ox_fisheye as a, _os_fisheye as o; print(f'{a.width}x{a.height} {o.width}x{o.height}')"); do
|
||||
WARP_PKL="${MODEL_DIR}/models/dm_warp_${res}_tinygrad.pkl"
|
||||
taskset -c 7 env ${TG_FLAGS} python3 ${MODEL_DIR}/compile_dm_warp.py \
|
||||
--camera-resolution ${res} \
|
||||
--warp-to ${DM_SIZE} \
|
||||
--output ${WARP_PKL}
|
||||
done
|
||||
|
||||
- name: Prepare DM output
|
||||
run: |
|
||||
mkdir -p dm_output
|
||||
cp ${{ github.workspace }}/${{ env.DM_PKL }}.chunk* dm_output/
|
||||
cp ${{ github.workspace }}/${{ env.DM_PKL }}.chunkmanifest dm_output/
|
||||
cp ${{ github.workspace }}/openpilot/selfdrive/modeld/models/dm_warp_* dm_output/
|
||||
|
||||
- name: Upload DM artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
|
||||
@@ -39,6 +39,11 @@ on:
|
||||
required: false
|
||||
type: string
|
||||
default: 'sunnypilot/sunnypilot_models_v1'
|
||||
docs_repo:
|
||||
description: 'GitHub repo holding the driving_models JSON on its gh-pages branch'
|
||||
required: false
|
||||
type: string
|
||||
default: 'sunnypilot/sunnypilot-models'
|
||||
set_min_version:
|
||||
description: 'Minimum selector version'
|
||||
required: false
|
||||
@@ -107,6 +112,11 @@ on:
|
||||
required: false
|
||||
type: string
|
||||
default: 'sunnypilot/sunnypilot_models_v1'
|
||||
docs_repo:
|
||||
description: 'GitHub repo holding the driving_models JSON on its gh-pages branch'
|
||||
required: false
|
||||
type: string
|
||||
default: 'sunnypilot/sunnypilot-models'
|
||||
env:
|
||||
RECOMPILED_DIR: recompiled${{ inputs.recompiled_dir }}
|
||||
JSON_FILE: docs/docs/driving_models_${{ inputs.target_hardware == 'chestnut' && 'chestnut_v' || 'v' }}${{ inputs.json_version }}.json
|
||||
@@ -136,7 +146,7 @@ jobs:
|
||||
- name: Checkout docs repo
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: sunnypilot/sunnypilot-models
|
||||
repository: ${{ inputs.docs_repo }}
|
||||
ref: gh-pages
|
||||
path: docs
|
||||
ssh-key: ${{ secrets.CI_SUNNYPILOT_DOCS_PRIVATE_KEY }}
|
||||
@@ -146,7 +156,7 @@ jobs:
|
||||
|
||||
- name: Validate hf_repo and JSON version
|
||||
env:
|
||||
HF_OIDC_RESOURCE: datasets/${{ inputs.hf_repo }}
|
||||
HF_TOKEN: ${{ secrets.HF_TOKEN }}
|
||||
run: |
|
||||
if [ ! -f "$JSON_FILE" ]; then
|
||||
echo "JSON file $JSON_FILE does not exist!"
|
||||
@@ -155,13 +165,8 @@ jobs:
|
||||
python3 -c "
|
||||
import sys
|
||||
from huggingface_hub import HfApi
|
||||
try:
|
||||
api = HfApi()
|
||||
api.repo_info(repo_id=sys.argv[1], repo_type='dataset')
|
||||
print(f'Success: Repo {sys.argv[1]} exists.')
|
||||
except Exception as e:
|
||||
print('HF validation failed:', e)
|
||||
sys.exit(1)
|
||||
HfApi().repo_info(repo_id=sys.argv[1], repo_type='dataset')
|
||||
print(f'Success: Repo {sys.argv[1]} exists.')
|
||||
" "${{ inputs.hf_repo }}"
|
||||
|
||||
- name: Download artifact name file
|
||||
@@ -192,7 +197,7 @@ jobs:
|
||||
|
||||
- name: Upload to Hugging Face
|
||||
env:
|
||||
HF_OIDC_RESOURCE: datasets/${{ inputs.hf_repo }}
|
||||
HF_TOKEN: ${{ secrets.HF_TOKEN }}
|
||||
ARTIFACT_NAME: ${{ steps.read-artifact-name.outputs.artifact_name }}
|
||||
run: |
|
||||
hf upload ${{ inputs.hf_repo }} \
|
||||
|
||||
@@ -46,6 +46,13 @@ runs:
|
||||
printf '%s\t%s\n' "$ENCODED_URL" "${DEST_DIR}/${CANONICAL}.chunk${CHUNK_IDX}" >> "$DOWNLOAD_LIST"
|
||||
done < <(echo "$ARTIFACT" | jq -r '.chunks[].file_name')
|
||||
echo "$NUM_CHUNKS" > "${DEST_DIR}/${CANONICAL}.chunkmanifest"
|
||||
|
||||
if [ "$CANONICAL" = "dmonitoring_model_tinygrad.pkl" ]; then
|
||||
for warp in dm_warp_1928x1208_tinygrad.pkl dm_warp_1344x760_tinygrad.pkl; do
|
||||
ENCODED_URL=$(python3 -c "import urllib.parse; print(urllib.parse.quote('${BASE_URL}/${warp}', safe=':/'))")
|
||||
printf '%s\t%s\n' "$ENCODED_URL" "${DEST_DIR}/${warp}" >> "$DOWNLOAD_LIST"
|
||||
done
|
||||
fi
|
||||
}
|
||||
|
||||
echo "$MODELS_JSON" | jq -c '.[]' | while IFS= read -r model; do
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
name: Release Drafter
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
tags:
|
||||
- 'v*'
|
||||
pull_request_target:
|
||||
types: [opened, reopened, synchronize]
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
update_release_draft:
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: release-drafter/release-drafter@v6
|
||||
with:
|
||||
config-name: release-drafter.yml
|
||||
prerelease: ${{ !startsWith(github.ref, 'refs/tags/v') }}
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -121,7 +121,7 @@ jobs:
|
||||
if-no-files-found: error
|
||||
|
||||
build_model:
|
||||
runs-on: [self-hosted, chestnut]
|
||||
runs-on: [self-hosted, "${{ inputs.target_hardware == 'chestnut' && 'chestnut' || 'tici' }}"]
|
||||
needs: get_model
|
||||
env:
|
||||
MODEL_NAME: ${{ inputs.custom_name || inputs.upstream_branch }} (${{ needs.get_model.outputs.model_date }})
|
||||
@@ -188,7 +188,7 @@ jobs:
|
||||
if [ "${{ inputs.target_hardware }}" == "chestnut" ]; then
|
||||
echo "CHESTNUT build"
|
||||
export CHESTNUT=1
|
||||
TG_FLAGS="DEBUG=1 DEV=USB+AMD:LLVM WARP_DEV=QCOM FLOAT16=1 JIT_BATCH_SIZE=0 GMMU=0 TC_OPT=2"
|
||||
TG_FLAGS="DEBUG=1 DEV=USB+AMD:LLVM FLOAT16=1 JIT_BATCH_SIZE=0 GMMU=0 TC_OPT=2 TC_OCCUPANCY_OPT=1"
|
||||
OUTPUT_PKL="${{ env.MODELS_DIR }}/big_driving_tinygrad.pkl"
|
||||
else
|
||||
echo "QCOM build"
|
||||
|
||||
@@ -216,6 +216,9 @@ jobs:
|
||||
needs: [ prepare_strategy ]
|
||||
runs-on: ubuntu-24.04
|
||||
if: ${{ needs.prepare_strategy.outputs.include_big_model == 'true' }}
|
||||
concurrency:
|
||||
group: prepare-chestnut
|
||||
cancel-in-progress: false
|
||||
outputs:
|
||||
onnx_sha256: ${{ steps.resolve.outputs.onnx_sha256 }}
|
||||
env:
|
||||
@@ -228,8 +231,10 @@ jobs:
|
||||
run: |
|
||||
REF="${{ github.head_ref || github.ref_name }}"
|
||||
|
||||
ONNX_HASH=$(gh api "repos/${GH_REPO}/contents/openpilot/selfdrive/modeld/models/big_driving_supercombo.onnx?ref=${REF}" --jq '.content' | base64 -d | grep '^oid sha256:' | cut -d: -f2)
|
||||
BLOB_SHA=$(gh api "repos/${GH_REPO}/contents/openpilot/selfdrive/modeld/models/big_driving_supercombo.onnx?ref=${REF}" --jq '.sha')
|
||||
ONNX_HASH=$(gh api "repos/${GH_REPO}/git/blobs/${BLOB_SHA}" --jq '.content' | base64 -d | grep '^oid sha256:' | cut -d: -f2)
|
||||
echo "ONNX hash: $ONNX_HASH"
|
||||
[ -n "$ONNX_HASH" ] || { echo "::error::Failed to extract ONNX hash"; exit 1; }
|
||||
echo "onnx_sha256=$ONNX_HASH" >> $GITHUB_OUTPUT
|
||||
|
||||
TINYGRAD_REF=$(gh api "repos/${GH_REPO}/contents/tinygrad_repo?ref=${REF}" --jq '.sha')
|
||||
@@ -238,7 +243,7 @@ jobs:
|
||||
JSON_URL="https://huggingface.co/datasets/${HF_REPO}/resolve/main/${HF_DEFAULTS_PATH}/default_models.json"
|
||||
|
||||
check_defaults() {
|
||||
DEFAULTS=$(curl -fsSL "$JSON_URL" 2>/dev/null) || return 1
|
||||
DEFAULTS=$(curl -fsSL "${JSON_URL}?t=$(date +%s)" 2>/dev/null) || return 1
|
||||
TINYGRAD_MATCH=$(echo "$DEFAULTS" | jq -r --arg ref "$TINYGRAD_REF" '.tinygrad_ref == $ref' 2>/dev/null)
|
||||
[ "$TINYGRAD_MATCH" = "true" ] || return 1
|
||||
BUNDLE=$(echo "$DEFAULTS" | jq --arg hash "$ONNX_HASH" '.bundles[] | select(.onnx_sha256 == $hash)' 2>/dev/null)
|
||||
@@ -252,18 +257,35 @@ jobs:
|
||||
|
||||
echo "No matching model on HF — dispatching build"
|
||||
gh workflow run build-default-models.yaml --ref "$REF" -f target=big
|
||||
sleep 10
|
||||
|
||||
echo "Polling HF for big model availability..."
|
||||
BUILD_RUN_ID=$(gh run list --workflow build-default-models.yaml --branch "$REF" --limit 1 --json databaseId --jq '.[0].databaseId')
|
||||
echo "Dispatched build run: $BUILD_RUN_ID"
|
||||
|
||||
echo "Waiting for build run to complete..."
|
||||
for i in $(seq 1 90); do
|
||||
sleep 30
|
||||
if check_defaults; then
|
||||
echo "Big model available on HF after $((i * 30))s"
|
||||
exit 0
|
||||
STATUS=$(gh api "repos/${GH_REPO}/actions/runs/${BUILD_RUN_ID}" --jq '.status')
|
||||
CONCLUSION=$(gh api "repos/${GH_REPO}/actions/runs/${BUILD_RUN_ID}" --jq '.conclusion')
|
||||
echo "Poll $i/90: status=$STATUS conclusion=$CONCLUSION"
|
||||
if [ "$STATUS" = "completed" ]; then
|
||||
if [ "$CONCLUSION" = "success" ]; then
|
||||
echo "Build run succeeded, verifying HF..."
|
||||
sleep 10
|
||||
if check_defaults; then
|
||||
echo "Big model verified on HF"
|
||||
exit 0
|
||||
fi
|
||||
echo "::error::Build succeeded but model not found on HF"
|
||||
exit 1
|
||||
else
|
||||
echo "::error::Build run failed with conclusion=$CONCLUSION"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
echo "Poll $i/90: not yet available"
|
||||
done
|
||||
|
||||
echo "::error::Big model not available on HF after 45 minutes"
|
||||
echo "::error::Build run did not complete within 45 minutes"
|
||||
exit 1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -277,6 +299,9 @@ jobs:
|
||||
prepare_small_model:
|
||||
needs: [ prepare_strategy ]
|
||||
runs-on: ubuntu-24.04
|
||||
concurrency:
|
||||
group: prepare-small-model
|
||||
cancel-in-progress: false
|
||||
outputs:
|
||||
driving_onnx_sha256: ${{ steps.resolve.outputs.driving_onnx_sha256 }}
|
||||
env:
|
||||
@@ -289,8 +314,10 @@ jobs:
|
||||
run: |
|
||||
REF="${{ github.head_ref || github.ref_name }}"
|
||||
|
||||
DRIVING_HASH=$(gh api "repos/${GH_REPO}/contents/openpilot/selfdrive/modeld/models/driving_supercombo.onnx?ref=${REF}" --jq '.content' | base64 -d | grep '^oid sha256:' | cut -d: -f2)
|
||||
BLOB_SHA=$(gh api "repos/${GH_REPO}/contents/openpilot/selfdrive/modeld/models/driving_supercombo.onnx?ref=${REF}" --jq '.sha')
|
||||
DRIVING_HASH=$(gh api "repos/${GH_REPO}/git/blobs/${BLOB_SHA}" --jq '.content' | base64 -d | grep '^oid sha256:' | cut -d: -f2)
|
||||
echo "Driving ONNX hash: $DRIVING_HASH"
|
||||
[ -n "$DRIVING_HASH" ] || { echo "::error::Failed to extract driving ONNX hash"; exit 1; }
|
||||
echo "driving_onnx_sha256=$DRIVING_HASH" >> $GITHUB_OUTPUT
|
||||
|
||||
TINYGRAD_REF=$(gh api "repos/${GH_REPO}/contents/tinygrad_repo?ref=${REF}" --jq '.sha')
|
||||
@@ -299,7 +326,7 @@ jobs:
|
||||
JSON_URL="https://huggingface.co/datasets/${HF_REPO}/resolve/main/${HF_DEFAULTS_PATH}/default_models.json"
|
||||
|
||||
check_defaults() {
|
||||
DEFAULTS=$(curl -fsSL "$JSON_URL" 2>/dev/null) || return 1
|
||||
DEFAULTS=$(curl -fsSL "${JSON_URL}?t=$(date +%s)" 2>/dev/null) || return 1
|
||||
TINYGRAD_MATCH=$(echo "$DEFAULTS" | jq -r --arg ref "$TINYGRAD_REF" '.tinygrad_ref == $ref' 2>/dev/null)
|
||||
[ "$TINYGRAD_MATCH" = "true" ] || return 1
|
||||
DRIVING=$(echo "$DEFAULTS" | jq --arg hash "$DRIVING_HASH" '.bundles[] | select(.onnx_sha256 == $hash)' 2>/dev/null)
|
||||
@@ -313,18 +340,35 @@ jobs:
|
||||
|
||||
echo "No matching model on HF — dispatching build"
|
||||
gh workflow run build-default-models.yaml --ref "$REF" -f target=small
|
||||
sleep 10
|
||||
|
||||
echo "Polling HF for model availability..."
|
||||
BUILD_RUN_ID=$(gh run list --workflow build-default-models.yaml --branch "$REF" --limit 1 --json databaseId --jq '.[0].databaseId')
|
||||
echo "Dispatched build run: $BUILD_RUN_ID"
|
||||
|
||||
echo "Waiting for build run to complete..."
|
||||
for i in $(seq 1 60); do
|
||||
sleep 30
|
||||
if check_defaults; then
|
||||
echo "Model available on HF after $((i * 30))s"
|
||||
exit 0
|
||||
STATUS=$(gh api "repos/${GH_REPO}/actions/runs/${BUILD_RUN_ID}" --jq '.status')
|
||||
CONCLUSION=$(gh api "repos/${GH_REPO}/actions/runs/${BUILD_RUN_ID}" --jq '.conclusion')
|
||||
echo "Poll $i/60: status=$STATUS conclusion=$CONCLUSION"
|
||||
if [ "$STATUS" = "completed" ]; then
|
||||
if [ "$CONCLUSION" = "success" ]; then
|
||||
echo "Build run succeeded, verifying HF..."
|
||||
sleep 10
|
||||
if check_defaults; then
|
||||
echo "Small model verified on HF"
|
||||
exit 0
|
||||
fi
|
||||
echo "::error::Build succeeded but model not found on HF"
|
||||
exit 1
|
||||
else
|
||||
echo "::error::Build run failed with conclusion=$CONCLUSION"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
echo "Poll $i/60: not yet available"
|
||||
done
|
||||
|
||||
echo "::error::Small driving model not available on HF after 30 minutes"
|
||||
echo "::error::Small model build did not complete within 30 minutes"
|
||||
exit 1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -338,6 +382,9 @@ jobs:
|
||||
prepare_dm_model:
|
||||
needs: [ prepare_strategy ]
|
||||
runs-on: ubuntu-24.04
|
||||
concurrency:
|
||||
group: prepare-dm-model
|
||||
cancel-in-progress: false
|
||||
outputs:
|
||||
dm_onnx_sha256: ${{ steps.resolve.outputs.dm_onnx_sha256 }}
|
||||
env:
|
||||
@@ -350,8 +397,10 @@ jobs:
|
||||
run: |
|
||||
REF="${{ github.head_ref || github.ref_name }}"
|
||||
|
||||
DM_HASH=$(gh api "repos/${GH_REPO}/contents/openpilot/selfdrive/modeld/models/dmonitoring_model.onnx?ref=${REF}" --jq '.content' | base64 -d | grep '^oid sha256:' | cut -d: -f2)
|
||||
BLOB_SHA=$(gh api "repos/${GH_REPO}/contents/openpilot/selfdrive/modeld/models/dmonitoring_model.onnx?ref=${REF}" --jq '.sha')
|
||||
DM_HASH=$(gh api "repos/${GH_REPO}/git/blobs/${BLOB_SHA}" --jq '.content' | base64 -d | grep '^oid sha256:' | cut -d: -f2)
|
||||
echo "DM ONNX hash: $DM_HASH"
|
||||
[ -n "$DM_HASH" ] || { echo "::error::Failed to extract DM ONNX hash"; exit 1; }
|
||||
echo "dm_onnx_sha256=$DM_HASH" >> $GITHUB_OUTPUT
|
||||
|
||||
TINYGRAD_REF=$(gh api "repos/${GH_REPO}/contents/tinygrad_repo?ref=${REF}" --jq '.sha')
|
||||
@@ -360,7 +409,7 @@ jobs:
|
||||
JSON_URL="https://huggingface.co/datasets/${HF_REPO}/resolve/main/${HF_DEFAULTS_PATH}/default_models.json"
|
||||
|
||||
check_defaults() {
|
||||
DEFAULTS=$(curl -fsSL "$JSON_URL" 2>/dev/null) || return 1
|
||||
DEFAULTS=$(curl -fsSL "${JSON_URL}?t=$(date +%s)" 2>/dev/null) || return 1
|
||||
TINYGRAD_MATCH=$(echo "$DEFAULTS" | jq -r --arg ref "$TINYGRAD_REF" '.tinygrad_ref == $ref' 2>/dev/null)
|
||||
[ "$TINYGRAD_MATCH" = "true" ] || return 1
|
||||
DM=$(echo "$DEFAULTS" | jq --arg hash "$DM_HASH" '.bundles[] | select(.onnx_sha256 == $hash)' 2>/dev/null)
|
||||
@@ -374,18 +423,35 @@ jobs:
|
||||
|
||||
echo "No matching DM model on HF — dispatching build"
|
||||
gh workflow run build-default-models.yaml --ref "$REF" -f target=dm
|
||||
sleep 10
|
||||
|
||||
echo "Polling HF for DM model availability..."
|
||||
BUILD_RUN_ID=$(gh run list --workflow build-default-models.yaml --branch "$REF" --limit 1 --json databaseId --jq '.[0].databaseId')
|
||||
echo "Dispatched build run: $BUILD_RUN_ID"
|
||||
|
||||
echo "Waiting for build run to complete..."
|
||||
for i in $(seq 1 60); do
|
||||
sleep 30
|
||||
if check_defaults; then
|
||||
echo "DM model available on HF after $((i * 30))s"
|
||||
exit 0
|
||||
STATUS=$(gh api "repos/${GH_REPO}/actions/runs/${BUILD_RUN_ID}" --jq '.status')
|
||||
CONCLUSION=$(gh api "repos/${GH_REPO}/actions/runs/${BUILD_RUN_ID}" --jq '.conclusion')
|
||||
echo "Poll $i/60: status=$STATUS conclusion=$CONCLUSION"
|
||||
if [ "$STATUS" = "completed" ]; then
|
||||
if [ "$CONCLUSION" = "success" ]; then
|
||||
echo "Build run succeeded, verifying HF..."
|
||||
sleep 10
|
||||
if check_defaults; then
|
||||
echo "DM model verified on HF"
|
||||
exit 0
|
||||
fi
|
||||
echo "::error::Build succeeded but DM model not found on HF"
|
||||
exit 1
|
||||
else
|
||||
echo "::error::Build run failed with conclusion=$CONCLUSION"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
echo "Poll $i/60: not yet available"
|
||||
done
|
||||
|
||||
echo "::error::DM model not available on HF after 30 minutes"
|
||||
echo "::error::DM model build did not complete within 30 minutes"
|
||||
exit 1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
@@ -1,78 +0,0 @@
|
||||
name: Debug Discourse Posting
|
||||
|
||||
on:
|
||||
push:
|
||||
|
||||
jobs:
|
||||
test-discourse-post:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Post test message to Discourse
|
||||
uses: ./.github/workflows/post-to-discourse
|
||||
with:
|
||||
discourse-url: ${{ vars.DISCOURSE_URL }}
|
||||
api-key: ${{ secrets.DISCOURSE_API_KEY }}
|
||||
api-username: ${{ secrets.DISCOURSE_API_USERNAME }}
|
||||
topic-id: ${{ vars.DISCOURSE_UPDATES_TOPIC_ID }}
|
||||
message: |
|
||||
## 🧪 Test Post from GitHub Actions
|
||||
|
||||
**This is a test post to verify Discourse integration**
|
||||
|
||||
- **Workflow**: ${{ github.workflow }}
|
||||
- **Run Number**: #${{ github.run_number }}
|
||||
- **Branch**: `${{ github.ref_name }}`
|
||||
- **Commit**: ${{ github.sha }}
|
||||
- **Actor**: @${{ github.actor }}
|
||||
- **Timestamp**: ${{ github.event.head_commit.timestamp }}
|
||||
|
||||
---
|
||||
|
||||
### Fake Build Info (for testing)
|
||||
- **Version**: 0.9.8-test
|
||||
- **Build**: #42
|
||||
- **Branch**: release-test
|
||||
|
||||
[View workflow run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})
|
||||
|
||||
*This is an automated test message. Drive safe! 🚗💨*
|
||||
|
||||
|
||||
- name: Create topic on Discourse
|
||||
uses: ./.github/workflows/post-to-discourse
|
||||
with:
|
||||
discourse-url: ${{ vars.DISCOURSE_URL }}
|
||||
api-key: ${{ secrets.DISCOURSE_API_KEY }}
|
||||
api-username: ${{ secrets.DISCOURSE_API_USERNAME }}
|
||||
#topic-id: ${{ vars.DISCOURSE_UPDATES_TOPIC_ID }}
|
||||
category-id: 4
|
||||
title: "This is a test of a new topic instead of a reply"
|
||||
message: |
|
||||
## 🧪 Test Post from GitHub Actions
|
||||
|
||||
**This is a test post to verify Discourse integration**
|
||||
|
||||
- **Workflow**: ${{ github.workflow }}
|
||||
- **Run Number**: #${{ github.run_number }}
|
||||
- **Branch**: `${{ github.ref_name }}`
|
||||
- **Commit**: ${{ github.sha }}
|
||||
- **Actor**: @${{ github.actor }}
|
||||
- **Timestamp**: ${{ github.event.head_commit.timestamp }}
|
||||
|
||||
---
|
||||
|
||||
### Fake Build Info (for testing)
|
||||
- **Version**: 0.9.8-test
|
||||
- **Build**: #42
|
||||
- **Branch**: release-test
|
||||
|
||||
[View workflow run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})
|
||||
|
||||
*This is an automated test message. Drive safe! 🚗💨*
|
||||
- name: Display results
|
||||
if: always()
|
||||
run: |
|
||||
echo "::notice::Discourse post test completed"
|
||||
echo "Check your Discourse topic to verify the post appeared correctly"
|
||||
@@ -0,0 +1,79 @@
|
||||
name: Test Models Compatibility With Tinygrad Changes
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- 'tinygrad_repo'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
generate-matrix:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
models: ${{ steps.set-matrix.outputs.models }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Fetch and Parse json
|
||||
id: set-matrix
|
||||
run: |
|
||||
python3 -c '
|
||||
import json, urllib.request, os, re
|
||||
|
||||
with open("openpilot/sunnypilot/models/fetcher.py", "r") as f:
|
||||
urls = re.findall(r"MODEL_URL(?:_CHESTNUT)?\s*=\s*[\"'"'"']([^\"'"'"']+)[\"'"'"']", f.read())
|
||||
|
||||
artifacts = []
|
||||
for url in urls:
|
||||
data = json.loads(urllib.request.urlopen(url).read())
|
||||
for bundle in data.get("bundles", []):
|
||||
for model in bundle.get("models", []):
|
||||
if "artifact" in model:
|
||||
artifacts.append(model["artifact"])
|
||||
|
||||
with open(os.environ["GITHUB_OUTPUT"], "a") as f:
|
||||
f.write(f"models={json.dumps(artifacts)}\n")
|
||||
'
|
||||
|
||||
test-model:
|
||||
name: Test ${{ matrix.artifact.file_name }}
|
||||
needs: generate-matrix
|
||||
runs-on: ubuntu-latest
|
||||
container: ghcr.io/commaai/openpilot-base:latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
artifact: ${{ fromJson(needs.generate-matrix.outputs.models) }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: true
|
||||
|
||||
- name: Download Model Chunks in Parallel
|
||||
run: |
|
||||
mkdir -p /tmp/model_chunks
|
||||
echo '${{ toJson(matrix.artifact.chunks) }}' > chunks.json
|
||||
|
||||
BASE_URL="${{ matrix.artifact.download_uri.url }}"
|
||||
export BASE_DIR=$(dirname "$BASE_URL")
|
||||
|
||||
python3 -c '
|
||||
import json, os
|
||||
with open("chunks.json") as f:
|
||||
chunks = json.load(f)
|
||||
manifest_path = f"/tmp/model_chunks/${{ matrix.artifact.file_name }}.chunkmanifest"
|
||||
with open(manifest_path, "w") as f:
|
||||
f.write(str(len(chunks)))
|
||||
base_dir = os.environ["BASE_DIR"]
|
||||
with open("/tmp/curl_config.txt", "w") as f:
|
||||
for c in chunks:
|
||||
fn = c["file_name"]
|
||||
f.write(f"url = \"{base_dir}/{fn}\"\noutput = \"/tmp/model_chunks/{fn}\"\n")
|
||||
'
|
||||
curl -Z --parallel-immediate --parallel-max 16 -s -S -f -L -K /tmp/curl_config.txt
|
||||
|
||||
- name: Run Model Compatibility Test
|
||||
env:
|
||||
MODEL_BASE_NAME: ${{ matrix.artifact.file_name }}
|
||||
MODEL_CHUNK_DIR: "/tmp/model_chunks"
|
||||
PYTHONPATH: ".:./tinygrad_repo"
|
||||
run: |
|
||||
python3 -m pytest openpilot/sunnypilot/modeld_v2/tests/test_models.py
|
||||
@@ -288,6 +288,7 @@ if arch == "comma_arm64":
|
||||
SConscript([
|
||||
'openpilot/selfdrive/pandad/SConscript',
|
||||
'openpilot/selfdrive/controls/lib/longitudinal_mpc_lib/SConscript',
|
||||
'openpilot/selfdrive/controls/lib/ford_joint/SConscript',
|
||||
'openpilot/selfdrive/locationd/SConscript',
|
||||
'openpilot/selfdrive/modeld/SConscript',
|
||||
'openpilot/selfdrive/ui/SConscript',
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
# Action controller: stronger proportional C0 correction
|
||||
|
||||
The model action remains the steering target. Increase action-mode C0 P from
|
||||
0.5 to 1.0 so an existing tracking error produces twice the immediate C0
|
||||
correction, before command clipping. This is a trial gain, not an identified
|
||||
optimum or a claim of twice the wheel response.
|
||||
|
||||
C0 base mapping, distances, C1 P=0.75, C1 I=1.0, command bounds, upstream request
|
||||
limits, driver arbitration and CAN cadence are unchanged. Direct-path mode
|
||||
retains C0 P=0.5. Explicit gain overrides remain available for offline analysis.
|
||||
|
||||
The C0 correction has no integral state. It vanishes at zero error and changes
|
||||
sign on overshoot; the base command and existing C1 integral still remain. A
|
||||
higher P can amplify noise or oscillation through delayed vehicle response.
|
||||
Command replay does not establish physical stability, faster tracking or better
|
||||
unwinding.
|
||||
|
||||
## Selecting the trial
|
||||
|
||||
In Sunnylink, while offroad:
|
||||
|
||||
- Enable **Selected-Action Path Tracking (Experimental)**.
|
||||
- Disable **Model Geometry Reference (Experimental)** to select the original
|
||||
model action. Leaving it enabled keeps the unchanged direct-path controller.
|
||||
- Keep **C0 one-second distance** disabled for the fixed 7 m configuration.
|
||||
|
||||
Restart the updated software while offroad before testing. The controller and
|
||||
reference selection are latched at startup. Disengagement alone does not switch
|
||||
the reference. Turning the master controller toggle off restores upstream Ford.
|
||||
|
||||
Action-mode diagnostics now identify `model-action-curvature-c0-feedback-v19`
|
||||
and record `c0_proportional_gain=1.0`. Direct path remains
|
||||
`model-path-direct-feedback-v17`, with its gain at 0.5.
|
||||
|
||||
## Verification
|
||||
|
||||
Baseline: `0020c0c41940fe1f4b33dd23297def77a0a67e92`.
|
||||
|
||||
The Ford controller, startup selection, controlsd publication/CAN, geometry and
|
||||
Sunnylink settings suites pass: 648 tests and 2 subtests. Coverage includes
|
||||
catch-up, opposite correction on overshoot, zero-reference unwind, unchanged C1
|
||||
state, upstream fallback and preservation of the direct-path gain.
|
||||
|
||||
Native-time replay over ten older Lightning routes checks every control cycle
|
||||
and performs a CAN pack/decode check every tenth cycle. The baseline receives
|
||||
the same curvature conversion as the new controller. C1 commands, proportional
|
||||
terms, integrals and command validity are identical; C0 proportional correction
|
||||
is exactly doubled within numerical tolerance, and commands retain field bounds.
|
||||
There is no C0 change when feedback is disabled or the correction is zero.
|
||||
|
||||
```sh
|
||||
PYTHONPATH=.:opendbc_repo python tools/ford_pscm_lab/c0_feedback_validate.py \
|
||||
--baseline 0020c0c41940fe1f4b33dd23297def77a0a67e92 \
|
||||
--output .cache/ford_action_c0_gain_v19 --workers 4
|
||||
```
|
||||
|
||||
Three recent routes (157, 15a, 15b) additionally compare original model action
|
||||
and direct-path modes against the same baseline, preserving recorded motion.
|
||||
Original action is taken from the intake on 157 and timestamp-matched telemetry
|
||||
on 15a/15b. All direct-path commands and their accumulated state remain identical.
|
||||
The supplementary harness and per-cycle inputs are in the local replay cache.
|
||||
Combined validation totals and route results are in
|
||||
`ford_action_c0_gain_v19_validation.json`.
|
||||
|
||||
On the recorded measurements, a clean route-157 example requesting 174 degrees
|
||||
with the wheel at 74 degrees changes C0 magnitude from 2.23 to 3.42 m while C1
|
||||
remains at 0.5 rad. A route-15b example requesting 206 degrees with the wheel at
|
||||
239 degrees reduces the remaining into-turn C0 magnitude from 0.85 to 0.46 m.
|
||||
These are different commands on frozen measurements, not predicted wheel motion.
|
||||
@@ -0,0 +1,621 @@
|
||||
{
|
||||
"baseline": "0020c0c41940fe1f4b33dd23297def77a0a67e92",
|
||||
"action_c0_gain": 1.0,
|
||||
"direct_path_c0_gain": 0.5,
|
||||
"scope": "Frozen recorded measurements; validates command differences and preserved direct-path behavior, not changed vehicle motion.",
|
||||
"tests_passed": 648,
|
||||
"subtests_passed": 2,
|
||||
"cycles": 1290812,
|
||||
"can_round_trips": 141158,
|
||||
"older_routes": [
|
||||
{
|
||||
"route": "112",
|
||||
"baseline": "0020c0c41940fe1f4b33dd23297def77a0a67e92",
|
||||
"baseline_sha256": "02b64f40a56cadefb181afbf7c3720affa3c7fe4ac18a2902dc9c546fd6b395d",
|
||||
"cycles": 108971,
|
||||
"wire_round_trips": 10898,
|
||||
"c1_identical": true,
|
||||
"c0_gain": 1.0,
|
||||
"metrics": {
|
||||
"all": {
|
||||
"n": 83590,
|
||||
"delta_c0_m_quantiles": [
|
||||
0.009999999999999787,
|
||||
0.28000000000000025,
|
||||
1.7200000000000006
|
||||
],
|
||||
"c0_old_capped": 0,
|
||||
"c0_new_capped": 0
|
||||
},
|
||||
"straight": {
|
||||
"n": 43982,
|
||||
"delta_c0_m_quantiles": [
|
||||
0.009999999999999787,
|
||||
0.05000000000000071,
|
||||
1.4600000000000009
|
||||
],
|
||||
"c0_old_capped": 0,
|
||||
"c0_new_capped": 0
|
||||
},
|
||||
"turn": {
|
||||
"n": 9939,
|
||||
"delta_c0_m_quantiles": [
|
||||
0.15999999999999925,
|
||||
1.12,
|
||||
1.7200000000000006
|
||||
],
|
||||
"c0_old_capped": 0,
|
||||
"c0_new_capped": 0
|
||||
},
|
||||
"large_error": {
|
||||
"n": 3604,
|
||||
"delta_c0_m_quantiles": [
|
||||
0.7299999999999995,
|
||||
1.4299999999999997,
|
||||
1.7200000000000006
|
||||
],
|
||||
"c0_old_capped": 0,
|
||||
"c0_new_capped": 0
|
||||
}
|
||||
},
|
||||
"sources": {
|
||||
".cache/ford_route112/route.npz": "2b08a2fb636f7d14556d7df4035eafc1d1b97932237955528562a16db2b31d3e",
|
||||
".cache/ford_route112/metadata.json": "726d78a7e7aa45307dcfe27cb00775c20ecc9d54538eb7f26a0166fc216226ce",
|
||||
".cache/ford_route112/model_paths.npz": "9837afe78aab4cad288cad98a595a5777fa8a66bb235986b1272a7f7c54e559a",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/c0_feedback_validate.py": "66eca5715db49a47d30d9f4ed74b1d41b2bdb1d1e513ffdf85be6d94c9c8ac39",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/openpilot/selfdrive/controls/lib/ford_model_action.py": "d5841c1f6a4368de20ff14de89a48d7dce737c6a41d79628ccb4f8b19f28f91b"
|
||||
}
|
||||
},
|
||||
{
|
||||
"route": "113",
|
||||
"baseline": "0020c0c41940fe1f4b33dd23297def77a0a67e92",
|
||||
"baseline_sha256": "02b64f40a56cadefb181afbf7c3720affa3c7fe4ac18a2902dc9c546fd6b395d",
|
||||
"cycles": 49614,
|
||||
"wire_round_trips": 4962,
|
||||
"c1_identical": true,
|
||||
"c0_gain": 1.0,
|
||||
"metrics": {
|
||||
"all": {
|
||||
"n": 24171,
|
||||
"delta_c0_m_quantiles": [
|
||||
0.009999999999999787,
|
||||
0.9499999999999993,
|
||||
2.2299999999999995
|
||||
],
|
||||
"c0_old_capped": 0,
|
||||
"c0_new_capped": 0
|
||||
},
|
||||
"straight": {
|
||||
"n": 16815,
|
||||
"delta_c0_m_quantiles": [
|
||||
0.009999999999999787,
|
||||
0.0529999999999931,
|
||||
1.0599999999999996
|
||||
],
|
||||
"c0_old_capped": 0,
|
||||
"c0_new_capped": 0
|
||||
},
|
||||
"turn": {
|
||||
"n": 2920,
|
||||
"delta_c0_m_quantiles": [
|
||||
0.6600000000000001,
|
||||
1.8699999999999997,
|
||||
2.2299999999999995
|
||||
],
|
||||
"c0_old_capped": 0,
|
||||
"c0_new_capped": 0
|
||||
},
|
||||
"large_error": {
|
||||
"n": 2831,
|
||||
"delta_c0_m_quantiles": [
|
||||
0.8600000000000003,
|
||||
1.9450000000000007,
|
||||
2.2299999999999995
|
||||
],
|
||||
"c0_old_capped": 0,
|
||||
"c0_new_capped": 0
|
||||
}
|
||||
},
|
||||
"sources": {
|
||||
".cache/ford_route113/route.npz": "774ca4a21b7113c2706d6130bc180c3216ea4833155300ab01e75b3486e36327",
|
||||
".cache/ford_route113/metadata.json": "1c0ca74dd48b90ab9d5444c5ca7f8aa9361700bbdf98bd5e853be50ad2895d7f",
|
||||
".cache/ford_route113/model_paths.npz": "93c41761eb85263f534f5371b905482cf7c948582eb1e9149966594be1d3768f",
|
||||
".cache/ford_route112/metadata.json": "726d78a7e7aa45307dcfe27cb00775c20ecc9d54538eb7f26a0166fc216226ce",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/c0_feedback_validate.py": "66eca5715db49a47d30d9f4ed74b1d41b2bdb1d1e513ffdf85be6d94c9c8ac39",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/openpilot/selfdrive/controls/lib/ford_model_action.py": "d5841c1f6a4368de20ff14de89a48d7dce737c6a41d79628ccb4f8b19f28f91b"
|
||||
}
|
||||
},
|
||||
{
|
||||
"route": "117",
|
||||
"baseline": "0020c0c41940fe1f4b33dd23297def77a0a67e92",
|
||||
"baseline_sha256": "02b64f40a56cadefb181afbf7c3720affa3c7fe4ac18a2902dc9c546fd6b395d",
|
||||
"cycles": 27301,
|
||||
"wire_round_trips": 2731,
|
||||
"c1_identical": true,
|
||||
"c0_gain": 1.0,
|
||||
"metrics": {
|
||||
"all": {
|
||||
"n": 13659,
|
||||
"delta_c0_m_quantiles": [
|
||||
0.03000000000000025,
|
||||
0.5,
|
||||
1.5999999999999996
|
||||
],
|
||||
"c0_old_capped": 0,
|
||||
"c0_new_capped": 0
|
||||
},
|
||||
"straight": {
|
||||
"n": 5001,
|
||||
"delta_c0_m_quantiles": [
|
||||
0.019999999999999574,
|
||||
0.2999999999999998,
|
||||
1.5999999999999996
|
||||
],
|
||||
"c0_old_capped": 0,
|
||||
"c0_new_capped": 0
|
||||
},
|
||||
"turn": {
|
||||
"n": 2891,
|
||||
"delta_c0_m_quantiles": [
|
||||
0.1200000000000001,
|
||||
0.7400000000000002,
|
||||
1.38
|
||||
],
|
||||
"c0_old_capped": 0,
|
||||
"c0_new_capped": 0
|
||||
},
|
||||
"large_error": {
|
||||
"n": 1195,
|
||||
"delta_c0_m_quantiles": [
|
||||
0.54,
|
||||
1.5600000000000005,
|
||||
1.5999999999999996
|
||||
],
|
||||
"c0_old_capped": 0,
|
||||
"c0_new_capped": 0
|
||||
}
|
||||
},
|
||||
"sources": {
|
||||
".cache/ford_route117/route.npz": "65e4ea6c76dce73018b636479b3777116f1e58bb9813e6c1a7b79018b219140e",
|
||||
".cache/ford_route117/metadata.json": "348396b3059c5f54d7938cd3acf2bb268a813b47ff47561da7a94cb75c3f3114",
|
||||
".cache/ford_route117/model_paths.npz": "079f6003968287530e1ed6f2243c727e80c2ef3d16fb8a7d945d7eca744d54e7",
|
||||
".cache/ford_route112/metadata.json": "726d78a7e7aa45307dcfe27cb00775c20ecc9d54538eb7f26a0166fc216226ce",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/c0_feedback_validate.py": "66eca5715db49a47d30d9f4ed74b1d41b2bdb1d1e513ffdf85be6d94c9c8ac39",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/openpilot/selfdrive/controls/lib/ford_model_action.py": "d5841c1f6a4368de20ff14de89a48d7dce737c6a41d79628ccb4f8b19f28f91b"
|
||||
}
|
||||
},
|
||||
{
|
||||
"route": "11c",
|
||||
"baseline": "0020c0c41940fe1f4b33dd23297def77a0a67e92",
|
||||
"baseline_sha256": "02b64f40a56cadefb181afbf7c3720affa3c7fe4ac18a2902dc9c546fd6b395d",
|
||||
"cycles": 54146,
|
||||
"wire_round_trips": 5415,
|
||||
"c1_identical": true,
|
||||
"c0_gain": 1.0,
|
||||
"metrics": {
|
||||
"all": {
|
||||
"n": 36780,
|
||||
"delta_c0_m_quantiles": [
|
||||
0.020000000000000462,
|
||||
0.3700000000000001,
|
||||
2.1900000000000004
|
||||
],
|
||||
"c0_old_capped": 0,
|
||||
"c0_new_capped": 0
|
||||
},
|
||||
"straight": {
|
||||
"n": 16750,
|
||||
"delta_c0_m_quantiles": [
|
||||
0.010000000000000675,
|
||||
0.09999999999999964,
|
||||
0.5099999999999998
|
||||
],
|
||||
"c0_old_capped": 0,
|
||||
"c0_new_capped": 0
|
||||
},
|
||||
"turn": {
|
||||
"n": 2461,
|
||||
"delta_c0_m_quantiles": [
|
||||
0.1899999999999995,
|
||||
0.8799999999999999,
|
||||
2.1900000000000004
|
||||
],
|
||||
"c0_old_capped": 0,
|
||||
"c0_new_capped": 0
|
||||
},
|
||||
"large_error": {
|
||||
"n": 1827,
|
||||
"delta_c0_m_quantiles": [
|
||||
0.5500000000000003,
|
||||
0.9500000000000002,
|
||||
2.1900000000000004
|
||||
],
|
||||
"c0_old_capped": 0,
|
||||
"c0_new_capped": 0
|
||||
}
|
||||
},
|
||||
"sources": {
|
||||
".cache/ford_maneuver_11c/analysis/route.npz": "1f3c37a7c7ae85ba69e9958435a9568f8244fa862f43ad1ee2a0192d1df13966",
|
||||
".cache/ford_maneuver_11c/analysis/metadata.json": "2d0051d23059145988ca905e5386704def24b8080ce92541cec5e4ab501bcd0c",
|
||||
".cache/ford_maneuver_11c/analysis/model_paths.npz": "0ff433da3dde1d4d7f10596cd53558ec5de9d56fbc49aec3e8afb4c9872377d5",
|
||||
".cache/ford_route112/metadata.json": "726d78a7e7aa45307dcfe27cb00775c20ecc9d54538eb7f26a0166fc216226ce",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/c0_feedback_validate.py": "66eca5715db49a47d30d9f4ed74b1d41b2bdb1d1e513ffdf85be6d94c9c8ac39",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/openpilot/selfdrive/controls/lib/ford_model_action.py": "d5841c1f6a4368de20ff14de89a48d7dce737c6a41d79628ccb4f8b19f28f91b"
|
||||
}
|
||||
},
|
||||
{
|
||||
"route": "125",
|
||||
"baseline": "0020c0c41940fe1f4b33dd23297def77a0a67e92",
|
||||
"baseline_sha256": "02b64f40a56cadefb181afbf7c3720affa3c7fe4ac18a2902dc9c546fd6b395d",
|
||||
"cycles": 41717,
|
||||
"wire_round_trips": 4172,
|
||||
"c1_identical": true,
|
||||
"c0_gain": 1.0,
|
||||
"metrics": {
|
||||
"all": {
|
||||
"n": 41107,
|
||||
"delta_c0_m_quantiles": [
|
||||
0.0,
|
||||
0.05000000000000071,
|
||||
0.39000000000000057
|
||||
],
|
||||
"c0_old_capped": 0,
|
||||
"c0_new_capped": 0
|
||||
},
|
||||
"straight": {
|
||||
"n": 33135,
|
||||
"delta_c0_m_quantiles": [
|
||||
0.0,
|
||||
0.010000000000000675,
|
||||
0.27000000000000046
|
||||
],
|
||||
"c0_old_capped": 0,
|
||||
"c0_new_capped": 0
|
||||
},
|
||||
"large_error": {
|
||||
"n": 192,
|
||||
"delta_c0_m_quantiles": [
|
||||
0.3200000000000003,
|
||||
0.3800000000000003,
|
||||
0.39000000000000057
|
||||
],
|
||||
"c0_old_capped": 0,
|
||||
"c0_new_capped": 0
|
||||
}
|
||||
},
|
||||
"sources": {
|
||||
".cache/ford_c0_response_124_125/route125/route.npz": "8dcf304871bf5c6bf1ce206c788d7034bf5f056522faded0aa7948ff1cccbb75",
|
||||
".cache/ford_c0_response_124_125/route125/metadata.json": "1f2b812d3c2cc8e5a2246098756940d6f8989a2591daf4d74b1ef4c8555e2d57",
|
||||
".cache/ford_c0_response_124_125/route125/model_paths.npz": "4be9c7809dabbcb52296ba93b4743146dc941e49a32aca07f008b75757703e49",
|
||||
".cache/ford_route112/metadata.json": "726d78a7e7aa45307dcfe27cb00775c20ecc9d54538eb7f26a0166fc216226ce",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/c0_feedback_validate.py": "66eca5715db49a47d30d9f4ed74b1d41b2bdb1d1e513ffdf85be6d94c9c8ac39",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/openpilot/selfdrive/controls/lib/ford_model_action.py": "d5841c1f6a4368de20ff14de89a48d7dce737c6a41d79628ccb4f8b19f28f91b"
|
||||
}
|
||||
},
|
||||
{
|
||||
"route": "146",
|
||||
"baseline": "0020c0c41940fe1f4b33dd23297def77a0a67e92",
|
||||
"baseline_sha256": "02b64f40a56cadefb181afbf7c3720affa3c7fe4ac18a2902dc9c546fd6b395d",
|
||||
"cycles": 53229,
|
||||
"wire_round_trips": 5323,
|
||||
"c1_identical": true,
|
||||
"c0_gain": 1.0,
|
||||
"metrics": {
|
||||
"all": {
|
||||
"n": 40717,
|
||||
"delta_c0_m_quantiles": [
|
||||
0.0,
|
||||
0.04999999999999982,
|
||||
1.13
|
||||
],
|
||||
"c0_old_capped": 0,
|
||||
"c0_new_capped": 0
|
||||
},
|
||||
"straight": {
|
||||
"n": 30796,
|
||||
"delta_c0_m_quantiles": [
|
||||
0.0,
|
||||
0.02999999999999936,
|
||||
0.35000000000000053
|
||||
],
|
||||
"c0_old_capped": 0,
|
||||
"c0_new_capped": 0
|
||||
},
|
||||
"turn": {
|
||||
"n": 585,
|
||||
"delta_c0_m_quantiles": [
|
||||
0.3700000000000001,
|
||||
1.0300000000000002,
|
||||
1.13
|
||||
],
|
||||
"c0_old_capped": 0,
|
||||
"c0_new_capped": 0
|
||||
},
|
||||
"large_error": {
|
||||
"n": 298,
|
||||
"delta_c0_m_quantiles": [
|
||||
0.4500000000000002,
|
||||
1.0615,
|
||||
1.13
|
||||
],
|
||||
"c0_old_capped": 0,
|
||||
"c0_new_capped": 0
|
||||
}
|
||||
},
|
||||
"sources": {
|
||||
".cache/ford_model_replay_146/full/route.npz": "b01db5bdd1c3b9fb83418a298571c98e60b5eb0509800d86cdaf07bafcdc6d63",
|
||||
".cache/ford_model_replay_146/full/metadata.json": "2be1254dd1eee422f3b2c2011faa91a21ab431751998525105381c2886df15ca",
|
||||
".cache/ford_model_replay_146/full/model_paths.npz": "11e0e9c8ce2959aac72d2f8a9cc4f6a025480034d5cd8b59b68a41b2ae3cf4a8",
|
||||
".cache/ford_route112/metadata.json": "726d78a7e7aa45307dcfe27cb00775c20ecc9d54538eb7f26a0166fc216226ce",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/c0_feedback_validate.py": "66eca5715db49a47d30d9f4ed74b1d41b2bdb1d1e513ffdf85be6d94c9c8ac39",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/openpilot/selfdrive/controls/lib/ford_model_action.py": "d5841c1f6a4368de20ff14de89a48d7dce737c6a41d79628ccb4f8b19f28f91b"
|
||||
}
|
||||
},
|
||||
{
|
||||
"route": "149",
|
||||
"baseline": "0020c0c41940fe1f4b33dd23297def77a0a67e92",
|
||||
"baseline_sha256": "02b64f40a56cadefb181afbf7c3720affa3c7fe4ac18a2902dc9c546fd6b395d",
|
||||
"cycles": 132334,
|
||||
"wire_round_trips": 13234,
|
||||
"c1_identical": true,
|
||||
"c0_gain": 1.0,
|
||||
"metrics": {
|
||||
"all": {
|
||||
"n": 109224,
|
||||
"delta_c0_m_quantiles": [
|
||||
0.019999999999999574,
|
||||
0.4400000000000004,
|
||||
2.2800000000000002
|
||||
],
|
||||
"c0_old_capped": 0,
|
||||
"c0_new_capped": 0
|
||||
},
|
||||
"straight": {
|
||||
"n": 54914,
|
||||
"delta_c0_m_quantiles": [
|
||||
0.009999999999999787,
|
||||
0.07000000000000028,
|
||||
2.039999999999999
|
||||
],
|
||||
"c0_old_capped": 0,
|
||||
"c0_new_capped": 0
|
||||
},
|
||||
"turn": {
|
||||
"n": 15161,
|
||||
"delta_c0_m_quantiles": [
|
||||
0.20999999999999996,
|
||||
1.0300000000000002,
|
||||
2.25
|
||||
],
|
||||
"c0_old_capped": 0,
|
||||
"c0_new_capped": 0
|
||||
},
|
||||
"large_error": {
|
||||
"n": 6659,
|
||||
"delta_c0_m_quantiles": [
|
||||
0.6399999999999997,
|
||||
1.4100000000000001,
|
||||
2.2800000000000002
|
||||
],
|
||||
"c0_old_capped": 0,
|
||||
"c0_new_capped": 0
|
||||
}
|
||||
},
|
||||
"sources": {
|
||||
".cache/ford_route149/full/route.npz": "aa5902877343cd033ee286b3668d91a85336ffbf740861849fb0b76b0ca24ade",
|
||||
".cache/ford_route149/full/metadata.json": "624fff03c25eb298661cb7b25f3dbe6d214d863f799d93635c0f4f05fc0d2b32",
|
||||
".cache/ford_route149/full/model_paths.npz": "19827800b8fb5983f3d6b72fcfaf36e35a40170bb17cd7c1e49374744fb449fb",
|
||||
".cache/ford_route112/metadata.json": "726d78a7e7aa45307dcfe27cb00775c20ecc9d54538eb7f26a0166fc216226ce",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/c0_feedback_validate.py": "66eca5715db49a47d30d9f4ed74b1d41b2bdb1d1e513ffdf85be6d94c9c8ac39",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/openpilot/selfdrive/controls/lib/ford_model_action.py": "d5841c1f6a4368de20ff14de89a48d7dce737c6a41d79628ccb4f8b19f28f91b"
|
||||
}
|
||||
},
|
||||
{
|
||||
"route": "151",
|
||||
"baseline": "0020c0c41940fe1f4b33dd23297def77a0a67e92",
|
||||
"baseline_sha256": "02b64f40a56cadefb181afbf7c3720affa3c7fe4ac18a2902dc9c546fd6b395d",
|
||||
"cycles": 325708,
|
||||
"wire_round_trips": 32571,
|
||||
"c1_identical": true,
|
||||
"c0_gain": 1.0,
|
||||
"metrics": {
|
||||
"all": {
|
||||
"n": 124806,
|
||||
"delta_c0_m_quantiles": [
|
||||
0.009999999999999787,
|
||||
0.10000000000000053,
|
||||
2.210000000000001
|
||||
],
|
||||
"c0_old_capped": 0,
|
||||
"c0_new_capped": 0
|
||||
},
|
||||
"straight": {
|
||||
"n": 85546,
|
||||
"delta_c0_m_quantiles": [
|
||||
0.009999999999999787,
|
||||
0.040000000000000036,
|
||||
0.6300000000000003
|
||||
],
|
||||
"c0_old_capped": 0,
|
||||
"c0_new_capped": 0
|
||||
},
|
||||
"turn": {
|
||||
"n": 3715,
|
||||
"delta_c0_m_quantiles": [
|
||||
0.25,
|
||||
1.1229999999999976,
|
||||
2.210000000000001
|
||||
],
|
||||
"c0_old_capped": 0,
|
||||
"c0_new_capped": 0
|
||||
},
|
||||
"large_error": {
|
||||
"n": 1839,
|
||||
"delta_c0_m_quantiles": [
|
||||
0.6200000000000001,
|
||||
1.3499999999999996,
|
||||
2.210000000000001
|
||||
],
|
||||
"c0_old_capped": 0,
|
||||
"c0_new_capped": 0
|
||||
}
|
||||
},
|
||||
"sources": {
|
||||
".cache/ford_route151/full/route.npz": "41a5b8bd388cf5a3d553f784542376ac9355fcdc5be4f427053d0504537babe1",
|
||||
".cache/ford_route151/full/metadata.json": "937825317a0edd470c54647240b922be8f79dda5b3365ffdd61281f0aca877a1",
|
||||
".cache/ford_route151/full/model_paths.npz": "980b3843cec04254280d744a5801cee1bf0f8ef371052398327ff245f9eee01b",
|
||||
".cache/ford_route112/metadata.json": "726d78a7e7aa45307dcfe27cb00775c20ecc9d54538eb7f26a0166fc216226ce",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/c0_feedback_validate.py": "66eca5715db49a47d30d9f4ed74b1d41b2bdb1d1e513ffdf85be6d94c9c8ac39",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/openpilot/selfdrive/controls/lib/ford_model_action.py": "d5841c1f6a4368de20ff14de89a48d7dce737c6a41d79628ccb4f8b19f28f91b"
|
||||
}
|
||||
},
|
||||
{
|
||||
"route": "a9",
|
||||
"baseline": "0020c0c41940fe1f4b33dd23297def77a0a67e92",
|
||||
"baseline_sha256": "02b64f40a56cadefb181afbf7c3720affa3c7fe4ac18a2902dc9c546fd6b395d",
|
||||
"cycles": 286319,
|
||||
"wire_round_trips": 28632,
|
||||
"c1_identical": true,
|
||||
"c0_gain": 1.0,
|
||||
"metrics": {
|
||||
"all": {
|
||||
"n": 142471,
|
||||
"delta_c0_m_quantiles": [
|
||||
0.019999999999999574,
|
||||
0.15000000000000036,
|
||||
1.5700000000000003
|
||||
],
|
||||
"c0_old_capped": 128,
|
||||
"c0_new_capped": 226
|
||||
},
|
||||
"straight": {
|
||||
"n": 99221,
|
||||
"delta_c0_m_quantiles": [
|
||||
0.010000000000000675,
|
||||
0.05999999999999961,
|
||||
1.4799999999999995
|
||||
],
|
||||
"c0_old_capped": 0,
|
||||
"c0_new_capped": 0
|
||||
},
|
||||
"turn": {
|
||||
"n": 6893,
|
||||
"delta_c0_m_quantiles": [
|
||||
0.21999999999999975,
|
||||
0.9000000000000004,
|
||||
1.4699999999999998
|
||||
],
|
||||
"c0_old_capped": 128,
|
||||
"c0_new_capped": 226
|
||||
},
|
||||
"large_error": {
|
||||
"n": 3224,
|
||||
"delta_c0_m_quantiles": [
|
||||
0.6299999999999999,
|
||||
1.1800000000000004,
|
||||
1.5700000000000003
|
||||
],
|
||||
"c0_old_capped": 128,
|
||||
"c0_new_capped": 226
|
||||
}
|
||||
},
|
||||
"sources": {
|
||||
".cache/ford_routea9/route.npz": "3fa8cabfb729dd42d689de38618d1d21c8965c9d60f914546f4d7cc58db8c975",
|
||||
".cache/ford_routea9/metadata.json": "403d35504bf596144845ac2060ce067d571f49a2955d16223af28a79a7a66e93",
|
||||
".cache/ford_routea9/model_paths.npz": "7f3646ad24aa52de5982c65600813de5bbc5f7664d92a2516bbba35da6e7b240",
|
||||
".cache/ford_route112/metadata.json": "726d78a7e7aa45307dcfe27cb00775c20ecc9d54538eb7f26a0166fc216226ce",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/c0_feedback_validate.py": "66eca5715db49a47d30d9f4ed74b1d41b2bdb1d1e513ffdf85be6d94c9c8ac39",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/openpilot/selfdrive/controls/lib/ford_model_action.py": "d5841c1f6a4368de20ff14de89a48d7dce737c6a41d79628ccb4f8b19f28f91b"
|
||||
}
|
||||
},
|
||||
{
|
||||
"route": "b9",
|
||||
"baseline": "0020c0c41940fe1f4b33dd23297def77a0a67e92",
|
||||
"baseline_sha256": "02b64f40a56cadefb181afbf7c3720affa3c7fe4ac18a2902dc9c546fd6b395d",
|
||||
"cycles": 90774,
|
||||
"wire_round_trips": 9078,
|
||||
"c1_identical": true,
|
||||
"c0_gain": 1.0,
|
||||
"metrics": {
|
||||
"all": {
|
||||
"n": 81066,
|
||||
"delta_c0_m_quantiles": [
|
||||
0.010000000000000675,
|
||||
0.3100000000000005,
|
||||
2.130000000000001
|
||||
],
|
||||
"c0_old_capped": 10,
|
||||
"c0_new_capped": 34
|
||||
},
|
||||
"straight": {
|
||||
"n": 42647,
|
||||
"delta_c0_m_quantiles": [
|
||||
0.009999999999999787,
|
||||
0.07000000000000028,
|
||||
1.63
|
||||
],
|
||||
"c0_old_capped": 0,
|
||||
"c0_new_capped": 0
|
||||
},
|
||||
"turn": {
|
||||
"n": 9816,
|
||||
"delta_c0_m_quantiles": [
|
||||
0.1200000000000001,
|
||||
0.82,
|
||||
2.130000000000001
|
||||
],
|
||||
"c0_old_capped": 10,
|
||||
"c0_new_capped": 34
|
||||
},
|
||||
"large_error": {
|
||||
"n": 3486,
|
||||
"delta_c0_m_quantiles": [
|
||||
0.6599999999999997,
|
||||
1.5,
|
||||
2.130000000000001
|
||||
],
|
||||
"c0_old_capped": 10,
|
||||
"c0_new_capped": 34
|
||||
}
|
||||
},
|
||||
"sources": {
|
||||
".cache/ford_routeb9/route.npz": "b07c789d8155335f5d120d0262fced6e4d5803fe767b0ff49b6413dce4140b5c",
|
||||
".cache/ford_routeb9/metadata.json": "9ce452220cab61b81883f32fc2fcaf5db6c78a674cb255a49cc77d5029580fee",
|
||||
".cache/ford_routeb9/model_paths.npz": "6b1f87897c050273fdc05af051307a049b6fc3a93072e7cda1721195ce7c3861",
|
||||
".cache/ford_route112/metadata.json": "726d78a7e7aa45307dcfe27cb00775c20ecc9d54538eb7f26a0166fc216226ce",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/c0_feedback_validate.py": "66eca5715db49a47d30d9f4ed74b1d41b2bdb1d1e513ffdf85be6d94c9c8ac39",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/openpilot/selfdrive/controls/lib/ford_model_action.py": "d5841c1f6a4368de20ff14de89a48d7dce737c6a41d79628ccb4f8b19f28f91b"
|
||||
}
|
||||
}
|
||||
],
|
||||
"recent_routes": [
|
||||
{
|
||||
"route": "157",
|
||||
"cycles": 76554,
|
||||
"wire_checks": 15312,
|
||||
"action_c1_and_integral_identical": true,
|
||||
"action_c0_correction_doubled": true,
|
||||
"direct_path_identical": true,
|
||||
"action_source": "Reconstructed geometry with recorded delay/model smoothing; speed approximated by first consuming control sample"
|
||||
},
|
||||
{
|
||||
"route": "15a",
|
||||
"cycles": 21867,
|
||||
"wire_checks": 4374,
|
||||
"action_c1_and_integral_identical": true,
|
||||
"action_c0_correction_doubled": true,
|
||||
"direct_path_identical": true,
|
||||
"action_source": "Logged original action and selected geometry; exact model timestamp joins"
|
||||
},
|
||||
{
|
||||
"route": "15b",
|
||||
"cycles": 22278,
|
||||
"wire_checks": 4456,
|
||||
"action_c1_and_integral_identical": true,
|
||||
"action_c0_correction_doubled": true,
|
||||
"direct_path_identical": true,
|
||||
"action_source": "Logged original action and selected geometry; exact model timestamp joins"
|
||||
}
|
||||
],
|
||||
"recent_harness_sha256": "b129b3b5d76a30ca647043a32207031a98a53eeab4620b4ebb7880c1b209a214"
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
# Ford C0 proportional feedback trial
|
||||
|
||||
This describes the original P=0.5 trial. The subsequent
|
||||
[action-mode gain trial](ford_action_c0_gain_v19.md) raises action C0 P to 1.0;
|
||||
direct-path mode retains P=0.5.
|
||||
|
||||
The existing controller sends its tracking correction through C1 only. Offline
|
||||
identification on the Lightning suggests that stronger C1 requests can stop
|
||||
producing faster wheel movement while additional C0 may still help. This trial
|
||||
adds proportional correction to C0. It does not change the selected curvature,
|
||||
C0 base geometry, C1 PI calculation, transmission rate or existing opt-in selection.
|
||||
|
||||
## Command
|
||||
|
||||
With host-sign road curvature error `e = reference - measured`:
|
||||
|
||||
```
|
||||
scale = VM.get_steer_from_curvature(1, speed, 0) / (CP.steerRatio * CP.wheelbase)
|
||||
C0_response = 0.010717679293424373 + 0.018122981795212647 / max(speed, 1.34)^2
|
||||
C0_P = 0.5 * e * scale / C0_response
|
||||
C0 = clip(existing_bounded_C0_base + C0_P, -5.11, 5.11)
|
||||
```
|
||||
|
||||
The response coefficients are an offline fit to isolated C0 commands on route
|
||||
`84865544361f55cb/00000145--5d9f02fee7`. They express **geometric steering curvature
|
||||
per metre of C0**, not road curvature or an instantaneous wheel response. The
|
||||
existing vehicle model converts the error into those units; its road-roll and
|
||||
steering-angle offsets cancel in the error. No plant identification, observer or
|
||||
dynamic plant simulation runs on the device.
|
||||
|
||||
`0.5` is an explicit trial feedback gain, not a fitted optimum. Before field
|
||||
clipping, the extra C0 has a fitted steady effect equal to half the current
|
||||
wheel-angle error. This does not mean 50% of the C0 field. The 1.34 m/s term
|
||||
holds the fitted conversion below the identification dataset's minimum speed;
|
||||
it does not disable steering or add a maneuver state.
|
||||
|
||||
C0 correction is recomputed every update, including updates that have no fresh
|
||||
integration interval. It has no integrator, request-change state, deadband, or
|
||||
additional slew limit. It becomes zero at zero error and changes sign on
|
||||
overshoot. Existing driver override and PSCM arbitration disable it together
|
||||
with other feedback. PSCM limit-reached continues to inhibit outward C1
|
||||
integration; it does not freeze either proportional command. C1 remains
|
||||
P=0.75, I=1.0. C2/C3 remain zero.
|
||||
|
||||
The existing `FordModelActionController` Sunnylink toggle still selects this
|
||||
controller for CAN FD. With the toggle off, startup selects upstream Ford
|
||||
control. No new UI or lateral-maneuver changes are included.
|
||||
|
||||
## Offline validation
|
||||
|
||||
The native-time replay compares the candidate against production commit
|
||||
`d425b3260b0785b22096d130702b54a2e0761c36`, with recorded measurements, requests,
|
||||
model health, driver input, service timestamps and PSCM arbitration held fixed.
|
||||
|
||||
- Ten Lightning routes: a9, b9, 112, 113, 117, 11c, 125, 146, 149 and 151.
|
||||
- 1,170,113 cycles; 117,016 in-memory Float32/CAN encode/decode checks.
|
||||
- Identical C1 output, C1 integral and command validity on every compared cycle.
|
||||
- All outputs finite, inside field bounds, with C2/C3 zero.
|
||||
- C0 exactly matches the old command whenever its new correction is zero or
|
||||
feedback is disabled.
|
||||
- Across eligible samples with at least 30 degrees of wheel-angle error, the
|
||||
median C0 change is 0.65 m and the 95th percentile is 1.47 m.
|
||||
- Across eligible requests below 5 degrees, the median change is 0.01 m and
|
||||
the 95th percentile is 0.05 m. This cohort includes turn exits with a still
|
||||
turned wheel; its largest correction is consequently much larger.
|
||||
- At route 151 time 306.89 s, the replay changes C0 from 0.75 to 1.68 m for
|
||||
approximately 76 degrees of tracking error. Both replays send the same C1.
|
||||
|
||||
The test suite covers immediate correction, no accumulation, reversal, catch-up,
|
||||
invalid inputs, real vehicle-model units across stiffness/speed/roll changes,
|
||||
driver/PSCM arbitration, upstream fallback, and actual controlsd publication
|
||||
through the 100 Hz CAN sender. The machine-readable route replay summary is
|
||||
`ford_c0_feedback_v15_validation.json`.
|
||||
|
||||
```
|
||||
PYTHONPATH=.:opendbc_repo python -m pytest -q \
|
||||
openpilot/selfdrive/controls/tests/test_ford_model_action*.py \
|
||||
openpilot/selfdrive/controls/tests/test_ford_path.py
|
||||
|
||||
PYTHONPATH=.:opendbc_repo python tools/ford_pscm_lab/c0_feedback_validate.py \
|
||||
--output .cache/ford_c0_feedback_v15 --workers 4
|
||||
```
|
||||
|
||||
The replay requires the existing local rlog extracts. Input hashes are recorded
|
||||
in its validation JSON. It does not assume the truck follows modified commands.
|
||||
|
||||
## What is still experimental
|
||||
|
||||
The earlier fitted plant overpredicted one second of wheel movement by about
|
||||
16 degrees in the route 151 example. It also misses the phase of a low-speed
|
||||
oscillation in route 125. Independent C0/C1 response contributions are an
|
||||
approximation; a shared PSCM limit could prevent the extra movement predicted
|
||||
from C0.
|
||||
|
||||
An additional four-second fitted-plant simulation, with controller feedback
|
||||
recomputed against simulated wheel angle, showed no regression for the tested
|
||||
0.5 gain in its turn, unwind and near-straight cohorts. Route 149 had 27 eligible
|
||||
turn windows; route 151 had no four-second turn windows surviving the strict
|
||||
intervention/status mask. Future reference and speed were frozen, and the model
|
||||
does not reproduce the route 125 failure faithfully. These results are a
|
||||
sanity check, not validation of road tracking or an optimized gain.
|
||||
|
||||
Actual acceptance is better desired-versus-actual wheel tracking on turn entry,
|
||||
without added oscillation, overshoot or delayed unwind. The software behavior
|
||||
is validated; the physical improvement remains to be measured.
|
||||
|
||||
Diagnostics identify `model-action-curvature-c0-feedback-v15` and record
|
||||
`offset_proportional`, `c0_proportional_gain`, and `curvature_scale` alongside
|
||||
the existing heading/feedforward/integral signals.
|
||||
@@ -0,0 +1,856 @@
|
||||
{
|
||||
"scope": "Replay C0 feedback against v14 using frozen native-time Lightning measurements.\n\nThis checks software/CAN behavior, not the physical response to new commands.\nUse cached route.npz, model_paths.npz and metadata.json from the rlog extractor.\n",
|
||||
"routes": [
|
||||
{
|
||||
"route": "112",
|
||||
"baseline": "d425b3260b0785b22096d130702b54a2e0761c36",
|
||||
"baseline_sha256": "c55464ecfbe7fad9f51905cf5f226be2676440ac5afec470b05515e32b065cc8",
|
||||
"cycles": 108971,
|
||||
"wire_round_trips": 10898,
|
||||
"c1_identical": true,
|
||||
"c0_gain": 0.5,
|
||||
"metrics": {
|
||||
"all": {
|
||||
"n": 83590,
|
||||
"delta_c0_m_quantiles": [
|
||||
0.009999999999999787,
|
||||
0.28000000000000025,
|
||||
1.7300000000000004
|
||||
],
|
||||
"c0_old_capped": 0,
|
||||
"c0_new_capped": 0
|
||||
},
|
||||
"straight": {
|
||||
"n": 43982,
|
||||
"delta_c0_m_quantiles": [
|
||||
0.009999999999999787,
|
||||
0.04999999999999982,
|
||||
1.4699999999999998
|
||||
],
|
||||
"c0_old_capped": 0,
|
||||
"c0_new_capped": 0
|
||||
},
|
||||
"turn": {
|
||||
"n": 9939,
|
||||
"delta_c0_m_quantiles": [
|
||||
0.15999999999999925,
|
||||
1.1199999999999997,
|
||||
1.7300000000000004
|
||||
],
|
||||
"c0_old_capped": 0,
|
||||
"c0_new_capped": 0
|
||||
},
|
||||
"large_error": {
|
||||
"n": 3604,
|
||||
"delta_c0_m_quantiles": [
|
||||
0.73,
|
||||
1.4299999999999997,
|
||||
1.7300000000000004
|
||||
],
|
||||
"c0_old_capped": 0,
|
||||
"c0_new_capped": 0
|
||||
}
|
||||
},
|
||||
"sources": {
|
||||
".cache/ford_route112/route.npz": "2b08a2fb636f7d14556d7df4035eafc1d1b97932237955528562a16db2b31d3e",
|
||||
".cache/ford_route112/metadata.json": "726d78a7e7aa45307dcfe27cb00775c20ecc9d54538eb7f26a0166fc216226ce",
|
||||
".cache/ford_route112/model_paths.npz": "9837afe78aab4cad288cad98a595a5777fa8a66bb235986b1272a7f7c54e559a",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/c0_feedback_validate.py": "12b843f024b671f8b4282e1981f6d68de879a9cdff20fee74fd5228b6e4168ad",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/openpilot/selfdrive/controls/lib/ford_model_action.py": "8bc8554ed6743c5faf433deaa6de3f07fae27d183f62b04f49f9c3b9f1e8dcca"
|
||||
}
|
||||
},
|
||||
{
|
||||
"route": "113",
|
||||
"baseline": "d425b3260b0785b22096d130702b54a2e0761c36",
|
||||
"baseline_sha256": "c55464ecfbe7fad9f51905cf5f226be2676440ac5afec470b05515e32b065cc8",
|
||||
"cycles": 49614,
|
||||
"wire_round_trips": 4962,
|
||||
"c1_identical": true,
|
||||
"c0_gain": 0.5,
|
||||
"metrics": {
|
||||
"all": {
|
||||
"n": 24171,
|
||||
"delta_c0_m_quantiles": [
|
||||
0.009999999999999787,
|
||||
0.9400000000000004,
|
||||
2.2200000000000006
|
||||
],
|
||||
"c0_old_capped": 0,
|
||||
"c0_new_capped": 0
|
||||
},
|
||||
"straight": {
|
||||
"n": 16815,
|
||||
"delta_c0_m_quantiles": [
|
||||
0.009999999999999787,
|
||||
0.05999999999999961,
|
||||
1.0499999999999998
|
||||
],
|
||||
"c0_old_capped": 0,
|
||||
"c0_new_capped": 0
|
||||
},
|
||||
"turn": {
|
||||
"n": 2920,
|
||||
"delta_c0_m_quantiles": [
|
||||
0.6600000000000001,
|
||||
1.8704999999999974,
|
||||
2.2200000000000006
|
||||
],
|
||||
"c0_old_capped": 0,
|
||||
"c0_new_capped": 0
|
||||
},
|
||||
"large_error": {
|
||||
"n": 2831,
|
||||
"delta_c0_m_quantiles": [
|
||||
0.8699999999999997,
|
||||
1.9399999999999995,
|
||||
2.2200000000000006
|
||||
],
|
||||
"c0_old_capped": 0,
|
||||
"c0_new_capped": 0
|
||||
}
|
||||
},
|
||||
"sources": {
|
||||
".cache/ford_route113/route.npz": "774ca4a21b7113c2706d6130bc180c3216ea4833155300ab01e75b3486e36327",
|
||||
".cache/ford_route113/metadata.json": "1c0ca74dd48b90ab9d5444c5ca7f8aa9361700bbdf98bd5e853be50ad2895d7f",
|
||||
".cache/ford_route113/model_paths.npz": "93c41761eb85263f534f5371b905482cf7c948582eb1e9149966594be1d3768f",
|
||||
".cache/ford_route112/metadata.json": "726d78a7e7aa45307dcfe27cb00775c20ecc9d54538eb7f26a0166fc216226ce",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/c0_feedback_validate.py": "12b843f024b671f8b4282e1981f6d68de879a9cdff20fee74fd5228b6e4168ad",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/openpilot/selfdrive/controls/lib/ford_model_action.py": "8bc8554ed6743c5faf433deaa6de3f07fae27d183f62b04f49f9c3b9f1e8dcca"
|
||||
}
|
||||
},
|
||||
{
|
||||
"route": "117",
|
||||
"baseline": "d425b3260b0785b22096d130702b54a2e0761c36",
|
||||
"baseline_sha256": "c55464ecfbe7fad9f51905cf5f226be2676440ac5afec470b05515e32b065cc8",
|
||||
"cycles": 27301,
|
||||
"wire_round_trips": 2731,
|
||||
"c1_identical": true,
|
||||
"c0_gain": 0.5,
|
||||
"metrics": {
|
||||
"all": {
|
||||
"n": 13659,
|
||||
"delta_c0_m_quantiles": [
|
||||
0.03000000000000025,
|
||||
0.5,
|
||||
1.5900000000000007
|
||||
],
|
||||
"c0_old_capped": 0,
|
||||
"c0_new_capped": 0
|
||||
},
|
||||
"straight": {
|
||||
"n": 5001,
|
||||
"delta_c0_m_quantiles": [
|
||||
0.010000000000000675,
|
||||
0.3000000000000007,
|
||||
1.5899999999999999
|
||||
],
|
||||
"c0_old_capped": 0,
|
||||
"c0_new_capped": 0
|
||||
},
|
||||
"turn": {
|
||||
"n": 2891,
|
||||
"delta_c0_m_quantiles": [
|
||||
0.1200000000000001,
|
||||
0.7400000000000002,
|
||||
1.38
|
||||
],
|
||||
"c0_old_capped": 0,
|
||||
"c0_new_capped": 0
|
||||
},
|
||||
"large_error": {
|
||||
"n": 1195,
|
||||
"delta_c0_m_quantiles": [
|
||||
0.54,
|
||||
1.5599999999999996,
|
||||
1.5900000000000007
|
||||
],
|
||||
"c0_old_capped": 0,
|
||||
"c0_new_capped": 0
|
||||
}
|
||||
},
|
||||
"sources": {
|
||||
".cache/ford_route117/route.npz": "65e4ea6c76dce73018b636479b3777116f1e58bb9813e6c1a7b79018b219140e",
|
||||
".cache/ford_route117/metadata.json": "348396b3059c5f54d7938cd3acf2bb268a813b47ff47561da7a94cb75c3f3114",
|
||||
".cache/ford_route117/model_paths.npz": "079f6003968287530e1ed6f2243c727e80c2ef3d16fb8a7d945d7eca744d54e7",
|
||||
".cache/ford_route112/metadata.json": "726d78a7e7aa45307dcfe27cb00775c20ecc9d54538eb7f26a0166fc216226ce",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/c0_feedback_validate.py": "12b843f024b671f8b4282e1981f6d68de879a9cdff20fee74fd5228b6e4168ad",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/openpilot/selfdrive/controls/lib/ford_model_action.py": "8bc8554ed6743c5faf433deaa6de3f07fae27d183f62b04f49f9c3b9f1e8dcca"
|
||||
}
|
||||
},
|
||||
{
|
||||
"route": "11c",
|
||||
"baseline": "d425b3260b0785b22096d130702b54a2e0761c36",
|
||||
"baseline_sha256": "c55464ecfbe7fad9f51905cf5f226be2676440ac5afec470b05515e32b065cc8",
|
||||
"cycles": 54146,
|
||||
"wire_round_trips": 5415,
|
||||
"c1_identical": true,
|
||||
"c0_gain": 0.5,
|
||||
"metrics": {
|
||||
"all": {
|
||||
"n": 36780,
|
||||
"delta_c0_m_quantiles": [
|
||||
0.020000000000000462,
|
||||
0.3700000000000001,
|
||||
2.1799999999999997
|
||||
],
|
||||
"c0_old_capped": 0,
|
||||
"c0_new_capped": 0
|
||||
},
|
||||
"straight": {
|
||||
"n": 16750,
|
||||
"delta_c0_m_quantiles": [
|
||||
0.009999999999999787,
|
||||
0.09999999999999964,
|
||||
0.5
|
||||
],
|
||||
"c0_old_capped": 0,
|
||||
"c0_new_capped": 0
|
||||
},
|
||||
"turn": {
|
||||
"n": 2461,
|
||||
"delta_c0_m_quantiles": [
|
||||
0.1899999999999995,
|
||||
0.8899999999999997,
|
||||
2.1799999999999997
|
||||
],
|
||||
"c0_old_capped": 0,
|
||||
"c0_new_capped": 0
|
||||
},
|
||||
"large_error": {
|
||||
"n": 1827,
|
||||
"delta_c0_m_quantiles": [
|
||||
0.5599999999999996,
|
||||
0.9569999999999982,
|
||||
2.1799999999999997
|
||||
],
|
||||
"c0_old_capped": 0,
|
||||
"c0_new_capped": 0
|
||||
}
|
||||
},
|
||||
"sources": {
|
||||
".cache/ford_maneuver_11c/analysis/route.npz": "1f3c37a7c7ae85ba69e9958435a9568f8244fa862f43ad1ee2a0192d1df13966",
|
||||
".cache/ford_maneuver_11c/analysis/metadata.json": "2d0051d23059145988ca905e5386704def24b8080ce92541cec5e4ab501bcd0c",
|
||||
".cache/ford_maneuver_11c/analysis/model_paths.npz": "0ff433da3dde1d4d7f10596cd53558ec5de9d56fbc49aec3e8afb4c9872377d5",
|
||||
".cache/ford_route112/metadata.json": "726d78a7e7aa45307dcfe27cb00775c20ecc9d54538eb7f26a0166fc216226ce",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/c0_feedback_validate.py": "12b843f024b671f8b4282e1981f6d68de879a9cdff20fee74fd5228b6e4168ad",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/openpilot/selfdrive/controls/lib/ford_model_action.py": "8bc8554ed6743c5faf433deaa6de3f07fae27d183f62b04f49f9c3b9f1e8dcca"
|
||||
}
|
||||
},
|
||||
{
|
||||
"route": "125",
|
||||
"baseline": "d425b3260b0785b22096d130702b54a2e0761c36",
|
||||
"baseline_sha256": "c55464ecfbe7fad9f51905cf5f226be2676440ac5afec470b05515e32b065cc8",
|
||||
"cycles": 41717,
|
||||
"wire_round_trips": 4172,
|
||||
"c1_identical": true,
|
||||
"c0_gain": 0.5,
|
||||
"metrics": {
|
||||
"all": {
|
||||
"n": 41107,
|
||||
"delta_c0_m_quantiles": [
|
||||
0.0,
|
||||
0.05999999999999961,
|
||||
0.40000000000000036
|
||||
],
|
||||
"c0_old_capped": 0,
|
||||
"c0_new_capped": 0
|
||||
},
|
||||
"straight": {
|
||||
"n": 33135,
|
||||
"delta_c0_m_quantiles": [
|
||||
0.0,
|
||||
0.010000000000000675,
|
||||
0.28000000000000025
|
||||
],
|
||||
"c0_old_capped": 0,
|
||||
"c0_new_capped": 0
|
||||
},
|
||||
"large_error": {
|
||||
"n": 192,
|
||||
"delta_c0_m_quantiles": [
|
||||
0.3200000000000003,
|
||||
0.3799999999999999,
|
||||
0.40000000000000036
|
||||
],
|
||||
"c0_old_capped": 0,
|
||||
"c0_new_capped": 0
|
||||
}
|
||||
},
|
||||
"sources": {
|
||||
".cache/ford_c0_response_124_125/route125/route.npz": "8dcf304871bf5c6bf1ce206c788d7034bf5f056522faded0aa7948ff1cccbb75",
|
||||
".cache/ford_c0_response_124_125/route125/metadata.json": "1f2b812d3c2cc8e5a2246098756940d6f8989a2591daf4d74b1ef4c8555e2d57",
|
||||
".cache/ford_c0_response_124_125/route125/model_paths.npz": "4be9c7809dabbcb52296ba93b4743146dc941e49a32aca07f008b75757703e49",
|
||||
".cache/ford_route112/metadata.json": "726d78a7e7aa45307dcfe27cb00775c20ecc9d54538eb7f26a0166fc216226ce",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/c0_feedback_validate.py": "12b843f024b671f8b4282e1981f6d68de879a9cdff20fee74fd5228b6e4168ad",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/openpilot/selfdrive/controls/lib/ford_model_action.py": "8bc8554ed6743c5faf433deaa6de3f07fae27d183f62b04f49f9c3b9f1e8dcca"
|
||||
}
|
||||
},
|
||||
{
|
||||
"route": "146",
|
||||
"baseline": "d425b3260b0785b22096d130702b54a2e0761c36",
|
||||
"baseline_sha256": "c55464ecfbe7fad9f51905cf5f226be2676440ac5afec470b05515e32b065cc8",
|
||||
"cycles": 53229,
|
||||
"wire_round_trips": 5323,
|
||||
"c1_identical": true,
|
||||
"c0_gain": 0.5,
|
||||
"metrics": {
|
||||
"all": {
|
||||
"n": 40717,
|
||||
"delta_c0_m_quantiles": [
|
||||
0.0,
|
||||
0.04999999999999982,
|
||||
1.1300000000000003
|
||||
],
|
||||
"c0_old_capped": 0,
|
||||
"c0_new_capped": 0
|
||||
},
|
||||
"straight": {
|
||||
"n": 30796,
|
||||
"delta_c0_m_quantiles": [
|
||||
0.0,
|
||||
0.02999999999999936,
|
||||
0.35000000000000053
|
||||
],
|
||||
"c0_old_capped": 0,
|
||||
"c0_new_capped": 0
|
||||
},
|
||||
"turn": {
|
||||
"n": 585,
|
||||
"delta_c0_m_quantiles": [
|
||||
0.3700000000000001,
|
||||
1.0300000000000002,
|
||||
1.1300000000000003
|
||||
],
|
||||
"c0_old_capped": 0,
|
||||
"c0_new_capped": 0
|
||||
},
|
||||
"large_error": {
|
||||
"n": 298,
|
||||
"delta_c0_m_quantiles": [
|
||||
0.4500000000000002,
|
||||
1.0614999999999999,
|
||||
1.1300000000000003
|
||||
],
|
||||
"c0_old_capped": 0,
|
||||
"c0_new_capped": 0
|
||||
}
|
||||
},
|
||||
"sources": {
|
||||
".cache/ford_model_replay_146/full/route.npz": "b01db5bdd1c3b9fb83418a298571c98e60b5eb0509800d86cdaf07bafcdc6d63",
|
||||
".cache/ford_model_replay_146/full/metadata.json": "2be1254dd1eee422f3b2c2011faa91a21ab431751998525105381c2886df15ca",
|
||||
".cache/ford_model_replay_146/full/model_paths.npz": "11e0e9c8ce2959aac72d2f8a9cc4f6a025480034d5cd8b59b68a41b2ae3cf4a8",
|
||||
".cache/ford_route112/metadata.json": "726d78a7e7aa45307dcfe27cb00775c20ecc9d54538eb7f26a0166fc216226ce",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/c0_feedback_validate.py": "12b843f024b671f8b4282e1981f6d68de879a9cdff20fee74fd5228b6e4168ad",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/openpilot/selfdrive/controls/lib/ford_model_action.py": "8bc8554ed6743c5faf433deaa6de3f07fae27d183f62b04f49f9c3b9f1e8dcca"
|
||||
}
|
||||
},
|
||||
{
|
||||
"route": "149",
|
||||
"baseline": "d425b3260b0785b22096d130702b54a2e0761c36",
|
||||
"baseline_sha256": "c55464ecfbe7fad9f51905cf5f226be2676440ac5afec470b05515e32b065cc8",
|
||||
"cycles": 132334,
|
||||
"wire_round_trips": 13234,
|
||||
"c1_identical": true,
|
||||
"c0_gain": 0.5,
|
||||
"metrics": {
|
||||
"all": {
|
||||
"n": 109224,
|
||||
"delta_c0_m_quantiles": [
|
||||
0.019999999999999574,
|
||||
0.43999999999999995,
|
||||
2.2800000000000002
|
||||
],
|
||||
"c0_old_capped": 0,
|
||||
"c0_new_capped": 0
|
||||
},
|
||||
"straight": {
|
||||
"n": 54914,
|
||||
"delta_c0_m_quantiles": [
|
||||
0.009999999999999787,
|
||||
0.07000000000000028,
|
||||
2.04
|
||||
],
|
||||
"c0_old_capped": 0,
|
||||
"c0_new_capped": 0
|
||||
},
|
||||
"turn": {
|
||||
"n": 15161,
|
||||
"delta_c0_m_quantiles": [
|
||||
0.20999999999999996,
|
||||
1.0300000000000002,
|
||||
2.25
|
||||
],
|
||||
"c0_old_capped": 0,
|
||||
"c0_new_capped": 0
|
||||
},
|
||||
"large_error": {
|
||||
"n": 6659,
|
||||
"delta_c0_m_quantiles": [
|
||||
0.6399999999999997,
|
||||
1.4100000000000001,
|
||||
2.2800000000000002
|
||||
],
|
||||
"c0_old_capped": 0,
|
||||
"c0_new_capped": 0
|
||||
}
|
||||
},
|
||||
"sources": {
|
||||
".cache/ford_route149/full/route.npz": "aa5902877343cd033ee286b3668d91a85336ffbf740861849fb0b76b0ca24ade",
|
||||
".cache/ford_route149/full/metadata.json": "624fff03c25eb298661cb7b25f3dbe6d214d863f799d93635c0f4f05fc0d2b32",
|
||||
".cache/ford_route149/full/model_paths.npz": "19827800b8fb5983f3d6b72fcfaf36e35a40170bb17cd7c1e49374744fb449fb",
|
||||
".cache/ford_route112/metadata.json": "726d78a7e7aa45307dcfe27cb00775c20ecc9d54538eb7f26a0166fc216226ce",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/c0_feedback_validate.py": "12b843f024b671f8b4282e1981f6d68de879a9cdff20fee74fd5228b6e4168ad",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/openpilot/selfdrive/controls/lib/ford_model_action.py": "8bc8554ed6743c5faf433deaa6de3f07fae27d183f62b04f49f9c3b9f1e8dcca"
|
||||
}
|
||||
},
|
||||
{
|
||||
"route": "151",
|
||||
"baseline": "d425b3260b0785b22096d130702b54a2e0761c36",
|
||||
"baseline_sha256": "c55464ecfbe7fad9f51905cf5f226be2676440ac5afec470b05515e32b065cc8",
|
||||
"cycles": 325708,
|
||||
"wire_round_trips": 32571,
|
||||
"c1_identical": true,
|
||||
"c0_gain": 0.5,
|
||||
"metrics": {
|
||||
"all": {
|
||||
"n": 124806,
|
||||
"delta_c0_m_quantiles": [
|
||||
0.009999999999999787,
|
||||
0.10000000000000053,
|
||||
2.21
|
||||
],
|
||||
"c0_old_capped": 0,
|
||||
"c0_new_capped": 0
|
||||
},
|
||||
"straight": {
|
||||
"n": 85546,
|
||||
"delta_c0_m_quantiles": [
|
||||
0.009999999999999787,
|
||||
0.03999999999999915,
|
||||
0.6300000000000008
|
||||
],
|
||||
"c0_old_capped": 0,
|
||||
"c0_new_capped": 0
|
||||
},
|
||||
"turn": {
|
||||
"n": 3715,
|
||||
"delta_c0_m_quantiles": [
|
||||
0.25,
|
||||
1.12,
|
||||
2.21
|
||||
],
|
||||
"c0_old_capped": 0,
|
||||
"c0_new_capped": 0
|
||||
},
|
||||
"large_error": {
|
||||
"n": 1839,
|
||||
"delta_c0_m_quantiles": [
|
||||
0.6200000000000001,
|
||||
1.35,
|
||||
2.21
|
||||
],
|
||||
"c0_old_capped": 0,
|
||||
"c0_new_capped": 0
|
||||
}
|
||||
},
|
||||
"sources": {
|
||||
".cache/ford_route151/full/route.npz": "41a5b8bd388cf5a3d553f784542376ac9355fcdc5be4f427053d0504537babe1",
|
||||
".cache/ford_route151/full/metadata.json": "937825317a0edd470c54647240b922be8f79dda5b3365ffdd61281f0aca877a1",
|
||||
".cache/ford_route151/full/model_paths.npz": "980b3843cec04254280d744a5801cee1bf0f8ef371052398327ff245f9eee01b",
|
||||
".cache/ford_route112/metadata.json": "726d78a7e7aa45307dcfe27cb00775c20ecc9d54538eb7f26a0166fc216226ce",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/c0_feedback_validate.py": "12b843f024b671f8b4282e1981f6d68de879a9cdff20fee74fd5228b6e4168ad",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/openpilot/selfdrive/controls/lib/ford_model_action.py": "8bc8554ed6743c5faf433deaa6de3f07fae27d183f62b04f49f9c3b9f1e8dcca"
|
||||
}
|
||||
},
|
||||
{
|
||||
"route": "a9",
|
||||
"baseline": "d425b3260b0785b22096d130702b54a2e0761c36",
|
||||
"baseline_sha256": "c55464ecfbe7fad9f51905cf5f226be2676440ac5afec470b05515e32b065cc8",
|
||||
"cycles": 286319,
|
||||
"wire_round_trips": 28632,
|
||||
"c1_identical": true,
|
||||
"c0_gain": 0.5,
|
||||
"metrics": {
|
||||
"all": {
|
||||
"n": 142471,
|
||||
"delta_c0_m_quantiles": [
|
||||
0.019999999999999574,
|
||||
0.15999999999999925,
|
||||
1.5699999999999994
|
||||
],
|
||||
"c0_old_capped": 25,
|
||||
"c0_new_capped": 128
|
||||
},
|
||||
"straight": {
|
||||
"n": 99221,
|
||||
"delta_c0_m_quantiles": [
|
||||
0.009999999999999787,
|
||||
0.05999999999999961,
|
||||
1.4800000000000004
|
||||
],
|
||||
"c0_old_capped": 0,
|
||||
"c0_new_capped": 0
|
||||
},
|
||||
"turn": {
|
||||
"n": 6893,
|
||||
"delta_c0_m_quantiles": [
|
||||
0.23000000000000043,
|
||||
0.9699999999999998,
|
||||
1.5600000000000005
|
||||
],
|
||||
"c0_old_capped": 25,
|
||||
"c0_new_capped": 128
|
||||
},
|
||||
"large_error": {
|
||||
"n": 3224,
|
||||
"delta_c0_m_quantiles": [
|
||||
0.6600000000000001,
|
||||
1.2599999999999998,
|
||||
1.5699999999999994
|
||||
],
|
||||
"c0_old_capped": 25,
|
||||
"c0_new_capped": 128
|
||||
}
|
||||
},
|
||||
"sources": {
|
||||
".cache/ford_routea9/route.npz": "3fa8cabfb729dd42d689de38618d1d21c8965c9d60f914546f4d7cc58db8c975",
|
||||
".cache/ford_routea9/metadata.json": "403d35504bf596144845ac2060ce067d571f49a2955d16223af28a79a7a66e93",
|
||||
".cache/ford_routea9/model_paths.npz": "7f3646ad24aa52de5982c65600813de5bbc5f7664d92a2516bbba35da6e7b240",
|
||||
".cache/ford_route112/metadata.json": "726d78a7e7aa45307dcfe27cb00775c20ecc9d54538eb7f26a0166fc216226ce",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/c0_feedback_validate.py": "12b843f024b671f8b4282e1981f6d68de879a9cdff20fee74fd5228b6e4168ad",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/openpilot/selfdrive/controls/lib/ford_model_action.py": "8bc8554ed6743c5faf433deaa6de3f07fae27d183f62b04f49f9c3b9f1e8dcca"
|
||||
}
|
||||
},
|
||||
{
|
||||
"route": "b9",
|
||||
"baseline": "d425b3260b0785b22096d130702b54a2e0761c36",
|
||||
"baseline_sha256": "c55464ecfbe7fad9f51905cf5f226be2676440ac5afec470b05515e32b065cc8",
|
||||
"cycles": 90774,
|
||||
"wire_round_trips": 9078,
|
||||
"c1_identical": true,
|
||||
"c0_gain": 0.5,
|
||||
"metrics": {
|
||||
"all": {
|
||||
"n": 81066,
|
||||
"delta_c0_m_quantiles": [
|
||||
0.010000000000000675,
|
||||
0.3100000000000005,
|
||||
2.12
|
||||
],
|
||||
"c0_old_capped": 0,
|
||||
"c0_new_capped": 10
|
||||
},
|
||||
"straight": {
|
||||
"n": 42647,
|
||||
"delta_c0_m_quantiles": [
|
||||
0.009999999999999787,
|
||||
0.07000000000000028,
|
||||
1.63
|
||||
],
|
||||
"c0_old_capped": 0,
|
||||
"c0_new_capped": 0
|
||||
},
|
||||
"turn": {
|
||||
"n": 9816,
|
||||
"delta_c0_m_quantiles": [
|
||||
0.1200000000000001,
|
||||
0.8300000000000001,
|
||||
2.12
|
||||
],
|
||||
"c0_old_capped": 0,
|
||||
"c0_new_capped": 10
|
||||
},
|
||||
"large_error": {
|
||||
"n": 3486,
|
||||
"delta_c0_m_quantiles": [
|
||||
0.6600000000000001,
|
||||
1.5100000000000005,
|
||||
2.12
|
||||
],
|
||||
"c0_old_capped": 0,
|
||||
"c0_new_capped": 10
|
||||
}
|
||||
},
|
||||
"sources": {
|
||||
".cache/ford_routeb9/route.npz": "b07c789d8155335f5d120d0262fced6e4d5803fe767b0ff49b6413dce4140b5c",
|
||||
".cache/ford_routeb9/metadata.json": "9ce452220cab61b81883f32fc2fcaf5db6c78a674cb255a49cc77d5029580fee",
|
||||
".cache/ford_routeb9/model_paths.npz": "6b1f87897c050273fdc05af051307a049b6fc3a93072e7cda1721195ce7c3861",
|
||||
".cache/ford_route112/metadata.json": "726d78a7e7aa45307dcfe27cb00775c20ecc9d54538eb7f26a0166fc216226ce",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/c0_feedback_validate.py": "12b843f024b671f8b4282e1981f6d68de879a9cdff20fee74fd5228b6e4168ad",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/openpilot/selfdrive/controls/lib/ford_model_action.py": "8bc8554ed6743c5faf433deaa6de3f07fae27d183f62b04f49f9c3b9f1e8dcca"
|
||||
}
|
||||
}
|
||||
],
|
||||
"test_summary": {
|
||||
"pytest_passed": 439,
|
||||
"ruff": "passed",
|
||||
"git_diff_check": "passed"
|
||||
},
|
||||
"fit_source_sha256": "536ea20ee1bfa9366453c6d01fae6c7dd7b83388f2a71662cc12b43311767c6f",
|
||||
"fitted_plant_sanity_check": {
|
||||
"scope": "Approximate fitted-plant simulation with frozen future reference/speed, recomputed controller feedback, 20 Hz, four-second clean windows. Not physical tracking validation.",
|
||||
"routes": {
|
||||
"149": {
|
||||
"0.0": {
|
||||
"all": {
|
||||
"windows": 515,
|
||||
"tracking_rmse_deg": 1.9599623978328702,
|
||||
"p95_abs_error_deg": 3.6108132362595673
|
||||
},
|
||||
"turn": {
|
||||
"windows": 27,
|
||||
"tracking_rmse_deg": 5.648897344766248,
|
||||
"p95_abs_error_deg": 10.154937282895988
|
||||
},
|
||||
"unwind": {
|
||||
"windows": 16,
|
||||
"tracking_rmse_deg": 4.56296777072208,
|
||||
"p95_abs_error_deg": 5.8047028901317175
|
||||
},
|
||||
"near_straight": {
|
||||
"windows": 280,
|
||||
"tracking_rmse_deg": 0.5053540034793153,
|
||||
"p95_abs_error_deg": 0.9099971698933058
|
||||
}
|
||||
},
|
||||
"0.25": {
|
||||
"all": {
|
||||
"windows": 515,
|
||||
"tracking_rmse_deg": 1.868833791306516,
|
||||
"p95_abs_error_deg": 3.4194303642888486
|
||||
},
|
||||
"turn": {
|
||||
"windows": 27,
|
||||
"tracking_rmse_deg": 5.3063969129788715,
|
||||
"p95_abs_error_deg": 9.669302705935252
|
||||
},
|
||||
"unwind": {
|
||||
"windows": 16,
|
||||
"tracking_rmse_deg": 4.307554036590833,
|
||||
"p95_abs_error_deg": 5.549381163721539
|
||||
},
|
||||
"near_straight": {
|
||||
"windows": 280,
|
||||
"tracking_rmse_deg": 0.4868309463403626,
|
||||
"p95_abs_error_deg": 0.8696197439962221
|
||||
}
|
||||
},
|
||||
"0.5": {
|
||||
"all": {
|
||||
"windows": 515,
|
||||
"tracking_rmse_deg": 1.7898071883511764,
|
||||
"p95_abs_error_deg": 3.260221243224674
|
||||
},
|
||||
"turn": {
|
||||
"windows": 27,
|
||||
"tracking_rmse_deg": 5.006440079078192,
|
||||
"p95_abs_error_deg": 9.002861391131468
|
||||
},
|
||||
"unwind": {
|
||||
"windows": 16,
|
||||
"tracking_rmse_deg": 4.078042889798365,
|
||||
"p95_abs_error_deg": 5.24331028633435
|
||||
},
|
||||
"near_straight": {
|
||||
"windows": 280,
|
||||
"tracking_rmse_deg": 0.4694670576005347,
|
||||
"p95_abs_error_deg": 0.8331203606609966
|
||||
}
|
||||
},
|
||||
"0.75": {
|
||||
"all": {
|
||||
"windows": 515,
|
||||
"tracking_rmse_deg": 1.7203138728888623,
|
||||
"p95_abs_error_deg": 3.1149598999491044
|
||||
},
|
||||
"turn": {
|
||||
"windows": 27,
|
||||
"tracking_rmse_deg": 4.743912352245749,
|
||||
"p95_abs_error_deg": 8.664577551257613
|
||||
},
|
||||
"unwind": {
|
||||
"windows": 16,
|
||||
"tracking_rmse_deg": 3.8750067743533827,
|
||||
"p95_abs_error_deg": 4.9966209407120425
|
||||
},
|
||||
"near_straight": {
|
||||
"windows": 280,
|
||||
"tracking_rmse_deg": 0.45412470534078503,
|
||||
"p95_abs_error_deg": 0.7964604020331516
|
||||
}
|
||||
},
|
||||
"1.0": {
|
||||
"all": {
|
||||
"windows": 515,
|
||||
"tracking_rmse_deg": 1.6590272653809652,
|
||||
"p95_abs_error_deg": 2.990976062659628
|
||||
},
|
||||
"turn": {
|
||||
"windows": 27,
|
||||
"tracking_rmse_deg": 4.507245023760021,
|
||||
"p95_abs_error_deg": 8.107112731535002
|
||||
},
|
||||
"unwind": {
|
||||
"windows": 16,
|
||||
"tracking_rmse_deg": 3.693644190831072,
|
||||
"p95_abs_error_deg": 4.749308802498215
|
||||
},
|
||||
"near_straight": {
|
||||
"windows": 280,
|
||||
"tracking_rmse_deg": 0.4404620597734029,
|
||||
"p95_abs_error_deg": 0.7694584025732562
|
||||
}
|
||||
}
|
||||
},
|
||||
"151": {
|
||||
"0.0": {
|
||||
"all": {
|
||||
"windows": 794,
|
||||
"tracking_rmse_deg": 0.9049924838460381,
|
||||
"p95_abs_error_deg": 1.496850675938314
|
||||
},
|
||||
"unwind": {
|
||||
"windows": 7,
|
||||
"tracking_rmse_deg": 1.3568942395552432,
|
||||
"p95_abs_error_deg": 2.4717442023707914
|
||||
},
|
||||
"near_straight": {
|
||||
"windows": 525,
|
||||
"tracking_rmse_deg": 0.47746381422226297,
|
||||
"p95_abs_error_deg": 0.9045192063209317
|
||||
}
|
||||
},
|
||||
"0.25": {
|
||||
"all": {
|
||||
"windows": 794,
|
||||
"tracking_rmse_deg": 0.8744021810097441,
|
||||
"p95_abs_error_deg": 1.4250449905919713
|
||||
},
|
||||
"unwind": {
|
||||
"windows": 7,
|
||||
"tracking_rmse_deg": 1.2734874215389322,
|
||||
"p95_abs_error_deg": 2.3213325000763567
|
||||
},
|
||||
"near_straight": {
|
||||
"windows": 525,
|
||||
"tracking_rmse_deg": 0.45648429088650827,
|
||||
"p95_abs_error_deg": 0.8575351438117865
|
||||
}
|
||||
},
|
||||
"0.5": {
|
||||
"all": {
|
||||
"windows": 794,
|
||||
"tracking_rmse_deg": 0.8487165771252436,
|
||||
"p95_abs_error_deg": 1.3633464420801624
|
||||
},
|
||||
"unwind": {
|
||||
"windows": 7,
|
||||
"tracking_rmse_deg": 1.1986282107421773,
|
||||
"p95_abs_error_deg": 2.163514655425466
|
||||
},
|
||||
"near_straight": {
|
||||
"windows": 525,
|
||||
"tracking_rmse_deg": 0.4395196212749708,
|
||||
"p95_abs_error_deg": 0.82723861668119
|
||||
}
|
||||
},
|
||||
"0.75": {
|
||||
"all": {
|
||||
"windows": 794,
|
||||
"tracking_rmse_deg": 0.8256116631456035,
|
||||
"p95_abs_error_deg": 1.309241916169811
|
||||
},
|
||||
"unwind": {
|
||||
"windows": 7,
|
||||
"tracking_rmse_deg": 1.1341202040172427,
|
||||
"p95_abs_error_deg": 2.0707107084230465
|
||||
},
|
||||
"near_straight": {
|
||||
"windows": 525,
|
||||
"tracking_rmse_deg": 0.42406706004679057,
|
||||
"p95_abs_error_deg": 0.7946947170401512
|
||||
}
|
||||
},
|
||||
"1.0": {
|
||||
"all": {
|
||||
"windows": 794,
|
||||
"tracking_rmse_deg": 0.8052189413919172,
|
||||
"p95_abs_error_deg": 1.2575495536727697
|
||||
},
|
||||
"unwind": {
|
||||
"windows": 7,
|
||||
"tracking_rmse_deg": 1.0739985410343649,
|
||||
"p95_abs_error_deg": 1.9599553503928666
|
||||
},
|
||||
"near_straight": {
|
||||
"windows": 525,
|
||||
"tracking_rmse_deg": 0.41043500769646296,
|
||||
"p95_abs_error_deg": 0.7683540262306987
|
||||
}
|
||||
}
|
||||
},
|
||||
"125": {
|
||||
"0.0": {
|
||||
"all": {
|
||||
"windows": 346,
|
||||
"tracking_rmse_deg": 1.5101438591368561,
|
||||
"p95_abs_error_deg": 1.3433430124202965
|
||||
},
|
||||
"near_straight": {
|
||||
"windows": 246,
|
||||
"tracking_rmse_deg": 0.3997814518183382,
|
||||
"p95_abs_error_deg": 0.8420670524669589
|
||||
}
|
||||
},
|
||||
"0.25": {
|
||||
"all": {
|
||||
"windows": 346,
|
||||
"tracking_rmse_deg": 1.460719401448126,
|
||||
"p95_abs_error_deg": 1.2614591679946765
|
||||
},
|
||||
"near_straight": {
|
||||
"windows": 246,
|
||||
"tracking_rmse_deg": 0.377498378600397,
|
||||
"p95_abs_error_deg": 0.7868969957812859
|
||||
}
|
||||
},
|
||||
"0.5": {
|
||||
"all": {
|
||||
"windows": 346,
|
||||
"tracking_rmse_deg": 1.4162750202728391,
|
||||
"p95_abs_error_deg": 1.188472341120946
|
||||
},
|
||||
"near_straight": {
|
||||
"windows": 246,
|
||||
"tracking_rmse_deg": 0.35896062148345687,
|
||||
"p95_abs_error_deg": 0.7523514432810818
|
||||
}
|
||||
},
|
||||
"0.75": {
|
||||
"all": {
|
||||
"windows": 346,
|
||||
"tracking_rmse_deg": 1.3766402449201502,
|
||||
"p95_abs_error_deg": 1.1248966870109076
|
||||
},
|
||||
"near_straight": {
|
||||
"windows": 246,
|
||||
"tracking_rmse_deg": 0.34130621657460447,
|
||||
"p95_abs_error_deg": 0.7141102879383627
|
||||
}
|
||||
},
|
||||
"1.0": {
|
||||
"all": {
|
||||
"windows": 346,
|
||||
"tracking_rmse_deg": 1.3414563262275077,
|
||||
"p95_abs_error_deg": 1.0712299506410858
|
||||
},
|
||||
"near_straight": {
|
||||
"windows": 246,
|
||||
"tracking_rmse_deg": 0.3255065971687205,
|
||||
"p95_abs_error_deg": 0.6807731116817476
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
# Action-mode C0 feedback softening
|
||||
|
||||
Route 166 used v21 and improved normal driving, but the first two user bookmarks
|
||||
showed roughly 2 Hz wheel oscillations around a smoother requested angle at
|
||||
17–31 mph. The first occurred with uninterrupted feedback, small I and no
|
||||
PSCM limitReached. Both C0 P and C1 P followed the error, so these recordings
|
||||
do not isolate either channel's contribution to physical oscillation.
|
||||
|
||||
This trial changes C0 P only. With the curvature error converted into `x` metres
|
||||
of C0 before applying the existing gain, the new correction is:
|
||||
|
||||
```text
|
||||
C0 P = gain * (x - 0.125 * tanh(x / 0.25))
|
||||
```
|
||||
|
||||
Near zero, the slope is half the old gain. It increases smoothly toward the old
|
||||
gain without exceeding it. The response stays symmetric, monotonic and nonzero
|
||||
for nonzero error; it introduces no time filter, deadband, cutoff or additional
|
||||
state in the control law. Zero error removes P immediately, and opposite error
|
||||
commands opposite P in the same cycle.
|
||||
|
||||
Large corrections lose at most 0.125 m times the existing C0 gain. This is a
|
||||
bound on the difference from the old correction, **not a C0 command cap**.
|
||||
The field bounds remain C0 ±5.11 m and C1 ±0.5 rad. The scale and minimum slope
|
||||
are explicit drive-trial choices, not an identified stable PSCM calibration.
|
||||
Scales 0.15, 0.25, 0.4 and 0.5 m were compared on route 166; 0.25 m preserves
|
||||
about 96% of the good left's peak entry command while reducing the repeated
|
||||
C0 command component in both wobbles.
|
||||
|
||||
The base action mapping, selected desired curvature, C1 P/I, delayed reference,
|
||||
integral cadence and anti-windup, driver/PSCM arbitration, CAN cadence and
|
||||
upstream limits are unchanged. The softening applies at all speeds in action
|
||||
mode: the bookmarked wobbles were above the proposed 11.2 mph freeze cutoff.
|
||||
Direct-path feedback retains its linear law; when it falls back to an action
|
||||
reference, the action softening applies. The master toggle still selects
|
||||
upstream Ford control when disabled. No new toggle is added.
|
||||
|
||||
Diagnostics identify v22 and include `offset_proportional_linear` alongside
|
||||
the actual `offset_proportional`, so a new log can show exactly what softening
|
||||
removed. The extra stored scalar is diagnostic only and resets with P.
|
||||
|
||||
## Validation
|
||||
|
||||
Five native-time route replays cover 646,178 cycles and 64,620 CAN encode/decode
|
||||
checks: routes 149, 151, 157, 162 and 166. The v21 baseline matches its archived
|
||||
commands and integral exactly. C1 commands, C1 P/I, reference, validity,
|
||||
arbitration and base mapping remain identical on every cycle. C2/C3 remain
|
||||
zero. The largest raw C0 P change is 0.125 m; after CAN quantization the
|
||||
command difference is at most 0.13 m.
|
||||
|
||||
| Route-166 event | Repeating total C0 component, before → after | Reduction |
|
||||
| --- | --- | --- |
|
||||
| First wobble | 0.146 → 0.076 m peak-to-peak | 48% |
|
||||
| Second wobble | 0.222 → 0.150 m peak-to-peak | 32% |
|
||||
| Second wobble exit | 0.335 → 0.199 m peak-to-peak | 41% |
|
||||
|
||||
These use a same-frequency sine fit with quadratic trend removal. They measure
|
||||
the **command**, not a predicted reduction in wheel oscillation. The good left's
|
||||
peak entry C0 changes from -2.93 to -2.81 m; its mid-turn unwind peak changes
|
||||
from +1.44 to +1.31 m. C1 is identical. Entry and unwind are both softened;
|
||||
neither physical response is proven better by a frozen-motion replay.
|
||||
|
||||
Regression tests first failed against v21, then passed with v22. They exercise
|
||||
small nonzero corrections, preserved large-error authority, monotonic bounded
|
||||
incremental gain, immediate reversal/release, unaffected C1 and base mapping,
|
||||
and the direct-path exception. Existing controls-to-CAN integration, model
|
||||
selection, driver override, request-history timing and default-upstream tests
|
||||
also pass. Exact counts and provenance are in
|
||||
`ford_c0_softening_v22_validation.json`.
|
||||
|
||||
Reproduce a route comparison with the built Python dependencies:
|
||||
|
||||
```sh
|
||||
PYTHONPATH=.:opendbc_repo:.cache/ford_geometry_deps python \
|
||||
tools/ford_pscm_lab/c0_softening_replay.py \
|
||||
--source .cache/ford_route166/full \
|
||||
--delay-intake .cache/ford_route166/intake.npz \
|
||||
--archive .cache/ford_feedback_delay_v21/166/commands.npz \
|
||||
--output .cache/ford_c0_softening_v22/166
|
||||
```
|
||||
|
||||
For the next drive, the discriminating observations are whether small
|
||||
left-right corrections settle sooner, whether centering becomes too loose,
|
||||
and whether strong entry and prompt release remain. Do not interpret this
|
||||
offline validation as a demonstrated physical stability fix.
|
||||
@@ -0,0 +1,415 @@
|
||||
{
|
||||
"baseline_commit": "ed9c44f57584b5f033f79db6331e21b9b3e0ad55",
|
||||
"method": "Native-time production-controller replay using recorded wheel motion. Tests command behavior, not changed physical tracking.",
|
||||
"cycles": 646178,
|
||||
"wire_checks": 64620,
|
||||
"tests": {
|
||||
"ford_controller_and_integration": 708,
|
||||
"subtests": 2,
|
||||
"ford_can": 11,
|
||||
"ruff": "passed",
|
||||
"diff_check": "passed",
|
||||
"ford_can_subtests": 23
|
||||
},
|
||||
"selection": {
|
||||
"0.15": {
|
||||
"wobble1": {
|
||||
"P_periodic_reduction_pct": 43.471957098342315,
|
||||
"P_before": 0.1502831327602382,
|
||||
"P_after": 0.08495211376066261
|
||||
},
|
||||
"wobble2": {
|
||||
"P_periodic_reduction_pct": 20.901758921591217,
|
||||
"P_before": 0.22856483271408398,
|
||||
"P_after": 0.1807907624006479
|
||||
},
|
||||
"wobble2_exit": {
|
||||
"P_periodic_reduction_pct": 31.59900962734852,
|
||||
"P_before": 0.3407619177930598,
|
||||
"P_after": 0.2330845265832934
|
||||
},
|
||||
"left_entry_peak": {
|
||||
"t": 557.084304569,
|
||||
"before": -2.9300000000000006,
|
||||
"after_unpacked": -2.8568021983086886,
|
||||
"retained_percent": 97.50178151224192
|
||||
}
|
||||
},
|
||||
"0.25": {
|
||||
"wobble1": {
|
||||
"P_periodic_reduction_pct": 47.14718986588794,
|
||||
"P_before": 0.1502831327602382,
|
||||
"P_after": 0.07942885882136426
|
||||
},
|
||||
"wobble2": {
|
||||
"P_periodic_reduction_pct": 31.648695347911747,
|
||||
"P_before": 0.22856483271408398,
|
||||
"P_after": 0.1562270451359394
|
||||
},
|
||||
"wobble2_exit": {
|
||||
"P_periodic_reduction_pct": 40.10137585312036,
|
||||
"P_before": 0.3407619177930598,
|
||||
"P_after": 0.2041117003745639
|
||||
},
|
||||
"left_entry_peak": {
|
||||
"t": 557.084304569,
|
||||
"before": -2.9300000000000006,
|
||||
"after_unpacked": -2.8068022480277577,
|
||||
"retained_percent": 95.79529856750024
|
||||
}
|
||||
},
|
||||
"0.4": {
|
||||
"wobble1": {
|
||||
"P_periodic_reduction_pct": 48.78814118850091,
|
||||
"P_before": 0.1502831327602382,
|
||||
"P_after": 0.07696278576667091
|
||||
},
|
||||
"wobble2": {
|
||||
"P_periodic_reduction_pct": 40.16673505205207,
|
||||
"P_before": 0.22856483271408398,
|
||||
"P_after": 0.13675780193565182
|
||||
},
|
||||
"wobble2_exit": {
|
||||
"P_periodic_reduction_pct": 45.26729702228269,
|
||||
"P_before": 0.3407619177930598,
|
||||
"P_after": 0.18650820832684864
|
||||
},
|
||||
"left_entry_peak": {
|
||||
"t": 557.084304569,
|
||||
"before": -2.9300000000000006,
|
||||
"after_unpacked": -2.7318281193111016,
|
||||
"retained_percent": 93.23645458399662
|
||||
}
|
||||
},
|
||||
"0.5": {
|
||||
"wobble1": {
|
||||
"P_periodic_reduction_pct": 49.20782335256252,
|
||||
"P_before": 0.1502831327602382,
|
||||
"P_after": 0.07633207426288317
|
||||
},
|
||||
"wobble2": {
|
||||
"P_periodic_reduction_pct": 43.09149572817541,
|
||||
"P_before": 0.22856483271408398,
|
||||
"P_after": 0.1300728275889832
|
||||
},
|
||||
"wobble2_exit": {
|
||||
"P_periodic_reduction_pct": 46.79902581397264,
|
||||
"P_before": 0.3407619177930598,
|
||||
"P_after": 0.18128865992089754
|
||||
},
|
||||
"left_entry_peak": {
|
||||
"t": 557.084304569,
|
||||
"before": -2.9300000000000006,
|
||||
"after_unpacked": -2.682025078944252,
|
||||
"retained_percent": 91.53669211413828
|
||||
}
|
||||
}
|
||||
},
|
||||
"bookmarks": {
|
||||
"wobble1": {
|
||||
"window": [
|
||||
420.8,
|
||||
424.0
|
||||
],
|
||||
"p": {
|
||||
"old_peak_to_peak": 0.1502831327602382,
|
||||
"new_peak_to_peak": 0.07942885882136426,
|
||||
"reduction_percent": 47.14718986588794
|
||||
},
|
||||
"c0": {
|
||||
"old_peak_to_peak": 0.1463643509329174,
|
||||
"new_peak_to_peak": 0.07619232987207852,
|
||||
"reduction_percent": 47.943382807061084
|
||||
}
|
||||
},
|
||||
"wobble2": {
|
||||
"window": [
|
||||
514.3,
|
||||
521.4
|
||||
],
|
||||
"p": {
|
||||
"old_peak_to_peak": 0.22856483271408398,
|
||||
"new_peak_to_peak": 0.15622704513593932,
|
||||
"reduction_percent": 31.648695347911794
|
||||
},
|
||||
"c0": {
|
||||
"old_peak_to_peak": 0.22189262078364064,
|
||||
"new_peak_to_peak": 0.15013115100291197,
|
||||
"reduction_percent": 32.3406292319656
|
||||
}
|
||||
},
|
||||
"wobble2_exit": {
|
||||
"window": [
|
||||
523.7,
|
||||
526.8
|
||||
],
|
||||
"p": {
|
||||
"old_peak_to_peak": 0.3407619177930598,
|
||||
"new_peak_to_peak": 0.2041117003745639,
|
||||
"reduction_percent": 40.10137585312036
|
||||
},
|
||||
"c0": {
|
||||
"old_peak_to_peak": 0.3347422828817533,
|
||||
"new_peak_to_peak": 0.19869356211391034,
|
||||
"reduction_percent": 40.642825159886286
|
||||
}
|
||||
}
|
||||
},
|
||||
"good_left_points": [
|
||||
{
|
||||
"meaning": "entry",
|
||||
"t": 557.084304569,
|
||||
"target_angle": 167.83670043945312,
|
||||
"actual_angle": 76.5999984741211,
|
||||
"old_c0": -2.9300000000000006,
|
||||
"new_c0": -2.8100000000000005,
|
||||
"old_p": -1.9288202876424623,
|
||||
"new_p": -1.8038203373625472,
|
||||
"c1": -0.4565,
|
||||
"i": -0.058477505092137665
|
||||
},
|
||||
{
|
||||
"meaning": "midturn_unwind",
|
||||
"t": 558.284113233,
|
||||
"target_angle": 109.08927154541016,
|
||||
"actual_angle": 183.6999969482422,
|
||||
"old_c0": 1.44,
|
||||
"new_c0": 1.31,
|
||||
"old_p": 2.0905105735349747,
|
||||
"new_p": 1.9655105871733405,
|
||||
"c1": -0.11499999999999999,
|
||||
"i": -0.05363273646512535
|
||||
}
|
||||
],
|
||||
"routes": {
|
||||
"166": {
|
||||
"cycles": 62579,
|
||||
"wire_checks": 6258,
|
||||
"baseline_matches_archived_v21_exactly": true,
|
||||
"c1_integral_reference_arbitration_equal_every_cycle": true,
|
||||
"P_reduction_p50_p95_p99_max_m": [
|
||||
0.003636227080371235,
|
||||
0.10752626130792256,
|
||||
0.12490297403763004,
|
||||
0.12499999961974328
|
||||
],
|
||||
"C0_change_p50_p95_p99_max_m": [
|
||||
0.0,
|
||||
0.10999999999999943,
|
||||
0.1299999999999999,
|
||||
0.13000000000000078
|
||||
],
|
||||
"variants": {
|
||||
"old": {
|
||||
"c0_step_p95_p99_max": [
|
||||
0.020000000000000018,
|
||||
0.050000000000000266,
|
||||
1.8900000000000006
|
||||
],
|
||||
"c0_bound_seconds": 0.0
|
||||
},
|
||||
"new": {
|
||||
"c0_step_p95_p99_max": [
|
||||
0.010000000000000675,
|
||||
0.040000000000000036,
|
||||
1.7700000000000005
|
||||
],
|
||||
"c0_bound_seconds": 0.0
|
||||
}
|
||||
},
|
||||
"baseline_commit": "ed9c44f57584b5f033f79db6331e21b9b3e0ad55",
|
||||
"method": "Replay C0 softening against v21 on frozen recorded motion, not a PSCM model.",
|
||||
"controller_sha256": "b0aff6d70ad8ae7693c69068dbe646767e4d9fc20a23af70cbddcf6ebf849f48",
|
||||
"source_sha256": {
|
||||
".cache/ford_route166/full/route.npz": "0ce273e4a12725c4481b11dab38b304950d364455b89f216b929a9c61b7feb1c",
|
||||
".cache/ford_route166/full/model_paths.npz": "720fe9ba9865a05937989449b0b0bd3304baf5f1730042da6b61adc2941c0f5a",
|
||||
".cache/ford_route166/intake.npz": "660b5e7f6789d931660ad71b10f7d8ce2b63436e7573b615ccc8e5da1f2a14d0",
|
||||
".cache/ford_feedback_delay_v21/166/commands.npz": "bfe51abd9e914edea505efc4fb42fe66950cfa5835e4f929d922992e90c88b5b"
|
||||
}
|
||||
},
|
||||
"162": {
|
||||
"cycles": 49003,
|
||||
"wire_checks": 4901,
|
||||
"baseline_matches_archived_v21_exactly": true,
|
||||
"c1_integral_reference_arbitration_equal_every_cycle": true,
|
||||
"P_reduction_p50_p95_p99_max_m": [
|
||||
0.01864462197101791,
|
||||
0.12428797325001033,
|
||||
0.12499916992638829,
|
||||
0.12499999992682431
|
||||
],
|
||||
"C0_change_p50_p95_p99_max_m": [
|
||||
0.019999999999999574,
|
||||
0.1200000000000001,
|
||||
0.1299999999999999,
|
||||
0.13000000000000078
|
||||
],
|
||||
"variants": {
|
||||
"old": {
|
||||
"c0_step_p95_p99_max": [
|
||||
0.03000000000000025,
|
||||
0.09999999999999964,
|
||||
2.7299999999999995
|
||||
],
|
||||
"c0_bound_seconds": 0.0
|
||||
},
|
||||
"new": {
|
||||
"c0_step_p95_p99_max": [
|
||||
0.03000000000000025,
|
||||
0.08999999999999986,
|
||||
2.5999999999999996
|
||||
],
|
||||
"c0_bound_seconds": 0.0
|
||||
}
|
||||
},
|
||||
"baseline_commit": "ed9c44f57584b5f033f79db6331e21b9b3e0ad55",
|
||||
"method": "Replay C0 softening against v21 on frozen recorded motion, not a PSCM model.",
|
||||
"controller_sha256": "b0aff6d70ad8ae7693c69068dbe646767e4d9fc20a23af70cbddcf6ebf849f48",
|
||||
"source_sha256": {
|
||||
".cache/ford_route162/full/route.npz": "a725f9ca309b90a2d750d51a9b9d5b459d99119a54ab048631bc0fce07348af8",
|
||||
".cache/ford_route162/full/model_paths.npz": "cffe310b122d9ae26a53ee504608156b4242a6bba5f681898d5b661951ac5ce5",
|
||||
".cache/ford_route162/intake.npz": "9455b01e1d1d3dcba08276b382a8c6cd54ff6fb19cb417b72d0ac89cf193034b",
|
||||
".cache/ford_feedback_delay_v21/162/commands.npz": "6c70518fb9b7232b689740662c4bd5c0c24cabee4e743a2b4f3b2313bcce1912"
|
||||
}
|
||||
},
|
||||
"157": {
|
||||
"cycles": 76554,
|
||||
"wire_checks": 7656,
|
||||
"baseline_matches_archived_v21_exactly": true,
|
||||
"c1_integral_reference_arbitration_equal_every_cycle": true,
|
||||
"P_reduction_p50_p95_p99_max_m": [
|
||||
0.00530841320802854,
|
||||
0.12371750128642126,
|
||||
0.12499999318487974,
|
||||
0.125
|
||||
],
|
||||
"C0_change_p50_p95_p99_max_m": [
|
||||
0.009999999999999787,
|
||||
0.1200000000000001,
|
||||
0.1299999999999999,
|
||||
0.13000000000000078
|
||||
],
|
||||
"variants": {
|
||||
"old": {
|
||||
"c0_step_p95_p99_max": [
|
||||
0.03000000000000025,
|
||||
0.09000000000000014,
|
||||
5.6000000000000005
|
||||
],
|
||||
"c0_bound_seconds": 0.2999506119999751
|
||||
},
|
||||
"new": {
|
||||
"c0_step_p95_p99_max": [
|
||||
0.02999999999999936,
|
||||
0.08000000000000007,
|
||||
5.6000000000000005
|
||||
],
|
||||
"c0_bound_seconds": 0.15561390599987135
|
||||
}
|
||||
},
|
||||
"baseline_commit": "ed9c44f57584b5f033f79db6331e21b9b3e0ad55",
|
||||
"method": "Replay C0 softening against v21 on frozen recorded motion, not a PSCM model.",
|
||||
"controller_sha256": "b0aff6d70ad8ae7693c69068dbe646767e4d9fc20a23af70cbddcf6ebf849f48",
|
||||
"source_sha256": {
|
||||
".cache/ford_route157/full/route.npz": "e2e2573f904aa11ee9e10450e7f5b965d475657b61127e827a67eadcd6b857fb",
|
||||
".cache/ford_route157/full/model_paths.npz": "25fc2526e092fde5ceb5aab63a5aa4f73a2c989a3263cedb0a26faf8da61c47b",
|
||||
".cache/ford_route157/intake.npz": "5dffd86fde68197727d6c6b9c5eae82320a456fa1206c4cf18e5e9c69419dde4",
|
||||
".cache/ford_feedback_delay_v21/157/commands.npz": "7d7659c54662e7f56015313e634b959be29dae524522b425f4c2adccc391fc9d"
|
||||
}
|
||||
},
|
||||
"151": {
|
||||
"cycles": 325708,
|
||||
"wire_checks": 32571,
|
||||
"baseline_matches_archived_v21_exactly": true,
|
||||
"c1_integral_reference_arbitration_equal_every_cycle": true,
|
||||
"P_reduction_p50_p95_p99_max_m": [
|
||||
0.004569097471431923,
|
||||
0.07115894622455567,
|
||||
0.12458615764509068,
|
||||
0.12499999999990274
|
||||
],
|
||||
"C0_change_p50_p95_p99_max_m": [
|
||||
0.009999999999999787,
|
||||
0.07000000000000028,
|
||||
0.1200000000000001,
|
||||
0.13000000000000078
|
||||
],
|
||||
"variants": {
|
||||
"old": {
|
||||
"c0_step_p95_p99_max": [
|
||||
0.010000000000000675,
|
||||
0.040000000000000036,
|
||||
3.45
|
||||
],
|
||||
"c0_bound_seconds": 0.0
|
||||
},
|
||||
"new": {
|
||||
"c0_step_p95_p99_max": [
|
||||
0.010000000000000231,
|
||||
0.03000000000000025,
|
||||
3.33
|
||||
],
|
||||
"c0_bound_seconds": 0.0
|
||||
}
|
||||
},
|
||||
"baseline_commit": "ed9c44f57584b5f033f79db6331e21b9b3e0ad55",
|
||||
"method": "Replay C0 softening against v21 on frozen recorded motion, not a PSCM model.",
|
||||
"controller_sha256": "b0aff6d70ad8ae7693c69068dbe646767e4d9fc20a23af70cbddcf6ebf849f48",
|
||||
"source_sha256": {
|
||||
".cache/ford_route151/full/route.npz": "41a5b8bd388cf5a3d553f784542376ac9355fcdc5be4f427053d0504537babe1",
|
||||
".cache/ford_route151/full/model_paths.npz": "980b3843cec04254280d744a5801cee1bf0f8ef371052398327ff245f9eee01b",
|
||||
".cache/ford_route151/intake.npz": "18bbefb7738cebd0071dc987e90f74469a0c8ed456f9412324030592cafd68c9",
|
||||
".cache/ford_feedback_delay_v21/151/commands.npz": "4ab25e4c31fcddf08184c396efc3db78953d5ce60da7c653964bc2fa7e3184c9"
|
||||
}
|
||||
},
|
||||
"149": {
|
||||
"cycles": 132334,
|
||||
"wire_checks": 13234,
|
||||
"baseline_matches_archived_v21_exactly": true,
|
||||
"c1_integral_reference_arbitration_equal_every_cycle": true,
|
||||
"P_reduction_p50_p95_p99_max_m": [
|
||||
0.010345416404636773,
|
||||
0.12405821661525897,
|
||||
0.12499991437968122,
|
||||
0.12499999999999556
|
||||
],
|
||||
"C0_change_p50_p95_p99_max_m": [
|
||||
0.009999999999999787,
|
||||
0.1200000000000001,
|
||||
0.1299999999999999,
|
||||
0.13000000000000078
|
||||
],
|
||||
"variants": {
|
||||
"old": {
|
||||
"c0_step_p95_p99_max": [
|
||||
0.03000000000000025,
|
||||
0.08999999999999986,
|
||||
3.87
|
||||
],
|
||||
"c0_bound_seconds": 0.0
|
||||
},
|
||||
"new": {
|
||||
"c0_step_p95_p99_max": [
|
||||
0.020000000000000462,
|
||||
0.08000000000000007,
|
||||
3.750000000000001
|
||||
],
|
||||
"c0_bound_seconds": 0.0
|
||||
}
|
||||
},
|
||||
"baseline_commit": "ed9c44f57584b5f033f79db6331e21b9b3e0ad55",
|
||||
"method": "Replay C0 softening against v21 on frozen recorded motion, not a PSCM model.",
|
||||
"controller_sha256": "b0aff6d70ad8ae7693c69068dbe646767e4d9fc20a23af70cbddcf6ebf849f48",
|
||||
"source_sha256": {
|
||||
".cache/ford_route149/full/route.npz": "aa5902877343cd033ee286b3668d91a85336ffbf740861849fb0b76b0ca24ade",
|
||||
".cache/ford_route149/full/model_paths.npz": "19827800b8fb5983f3d6b72fcfaf36e35a40170bb17cd7c1e49374744fb449fb",
|
||||
".cache/ford_route149/intake.npz": "503919c4f1566ef850007c59f16ce074697ba57732f21beb7b158c1ca7be3a9d",
|
||||
".cache/ford_feedback_delay_v21/149/commands.npz": "15c4ad68338a921cd6b2cd22d0b3d6b5b8637614dacbe61f954a3a6d7cb8d8d9"
|
||||
}
|
||||
}
|
||||
},
|
||||
"code_sha256": {
|
||||
"openpilot/selfdrive/controls/lib/ford_model_action.py": "b0aff6d70ad8ae7693c69068dbe646767e4d9fc20a23af70cbddcf6ebf849f48",
|
||||
"tools/ford_pscm_lab/c0_softening_replay.py": "43e185871e5b9875cee5288160d344fe4d606c4bdf63dd719fb6a6cd9404307e"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
# Ford C1 correction carryover experiment
|
||||
|
||||
The feedback controller at `5fbb583e5` can retain a correction from an earlier
|
||||
turn that outweighs the new base C1. The measured curvature can already be
|
||||
opposite the desired curvature, yet total C1 continues to request the old
|
||||
direction while the integral works back toward zero.
|
||||
|
||||
This experiment keeps the existing 1:1 feedback strength and adds a conditional
|
||||
reset of that correction. It is a command-policy experiment, not a demonstrated
|
||||
improvement in physical steering response.
|
||||
|
||||
## Release rule
|
||||
|
||||
All of the following must be true on a valid, active cycle:
|
||||
|
||||
- Feedback is enabled and a fresh steering publication advances measurement time.
|
||||
- Base C1 is nonzero by at least one DBC step (0.0005 rad).
|
||||
- Both target C0 and the slewed C0 request agree with base C1's direction,
|
||||
by at least one DBC step (0.01 m).
|
||||
- Measured steering-derived curvature points opposite the desired curvature.
|
||||
- The accumulated correction prevents total C1 from requesting the base direction:
|
||||
the sum of base C1 and correction is zero or opposite base C1.
|
||||
|
||||
The stored correction is then set to zero before the usual feedback increment.
|
||||
The final C1 command still passes through its existing ±0.5 rad amplitude and
|
||||
0.5 rad/s slew limits. The reset cannot directly jump the transmitted command.
|
||||
The DBC steps reject requests smaller than one representable step; they are
|
||||
not new strength multipliers. This reset policy is itself an engineering choice.
|
||||
|
||||
There is no reset simply because steering error crosses zero, or because C1
|
||||
and its correction have opposite signs. Matched curvature, neutral/conflicting
|
||||
C0, a correction that does not outweigh base C1, and repeated measurements all
|
||||
preserve normal integration. The condition can apply to small steering
|
||||
corrections as well as large turns; it has no turn-size or speed threshold.
|
||||
|
||||
No previous-turn direction or timer is stored. Agreement between current path
|
||||
requests and disagreement with measured curvature are the confirmation. This
|
||||
does not establish which part of the combined C0/C1 request a PSCM physically
|
||||
needs. In particular, when C0 still points into the previous turn, this rule
|
||||
deliberately leaves the integral alone.
|
||||
|
||||
## Preserved behavior and diagnostics
|
||||
|
||||
C0's 7 m mapping, its limits, the base C1 mapping, upstream curvature limiting,
|
||||
the original integral strength, driver/PSCM arbitration, C2=C3=0 and the 100 Hz
|
||||
sender are unchanged. No fitted PSCM model, proportional term or gain schedule
|
||||
is added. There are still three values used by the command law: C0, C1 and
|
||||
the correction. A diagnostic-only `carryover_release_count` is added and resets
|
||||
with the controller. It is included in the existing periodic diagnostic event.
|
||||
|
||||
The same default-off Sunnylink toggle selects this version. Its diagnostic
|
||||
identity is `model-action-c1-feedback-v2`. See the [drive-test instructions](ford_model_action_drive_test.md).
|
||||
|
||||
## Offline evidence
|
||||
|
||||
The two mirrored command-regression tests failed before the change. After
|
||||
building correction through actual feedback, the old controller still requested
|
||||
the old C1 direction 0.4 s into a reversal. Both tests now pass with the original
|
||||
output slew. Additional tests cover holding a steady curve, small error
|
||||
crossings, neutral and conflicting C0, representable command boundaries,
|
||||
freshness, driver override and PSCM limits. Integration tests execute the actual
|
||||
controlsd selection and upstream limiter, Float32 publication and Ford CAN
|
||||
builder, using both model and maneuver-plan requests and both turn directions.
|
||||
|
||||
The combined suite passes **567 tests and 9,146 subtests**, with the same 178
|
||||
inherited/unsupported safety-test skips as the original feedback validation.
|
||||
The randomized checks include mirrored inputs, zero-error compatibility, and
|
||||
comparison against the exact previous controller from cloned pre-update states.
|
||||
|
||||
Frozen b8 replay triggers 11 releases; b9 triggers 14. Activation and C0 match
|
||||
the previous controller exactly on every reconstructed cycle. In b9, most
|
||||
releases concern small corrections; one follows the large turn around 13:28.
|
||||
The C0/C1 disagreement at 14:36 is preserved. Numerical details and source
|
||||
hashes are in `ford_c1_carryover_validation.json`.
|
||||
|
||||
At the release around 13:28, the candidate C1 crosses into the requested
|
||||
direction 0.255 s earlier than the previous controller on identical frozen
|
||||
inputs. This is a command zero-crossing comparison, not a measured improvement
|
||||
in the truck's steering response. The lab checks total 669,343 Float32/CAN
|
||||
round trips, in addition to the integration tests.
|
||||
|
||||
Replay preserves recorded model requests and measured motion. A difference
|
||||
between candidate and baseline commands can persist because the recorded
|
||||
steering does not respond to the changed command. Replay cannot predict wheel
|
||||
angles, centering, oscillation, or how much earlier the vehicle would unwind.
|
||||
No device build, boot, installation or physical validation was performed.
|
||||
|
||||
## Reproduction
|
||||
|
||||
Use the branch's native dependencies and pinned opendbc revision
|
||||
`c21a9013700734dd20b09e05aa68329ad8cc20f9`. The route commands require the existing
|
||||
full-rlog b8/b9 extracts and the baseline Git revision. Run:
|
||||
|
||||
```sh
|
||||
export PYTHONDONTWRITEBYTECODE=1
|
||||
export PYTHONPATH=.:opendbc_repo
|
||||
python -m pytest -q -p no:cacheprovider openpilot/selfdrive/controls/tests/test_ford_*.py tools/ford_pscm_lab openpilot/selfdrive/car/tests/test_ford_pscm_status.py openpilot/sunnypilot/sunnylink/tests openpilot/common/tests/test_params.py opendbc_repo/opendbc/car/ford/tests/test_ford.py opendbc_repo/opendbc/safety/tests/test_ford.py
|
||||
python -m tools.ford_pscm_lab.feedback_replay stress --cycles 200000 --output .cache/ford_c1_carryover/stress.json
|
||||
python -m tools.ford_pscm_lab.stress_model_action --cycles 200000 --seed 20260907 --opendbc-revision c21a9013700734dd20b09e05aa68329ad8cc20f9 --output .cache/ford_c1_carryover/zero_error_stress.json
|
||||
python -m tools.ford_pscm_lab.feedback_replay route .cache/ford_routeb8 --baseline 5fbb583e592d30de266f8160a5d6b9c620c97f56 --output .cache/ford_c1_carryover/routeb8
|
||||
python -m tools.ford_pscm_lab.feedback_replay route .cache/ford_routeb9 --baseline 5fbb583e592d30de266f8160a5d6b9c620c97f56 --output .cache/ford_c1_carryover/routeb9
|
||||
```
|
||||
@@ -0,0 +1,177 @@
|
||||
{
|
||||
"created_at_utc": "2026-09-10T14:00:37.853762+00:00",
|
||||
"scope": "Conditional release of accumulated C1 correction; offline command behavior only, no predicted vehicle response.",
|
||||
"baseline_commit": "5fbb583e592d30de266f8160a5d6b9c620c97f56",
|
||||
"baseline_source_sha256": "4499defbb7fc5ddf5029ca42c549f0935b0758b08818c5bf0490fb52221f9a34",
|
||||
"deployment_target": {
|
||||
"repository": "sunnypilot/sunnypilot",
|
||||
"branch": "hiimisaac-dev"
|
||||
},
|
||||
"hypothesis": "model-action-c1-feedback-v2",
|
||||
"calibration_approved": false,
|
||||
"toggle": {
|
||||
"key": "FordModelActionController",
|
||||
"default_enabled": false,
|
||||
"activation": "Existing controlsd startup selection"
|
||||
},
|
||||
"release_rule": "Fresh enabled feedback; target and slewed C0 agree with base C1 by >= one DBC step; measured curvature is opposite; stored correction makes total C1 zero or opposite base. Clear correction, then apply original integration and output slew.",
|
||||
"engineering_choices": "Conditional reset policy, using existing DBC steps (0.01 m, 0.0005 rad) to confirm nonzero commands. Original 1:1 integral strength is unchanged.",
|
||||
"preserved": [
|
||||
"C0 mapping and limits",
|
||||
"Base C1 mapping",
|
||||
"Original integral strength",
|
||||
"Final C1 amplitude and slew limits",
|
||||
"Driver and PSCM arbitration",
|
||||
"Upstream selection and limiting",
|
||||
"100 Hz sender",
|
||||
"C2=C3=0"
|
||||
],
|
||||
"panda_safety_changed": false,
|
||||
"opendbc_submodule_changed": false,
|
||||
"opendbc_head": "c21a9013700734dd20b09e05aa68329ad8cc20f9",
|
||||
"controller_size": {
|
||||
"total_lines": 194,
|
||||
"code_lines_excluding_blanks_comments_docstrings": 131,
|
||||
"core_command_state_values": 3,
|
||||
"core_diagnostic_counters": 1
|
||||
},
|
||||
"tests": {
|
||||
"combined_suite": "567 passed, 178 skipped, 9146 subtests passed in 6.45s",
|
||||
"safety_skips": "Same 178 inherited or unsupported variants recorded in ford_c1_feedback_validation.json.",
|
||||
"regression": "Two mirrored carryover command tests fail on the exact baseline class and pass in the candidate suite.",
|
||||
"ruff_changed_python": "pass",
|
||||
"ty_controller": "pass",
|
||||
"settings_compiler_check": "pass",
|
||||
"carryover_controlsd_to_can_frames": 1120,
|
||||
"existing_feedback_controlsd_to_can_frames": 1010,
|
||||
"integration_scope": "Actual source selection, upstream limiting, controller, Float32 publication, Ford sender, both plan sources and signs, all counters and checksums."
|
||||
},
|
||||
"routes": {
|
||||
"b8": {
|
||||
"cycles": 160431,
|
||||
"active_cycles": 68217,
|
||||
"validity_and_c0_match_baseline_exactly": true,
|
||||
"c1_changed_cycles": 6065,
|
||||
"max_abs_c1_change_rad": 0.09250000000000003,
|
||||
"can_round_trips": 160431,
|
||||
"timing_limit": "Controls publication time proxies the computation clock; full SubMaster checks are unavailable.",
|
||||
"reference_limit": "Uses exact consumed model publication as reference; b8 and b9 have no maneuver-plan messages.",
|
||||
"carryover_release_count": 11,
|
||||
"input_sha256": {
|
||||
"route.npz": "6f5dd369b70eaed4b95b28c8b25c9f2e9b830fa07a334881a185505481667c8b",
|
||||
"model_paths.npz": "939af6cf7e74251d8842581cc078d26d9fbfd22a0d7817cb0e368697d419b615",
|
||||
"metadata.json": "73b439132d1de37ec187b544c04d2b05c80965065515a4b7dec29ba57ae37e7c"
|
||||
}
|
||||
},
|
||||
"b9": {
|
||||
"cycles": 90774,
|
||||
"active_cycles": 86474,
|
||||
"validity_and_c0_match_baseline_exactly": true,
|
||||
"c1_changed_cycles": 15208,
|
||||
"max_abs_c1_change_rad": 0.10400000000000004,
|
||||
"can_round_trips": 90774,
|
||||
"timing_limit": "Controls publication time proxies the computation clock; full SubMaster checks are unavailable.",
|
||||
"reference_limit": "Uses exact consumed model publication as reference; b8 and b9 have no maneuver-plan messages.",
|
||||
"carryover_release_count": 14,
|
||||
"input_sha256": {
|
||||
"route.npz": "b07c789d8155335f5d120d0262fced6e4d5803fe767b0ff49b6413dce4140b5c",
|
||||
"model_paths.npz": "6b1f87897c050273fdc05af051307a049b6fc3a93072e7cda1721195ce7c3861",
|
||||
"metadata.json": "9ce452220cab61b81883f32fc2fcaf5db6c78a674cb255a49cc77d5029580fee"
|
||||
}
|
||||
}
|
||||
},
|
||||
"command_timing_example": {
|
||||
"event": {
|
||||
"time_s": 808.286646083,
|
||||
"correction_before_rad": -0.13089810321135922,
|
||||
"correction_after_rad": 0.0,
|
||||
"base_c1_rad": 0.06412824021622576,
|
||||
"desired_angle_deg": -24.17155647277832,
|
||||
"actual_angle_deg": -0.30000001192092896,
|
||||
"speed_m_s": 11.804088592529297,
|
||||
"baseline_c0_c1": [
|
||||
0.15000000000000036,
|
||||
-0.06600000000000006
|
||||
],
|
||||
"candidate_c0_c1": [
|
||||
0.15000000000000036,
|
||||
-0.062000000000000055
|
||||
]
|
||||
},
|
||||
"scope": "Command zero crossing on identical frozen recorded inputs; not wheel response.",
|
||||
"baseline_c1_rightward_at_s": 808.67241324,
|
||||
"candidate_c1_rightward_at_s": 808.4169884780001,
|
||||
"command_crossing_advance_s": 0.25542476199984776
|
||||
},
|
||||
"feedback_stress": {
|
||||
"cycles": 200000,
|
||||
"mirrored_updates": 200000,
|
||||
"can_round_trips": 200000,
|
||||
"carryover_release_count": 946,
|
||||
"baseline_revision": "5fbb583e592d30de266f8160a5d6b9c620c97f56",
|
||||
"baseline_source_sha256": "4499defbb7fc5ddf5029ca42c549f0935b0758b08818c5bf0490fb52221f9a34",
|
||||
"exact_unchanged_state_and_commands_without_release": 199054,
|
||||
"checks": "Mirror symmetry, reset/override, amplitude, slew, correction bounds, carryover direction/confirmation, integration, PSCM limits, CAN.",
|
||||
"scope": "Numerical software invariants only; no model of vehicle motion.",
|
||||
"calibration_approved": false,
|
||||
"controller_sha256": "6f40a05977253987a2c96e74c8c18d912367ed1e55630558ed7b28d52576e552"
|
||||
},
|
||||
"zero_error_stress": {
|
||||
"seed": 20260907,
|
||||
"random_cycles": 200000,
|
||||
"mirrored_core_updates": 200000,
|
||||
"invalid_or_inactive_resets": 3537,
|
||||
"field_boundary_cases": 18138,
|
||||
"float32_can_round_trips": 218138,
|
||||
"analytic_targets_scalar_slew_and_mirror_checks_pass": true,
|
||||
"direct_raw_float32_packing_matches_host_output": true,
|
||||
"max_continuous_step_c0_c1": [
|
||||
0.40000000000000147,
|
||||
0.05000000000000002
|
||||
],
|
||||
"calibration_approved": false,
|
||||
"scope": "Zero-error numerical construction: measured equals requested curvature. No PSCM response claims.",
|
||||
"opendbc_import_head": "c21a9013700734dd20b09e05aa68329ad8cc20f9"
|
||||
},
|
||||
"total_lab_float32_can_round_trips": 669343,
|
||||
"source_sha256": {
|
||||
"openpilot/selfdrive/controls/lib/ford_model_action.py": "6f40a05977253987a2c96e74c8c18d912367ed1e55630558ed7b28d52576e552",
|
||||
"openpilot/selfdrive/controls/tests/test_ford_model_action_feedback.py": "04935fb941a795cb243870a4c03f7073c68147b01da3cedf2872476aa5fb798e",
|
||||
"openpilot/selfdrive/controls/tests/test_ford_model_action_adapter.py": "e2e98d3a0a531235abd032fc4d3564796613ad51ff6ba22a230c48e36f6f6848",
|
||||
"openpilot/selfdrive/controls/tests/test_ford_controlsd_logging.py": "90fb42ce580f0085e349467086a2eef28c2512c671755d0771f6331d30b19035",
|
||||
"tools/ford_pscm_lab/feedback_replay.py": "9bf145fbff6ed685aec2c0e5d0584e2dc7ff831021f15110f939e8a94c93280b",
|
||||
"tools/ford_pscm_lab/stress_model_action.py": "0b25188edf2b248ebe741173ce02ce75bd59f1f39fd5bd909d41a3dca2294aa8",
|
||||
"tools/ford_pscm_lab/model_action_replay.py": "af97c665f342c66b1be2502e188c63e6f3ee106d0a0d5e80997bc3040373ff9f",
|
||||
"docs/ford_c1_carryover.md": "e8d08375963efc6d1ae6ce503bb580ba00cfa90adb71c941d56fe6e3701d5cbb",
|
||||
"docs/ford_c1_feedback.md": "1b440a03082e5cec264a1d6693833ed0a7e07b6c6e2122f8e4455c5971121b57",
|
||||
"docs/ford_model_action_drive_test.md": "7ac5ca0faf9690a23e7058d09b55f23e21e2ba74666001cacb56b67e8d8b4376",
|
||||
"openpilot/selfdrive/controls/controlsd.py": "2b7e246f00bccce3a2bb9f6f44009ca77690cadb8527cd2bdfe855e9ad72ad1e",
|
||||
"opendbc_repo/opendbc/car/ford/carcontroller.py": "b2d327a1833fb1f0d09ee17f54c9c8d45517fa29beb04a4543cfbf1b43f1a65e",
|
||||
"opendbc_repo/opendbc/safety/modes/ford.h": "1d9d996292d6697ab4f02d55fae348d6aca1df94a07f7bdae48b68971b91afe7",
|
||||
"openpilot/sunnypilot/sunnylink/settings_ui.json": "7d38f315a7c5ce6d46d01a06f7eaddd4933f85639e5325ff71fdce22866ef401"
|
||||
},
|
||||
"artifact_sha256": {
|
||||
".cache/ford_c1_carryover/tests.txt": "cd65146df93632e4a2c1e086781e1da4673db7d038c7e673124f227efefbb567",
|
||||
".cache/ford_c1_carryover/baseline_regression.txt": "4469873f95ccf45a376a27fed05be3bdad5f808af7ceca472c6e2cb9d973eb7f",
|
||||
".cache/ford_c1_carryover/stress.json": "7a8d20d7abd7cf444f55b7316e8bedbed0fbe8e9587e09f90d5b1946c3c2e98c",
|
||||
".cache/ford_c1_carryover/zero_error_stress.json": "09e64eaac35df4ec324b41fabdc8baf91931106ac98b89c1ca71f4c8bf8796a4",
|
||||
".cache/ford_c1_carryover/timing.json": "0d18ed4164803adedbbd660fe024f4c28de8eb7921caa60659346ff386d3847f",
|
||||
".cache/ford_c1_carryover/routeb8/report.json": "d2c6f767cef29a74e292b6a16263d2da13b8c302e4653e419b0e232e1aaf762e",
|
||||
".cache/ford_c1_carryover/routeb8/commands.npz": "89b5c3474940b61afce060111c27fd9bad9e24d703c59fca61adf4ce10473df3",
|
||||
".cache/ford_c1_carryover/routeb9/report.json": "e3ff7bfa70e770eca763b125c283fd8a1d509ef1b6e7f26a81c398aca89a87da",
|
||||
".cache/ford_c1_carryover/routeb9/commands.npz": "e4f5f341146e2897a479baf222d678fd16352c8da931876a2471c3719faf9edf"
|
||||
},
|
||||
"test_environment": {
|
||||
"python": "/Users/ibpersonal/dev/sunnypilot/.venv/bin/python",
|
||||
"PYTHONPATH": ".:opendbc_repo:.cache/ford_v6/test_deps",
|
||||
"PYTHONDONTWRITEBYTECODE": "1",
|
||||
"LOG_ROOT": "/private/tmp/ford-carryover-logs",
|
||||
"PARAMS_ROOT": "/private/tmp/ford-carryover-params"
|
||||
},
|
||||
"limitations": [
|
||||
"Frozen replay preserves recorded requests and measured motion; changed commands do not establish changed wheel angles, centering or stability.",
|
||||
"C0/C1 agreement is a reset-policy choice, not an identified relationship between PSCM input and wheel angle.",
|
||||
"The rule can release small corrections and does not promise unchanged centering during transients.",
|
||||
"No device build, boot, installation or physical validation was performed."
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
# Ford C1 feedback experiment
|
||||
|
||||
This document records the original feedback change at `5fbb583e5`. The current
|
||||
version retains its feedback law and adds [conditional carryover release](ford_c1_carryover.md).
|
||||
The validation counts below describe the original change; current results are
|
||||
recorded in `ford_c1_carryover_validation.json`.
|
||||
|
||||
The restored original v1 can leave a steering error while C0 and C1 still have
|
||||
room. Its command law does not directly correct measured steering error. This
|
||||
experiment keeps that mapping and adds one accumulated C1 correction:
|
||||
|
||||
```text
|
||||
error = selected_limited_desired_curvature - measured_curvature
|
||||
correction += error * speed * elapsed_measurement_time
|
||||
C1_target = original_model_C1 + correction
|
||||
```
|
||||
|
||||
Curvature (1/m) multiplied by traveled distance (m) gives heading mismatch in
|
||||
radians. Applying that mismatch to C1 at **1:1 is an explicit feedback-strength
|
||||
choice**. Dimensional consistency does not prove that every PSCM responds
|
||||
correctly to that strength. There is no fitted PSCM response model or new
|
||||
tunable multiplier.
|
||||
|
||||
For example, at 20 m/s, a constant curvature shortfall of 0.001/m adds 0.02 rad
|
||||
to C1 over one second when the output can accept it. When measured curvature
|
||||
matches the request, the correction holds. If the vehicle turns more than
|
||||
requested, the correction moves in the unwind direction. Changing the model
|
||||
request still changes the base immediately, subject to the existing slew.
|
||||
|
||||
## Preserved mapping and limits
|
||||
|
||||
- C0 is the current model path's lateral offset at 7 m of arc distance, holding
|
||||
the available endpoint for shorter paths; its limits remain ±5.11 m and 4 m/s.
|
||||
- Base C1 is `max(7 m, speed × 1 s) × selected_limited_desired_curvature`, clipped
|
||||
to ±0.5 rad. Final C1 uses the same ±0.5 rad and 0.5 rad/s limits as v1.
|
||||
- C2 and C3 are zero. Sign conversion, Float32/CAN rounding, upstream curvature
|
||||
limiting and the 100 Hz sender retain their existing behavior.
|
||||
|
||||
The core holds three values: unquantized C0, unquantized C1 and the correction.
|
||||
Zero error from a reset leaves the correction at zero and preserves the old
|
||||
command arithmetic exactly. There is no separate percentage or distance cap
|
||||
on the correction.
|
||||
|
||||
## Feedback measurement, timing and limits
|
||||
|
||||
The measurement is `controlsd.curvature`, computed from measured steering
|
||||
angle with the existing live vehicle parameters. It matches the curvature
|
||||
used for the desired-versus-actual steering comparison. It is not an independent
|
||||
measurement of tire slip or the vehicle's actual ground path. CAN yaw remains
|
||||
an input-health gate and does not drive this feedback.
|
||||
|
||||
The adapter integrates only elapsed time between fresh `carState` publications.
|
||||
The first publication after reset integrates zero time. Duplicate timestamps
|
||||
integrate zero; a fresh timestamp accounts for the elapsed measurement interval.
|
||||
Output slew continues on valid control cycles. Existing service-age, speed,
|
||||
model-geometry and clock-order gates remain, with the same finite/range check
|
||||
also applied to measured curvature. Disengagement or invalid input clears all
|
||||
three core states.
|
||||
|
||||
The correction cannot accumulate farther into an unavailable C1 amplitude or
|
||||
slew request. Increments that move back toward the available output remain
|
||||
allowed. Moving the base request does not itself rewrite the correction.
|
||||
|
||||
Fresh PSCM status means a valid message whose original CAN receipt timestamp
|
||||
is within the existing −5 to +150 ms age allowance. Reached-limit status (2)
|
||||
prevents extra accumulation in the measured turn direction. An old correction
|
||||
opposing that direction can return to zero; it cannot be trapped below the
|
||||
base request by the limit flag. Unwind and base model changes remain available.
|
||||
Close-to-limit status (1) does not block feedback. Missing or stale status
|
||||
does not gate it; local amplitude and slew anti-windup still apply.
|
||||
|
||||
Driver steering-pressed, torque above the existing 1 Nm allowance, nonfinite
|
||||
torque, or fresh driver-limit status (3) clears the correction. Fresh denied
|
||||
or inactive PSCM status also clears it. The base model request continues
|
||||
through existing engagement and driver arbitration; clearing the correction
|
||||
does not bypass the final output slew.
|
||||
|
||||
## Offline evidence and reproduction
|
||||
|
||||
`ford_c1_feedback_validation.json` records the source hashes and completed
|
||||
checks. Tests exercise build, hold, unwind, saturation, limit flags, immediate
|
||||
driver input, stale and repeated measurements, invalid inputs and both signs.
|
||||
Integration tests execute actual controlsd selection and limiting, Float32
|
||||
publication, CarControlSP conversion and the Ford CarController CAN builder.
|
||||
Randomized runs check feedback invariants separately from zero-error
|
||||
compatibility with the original independent scalar oracle.
|
||||
|
||||
The combined suite passes **511 tests and 9,146 subtests**. Its 178 skips are
|
||||
in inherited safety base classes or unsupported safety-test variants. Ruff,
|
||||
the controller's Ty check and settings compilation pass. Feedback stress,
|
||||
zero-error stress and the b8 replay total **578,569 Float32/CAN round trips**;
|
||||
the integration test separately verifies 1,010 transmitted packet constructions,
|
||||
including every counter and checksum. No packets are sent to hardware.
|
||||
|
||||
The b8 replay retains recorded desired/measured curvature, model publications,
|
||||
driver input and PSCM flags. It compares candidate commands with the restored
|
||||
v1 at `a7d70e2b0890184636827351e4789d866f2a7c97`. All 160,431 reconstructed
|
||||
activation decisions and C0 commands match. C1 changes on 58,106 cycles.
|
||||
At 4:12.493, for example, reconstructed host C1 changes from −0.1625 to
|
||||
−0.2035 rad; at 3:56.250 it changes from −0.1280 to −0.1080 rad. These are
|
||||
changes to commands on frozen measurements, not predicted wheel angles.
|
||||
|
||||
Controls publication time proxies the unlogged computation clock, and the
|
||||
full SubMaster health state cannot be reconstructed. This route uses the
|
||||
consumed model publication as its reference and has no maneuver-plan messages.
|
||||
Replay cannot show whether this feedback fixes weak turns, hanging turns or
|
||||
oscillation. A new drive is needed to measure those outcomes.
|
||||
|
||||
Use the branch's native dependencies and pinned opendbc revision
|
||||
`c21a9013700734dd20b09e05aa68329ad8cc20f9`:
|
||||
|
||||
```sh
|
||||
export PYTHONDONTWRITEBYTECODE=1
|
||||
export PYTHONPATH=.:opendbc_repo
|
||||
python -m pytest -q -p no:cacheprovider openpilot/selfdrive/controls/tests/test_ford_*.py tools/ford_pscm_lab openpilot/selfdrive/car/tests/test_ford_pscm_status.py openpilot/sunnypilot/sunnylink/tests openpilot/common/tests/test_params.py opendbc_repo/opendbc/car/ford/tests/test_ford.py opendbc_repo/opendbc/safety/tests/test_ford.py
|
||||
python openpilot/sunnypilot/sunnylink/tools/compile_settings_ui.py --check
|
||||
python -m tools.ford_pscm_lab.feedback_replay stress --cycles 200000 --output .cache/ford_c1_feedback/feedback_stress.json
|
||||
python -m tools.ford_pscm_lab.stress_model_action --cycles 200000 --seed 20260907 --opendbc-revision c21a9013700734dd20b09e05aa68329ad8cc20f9 --output .cache/ford_c1_feedback/zero_error_stress.json
|
||||
python -m tools.ford_pscm_lab.feedback_replay route .cache/ford_routeb8 --output .cache/ford_c1_feedback/routeb8
|
||||
```
|
||||
|
||||
The last command requires the existing full-rlog b8 extract (`route.npz`,
|
||||
`model_paths.npz`, `metadata.json`), identified by hashes in the validation
|
||||
record. The historical route90/95 replay deliberately sets measured curvature
|
||||
equal to requested curvature to check zero-error compatibility; it does not
|
||||
exercise recorded steering feedback.
|
||||
|
||||
Enable using the [existing Sunnylink toggle](ford_model_action_drive_test.md).
|
||||
The diagnostic identity is `model-action-c1-feedback-v1`.
|
||||
@@ -0,0 +1,258 @@
|
||||
{
|
||||
"created_at_utc": "2026-09-09T14:45:33.853345+00:00",
|
||||
"baseline_commit": "a7d70e2b0890184636827351e4789d866f2a7c97",
|
||||
"deployment_target": {
|
||||
"repository": "sunnypilot/sunnypilot",
|
||||
"branch": "hiimisaac-dev"
|
||||
},
|
||||
"scope": "C1 measured-curvature feedback on restored original v1. Offline software validation only; no predicted or measured physical improvement.",
|
||||
"calibration_approved": false,
|
||||
"toggle": {
|
||||
"key": "FordModelActionController",
|
||||
"default_enabled": false,
|
||||
"activation": "Existing startup selection after offroad-to-onroad cycle"
|
||||
},
|
||||
"feedback_law": "correction += (desired_curvature - measured_curvature) * speed * elapsed_measurement_time, subject to output and PSCM anti-windup",
|
||||
"feedback_strength": "Explicit 1:1 heading-error-to-C1 choice; no fitted PSCM plant or new tunable multiplier",
|
||||
"preserved": [
|
||||
"C0 mapping and limits",
|
||||
"C2=C3=0",
|
||||
"C1 final amplitude and slew limits",
|
||||
"100 Hz sender",
|
||||
"upstream selection and limiting"
|
||||
],
|
||||
"panda_safety_changed": false,
|
||||
"opendbc_submodule_changed": false,
|
||||
"opendbc_head": "c21a9013700734dd20b09e05aa68329ad8cc20f9",
|
||||
"controller_size": {
|
||||
"total_lines": 181,
|
||||
"code_lines_excluding_blanks_comments_docstrings": 123,
|
||||
"core_persistent_values": 3,
|
||||
"adapter_timestamps": 3
|
||||
},
|
||||
"tests": {
|
||||
"combined_suite": "511 passed, 178 skipped, 9146 subtests passed in 5.14s",
|
||||
"suite_log_sha256": "001ef6633b22513317593dd8debc160a0ca8aaf78ea53418c5f7a50c370cc818",
|
||||
"ruff_changed_python": "pass",
|
||||
"ty_controller": "pass",
|
||||
"settings_compiler_check": "pass",
|
||||
"safety_skip_reasons": [
|
||||
"SKIPPED [145] ../../../../dev/sunnypilot/.venv/lib/python3.12/site-packages/_pytest/unittest.py:523: Skipped",
|
||||
"SKIPPED [9] opendbc_repo/opendbc/safety/tests/common.py:64: Safety mode implements no _user_regen_msg",
|
||||
"SKIPPED [3] opendbc_repo/opendbc/safety/tests/mads_common.py:51: Skipping test because MADS button is not supported",
|
||||
"SKIPPED [3] opendbc_repo/opendbc/safety/tests/mads_common.py:254: Skipping test because MADS button is not supported",
|
||||
"SKIPPED [3] opendbc_repo/opendbc/safety/tests/mads_common.py:67: Skipping test because _acc_state_msg is not implemented for this car",
|
||||
"SKIPPED [3] opendbc_repo/opendbc/safety/tests/mads_common.py:165: Skipping test because MADS button is not supported",
|
||||
"SKIPPED [3] opendbc_repo/opendbc/safety/tests/mads_common.py:165: Skipping test because ACC main is not supported",
|
||||
"SKIPPED [3] opendbc_repo/opendbc/safety/tests/mads_common.py:411: MADS button not supported",
|
||||
"SKIPPED [1] opendbc_repo/opendbc/safety/tests/test_ford.py:378: CAN FD only",
|
||||
"SKIPPED [1] opendbc_repo/opendbc/safety/tests/test_ford.py:361: CAN FD only",
|
||||
"SKIPPED [1] opendbc_repo/opendbc/safety/tests/test_ford.py:351: CAN FD only",
|
||||
"SKIPPED [1] opendbc_repo/opendbc/safety/tests/test_ford.py:327: CAN FD only",
|
||||
"SKIPPED [1] opendbc_repo/opendbc/safety/tests/test_ford.py:341: CAN FD only",
|
||||
"SKIPPED [1] opendbc_repo/opendbc/safety/tests/test_ford.py:320: CAN FD only"
|
||||
],
|
||||
"safety_native_build": "Pinned safety C source is compiled locally by libsafety_py before testing.",
|
||||
"controlsd_to_can_feedback_integration_frames": 1010,
|
||||
"integration_checks": "Both signs: build, hold, unwind, rebuild, immediate driver override; actual 100 Hz sender, counter, checksum, fields and publication. Separate integration tests validate PSCM service forwarding.",
|
||||
"regression_test_evidence": [
|
||||
"Nonzero-error integration failed with zero correction before implementing feedback.",
|
||||
"Both sign tests failed when a reached limit trapped an old opposing correction; they pass after allowing return to zero."
|
||||
]
|
||||
},
|
||||
"route_b8": {
|
||||
"baseline_revision": "a7d70e2b0890184636827351e4789d866f2a7c97",
|
||||
"baseline_source_sha256": "8f3bc5d68e0051776f614a2ccffae84a88f7898dc95bdc12c23dcfe10dfe676a",
|
||||
"cycles": 160431,
|
||||
"active_cycles": 68217,
|
||||
"validity_and_c0_match_original_v1_exactly": true,
|
||||
"status_counts": {
|
||||
"inactive": 92214,
|
||||
"active": 68217
|
||||
},
|
||||
"feedback_enabled_seconds": 598.256888772994,
|
||||
"pscm_limit_2_seconds": 12.139079590997426,
|
||||
"c1_changed_cycles": 58106,
|
||||
"max_abs_c1_change_rad": 0.29800000000000004,
|
||||
"max_abs_correction_rad": 0.29816844327770786,
|
||||
"can_round_trips": 160431,
|
||||
"timing_limit": "Controls publication time proxies the computation clock; full SubMaster checks are unavailable.",
|
||||
"reference_limit": "Uses exact consumed model publication as reference; the b8 route has no maneuver-plan messages.",
|
||||
"example_points": [
|
||||
{
|
||||
"time_s": 130.9368894940053,
|
||||
"old_c0_c1": [
|
||||
-0.7400000000000002,
|
||||
-0.18700000000000006
|
||||
],
|
||||
"candidate_c0_c1": [
|
||||
-0.7400000000000002,
|
||||
-0.22899999999999998
|
||||
],
|
||||
"correction_rad": -0.042171663052515254,
|
||||
"feedback_enabled": true,
|
||||
"pscm_limited": false
|
||||
},
|
||||
{
|
||||
"time_s": 235.3960996990063,
|
||||
"old_c0_c1": [
|
||||
-2.04,
|
||||
-0.40449999999999997
|
||||
],
|
||||
"candidate_c0_c1": [
|
||||
-2.04,
|
||||
-0.4145
|
||||
],
|
||||
"correction_rad": -0.00989648519895422,
|
||||
"feedback_enabled": true,
|
||||
"pscm_limited": true
|
||||
},
|
||||
{
|
||||
"time_s": 236.25034470800165,
|
||||
"old_c0_c1": [
|
||||
-1.46,
|
||||
-0.128
|
||||
],
|
||||
"candidate_c0_c1": [
|
||||
-1.46,
|
||||
-0.10799999999999998
|
||||
],
|
||||
"correction_rad": 0.01990758350705991,
|
||||
"feedback_enabled": true,
|
||||
"pscm_limited": false
|
||||
},
|
||||
{
|
||||
"time_s": 252.49320156300382,
|
||||
"old_c0_c1": [
|
||||
-0.6699999999999999,
|
||||
-0.16249999999999998
|
||||
],
|
||||
"candidate_c0_c1": [
|
||||
-0.6699999999999999,
|
||||
-0.20350000000000001
|
||||
],
|
||||
"correction_rad": -0.040907632902654506,
|
||||
"feedback_enabled": true,
|
||||
"pscm_limited": false
|
||||
},
|
||||
{
|
||||
"time_s": 674.430371745002,
|
||||
"old_c0_c1": [
|
||||
0.4299999999999997,
|
||||
0.128
|
||||
],
|
||||
"candidate_c0_c1": [
|
||||
0.4299999999999997,
|
||||
0.1345
|
||||
],
|
||||
"correction_rad": 0.00639271291315417,
|
||||
"feedback_enabled": true,
|
||||
"pscm_limited": false
|
||||
},
|
||||
{
|
||||
"time_s": 1534.5190040400048,
|
||||
"old_c0_c1": [
|
||||
2.62,
|
||||
0.5
|
||||
],
|
||||
"candidate_c0_c1": [
|
||||
2.62,
|
||||
0.5
|
||||
],
|
||||
"correction_rad": 0.0,
|
||||
"feedback_enabled": true,
|
||||
"pscm_limited": true
|
||||
},
|
||||
{
|
||||
"time_s": 1562.5074677500015,
|
||||
"old_c0_c1": [
|
||||
-0.1200000000000001,
|
||||
-0.051000000000000045
|
||||
],
|
||||
"candidate_c0_c1": [
|
||||
-0.1200000000000001,
|
||||
-0.046499999999999986
|
||||
],
|
||||
"correction_rad": 0.004453988923883501,
|
||||
"feedback_enabled": true,
|
||||
"pscm_limited": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"route_input_sha256": {
|
||||
"route.npz": "6f5dd369b70eaed4b95b28c8b25c9f2e9b830fa07a334881a185505481667c8b",
|
||||
"model_paths.npz": "939af6cf7e74251d8842581cc078d26d9fbfd22a0d7817cb0e368697d419b615",
|
||||
"metadata.json": "73b439132d1de37ec187b544c04d2b05c80965065515a4b7dec29ba57ae37e7c"
|
||||
},
|
||||
"feedback_stress": {
|
||||
"cycles": 200000,
|
||||
"mirrored_updates": 200000,
|
||||
"can_round_trips": 200000,
|
||||
"checks": "Mirror symmetry, reset/override, amplitude, slew, correction bounds, integration direction/size, PSCM anti-windup, CAN fields.",
|
||||
"scope": "Numerical software invariants only; no model of vehicle motion.",
|
||||
"calibration_approved": false,
|
||||
"controller_sha256": "4499defbb7fc5ddf5029ca42c549f0935b0758b08818c5bf0490fb52221f9a34"
|
||||
},
|
||||
"zero_error_stress": {
|
||||
"seed": 20260907,
|
||||
"random_cycles": 200000,
|
||||
"mirrored_core_updates": 200000,
|
||||
"invalid_or_inactive_resets": 3537,
|
||||
"field_boundary_cases": 18138,
|
||||
"float32_can_round_trips": 218138,
|
||||
"analytic_targets_scalar_slew_and_mirror_checks_pass": true,
|
||||
"direct_raw_float32_packing_matches_host_output": true,
|
||||
"max_continuous_step_c0_c1": [
|
||||
0.40000000000000147,
|
||||
0.05000000000000002
|
||||
],
|
||||
"calibration_approved": false,
|
||||
"scope": "Zero-error numerical construction: measured equals requested curvature. No PSCM response claims.",
|
||||
"opendbc_import_head": "c21a9013700734dd20b09e05aa68329ad8cc20f9"
|
||||
},
|
||||
"total_lab_float32_can_round_trips": 578569,
|
||||
"artifact_sha256": {
|
||||
".cache/ford_c1_feedback/routeb8/report.json": "648887eebb76260a60f7c0f0d9aaac83d0f1c06b443e28bc6fdf296bad4526c0",
|
||||
".cache/ford_c1_feedback/routeb8/commands.npz": "1aef98365572b3fb3cb8ba2a93cf30041ff3be133718d469145b710f2c94dc32",
|
||||
".cache/ford_c1_feedback/feedback_stress.json": "abf4e7bccc1e460008cc7450fcd92e9b2a6108bd71e53a01bdc24e31e5b5ad32",
|
||||
".cache/ford_c1_feedback/zero_error_stress.json": "2a3f284e10e5054205a788cce59bcf57bd837e13afff327457244141ba5522f0",
|
||||
".cache/ford_c1_feedback/safety_skip_reasons.txt": "5384c82b07b7cc20c6b22b8e94246cb104d53f8866af02b28fda7d4138cf377f"
|
||||
},
|
||||
"native_params": {
|
||||
"library_sha256": "270bf43241cf7c02cc432cf78ec9411a62d7653ca445695efe785ae82241aa09",
|
||||
"sources_match_original_rebuild_record": true,
|
||||
"provenance": "Same locally rebuilt native library and source hashes recorded in ford_model_action_drive_test_validation.json; verified for this run."
|
||||
},
|
||||
"test_environment": {
|
||||
"python": "/Users/ibpersonal/dev/sunnypilot/.venv/bin/python",
|
||||
"PYTHONPATH": ".:opendbc_repo:.cache/ford_v6/test_deps",
|
||||
"LOG_ROOT": "/private/tmp/ford-feedback-logs",
|
||||
"PARAMS_ROOT": "/private/tmp/ford-feedback-params",
|
||||
"PYTHONDONTWRITEBYTECODE": "1"
|
||||
},
|
||||
"source_sha256": {
|
||||
"docs/ford_c1_feedback.md": "c1bc7f24c5ebe28679b4a04d09085d7b937926e63a38d43a3abfac93dfcfa0f9",
|
||||
"docs/ford_model_action_candidate.md": "20cd8d10008cd796cc8719f5795ee80f50d8133d6e7684fb057a78fb05323fbe",
|
||||
"docs/ford_model_action_drive_test.md": "7860ae26a61682aff86743ba302eb23c8f271d5700a2e616da1b6b38d438b57d",
|
||||
"opendbc_repo/opendbc/car/vehicle_model.py": "ddc2a93d9c2b2ef6c9a913a5aef4c51e2bc387db1f7640473657e5ade4e50fac",
|
||||
"openpilot/selfdrive/controls/controlsd.py": "2b7e246f00bccce3a2bb9f6f44009ca77690cadb8527cd2bdfe855e9ad72ad1e",
|
||||
"openpilot/selfdrive/controls/lib/drive_helpers.py": "916bcd83c2a909a89795da58c7c43d7b168c9b82e1a6d281484bae45c667c01e",
|
||||
"openpilot/selfdrive/controls/lib/ford_model_action.py": "4499defbb7fc5ddf5029ca42c549f0935b0758b08818c5bf0490fb52221f9a34",
|
||||
"openpilot/selfdrive/controls/lib/ford_path.py": "383538fc7cdae3bc28dffb71fe12ac5f3f9866ffbe6adfb7457f3593e9fc903a",
|
||||
"openpilot/selfdrive/controls/tests/test_ford_controlsd_logging.py": "84113b1b7800c868117af0034278f53a1a6153c7bb5fadc1ea45958e62c4f0d0",
|
||||
"openpilot/selfdrive/controls/tests/test_ford_model_action.py": "cbe1b2aa1961deba3a42e1d82f5f75ae0c3d7a219428dea5f50cb70e1b27fd11",
|
||||
"openpilot/selfdrive/controls/tests/test_ford_model_action_adapter.py": "c7f5ffd650e804e6e02fa12d435e0867b56b13a49c3d9fa511993188d5cb625a",
|
||||
"openpilot/selfdrive/controls/tests/test_ford_model_action_feedback.py": "f7a956c082a246d9506e21adbf348cbdc7f94d5342d832841058c71f7e264eeb",
|
||||
"openpilot/sunnypilot/selfdrive/controls/controlsd_ext.py": "7a13dc5ce49b40e27e05e62cdb9ef1bb764de8ed8167f7e982d54a4dffe97ed4",
|
||||
"openpilot/sunnypilot/sunnylink/settings_ui.json": "7d38f315a7c5ce6d46d01a06f7eaddd4933f85639e5325ff71fdce22866ef401",
|
||||
"openpilot/sunnypilot/sunnylink/settings_ui_src/pages/vehicle.yaml": "410e306958ece12e49fc114707741c57a2dd927c6ba3410e160834e52a759ea9",
|
||||
"tools/ford_pscm_lab/feedback_replay.py": "ca552217953f3cce35da0b1666252fd43b6f8ab6c067e5c9102da8ea8d97c2f3",
|
||||
"tools/ford_pscm_lab/model_action_replay.py": "af97c665f342c66b1be2502e188c63e6f3ee106d0a0d5e80997bc3040373ff9f",
|
||||
"tools/ford_pscm_lab/stress_model_action.py": "0b25188edf2b248ebe741173ce02ce75bd59f1f39fd5bd909d41a3dca2294aa8"
|
||||
},
|
||||
"limitations": [
|
||||
"Frozen route replay changes commands only; it cannot establish tracking, unwind response or closed-loop stability.",
|
||||
"Measured curvature uses the existing steering-angle vehicle model; it is not an independent ground-path measurement.",
|
||||
"No full device build, device boot, installation or road validation was performed."
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
# Ford C1 integral trial: I=1.0
|
||||
|
||||
Route 151 had large entry shortfalls before driver input while C1 still had
|
||||
command range available. At the segment-5 right-turn shortfall, the selected
|
||||
request was 125 degrees right and the wheel was at 49 degrees right. The stored
|
||||
correction was only 0.015 rad and was growing at about 0.0245 rad/s. Neither the
|
||||
C1 field bound nor the PSCM reached-limit flag explained that point.
|
||||
|
||||
This trial raises I from 0.25 to **1.0**, keeping P at **0.75**. The fresh-error
|
||||
increment is four times larger for the same curvature error, speed and elapsed
|
||||
measurement time. This also retires existing correction four times faster for
|
||||
the same opposing error, before the existing accumulation/headroom rules apply.
|
||||
It does not introduce a new state, rate limit, threshold or release heuristic.
|
||||
Runtime changes are one gain constant and the diagnostic identifier
|
||||
`model-action-curvature-c0-distance-pi-v14`.
|
||||
|
||||
C0's desired-curvature formula and distance toggle, base C1, +0.40 s low-speed
|
||||
model preview, feedback measurement, driver override, PSCM arbitration, field
|
||||
bounds and 100 Hz sender remain unchanged. Zero error holds I. Driver override
|
||||
clears P and I. Fresh limitReached blocks outward accumulation while allowing
|
||||
retirement. Accumulation cannot charge beyond the combined command's available
|
||||
range. C2/C3 stay zero, and toggle-off still selects upstream Ford control.
|
||||
|
||||
## Why this coefficient
|
||||
|
||||
Compared I=0.25, 0.50, 1.0 and 2.0 with P=0.75 on routes 149 and 151, using
|
||||
the production adapter and Float32/CAN path for every sample. I=1.0 gives a
|
||||
substantial increase in retained correction. I=2.0 adds considerably more
|
||||
opposite-direction correction on reviewed exits and approaches the C1 bound
|
||||
in the route-151 right turn. I=1.0 is a fourfold experimental step, not an
|
||||
offline-fitted optimum or a validated physical calibration.
|
||||
|
||||
Paired fixed-motion examples with I=0.25 / I=1.0:
|
||||
|
||||
| Route / time | Recorded situation | C1 before | C1 candidate |
|
||||
| --- | --- | ---: | ---: |
|
||||
| 151 / 306.891 s | 125-degree right request, 49-degree wheel | +0.3270 | +0.3725 |
|
||||
| 151 / 307.999 s | Wheel remains behind on the same right turn | +0.2675 | +0.3625 |
|
||||
| 151 / 2363.807 s | 137-degree left request, 64-degree wheel | -0.3410 | -0.3785 |
|
||||
| 149 / 319.549 s | Right-turn entry shortfall | +0.4190 | +0.4840 |
|
||||
| 149 / 850.497 s | Right-turn release | +0.0670 | +0.0010 |
|
||||
| 149 / 851.331 s | Near center on that release | +0.0105 | -0.0460 |
|
||||
|
||||
These are same-cycle controller requests, not necessarily the preceding CAN
|
||||
message at that timestamp. Positive C1 requests right steering; positive logged
|
||||
wheel angle means left. Both replays use P=0.75; route 149 originally drove
|
||||
P=0.50, so the old-I replay is not its historical command trace.
|
||||
|
||||
The exit examples show why faster retirement does not guarantee a smoother
|
||||
unwind: the candidate can accumulate more correction in the opposite direction
|
||||
and retain it when error reaches zero. Higher I also affects centering. On
|
||||
route 151's clean requests under 10 degrees, mean absolute C1 rises from about
|
||||
0.0083 to 0.0122 rad on the fixed recording; route 149 rises from 0.0077 to
|
||||
0.0154 rad. The vehicle would generate different errors under the candidate.
|
||||
These are command observations, not predictions of wheel motion, stability,
|
||||
overshoot, or future tracking accuracy.
|
||||
|
||||
## Broader validation
|
||||
|
||||
The paired production replay covers 22 extracts: 112–117, 119, 11a, 120, 124,
|
||||
125, 146, 149, 151, a0, a2, a5, a9, b8, b9, ca and Raptor 02. It processes
|
||||
2,600,956 source cycles and 5,201,912 Float32/CAN round trips. Both settings have
|
||||
identical eligibility, C0, P, base heading, overflow, and driver/PSCM feedback
|
||||
gates on every cycle. Output remains finite and bounded; inactive commands and
|
||||
C2/C3 are zero. The decoder checks fields, mode and counter on every command.
|
||||
|
||||
Scoring excludes driver override, inactive/invalid control, disabled feedback,
|
||||
the following second, and speeds below 3 mph. There are 12,213.62 scored seconds.
|
||||
C1-bound time rises from 22.67 to 25.13 seconds. Route 151 has no C1-bound samples
|
||||
in that cohort under either setting. Its I=0.25 baseline matches the recorded
|
||||
path output to Float32 precision: maximum C0 error 5.8e-8 m, C1 error 1.5e-8 rad.
|
||||
Older routes intentionally retain their original model requests and physical
|
||||
measurements, including any historical tracking errors. Their baseline command
|
||||
traces need not match older controller implementations.
|
||||
|
||||
The existing controlsd-to-publication-to-CAN feedback test was updated before
|
||||
the gain change. It failed on the old default (0.005 rad accumulated versus
|
||||
0.020 rad required over its one-second error interval), then passed with the
|
||||
new default. **468 tests and 25 subtests pass**, covering selection, current
|
||||
references, accumulation/hold/retirement, reversals, duplicate measurements,
|
||||
PSCM limits, driver overrides, downstream checksums and upstream fallback.
|
||||
Ruff and `git diff --check` pass. No device build or physical evaluation is
|
||||
claimed by these offline checks.
|
||||
|
||||
Evidence: [ford_c1_i1_validation.json](ford_c1_i1_validation.json). Local arrays
|
||||
are in `.cache/ford_i1_trial`, with the four-setting comparison in
|
||||
`.cache/ford_i_trial_sweep`. Reproduce a comparison:
|
||||
|
||||
```sh
|
||||
PYTHONPATH=.:opendbc_repo PYTHONDONTWRITEBYTECODE=1 python \
|
||||
tools/ford_pscm_lab/proportional_replay.py \
|
||||
--routes 151=.cache/ford_route151/full 149=.cache/ford_route149/full \
|
||||
--settings .75:.25 .75:1.0 \
|
||||
--output .cache/ford_i1_recheck --workers 2
|
||||
```
|
||||
|
||||
The replay tool's default P=0.50 / P=0.75 comparison with I=0.25 is preserved.
|
||||
Explicit `--settings` accepts P:I pairs; the first is the baseline. Publication
|
||||
timestamps approximate execution time because full process scheduling and
|
||||
SubMaster state are not logged. The trial still needs physical measurement;
|
||||
route 151 also changed the big model from CTMV2 to Tee Time, so its comparison
|
||||
with route 149 cannot isolate the controller's physical effect.
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,82 @@
|
||||
# Ford continuous PI drive trial (v7)
|
||||
|
||||
The selected controller follows the current, upstream-limited desired curvature
|
||||
with P=0.50 and I=0.25. It replaces v6's request-change, C0-confirmation and
|
||||
unwind-catchup release rules. C0 mapping, heading overflow allocation, input
|
||||
health gates, driver/PSCM arbitration and final output limits remain unchanged.
|
||||
Only C0, C1 and I carry control history. C2/C3 stay zero.
|
||||
|
||||
```text
|
||||
D = max(7 m, speed × 1 s)
|
||||
error = selected desired curvature − measured steering-derived curvature
|
||||
base = clip(D × desired curvature, −0.5, +0.5)
|
||||
P = 0.50 × D × error
|
||||
increment = 0.25 × speed × error × fresh steering measurement interval
|
||||
C1 target = clip(base + P + I, −0.5, +0.5)
|
||||
```
|
||||
|
||||
After PSCM outward-accumulation arbitration, the increment first cancels up to
|
||||
its own magnitude of opposing I. It cannot cross zero in that step. Any remainder
|
||||
is bounded by the combined command's amplitude/slew headroom. C1 output still
|
||||
slews at 0.5 rad/s. This allows old I to unwind even when the output is already
|
||||
slewing. It adds no timer, request history, turn detection or position threshold.
|
||||
|
||||
P doubles relative to v6, and I builds at one quarter the previous rate for an
|
||||
identical error and fresh measurement interval. Lower I also unwinds more slowly
|
||||
for an identical existing state and error; its replay benefit comes mainly from
|
||||
storing less correction. Zero error removes P and holds I. Persistent tracking
|
||||
bias may need that holding correction. There is no claim that all I is unwanted.
|
||||
|
||||
## Evidence and limitations
|
||||
|
||||
Six offline settings were compared across fourteen routes and 1,578,250 source
|
||||
cycles before selecting this candidate. The production selector and adapter now
|
||||
exactly reproduce the selected P=0.50/I=0.25 archived commands, validity, P, I,
|
||||
feedforward, C0 overflow and feedback/PSCM gates on every cycle of all fourteen
|
||||
routes. These comprise Lightning 112–117, a0, a2, a5, a9, b8, b9, ca and Raptor 02.
|
||||
The integration replay performs 1,578,250 actual Float32/CAN round trips.
|
||||
|
||||
A further 20,000 randomized cycles with mirrored and independent C0 paths perform
|
||||
60,000 CAN checks on production, checking independent scalar arithmetic,
|
||||
C0-independent feedback, symmetry, reset, amplitude and slew behavior.
|
||||
The Ford, Sunnylink, params, sender and safety suite passes 679 tests and
|
||||
9,145 subtests; 178 are skipped by the platform test suite. Removed maneuver
|
||||
heuristic tests are replaced with continuous-error, cancellation, freshness,
|
||||
three-state reproduction and actual controlsd-to-CAN entry/exit checks.
|
||||
Historical untracked offline experiment tests are outside this deployment suite.
|
||||
|
||||
At a previously reviewed route-115 exit (133.595 s), the original small PI
|
||||
controller requested +0.1090 rad C1; this trial requests +0.0175. That sample
|
||||
includes driver context. Reviewed large entries remain similar, but commands
|
||||
are not identical everywhere. In a previously well-tracked route-116 bend
|
||||
(112 s), C1 falls from +0.1530 to +0.1355 rad. Lower I could weaken a persistent
|
||||
bend, while higher P can increase response to measurement fluctuations.
|
||||
|
||||
Recorded wheel motion remains fixed in replay. These checks establish software
|
||||
behavior and exact integration of the candidate; they do not establish improved
|
||||
physical tracking or stability. Gains remain experimental, not an identified
|
||||
universal PSCM calibration. No device build, boot or new drive is claimed.
|
||||
|
||||
## Selection and reproduction
|
||||
|
||||
Use the existing default-off Sunnylink **Selected-Action Path Tracking
|
||||
(Experimental)** toggle on any Ford CAN FD, followed by a real offroad-to-onroad
|
||||
cycle. Logs identify `model-action-c1-pi-v7`, `proportional_gain=0.5`,
|
||||
`integral_gain=0.25`. Toggle-off selects upstream Ford control. See the
|
||||
[drive instructions](ford_model_action_drive_test.md).
|
||||
|
||||
With the built cereal/opendbc environment and archived local extracts:
|
||||
|
||||
```sh
|
||||
export PYTHONPATH=.:opendbc_repo:.cache/ford_v6/test_deps
|
||||
export PYTHONDONTWRITEBYTECODE=1
|
||||
export PARAMS_ROOT=/tmp/ford-v7-params
|
||||
export LOG_ROOT=/tmp/ford-v7-logs
|
||||
python -m tools.ford_pscm_lab.minimal_pi_validate --output .cache/ford_minimal_tuning/production --workers 4
|
||||
python -m tools.ford_pscm_lab.minimal_pi_production_stress --cycles 20000 --output .cache/ford_minimal_tuning/production_stress.json
|
||||
```
|
||||
|
||||
The validation JSON records route and source hashes. The archived six-setting
|
||||
sweep is local evidence, not a checked-in dataset. Historical v5/v6 lab tools
|
||||
load their pinned controller revisions so their baseline comparisons retain
|
||||
their original meaning after production changes.
|
||||
@@ -0,0 +1,332 @@
|
||||
{
|
||||
"scope": "Production integration of the selected continuous PI drive trial; fixed recorded motion, not a physical tracking prediction.",
|
||||
"parent_revision": "5e6993aab",
|
||||
"previous_controller_revision": "bf00bc691def830e1beb15363d05416714c1dc42",
|
||||
"hypothesis": "model-action-c1-pi-v7",
|
||||
"kp": 0.5,
|
||||
"ki": 0.25,
|
||||
"control_history": [
|
||||
"c0",
|
||||
"c1",
|
||||
"correction"
|
||||
],
|
||||
"physical_lines": {
|
||||
"module": 201,
|
||||
"core_class": 58,
|
||||
"core_update": 42
|
||||
},
|
||||
"production_source_sha256": "a899b8595e903d6fc4401392a8d123ef77725f6d6d7fa10392a7b3fe72e732b8",
|
||||
"route_cycles": 1578250,
|
||||
"production_can_round_trips": 1638250,
|
||||
"tests": {
|
||||
"passed": 679,
|
||||
"skipped": 178,
|
||||
"subtests_passed": 9145,
|
||||
"log_sha256": "2bc3dfd7ba08d4985d3dea98f235eb1fc99b23e5bbb3927e823a2439c8e9a481",
|
||||
"scope": "Ford controls, tracked PSCM lab tests, PSCM status, Sunnylink, params, Ford car and safety suites; historical untracked experiments excluded."
|
||||
},
|
||||
"ruff": "pass",
|
||||
"ty_production": "pass",
|
||||
"routes": [
|
||||
{
|
||||
"scope": "Verify production commands against the archived P=.50/I=.25 offline candidate.\n\nFixed recorded motion verifies integration parity, not physical tracking.\n",
|
||||
"route": "112",
|
||||
"cycles": 108971,
|
||||
"can_round_trips": 108971,
|
||||
"production_matches_archived_trial_exactly": true,
|
||||
"opendbc_revision": "64aa61b9b3fd26e70a7caa915acab207ff3cd64a",
|
||||
"hypothesis": "model-action-c1-pi-v7",
|
||||
"kp": 0.5,
|
||||
"ki": 0.25,
|
||||
"source_sha256": {
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route112/route.npz": "2b08a2fb636f7d14556d7df4035eafc1d1b97932237955528562a16db2b31d3e",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route112/model_paths.npz": "9837afe78aab4cad288cad98a595a5777fa8a66bb235986b1272a7f7c54e559a",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route112/metadata.json": "726d78a7e7aa45307dcfe27cb00775c20ecc9d54538eb7f26a0166fc216226ce",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_minimal_tuning/sweep/112/report.json": "d8cbcf8b676a67a90310390611da31783b4e025a0a965239ef4672caa47aae56",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_minimal_tuning/sweep/112/commands.npz": "8d91f449c7f8741ce465ec4326949a9d9527ed6b5de7a526ad7d30212f16c469",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/minimal_pi_validate.py": "35b5334f52eb6069b79fc3ed3be71ceaa81a3db8e1f76e8b1ab6e363cd4f18ae",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/openpilot/selfdrive/controls/lib/ford_model_action.py": "a899b8595e903d6fc4401392a8d123ef77725f6d6d7fa10392a7b3fe72e732b8"
|
||||
}
|
||||
},
|
||||
{
|
||||
"scope": "Verify production commands against the archived P=.50/I=.25 offline candidate.\n\nFixed recorded motion verifies integration parity, not physical tracking.\n",
|
||||
"route": "113",
|
||||
"cycles": 49614,
|
||||
"can_round_trips": 49614,
|
||||
"production_matches_archived_trial_exactly": true,
|
||||
"opendbc_revision": "64aa61b9b3fd26e70a7caa915acab207ff3cd64a",
|
||||
"hypothesis": "model-action-c1-pi-v7",
|
||||
"kp": 0.5,
|
||||
"ki": 0.25,
|
||||
"source_sha256": {
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route113/route.npz": "774ca4a21b7113c2706d6130bc180c3216ea4833155300ab01e75b3486e36327",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route113/model_paths.npz": "93c41761eb85263f534f5371b905482cf7c948582eb1e9149966594be1d3768f",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route113/metadata.json": "1c0ca74dd48b90ab9d5444c5ca7f8aa9361700bbdf98bd5e853be50ad2895d7f",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_minimal_tuning/sweep/113/report.json": "e2dfeba5d3fe8a1147f82bce9ddf2ad3f14271bda35fac1639d383f2a0189228",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_minimal_tuning/sweep/113/commands.npz": "3890c802072dd2d38b4cded5f9f80310d1f86eb8a8f3598138235a0e2cc16460",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/minimal_pi_validate.py": "35b5334f52eb6069b79fc3ed3be71ceaa81a3db8e1f76e8b1ab6e363cd4f18ae",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/openpilot/selfdrive/controls/lib/ford_model_action.py": "a899b8595e903d6fc4401392a8d123ef77725f6d6d7fa10392a7b3fe72e732b8"
|
||||
}
|
||||
},
|
||||
{
|
||||
"scope": "Verify production commands against the archived P=.50/I=.25 offline candidate.\n\nFixed recorded motion verifies integration parity, not physical tracking.\n",
|
||||
"route": "114",
|
||||
"cycles": 61027,
|
||||
"can_round_trips": 61027,
|
||||
"production_matches_archived_trial_exactly": true,
|
||||
"opendbc_revision": "64aa61b9b3fd26e70a7caa915acab207ff3cd64a",
|
||||
"hypothesis": "model-action-c1-pi-v7",
|
||||
"kp": 0.5,
|
||||
"ki": 0.25,
|
||||
"source_sha256": {
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route114/route.npz": "83524f07d61104b84b004ddc46bb751ca7f61da67798aa47db56d307765f5b3e",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route114/model_paths.npz": "14d14362d1a3e46398edd8e22b7cc4e36277a7596a73f546192d0e14c6642b07",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route114/metadata.json": "ec677b9275c1707e477ebd0ffa235d49b5ec63a1ee717b16040c3acdbcd3bdc0",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_minimal_tuning/sweep/114/report.json": "eb630bb5f35a43c8902e15a97b53d83db3dc3cfa843a14d09cb8a1e6dab89289",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_minimal_tuning/sweep/114/commands.npz": "899033d0c9eeffe7929aa1289bfbc0e7da5608833b03989ce10a87d975344d3d",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/minimal_pi_validate.py": "35b5334f52eb6069b79fc3ed3be71ceaa81a3db8e1f76e8b1ab6e363cd4f18ae",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/openpilot/selfdrive/controls/lib/ford_model_action.py": "a899b8595e903d6fc4401392a8d123ef77725f6d6d7fa10392a7b3fe72e732b8"
|
||||
}
|
||||
},
|
||||
{
|
||||
"scope": "Verify production commands against the archived P=.50/I=.25 offline candidate.\n\nFixed recorded motion verifies integration parity, not physical tracking.\n",
|
||||
"route": "115",
|
||||
"cycles": 40037,
|
||||
"can_round_trips": 40037,
|
||||
"production_matches_archived_trial_exactly": true,
|
||||
"opendbc_revision": "64aa61b9b3fd26e70a7caa915acab207ff3cd64a",
|
||||
"hypothesis": "model-action-c1-pi-v7",
|
||||
"kp": 0.5,
|
||||
"ki": 0.25,
|
||||
"source_sha256": {
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route115/route.npz": "e0df32d9e80f1c6b7d56327b37cf07af60070ffc212b8d111e343306148bff23",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route115/model_paths.npz": "52f3ed9e188e4947618a57a3ed872fd1966a887fe1014999df741553b6c13bc0",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route115/metadata.json": "d3c73035bad8eb5059e07962fd274c19cb70c8952b6f1514835d312d08b87a16",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_minimal_tuning/sweep/115/report.json": "58cee499da3191808aedbd79a9497d9c1553f49007f24543c44bc2b58ff971fa",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_minimal_tuning/sweep/115/commands.npz": "09c2da33dabbc0072c61578224cd5ab27ed44690394315350c1c33708b0a67bc",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/minimal_pi_validate.py": "35b5334f52eb6069b79fc3ed3be71ceaa81a3db8e1f76e8b1ab6e363cd4f18ae",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/openpilot/selfdrive/controls/lib/ford_model_action.py": "a899b8595e903d6fc4401392a8d123ef77725f6d6d7fa10392a7b3fe72e732b8"
|
||||
}
|
||||
},
|
||||
{
|
||||
"scope": "Verify production commands against the archived P=.50/I=.25 offline candidate.\n\nFixed recorded motion verifies integration parity, not physical tracking.\n",
|
||||
"route": "116",
|
||||
"cycles": 68793,
|
||||
"can_round_trips": 68793,
|
||||
"production_matches_archived_trial_exactly": true,
|
||||
"opendbc_revision": "64aa61b9b3fd26e70a7caa915acab207ff3cd64a",
|
||||
"hypothesis": "model-action-c1-pi-v7",
|
||||
"kp": 0.5,
|
||||
"ki": 0.25,
|
||||
"source_sha256": {
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route116/route.npz": "13df1d49edd0c92bd4a4236c766ee76baa6dad477824b9de4b79927c98410394",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route116/model_paths.npz": "85d1112b5e89340e8a955b7fe4040b3135cf1c91a54ff756826c6119cc9cc01d",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route116/metadata.json": "d52fe63d013fa546524ad9522a26943d9372730d397d92d75dfd9d75c63ce766",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_minimal_tuning/sweep/116/report.json": "01d2e608a53f9fd805baee853d18acc63b5506319e0f0ceeeb3e59a92bae0385",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_minimal_tuning/sweep/116/commands.npz": "2186a1a1fea2ea98390fb90c9df1288d321cd165f70e3aef0c268cbbd2fd8c95",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/minimal_pi_validate.py": "35b5334f52eb6069b79fc3ed3be71ceaa81a3db8e1f76e8b1ab6e363cd4f18ae",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/openpilot/selfdrive/controls/lib/ford_model_action.py": "a899b8595e903d6fc4401392a8d123ef77725f6d6d7fa10392a7b3fe72e732b8"
|
||||
}
|
||||
},
|
||||
{
|
||||
"scope": "Verify production commands against the archived P=.50/I=.25 offline candidate.\n\nFixed recorded motion verifies integration parity, not physical tracking.\n",
|
||||
"route": "117",
|
||||
"cycles": 27301,
|
||||
"can_round_trips": 27301,
|
||||
"production_matches_archived_trial_exactly": true,
|
||||
"opendbc_revision": "64aa61b9b3fd26e70a7caa915acab207ff3cd64a",
|
||||
"hypothesis": "model-action-c1-pi-v7",
|
||||
"kp": 0.5,
|
||||
"ki": 0.25,
|
||||
"source_sha256": {
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route117/route.npz": "65e4ea6c76dce73018b636479b3777116f1e58bb9813e6c1a7b79018b219140e",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route117/model_paths.npz": "079f6003968287530e1ed6f2243c727e80c2ef3d16fb8a7d945d7eca744d54e7",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route117/metadata.json": "348396b3059c5f54d7938cd3acf2bb268a813b47ff47561da7a94cb75c3f3114",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_minimal_tuning/sweep/117/report.json": "80efcf48f018d3a887e8b9fdb36f67d9c5d9a57b09f117c8fdecd4d5556ff80b",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_minimal_tuning/sweep/117/commands.npz": "c2056b5d9faf765cb4713f73e9c4937bb24c9eeb1d4dac0d21e141322d52609e",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/minimal_pi_validate.py": "35b5334f52eb6069b79fc3ed3be71ceaa81a3db8e1f76e8b1ab6e363cd4f18ae",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/openpilot/selfdrive/controls/lib/ford_model_action.py": "a899b8595e903d6fc4401392a8d123ef77725f6d6d7fa10392a7b3fe72e732b8"
|
||||
}
|
||||
},
|
||||
{
|
||||
"scope": "Verify production commands against the archived P=.50/I=.25 offline candidate.\n\nFixed recorded motion verifies integration parity, not physical tracking.\n",
|
||||
"route": "a0",
|
||||
"cycles": 114582,
|
||||
"can_round_trips": 114582,
|
||||
"production_matches_archived_trial_exactly": true,
|
||||
"opendbc_revision": "64aa61b9b3fd26e70a7caa915acab207ff3cd64a",
|
||||
"hypothesis": "model-action-c1-pi-v7",
|
||||
"kp": 0.5,
|
||||
"ki": 0.25,
|
||||
"source_sha256": {
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_routea0/route.npz": "01ce8b0361c3154d07d5449525a650baa2cc261675095cb1f04ec9e37a9174d3",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_routea0/model_paths.npz": "5d0c69d10a6d13882b47d950ae64c4d98f7ca2b59eab27e4e9557eef3bc4917b",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_routea0/metadata.json": "00496a11afbee475e9b5d66838a4d1663a3c0063cd562b4d1408cce6d71e586e",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_minimal_tuning/sweep/a0/report.json": "5f3ca84b700e019cc72201d9e4ff85c42fbd15e9a3850434caf1bac688b332d8",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_minimal_tuning/sweep/a0/commands.npz": "5c52d587ba45446117677686aabc6af81d5d2eacab5c04b02b53fe6fb95e36d0",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/minimal_pi_validate.py": "35b5334f52eb6069b79fc3ed3be71ceaa81a3db8e1f76e8b1ab6e363cd4f18ae",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/openpilot/selfdrive/controls/lib/ford_model_action.py": "a899b8595e903d6fc4401392a8d123ef77725f6d6d7fa10392a7b3fe72e732b8"
|
||||
}
|
||||
},
|
||||
{
|
||||
"scope": "Verify production commands against the archived P=.50/I=.25 offline candidate.\n\nFixed recorded motion verifies integration parity, not physical tracking.\n",
|
||||
"route": "a2",
|
||||
"cycles": 71111,
|
||||
"can_round_trips": 71111,
|
||||
"production_matches_archived_trial_exactly": true,
|
||||
"opendbc_revision": "64aa61b9b3fd26e70a7caa915acab207ff3cd64a",
|
||||
"hypothesis": "model-action-c1-pi-v7",
|
||||
"kp": 0.5,
|
||||
"ki": 0.25,
|
||||
"source_sha256": {
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_routea2/route.npz": "af4128d278be6be0042f72a02255ddbec96a8a5aa435b9852c8202e4a5cb39e5",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_routea2/model_paths.npz": "5a5a7c4fc30c7b3bc7f2413daa8b11fe86b69dd3249e8df2d35b0a0636a49717",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_routea2/metadata.json": "cfecd240216e409a1e2591c6c2e6f0bad002ecd2a50c8f6dbf22a8f9174708da",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_minimal_tuning/sweep/a2/report.json": "de61021f0c13af16e98688b17aadae9d61c99d20be82329b7cc235836defa41a",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_minimal_tuning/sweep/a2/commands.npz": "fd263ab667b97905d840751577179ad75472421104cf20663e3ee51bf32dd2ee",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/minimal_pi_validate.py": "35b5334f52eb6069b79fc3ed3be71ceaa81a3db8e1f76e8b1ab6e363cd4f18ae",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/openpilot/selfdrive/controls/lib/ford_model_action.py": "a899b8595e903d6fc4401392a8d123ef77725f6d6d7fa10392a7b3fe72e732b8"
|
||||
}
|
||||
},
|
||||
{
|
||||
"scope": "Verify production commands against the archived P=.50/I=.25 offline candidate.\n\nFixed recorded motion verifies integration parity, not physical tracking.\n",
|
||||
"route": "a5",
|
||||
"cycles": 38961,
|
||||
"can_round_trips": 38961,
|
||||
"production_matches_archived_trial_exactly": true,
|
||||
"opendbc_revision": "64aa61b9b3fd26e70a7caa915acab207ff3cd64a",
|
||||
"hypothesis": "model-action-c1-pi-v7",
|
||||
"kp": 0.5,
|
||||
"ki": 0.25,
|
||||
"source_sha256": {
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_routea5/route.npz": "815d1e248ff5c3e5e5cfc11dfbd0690d0d13ddafe1436890e239975a72dae9b8",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_routea5/model_paths.npz": "72e014b844322df6318f6af588c27175ce68d9a37bda62b70eafca5bdfe6dfe4",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_routea5/metadata.json": "c533b06d1b7c598330b36b117bdb3624221bc4193ded8354246d5c86f36b2628",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_minimal_tuning/sweep/a5/report.json": "dc6c05043c5caef6bf12b12eff69ef53d213fe8606f59865859d2c475e0aa3ee",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_minimal_tuning/sweep/a5/commands.npz": "45f551b32a354a65bff585b81a89091abc5edad406b6ca100a8dcabc50c5dbe3",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/minimal_pi_validate.py": "35b5334f52eb6069b79fc3ed3be71ceaa81a3db8e1f76e8b1ab6e363cd4f18ae",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/openpilot/selfdrive/controls/lib/ford_model_action.py": "a899b8595e903d6fc4401392a8d123ef77725f6d6d7fa10392a7b3fe72e732b8"
|
||||
}
|
||||
},
|
||||
{
|
||||
"scope": "Verify production commands against the archived P=.50/I=.25 offline candidate.\n\nFixed recorded motion verifies integration parity, not physical tracking.\n",
|
||||
"route": "a9",
|
||||
"cycles": 286319,
|
||||
"can_round_trips": 286319,
|
||||
"production_matches_archived_trial_exactly": true,
|
||||
"opendbc_revision": "64aa61b9b3fd26e70a7caa915acab207ff3cd64a",
|
||||
"hypothesis": "model-action-c1-pi-v7",
|
||||
"kp": 0.5,
|
||||
"ki": 0.25,
|
||||
"source_sha256": {
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_routea9/route.npz": "3fa8cabfb729dd42d689de38618d1d21c8965c9d60f914546f4d7cc58db8c975",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_routea9/model_paths.npz": "7f3646ad24aa52de5982c65600813de5bbc5f7664d92a2516bbba35da6e7b240",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_routea9/metadata.json": "403d35504bf596144845ac2060ce067d571f49a2955d16223af28a79a7a66e93",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_minimal_tuning/sweep/a9/report.json": "17c41415b9fb9fcd8d72835347a205113c32d32d955c0de68850a9764968b3e9",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_minimal_tuning/sweep/a9/commands.npz": "27af5f6fb8f6e08f31d54e24c90430d30c72d31b1448bc1fbcb31f35804afe8a",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/minimal_pi_validate.py": "35b5334f52eb6069b79fc3ed3be71ceaa81a3db8e1f76e8b1ab6e363cd4f18ae",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/openpilot/selfdrive/controls/lib/ford_model_action.py": "a899b8595e903d6fc4401392a8d123ef77725f6d6d7fa10392a7b3fe72e732b8"
|
||||
}
|
||||
},
|
||||
{
|
||||
"scope": "Verify production commands against the archived P=.50/I=.25 offline candidate.\n\nFixed recorded motion verifies integration parity, not physical tracking.\n",
|
||||
"route": "b8",
|
||||
"cycles": 160431,
|
||||
"can_round_trips": 160431,
|
||||
"production_matches_archived_trial_exactly": true,
|
||||
"opendbc_revision": "64aa61b9b3fd26e70a7caa915acab207ff3cd64a",
|
||||
"hypothesis": "model-action-c1-pi-v7",
|
||||
"kp": 0.5,
|
||||
"ki": 0.25,
|
||||
"source_sha256": {
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_routeb8/route.npz": "6f5dd369b70eaed4b95b28c8b25c9f2e9b830fa07a334881a185505481667c8b",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_routeb8/model_paths.npz": "939af6cf7e74251d8842581cc078d26d9fbfd22a0d7817cb0e368697d419b615",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_routeb8/metadata.json": "73b439132d1de37ec187b544c04d2b05c80965065515a4b7dec29ba57ae37e7c",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_minimal_tuning/sweep/b8/report.json": "5c6b52222abbb95147dc756a34e62ada97c42aa550b952fd6ae7b26d8d9639fa",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_minimal_tuning/sweep/b8/commands.npz": "9c158eff98009bcb039a4e7c29b97b0d3a9c67d61160a4e4766a176174de52d7",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/minimal_pi_validate.py": "35b5334f52eb6069b79fc3ed3be71ceaa81a3db8e1f76e8b1ab6e363cd4f18ae",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/openpilot/selfdrive/controls/lib/ford_model_action.py": "a899b8595e903d6fc4401392a8d123ef77725f6d6d7fa10392a7b3fe72e732b8"
|
||||
}
|
||||
},
|
||||
{
|
||||
"scope": "Verify production commands against the archived P=.50/I=.25 offline candidate.\n\nFixed recorded motion verifies integration parity, not physical tracking.\n",
|
||||
"route": "b9",
|
||||
"cycles": 90774,
|
||||
"can_round_trips": 90774,
|
||||
"production_matches_archived_trial_exactly": true,
|
||||
"opendbc_revision": "64aa61b9b3fd26e70a7caa915acab207ff3cd64a",
|
||||
"hypothesis": "model-action-c1-pi-v7",
|
||||
"kp": 0.5,
|
||||
"ki": 0.25,
|
||||
"source_sha256": {
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_routeb9/route.npz": "b07c789d8155335f5d120d0262fced6e4d5803fe767b0ff49b6413dce4140b5c",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_routeb9/model_paths.npz": "6b1f87897c050273fdc05af051307a049b6fc3a93072e7cda1721195ce7c3861",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_routeb9/metadata.json": "9ce452220cab61b81883f32fc2fcaf5db6c78a674cb255a49cc77d5029580fee",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_minimal_tuning/sweep/b9/report.json": "9eeda0c24ede7a55ef8a924e0ebe0bcb0dea7a9612457af0c5aff898a828c195",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_minimal_tuning/sweep/b9/commands.npz": "2dcc0865591c988d8a08671311a9631f3c4aa3ded4e90ee6f76d28b1654809ad",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/minimal_pi_validate.py": "35b5334f52eb6069b79fc3ed3be71ceaa81a3db8e1f76e8b1ab6e363cd4f18ae",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/openpilot/selfdrive/controls/lib/ford_model_action.py": "a899b8595e903d6fc4401392a8d123ef77725f6d6d7fa10392a7b3fe72e732b8"
|
||||
}
|
||||
},
|
||||
{
|
||||
"scope": "Verify production commands against the archived P=.50/I=.25 offline candidate.\n\nFixed recorded motion verifies integration parity, not physical tracking.\n",
|
||||
"route": "ca",
|
||||
"cycles": 327448,
|
||||
"can_round_trips": 327448,
|
||||
"production_matches_archived_trial_exactly": true,
|
||||
"opendbc_revision": "64aa61b9b3fd26e70a7caa915acab207ff3cd64a",
|
||||
"hypothesis": "model-action-c1-pi-v7",
|
||||
"kp": 0.5,
|
||||
"ki": 0.25,
|
||||
"source_sha256": {
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_routeca/route.npz": "ae9d46770eaf0dbbac6af86aebc926320eed0cf114eb43d5f78b0676e8e0dbf9",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_routeca/model_paths.npz": "bf17deb442383aaa79432566cd382df24a1bbbbd0521d0cafab956618f5bdd96",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_routeca/metadata.json": "a759d5cdf878df8b05d91db637b1935b6b4bdd87af96f0f256b67e7d809b3525",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_minimal_tuning/sweep/ca/report.json": "025f2f49f68ec2164a10474a4c6ec0eb1b6022990e135fc38134a052a8710dff",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_minimal_tuning/sweep/ca/commands.npz": "cbe9f63955a409495f57466dc5811d3eed65af2cf096ee060d924437488c3f76",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/minimal_pi_validate.py": "35b5334f52eb6069b79fc3ed3be71ceaa81a3db8e1f76e8b1ab6e363cd4f18ae",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/openpilot/selfdrive/controls/lib/ford_model_action.py": "a899b8595e903d6fc4401392a8d123ef77725f6d6d7fa10392a7b3fe72e732b8"
|
||||
}
|
||||
},
|
||||
{
|
||||
"scope": "Verify production commands against the archived P=.50/I=.25 offline candidate.\n\nFixed recorded motion verifies integration parity, not physical tracking.\n",
|
||||
"route": "raptor02",
|
||||
"cycles": 132881,
|
||||
"can_round_trips": 132881,
|
||||
"production_matches_archived_trial_exactly": true,
|
||||
"opendbc_revision": "64aa61b9b3fd26e70a7caa915acab207ff3cd64a",
|
||||
"hypothesis": "model-action-c1-pi-v7",
|
||||
"kp": 0.5,
|
||||
"ki": 0.25,
|
||||
"source_sha256": {
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_raptor_route02/route.npz": "8a4cfe53994988d053b47d9caadf21ebd064fd20e598fa05ddc2da245bd0e980",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_raptor_route02/model_paths.npz": "d5dc6f72d91472b8f2fb1add1680cdaea1e122774ddd6d8519fd7673daf71bb5",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_raptor_route02/metadata.json": "9602e08efc2bd784133837c4156c075bb89bad3bedd6b309f15e884ed20e338a",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_minimal_tuning/sweep/raptor02/report.json": "2c2dda2a5e4e8a764a10852cca3e5caf319b62e3d1d841ef9e1b8c11be77ebf6",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_minimal_tuning/sweep/raptor02/commands.npz": "e0440d0a4035b975a9c4a47d85335b5b0f0903fcd00dd7e7c858856deffd1249",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/minimal_pi_validate.py": "35b5334f52eb6069b79fc3ed3be71ceaa81a3db8e1f76e8b1ab6e363cd4f18ae",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/openpilot/selfdrive/controls/lib/ford_model_action.py": "a899b8595e903d6fc4401392a8d123ef77725f6d6d7fa10392a7b3fe72e732b8"
|
||||
}
|
||||
}
|
||||
],
|
||||
"production_stress": {
|
||||
"cycles": 20000,
|
||||
"kp": 0.5,
|
||||
"ki": 0.25,
|
||||
"controller_updates": 60000,
|
||||
"can_round_trips": 60000,
|
||||
"seed": 20260913,
|
||||
"independent_c0_comparisons": 20000,
|
||||
"checks": "Independent scalar PI/unwind-first/anti-windup arithmetic, mirror symmetry, exact C0 independence, limits, slew, resets, CAN.",
|
||||
"source_sha256": {
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/minimal_pi_production_stress.py": "fa9058826512498be1938797efba25666df832080fb7ad6ff18788ee4bda333d",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/openpilot/selfdrive/controls/lib/ford_model_action.py": "a899b8595e903d6fc4401392a8d123ef77725f6d6d7fa10392a7b3fe72e732b8"
|
||||
}
|
||||
},
|
||||
"limits": [
|
||||
"No hardware build, device boot or new physical drive performed.",
|
||||
"Matched offline commands do not establish improved tracking or closed-loop stability.",
|
||||
"Higher P can amplify measurement fluctuations; lower I can take longer to correct persistent error.",
|
||||
"Toggle defaults off and off selects upstream Ford control; on applies to any Ford CAN FD."
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
# Ford base-heading overflow experiment
|
||||
|
||||
This records the overflow implementation and validation at `b81c00f5b`.
|
||||
The later [toggle-off restoration](ford_upstream_fallback.md) updates selection
|
||||
and the opendbc sender while preserving the enabled experiment's command law.
|
||||
|
||||
The Lightning ca route recorded controller `959ae3d6e`. Its large turns included
|
||||
flat C1 requests at ±0.5 rad while C0 still had available range. Those were
|
||||
nonzero, active commands, but increasing base heading above the C1 limit was
|
||||
discarded. Other apparent pauses followed reductions in the selected model
|
||||
request; this change continues to follow those reductions.
|
||||
|
||||
The new experiment allocates clipped-away **base heading** to C0 using the
|
||||
existing 7 m reference. It does not allocate the accumulated feedback correction.
|
||||
This is a hypothesis about command allocation, not a measured improvement in
|
||||
PSCM response or a claim that C0 and C1 are physically interchangeable.
|
||||
|
||||
## Command rule
|
||||
|
||||
Using the selected, upstream-limited desired curvature:
|
||||
|
||||
```text
|
||||
raw_base_c1 = max(7 m, speed × 1 s) × desired_curvature
|
||||
base_c1 = clip(raw_base_c1, -0.5 rad, +0.5 rad)
|
||||
extra_c0 = 7 m × (raw_base_c1 - base_c1)
|
||||
c0_target = clip(model_y_at_7m + extra_c0, -5.11 m, +5.11 m)
|
||||
```
|
||||
|
||||
The combined C0 target still passes through the existing 4 m/s slew limit.
|
||||
C1 retains its existing feedback, ±0.5 rad amplitude and 0.5 rad/s slew limits.
|
||||
C2 and C3 remain zero. Short model paths retain their existing endpoint hold.
|
||||
|
||||
Before amplitude/slew limits, the allocation preserves the linear reference
|
||||
`C0 + 7*C1` for the base request. This is a single-reference identity; it does
|
||||
not preserve the entire path or predict steering torque. The 7 m reference is
|
||||
an existing engineering choice. No fitted plant, new tunable strength multiplier,
|
||||
timer or stored overflow is added. The existing 1:1 feedback strength remains.
|
||||
|
||||
Extra C0 falls with raw base heading and its target becomes zero at the C1 cap.
|
||||
The output can take longer to return because of its existing slew state. There
|
||||
is no guarantee that increasing C0 makes every PSCM turn better or release sooner.
|
||||
|
||||
## Preserved integration
|
||||
|
||||
The conditional correction release still requires **original model C0** and
|
||||
applied C0 to confirm base C1's direction. Added overflow cannot itself substitute
|
||||
for model confirmation. Changed applied C0 can nevertheless affect release
|
||||
timing in some histories. Driver/PSCM arbitration, service freshness, resets,
|
||||
upstream curvature limiting, Float32 publication and the 100 Hz sender remain.
|
||||
No opendbc dependency or Panda safety change is made.
|
||||
|
||||
The existing default-off Sunnylink toggle selects this version on any Ford
|
||||
CAN FD vehicle. Diagnostic identity is `model-action-c1-feedback-v3`.
|
||||
`offset_overflow` records extra target meters before C0 amplitude and slew;
|
||||
`offset_request` continues to record the actual continuous C0 state.
|
||||
See the [drive-test instructions](ford_model_action_drive_test.md).
|
||||
|
||||
## Offline evidence
|
||||
|
||||
The focused overflow regressions initially produced 28 failures and 18 passes
|
||||
against the prior controller. They now pass. They cover both signs, several
|
||||
speeds, the heading threshold, combined C0 clipping, short paths, release,
|
||||
feedback-only saturation, driver/PSCM feedback gates and independent model
|
||||
confirmation. Twelve integration cases send 4,800 frames through actual
|
||||
controlsd selection/limiting, Float32 publication and Ford CAN packing on all
|
||||
six listed Ford CAN FD platforms, checking counters and checksums.
|
||||
|
||||
The combined suite passes **670 tests and 9,146 subtests**, with 178 inherited
|
||||
or unsupported safety-test skips. Both 200,000-cycle randomized runs pass,
|
||||
including mirrored inputs, independent scalar target/slew checks, feedback
|
||||
invariants, comparison with the exact prior controller from cloned states,
|
||||
and 18,138 exhaustive field/Float32 boundary cases. These checks and the ca
|
||||
replay total **745,586 Float32/CAN round trips**, in addition to integration tests.
|
||||
|
||||
Frozen ca replay covers 327,448 control cycles across all 55 extracted segments.
|
||||
Activation is identical; C1, C2 and C3 are identical on every cycle. C0 differs
|
||||
for 855 cycles (8.607 s), concentrated in the large turns and their slew tails.
|
||||
The extra target is present for 7.359 s. Before the first overflow, every command
|
||||
matches the prior controller. All disabled cycles have zero commands.
|
||||
|
||||
At the same recorded peak-request timestamps, absolute packed C0 changes as follows:
|
||||
|
||||
| Segment | Previous C0 | Candidate C0 | C1 magnitude, both |
|
||||
| --- | ---: | ---: | ---: |
|
||||
| 10 | 3.90 m | 5.11 m | 0.50 rad |
|
||||
| 31 | 2.85 m | 3.08 m | 0.50 rad |
|
||||
| 35 | 3.67 m | 5.11 m | 0.50 rad |
|
||||
| 52 | 3.11 m | 3.42 m | 0.50 rad |
|
||||
|
||||
The candidate reaches the existing C0 cap for 2.054 s. These are reconstructed
|
||||
commands on original inputs, not newly transmitted commands or predicted wheel
|
||||
angles. Segment 52's output is still slewing at the selected timestamp.
|
||||
|
||||
Every overflow episode returns to the previous C0 output without a reset or
|
||||
another overflow interrupting the comparison. After overflow first becomes
|
||||
zero, the longest output tails are **0.475 s in segment 10** and **0.712 s in
|
||||
segment 35**. This is the added slew tail relative to the prior command, not
|
||||
the truck's physical release delay. It is a material behavior to inspect during
|
||||
controlled evaluation: more pull through capped turns may also add hanging
|
||||
on exit. Ordinary requests below the cap retain the original target mapping.
|
||||
|
||||
The recorded model, vehicle motion, driver input and PSCM flags remain fixed.
|
||||
Replay cannot establish resulting tracking, centering, torque or stability.
|
||||
The route has no maneuver-plan messages; the replay uses the consumed model
|
||||
reference and recorded selected curvature. Computation time is approximated
|
||||
by control publication time, and full SubMaster health checks are unavailable.
|
||||
No device build, boot or installation is performed offline.
|
||||
|
||||
## Reproduction
|
||||
|
||||
Numeric results and source hashes are in `ford_c1_overflow_validation.json`.
|
||||
Use native project dependencies and pinned opendbc
|
||||
`c21a9013700734dd20b09e05aa68329ad8cc20f9`. The ca replay requires the existing
|
||||
full-rlog extract (`route.npz`, `model_paths.npz`, `metadata.json`).
|
||||
The following commands apply to this version; earlier validation documents
|
||||
record their named historical controllers.
|
||||
|
||||
```sh
|
||||
export PYTHONDONTWRITEBYTECODE=1
|
||||
export PYTHONPATH=.:opendbc_repo
|
||||
python -m pytest -q -p no:cacheprovider openpilot/selfdrive/controls/tests/test_ford_*.py tools/ford_pscm_lab openpilot/selfdrive/car/tests/test_ford_pscm_status.py openpilot/sunnypilot/sunnylink/tests openpilot/common/tests/test_params.py opendbc_repo/opendbc/car/ford/tests/test_ford.py opendbc_repo/opendbc/safety/tests/test_ford.py
|
||||
python -m tools.ford_pscm_lab.feedback_replay stress --cycles 200000 --output .cache/ford_c1_overflow/stress.json
|
||||
python -m tools.ford_pscm_lab.stress_model_action --cycles 200000 --seed 20260910 --opendbc-revision c21a9013700734dd20b09e05aa68329ad8cc20f9 --output .cache/ford_c1_overflow/zero_error_stress.json
|
||||
python -m tools.ford_pscm_lab.feedback_replay route .cache/ford_routeca --baseline 959ae3d6e76c479f48e081c060b0f3569a6f15f4 --output .cache/ford_c1_overflow/routeca
|
||||
python openpilot/sunnypilot/sunnylink/tools/compile_settings_ui.py --check
|
||||
```
|
||||
|
||||
For slew-tail analysis, in the replay's `commands.npz` find each nonzero run of
|
||||
`offset_overflow`. From its first zero sample, measure until packed candidate
|
||||
and baseline C0 agree within 1e-8 m, stopping separately at another overflow or
|
||||
inactive cycle. Sum sample durations capped at 30 ms for weighted time totals.
|
||||
@@ -0,0 +1,307 @@
|
||||
{
|
||||
"created_at_utc": "2026-09-10T22:04:22.589047+00:00",
|
||||
"scope": "Base heading overflow allocated to C0; frozen-input command verification only, no vehicle response prediction.",
|
||||
"baseline_commit": "959ae3d6e76c479f48e081c060b0f3569a6f15f4",
|
||||
"baseline_source_sha256": "47fff1fd1bd7e65ca6d6b437fa9d2e8a864d421622d9efdfe0fe04b927c6d972",
|
||||
"deployment_target": {
|
||||
"repository": "sunnypilot/sunnypilot",
|
||||
"branch": "hiimisaac-dev"
|
||||
},
|
||||
"hypothesis": "model-action-c1-feedback-v3",
|
||||
"calibration_approved": false,
|
||||
"toggle": {
|
||||
"key": "FordModelActionController",
|
||||
"default_enabled": false,
|
||||
"eligibility": "Any Ford CAN FD",
|
||||
"activation": "Existing controlsd startup selection"
|
||||
},
|
||||
"command_rule": "extra C0 = 7 m * (raw base C1 - clip(raw base C1, -0.5, 0.5)); add to original model C0, then existing C0 amplitude/slew limits.",
|
||||
"engineering_choices": "Single-reference linear allocation at existing 7 m. No new tuning parameter or stored overflow. Does not establish physical C0/C1 interchangeability. Existing 1:1 integral feedback strength remains.",
|
||||
"output_limits": {
|
||||
"c0_m": [
|
||||
-5.11,
|
||||
5.11
|
||||
],
|
||||
"c1_rad": [
|
||||
-0.5,
|
||||
0.5
|
||||
],
|
||||
"c0_slew_m_s": 4.0,
|
||||
"c1_slew_rad_s": 0.5,
|
||||
"c2": 0.0,
|
||||
"c3": 0.0
|
||||
},
|
||||
"preserved": [
|
||||
"Upstream reference selection/limiting",
|
||||
"Driver and PSCM arbitration",
|
||||
"Freshness/reset gates",
|
||||
"C1 feedback law",
|
||||
"Original model C0 required for carryover confirmation",
|
||||
"100 Hz CAN sender",
|
||||
"Float32 publication"
|
||||
],
|
||||
"controller_command_state_values": 3,
|
||||
"controller_diagnostic_counters": 1,
|
||||
"controller_total_lines": 200,
|
||||
"opendbc_submodule_changed": false,
|
||||
"panda_safety_changed": false,
|
||||
"opendbc_head": "c21a9013700734dd20b09e05aa68329ad8cc20f9",
|
||||
"tests": {
|
||||
"combined_suite": "670 passed, 178 skipped, 9146 subtests passed in 5.77s",
|
||||
"safety_skips": "Inherited or unsupported variants; unchanged from prior feedback validations.",
|
||||
"new_core_regressions": "Before implementation: 28 failed, 18 passed; after: all 46 pass.",
|
||||
"overflow_controlsd_to_can_cases": 12,
|
||||
"overflow_controlsd_to_can_frames": 4800,
|
||||
"integration_scope": "All six listed CAN FD platforms; both signs; selected upstream-limited request, release, limits, Float32 publication, decoded commands, zero C2/C3, active mode, counters, checksums.",
|
||||
"ruff_changed_python": "pass",
|
||||
"ty_controller": "pass",
|
||||
"git_diff_check": "pass",
|
||||
"settings_compiler_check": "pass"
|
||||
},
|
||||
"stress": {
|
||||
"cycles": 200000,
|
||||
"mirrored_updates": 200000,
|
||||
"can_round_trips": 200000,
|
||||
"carryover_release_count": 864,
|
||||
"baseline_revision": "959ae3d6e76c479f48e081c060b0f3569a6f15f4",
|
||||
"baseline_source_sha256": "47fff1fd1bd7e65ca6d6b437fa9d2e8a864d421622d9efdfe0fe04b927c6d972",
|
||||
"exact_unchanged_state_and_commands_without_overflow": 56061,
|
||||
"checks": "Mirror symmetry, reset/override, amplitude, slew, correction bounds, carryover direction/confirmation, integration, PSCM limits, CAN.",
|
||||
"scope": "Numerical software invariants only; no model of vehicle motion.",
|
||||
"calibration_approved": false
|
||||
},
|
||||
"zero_error_stress": {
|
||||
"seed": 20260910,
|
||||
"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,
|
||||
"direct_raw_float32_packing_matches_host_output": true,
|
||||
"max_continuous_step_c0_c1": [
|
||||
0.4000000000000019,
|
||||
0.05000000000000002
|
||||
],
|
||||
"calibration_approved": false,
|
||||
"scope": "Zero-error numerical construction: measured equals requested curvature. No PSCM response claims.",
|
||||
"opendbc_import_head": "c21a9013700734dd20b09e05aa68329ad8cc20f9"
|
||||
},
|
||||
"route_ca": {
|
||||
"baseline_revision": "959ae3d6e76c479f48e081c060b0f3569a6f15f4",
|
||||
"baseline_source_sha256": "47fff1fd1bd7e65ca6d6b437fa9d2e8a864d421622d9efdfe0fe04b927c6d972",
|
||||
"calibration_approved": false,
|
||||
"cycles": 327448,
|
||||
"active_cycles": 118756,
|
||||
"validity_matches_baseline_exactly": true,
|
||||
"status_counts": {
|
||||
"inactive": 208692,
|
||||
"active": 118756
|
||||
},
|
||||
"c0_matches_baseline_exactly": false,
|
||||
"c0_changed_cycles": 855,
|
||||
"max_abs_c0_change_m": 2.2,
|
||||
"offset_overflow_seconds": 7.359375754000212,
|
||||
"max_abs_offset_overflow_target_m": 2.867466852068901,
|
||||
"feedback_enabled_seconds": 1128.448330694985,
|
||||
"pscm_limit_2_seconds": 1.5229074359986043,
|
||||
"c1_changed_cycles": 0,
|
||||
"max_abs_c1_change_rad": 0.0,
|
||||
"max_abs_correction_rad": 0.13238253764709199,
|
||||
"can_round_trips": 327448,
|
||||
"timing_limit": "Controls publication time proxies the computation clock; full SubMaster checks are unavailable.",
|
||||
"reference_limit": "Uses exact consumed model publication as reference; selected maneuver-plan messages are not reconstructed.",
|
||||
"carryover_release_count": 32
|
||||
},
|
||||
"route_ca_release": {
|
||||
"scope": "Describe candidate command tails on frozen ca measurements, not wheel response.",
|
||||
"changed_c0_seconds": 8.606610047996583,
|
||||
"overflow_target_seconds": 7.359375754000212,
|
||||
"changed_c0_without_current_overflow_seconds": 1.4386431469993113,
|
||||
"candidate_c0_at_cap_seconds": 2.054423716999736,
|
||||
"baseline_c0_at_cap_seconds": 0.0,
|
||||
"all_c1_c2_c3_match_baseline_exactly": true,
|
||||
"all_pre_overflow_commands_match_baseline_exactly": true,
|
||||
"all_inactive_commands_zero": true,
|
||||
"windows": [
|
||||
{
|
||||
"start_s": 645.4515506280004,
|
||||
"last_overflow_s": 647.9591778859995,
|
||||
"duration_s": 2.5175176129996544,
|
||||
"extra_target_peak_m": 2.867466852068901,
|
||||
"c0_change_peak_m": 2.2,
|
||||
"post_overflow_tail_s": 0.4753179760009516,
|
||||
"tail_end_s": 648.444386217001,
|
||||
"tail_ended_by": "matches baseline"
|
||||
},
|
||||
{
|
||||
"start_s": 1896.7594062080007,
|
||||
"last_overflow_s": 1897.0382758169999,
|
||||
"duration_s": 0.29005605599923,
|
||||
"extra_target_peak_m": 0.16229432076215744,
|
||||
"c0_change_peak_m": 0.16999999999999993,
|
||||
"post_overflow_tail_s": 0.0,
|
||||
"tail_end_s": 1897.0494622639999,
|
||||
"tail_ended_by": "matches baseline"
|
||||
},
|
||||
{
|
||||
"start_s": 1897.3739526980007,
|
||||
"last_overflow_s": 1897.4413651220002,
|
||||
"duration_s": 0.0802515539999149,
|
||||
"extra_target_peak_m": 0.049945808947086334,
|
||||
"c0_change_peak_m": 0.04999999999999982,
|
||||
"post_overflow_tail_s": 0.0,
|
||||
"tail_end_s": 1897.4542042520006,
|
||||
"tail_ended_by": "matches baseline"
|
||||
},
|
||||
{
|
||||
"start_s": 1897.492552009,
|
||||
"last_overflow_s": 1897.681751143,
|
||||
"duration_s": 0.19981637000091723,
|
||||
"extra_target_peak_m": 0.10685679316520691,
|
||||
"c0_change_peak_m": 0.11000000000000032,
|
||||
"post_overflow_tail_s": 0.01193945599879953,
|
||||
"tail_end_s": 1897.7043078349998,
|
||||
"tail_ended_by": "matches baseline"
|
||||
},
|
||||
{
|
||||
"start_s": 1897.743499225,
|
||||
"last_overflow_s": 1897.7842587169998,
|
||||
"duration_s": 0.05174380599964934,
|
||||
"extra_target_peak_m": 0.04502199590206146,
|
||||
"c0_change_peak_m": 0.040000000000000036,
|
||||
"post_overflow_tail_s": 0.011911696001334349,
|
||||
"tail_end_s": 1897.807154727001,
|
||||
"tail_ended_by": "matches baseline"
|
||||
},
|
||||
{
|
||||
"start_s": 1897.9437203819998,
|
||||
"last_overflow_s": 1899.2943476260007,
|
||||
"duration_s": 1.361525778000214,
|
||||
"extra_target_peak_m": 0.22848158329725266,
|
||||
"c0_change_peak_m": 0.22999999999999954,
|
||||
"post_overflow_tail_s": 0.021877274999496876,
|
||||
"tail_end_s": 1899.3271234349995,
|
||||
"tail_ended_by": "matches baseline"
|
||||
},
|
||||
{
|
||||
"start_s": 2146.154050938001,
|
||||
"last_overflow_s": 2146.185627021001,
|
||||
"duration_s": 0.04039318899958744,
|
||||
"extra_target_peak_m": 0.045987628400325775,
|
||||
"c0_change_peak_m": 0.050000000000000266,
|
||||
"post_overflow_tail_s": 0.034061254000334884,
|
||||
"tail_end_s": 2146.228505381001,
|
||||
"tail_ended_by": "matches baseline"
|
||||
},
|
||||
{
|
||||
"start_s": 2146.2950925490004,
|
||||
"last_overflow_s": 2146.5387542179997,
|
||||
"duration_s": 0.25293986199903884,
|
||||
"extra_target_peak_m": 0.21167446672916412,
|
||||
"c0_change_peak_m": 0.17999999999999972,
|
||||
"post_overflow_tail_s": 0.040063559001282556,
|
||||
"tail_end_s": 2146.5880959700007,
|
||||
"tail_ended_by": "matches baseline"
|
||||
},
|
||||
{
|
||||
"start_s": 2146.6577708509994,
|
||||
"last_overflow_s": 2148.3543101739997,
|
||||
"duration_s": 1.7074812410010054,
|
||||
"extra_target_peak_m": 2.0555079206824303,
|
||||
"c0_change_peak_m": 1.58,
|
||||
"post_overflow_tail_s": 0.7118495689992415,
|
||||
"tail_end_s": 2149.0771016609997,
|
||||
"tail_ended_by": "matches baseline"
|
||||
},
|
||||
{
|
||||
"start_s": 3121.067818462001,
|
||||
"last_overflow_s": 3121.8665919270006,
|
||||
"duration_s": 0.8071899679998751,
|
||||
"extra_target_peak_m": 0.5380096957087517,
|
||||
"c0_change_peak_m": 0.5099999999999998,
|
||||
"post_overflow_tail_s": 0.06879738699899463,
|
||||
"tail_end_s": 3121.943805817,
|
||||
"tail_ended_by": "matches baseline"
|
||||
},
|
||||
{
|
||||
"start_s": 3122.0664172689994,
|
||||
"last_overflow_s": 3122.105900957,
|
||||
"duration_s": 0.05046031700112508,
|
||||
"extra_target_peak_m": 0.008371405303478241,
|
||||
"c0_change_peak_m": 0.010000000000000231,
|
||||
"post_overflow_tail_s": 0.02107719199921121,
|
||||
"tail_end_s": 3122.1379547779998,
|
||||
"tail_ended_by": "matches baseline"
|
||||
}
|
||||
],
|
||||
"example_points": [
|
||||
{
|
||||
"segment": 10,
|
||||
"time_s": 646.7503591629993,
|
||||
"baseline_c0_m": 3.9000000000000004,
|
||||
"candidate_c0_m": 5.11,
|
||||
"extra_c0_target_m": 2.867466852068901,
|
||||
"baseline_c1_rad": 0.5,
|
||||
"candidate_c1_rad": 0.5
|
||||
},
|
||||
{
|
||||
"segment": 31,
|
||||
"time_s": 1898.0837712450011,
|
||||
"baseline_c0_m": -2.8499999999999996,
|
||||
"candidate_c0_m": -3.079999999999999,
|
||||
"extra_c0_target_m": -0.22848158329725266,
|
||||
"baseline_c1_rad": -0.5,
|
||||
"candidate_c1_rad": -0.5
|
||||
},
|
||||
{
|
||||
"segment": 35,
|
||||
"time_s": 2147.4098699660008,
|
||||
"baseline_c0_m": 3.67,
|
||||
"candidate_c0_m": 5.11,
|
||||
"extra_c0_target_m": 2.0555079206824303,
|
||||
"baseline_c1_rad": 0.5,
|
||||
"candidate_c1_rad": 0.5
|
||||
},
|
||||
{
|
||||
"segment": 52,
|
||||
"time_s": 3121.239466367,
|
||||
"baseline_c0_m": 3.11,
|
||||
"candidate_c0_m": 3.42,
|
||||
"extra_c0_target_m": 0.5380096957087517,
|
||||
"baseline_c1_rad": 0.5,
|
||||
"candidate_c1_rad": 0.5
|
||||
}
|
||||
]
|
||||
},
|
||||
"route_ca_input_sha256": {
|
||||
"route.npz": "ae9d46770eaf0dbbac6af86aebc926320eed0cf114eb43d5f78b0676e8e0dbf9",
|
||||
"model_paths.npz": "bf17deb442383aaa79432566cd382df24a1bbbbd0521d0cafab956618f5bdd96",
|
||||
"metadata.json": "a759d5cdf878df8b05d91db637b1935b6b4bdd87af96f0f256b67e7d809b3525"
|
||||
},
|
||||
"lab_can_round_trips_excluding_integration": 745586,
|
||||
"validation_environment": {
|
||||
"python": "/Users/ibpersonal/dev/sunnypilot/.venv/bin/python",
|
||||
"PYTHONPATH": ".:opendbc_repo:.cache/ford_v6/test_deps",
|
||||
"PYTHONDONTWRITEBYTECODE": "1",
|
||||
"LOG_ROOT": "/private/tmp/ford-overflow-logs",
|
||||
"PARAMS_ROOT": "/private/tmp/ford-overflow-params"
|
||||
},
|
||||
"source_sha256": {
|
||||
"openpilot/selfdrive/controls/lib/ford_model_action.py": "90269d748d55558bf2495d3b4afcfd7429f373108df1f9259e154d1b92184262",
|
||||
"openpilot/selfdrive/controls/tests/test_ford_model_action_overflow.py": "9086434d2b76ab51f69ef08c4f0033c4eaa1950083cc9cce4b34279eb17c5b1b",
|
||||
"openpilot/selfdrive/controls/tests/test_ford_model_action_adapter.py": "dc552f3088b7e437a928f349989b74c4e7772d2a88b14a93e933ea8b8344d23b",
|
||||
"openpilot/selfdrive/controls/tests/test_ford_model_action.py": "4e82101463c5d83f6b1f72ba4918c2b37731bc2db6ec7e29e6b402ea67276db2",
|
||||
"openpilot/selfdrive/controls/tests/test_ford_controlsd_logging.py": "6b73e70b120527275a9e4e3dff07dd7d19648da517402187d57a90ba09b017eb",
|
||||
"tools/ford_pscm_lab/feedback_replay.py": "7c65515d37aac900eaaef8451a7d643b72c566a2366651f843de2ffca5f24b8d",
|
||||
"tools/ford_pscm_lab/stress_model_action.py": "cec2619285dd41274562ac035ee8ea0a389269a0c4ef1b62efa6252ad1a714aa",
|
||||
"openpilot/sunnypilot/sunnylink/settings_ui_src/pages/vehicle.yaml": "3ae6b16a8c267ab181e00f4b59be8b65134480bec26c8c039bcbae88cb745feb",
|
||||
"openpilot/sunnypilot/sunnylink/settings_ui.json": "ba803819751b304e82936a95902afae63ff88e16e64988622715a7b713f3b982"
|
||||
},
|
||||
"limitations": [
|
||||
"Recorded vehicle motion does not react to changed commands.",
|
||||
"Computation time proxies and service-check reconstruction limits apply.",
|
||||
"C0 slew can leave extra command after overflow stops; maximum observed tail 0.712 s.",
|
||||
"No device build, boot, installation, road tracking or physical stability validation."
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
# Ford C1 proportional trial: P=0.75
|
||||
|
||||
Route 149 contains large steering shortfalls before driver intervention while
|
||||
the selected curvature matches the model and neither C1's bound nor the PSCM
|
||||
reached-limit flag explains the shortfall. This trial raises the immediate C1
|
||||
error correction from P=0.50 to P=0.75. I remains 0.25. Runtime changes are the
|
||||
gain constant and the diagnostic version, `model-action-curvature-c0-distance-pi-v13`.
|
||||
|
||||
The intended effect is more correction while behind and more release correction
|
||||
when measured steering exceeds the request. Increasing P does not establish a
|
||||
faster physical response: it can also amplify measurement fluctuations and
|
||||
produce oscillation. This is a trial coefficient, not a learned calibration.
|
||||
|
||||
The selected model action, +0.40 s low-speed preview, C0 distance setting and
|
||||
formula, integral arithmetic, PSCM arbitration, field bounds, and 100 Hz sender
|
||||
remain unchanged. C2/C3 stay zero. The existing default-off Sunnylink toggle
|
||||
still selects the experiment on Ford CAN FD; toggle-off selects upstream Ford.
|
||||
|
||||
## Paired production replay
|
||||
|
||||
Compared explicit P=0.50 and P=0.75 production adapters with I=0.25 and fixed-7 m
|
||||
C0 across 21 route extracts: 112–117, 119, 11a, 120, 124, 125, 146, 149, a0, a2,
|
||||
a5, a9, b8, b9, ca, and Raptor 02. The passes cover 2,275,248 source cycles and
|
||||
4,550,496 real Float32-to-CAN encode/decode round trips.
|
||||
|
||||
Both passes use the same recorded selected curvature, measured motion, model
|
||||
geometry, input timestamps, and driver/PSCM flags. Older routes retain their
|
||||
original model requests; their neural inference is not rerun with the new delay.
|
||||
This compares commands, not predicted wheel motion or tracking accuracy.
|
||||
|
||||
Checks passed on every cycle: identical C0, eligibility, feedforward, overflow,
|
||||
and feedback/driver/PSCM gates; finite and bounded output; inactive zero output;
|
||||
zero C2/C3; exact decoded fields, mode and counter; and the expected 1.5 ratio
|
||||
between proportional terms. Integration tests separately check the selected
|
||||
defaults, downstream checksums, reference selection, reversals, driver override,
|
||||
reached-limit behavior, duplicate measurements, and toggle-off upstream behavior.
|
||||
|
||||
On route 149, the P=0.50 replay agrees with the recorded path commands over the
|
||||
clean scoring cohort to Float32 precision: maximum C0 difference 5.8e-8 m and C1
|
||||
difference 1.5e-8 rad. Full SubMaster health and exact control execution clocks
|
||||
are not in the extract; publication timestamps approximate them. Historical
|
||||
versions used different command laws, so their recorded commands are not
|
||||
expected to match this baseline.
|
||||
|
||||
Clean scoring excludes driver steering, unavailable feedback, inactive/invalid
|
||||
control, the following second, and speed below 3 mph. It contains 11,128.80 s.
|
||||
Durations use original timestamps, clipping gaps to 30 ms. Percentiles are
|
||||
sample-based. Request-angle categories do not identify road geometry.
|
||||
|
||||
| Recorded request magnitude | Scored seconds | Mean absolute C1 change | P95 C1 change |
|
||||
| --- | ---: | ---: | ---: |
|
||||
| Under 10 degrees | 9,051.37 | 0.00088 rad | 0.00250 rad |
|
||||
| 10–45 degrees | 1,631.68 | 0.00250 rad | 0.00900 rad |
|
||||
| At least 45 degrees | 445.75 | 0.00829 rad | 0.03100 rad |
|
||||
|
||||
C1 bound exposure increases from 21.30 to 22.67 s over the clean cohort. Mean
|
||||
absolute stored I changes from 0.005987 to 0.005984 rad; a larger P term changes
|
||||
the remaining accumulation headroom even though I's gain is unchanged.
|
||||
|
||||
The largest small-request C1 difference is 0.093 rad on route 117, where the
|
||||
recorded request is +4.6 degrees and the wheel is still at -210 degrees. This is
|
||||
a large release error, not ordinary centering. Restricting both requested and
|
||||
actual wheel angle to within 10 degrees leaves 8,788.38 s: mean absolute C1
|
||||
change 0.00073 rad, P95 0.00200 rad, maximum 0.01250 rad. These measurements do not
|
||||
establish preserved centering or closed-loop stability.
|
||||
|
||||
In route 149, the candidate increases the C1 request at the reviewed entry
|
||||
misses and reduces the remaining turn command during the clean segment-14
|
||||
release. Its clean C1 bound exposure rises from 0.23 to 0.61 s. The paired I
|
||||
traces remain nearly identical. The local report includes five entry, reversal,
|
||||
and exit comparisons with the recorded wheel trace clearly distinguished from
|
||||
replayed command traces.
|
||||
|
||||
## Validation and reproduction
|
||||
|
||||
455 tests and 25 subtests pass, including Ford controller/adapter/selection,
|
||||
C0 distance settings, diagnostic logging, delay helpers, and Ford CAN tests.
|
||||
The actual controlsd-to-CAN integration test failed at the old proportional
|
||||
output before changing the default, then passed at the new setting. Ruff and
|
||||
`git diff --check` pass. A device build, installation, and physical evaluation
|
||||
are not part of these offline checks.
|
||||
|
||||
The compact evidence record is [ford_c1_p75_validation.json](ford_c1_p75_validation.json).
|
||||
Full command arrays and per-route reports are in `.cache/ford_p75_trial` locally.
|
||||
Reproduce one route using the built cereal/opendbc environment:
|
||||
|
||||
```sh
|
||||
PYTHONPATH=.:opendbc_repo PYTHONDONTWRITEBYTECODE=1 python \
|
||||
tools/ford_pscm_lab/proportional_replay.py \
|
||||
--routes 149=.cache/ford_route149/full \
|
||||
--output .cache/ford_p75_recheck --workers 1
|
||||
```
|
||||
|
||||
Additional `label=extract-directory` pairs replay independently. Each directory
|
||||
must contain `route.npz`, `model_paths.npz`, and `metadata.json`; injection routes
|
||||
are rejected. The input hashes are recorded in each result. The new test is
|
||||
worth evaluating as a bounded change, but improved entry and preserved smooth
|
||||
release still require measured vehicle response.
|
||||
@@ -0,0 +1,707 @@
|
||||
{
|
||||
"routes": 21,
|
||||
"cycles": 2275248,
|
||||
"can_round_trips": 4550496,
|
||||
"scope": "Fixed-motion production controller and CAN replay. No predicted wheel motion or stability claim.",
|
||||
"cohorts": {
|
||||
"all": {
|
||||
"seconds": 11128.798286258982,
|
||||
"mean_abs_c1_change": 0.0014142689534451223,
|
||||
"p95_abs_c1_change": 0.004500000000000004,
|
||||
"max_abs_c1_change": 0.09299999999999997,
|
||||
"c1_bound_seconds": [
|
||||
21.296853938993763,
|
||||
22.66821589999995
|
||||
],
|
||||
"mean_abs_integral": [
|
||||
0.00598675347437431,
|
||||
0.005984439210041702
|
||||
]
|
||||
},
|
||||
"small": {
|
||||
"seconds": 9051.37332302302,
|
||||
"mean_abs_c1_change": 0.0008801267467942443,
|
||||
"p95_abs_c1_change": 0.0025000000000000022,
|
||||
"max_abs_c1_change": 0.09299999999999997,
|
||||
"c1_bound_seconds": [
|
||||
0.0,
|
||||
0.0
|
||||
],
|
||||
"mean_abs_integral": [
|
||||
0.005708567029833155,
|
||||
0.005708567029833155
|
||||
]
|
||||
},
|
||||
"bend": {
|
||||
"seconds": 1631.677856559975,
|
||||
"mean_abs_c1_change": 0.0024991973101179256,
|
||||
"p95_abs_c1_change": 0.008999999999999897,
|
||||
"max_abs_c1_change": 0.08899999999999997,
|
||||
"c1_bound_seconds": [
|
||||
0.0,
|
||||
0.0
|
||||
],
|
||||
"mean_abs_integral": [
|
||||
0.006396022651557497,
|
||||
0.006396128425459091
|
||||
]
|
||||
},
|
||||
"turn": {
|
||||
"seconds": 445.74710667598833,
|
||||
"mean_abs_c1_change": 0.008289169314090508,
|
||||
"p95_abs_c1_change": 0.031000000000000028,
|
||||
"max_abs_c1_change": 0.08000000000000002,
|
||||
"c1_bound_seconds": [
|
||||
21.296853938993763,
|
||||
22.66821589999995
|
||||
],
|
||||
"mean_abs_integral": [
|
||||
0.01013747903479613,
|
||||
0.010079312488152544
|
||||
]
|
||||
},
|
||||
"releasing": {
|
||||
"seconds": 106.96494071107061,
|
||||
"mean_abs_c1_change": 0.005058356779798081,
|
||||
"p95_abs_c1_change": 0.025500000000000023,
|
||||
"max_abs_c1_change": 0.08899999999999997,
|
||||
"c1_bound_seconds": [
|
||||
0.009879735000026812,
|
||||
0.0
|
||||
],
|
||||
"mean_abs_integral": [
|
||||
0.006354435594257913,
|
||||
0.006350676023352573
|
||||
]
|
||||
}
|
||||
},
|
||||
"baseline_commit": "b720e9f1bb805c69e5560f13dd86034d4bf50260",
|
||||
"gains": [
|
||||
0.5,
|
||||
0.75
|
||||
],
|
||||
"integral_gain": 0.25,
|
||||
"c0_time_based": false,
|
||||
"hypothesis": "model-action-curvature-c0-distance-pi-v13",
|
||||
"tests": {
|
||||
"passed": 455,
|
||||
"subtests_passed": 25,
|
||||
"ruff": "passed",
|
||||
"diff_check": "passed"
|
||||
},
|
||||
"replay_command_invariants_passed": true,
|
||||
"physical_tracking_or_stability_validated": false,
|
||||
"route_results": [
|
||||
{
|
||||
"route": "112",
|
||||
"cycles": 108971,
|
||||
"can_round_trips": 217942,
|
||||
"baseline_difference_from_recorded_path": {
|
||||
"c0": {
|
||||
"mean": 0.0679107408662434,
|
||||
"p95": 0.26000000476837126,
|
||||
"max": 3.119999904632568
|
||||
},
|
||||
"c1": {
|
||||
"mean": 0.005686226432883118,
|
||||
"p95": 0.014999999850988377,
|
||||
"max": 0.18949999547004703
|
||||
}
|
||||
},
|
||||
"clean_seconds": 732.0411244650002,
|
||||
"c1_bound_seconds": [
|
||||
0.0,
|
||||
0.021201738000058867
|
||||
],
|
||||
"source_sha256": {
|
||||
".cache/ford_route112/route.npz": "2b08a2fb636f7d14556d7df4035eafc1d1b97932237955528562a16db2b31d3e",
|
||||
".cache/ford_route112/model_paths.npz": "9837afe78aab4cad288cad98a595a5777fa8a66bb235986b1272a7f7c54e559a",
|
||||
".cache/ford_route112/metadata.json": "726d78a7e7aa45307dcfe27cb00775c20ecc9d54538eb7f26a0166fc216226ce",
|
||||
"tools/ford_pscm_lab/proportional_replay.py": "0296fedbc134c3e11022c51e0b94cc16e1135b620fd278bf381ce040e953a7ed",
|
||||
"openpilot/selfdrive/controls/lib/ford_model_action.py": "0c07b85db9c5cb0f42433116aea5d270fb244d8840119b4d7e8a526ec5b090d1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"route": "113",
|
||||
"cycles": 49614,
|
||||
"can_round_trips": 99228,
|
||||
"baseline_difference_from_recorded_path": {
|
||||
"c0": {
|
||||
"mean": 0.1281940820409913,
|
||||
"p95": 0.897999979019158,
|
||||
"max": 3.260000095367432
|
||||
},
|
||||
"c1": {
|
||||
"mean": 0.006052000019791209,
|
||||
"p95": 0.033500000965595265,
|
||||
"max": 0.13200000381469723
|
||||
}
|
||||
},
|
||||
"clean_seconds": 212.64164745499875,
|
||||
"c1_bound_seconds": [
|
||||
1.2653679959998954,
|
||||
1.2653679959998954
|
||||
],
|
||||
"source_sha256": {
|
||||
".cache/ford_route113/route.npz": "774ca4a21b7113c2706d6130bc180c3216ea4833155300ab01e75b3486e36327",
|
||||
".cache/ford_route113/model_paths.npz": "93c41761eb85263f534f5371b905482cf7c948582eb1e9149966594be1d3768f",
|
||||
".cache/ford_route113/metadata.json": "1c0ca74dd48b90ab9d5444c5ca7f8aa9361700bbdf98bd5e853be50ad2895d7f",
|
||||
"tools/ford_pscm_lab/proportional_replay.py": "0296fedbc134c3e11022c51e0b94cc16e1135b620fd278bf381ce040e953a7ed",
|
||||
"openpilot/selfdrive/controls/lib/ford_model_action.py": "0c07b85db9c5cb0f42433116aea5d270fb244d8840119b4d7e8a526ec5b090d1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"route": "114",
|
||||
"cycles": 61027,
|
||||
"can_round_trips": 122054,
|
||||
"baseline_difference_from_recorded_path": {
|
||||
"c0": {
|
||||
"mean": 0.11075714097529091,
|
||||
"p95": 0.3999999904632565,
|
||||
"max": 3.500000133514404
|
||||
},
|
||||
"c1": {
|
||||
"mean": 0.006464089004288734,
|
||||
"p95": 0.01899999927878381,
|
||||
"max": 0.17200000238418578
|
||||
}
|
||||
},
|
||||
"clean_seconds": 336.37317285400025,
|
||||
"c1_bound_seconds": [
|
||||
0.26486993000000325,
|
||||
0.26486993000000325
|
||||
],
|
||||
"source_sha256": {
|
||||
".cache/ford_route114/route.npz": "83524f07d61104b84b004ddc46bb751ca7f61da67798aa47db56d307765f5b3e",
|
||||
".cache/ford_route114/model_paths.npz": "14d14362d1a3e46398edd8e22b7cc4e36277a7596a73f546192d0e14c6642b07",
|
||||
".cache/ford_route114/metadata.json": "ec677b9275c1707e477ebd0ffa235d49b5ec63a1ee717b16040c3acdbcd3bdc0",
|
||||
"tools/ford_pscm_lab/proportional_replay.py": "0296fedbc134c3e11022c51e0b94cc16e1135b620fd278bf381ce040e953a7ed",
|
||||
"openpilot/selfdrive/controls/lib/ford_model_action.py": "0c07b85db9c5cb0f42433116aea5d270fb244d8840119b4d7e8a526ec5b090d1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"route": "115",
|
||||
"cycles": 40037,
|
||||
"can_round_trips": 80074,
|
||||
"baseline_difference_from_recorded_path": {
|
||||
"c0": {
|
||||
"mean": 0.08961574629129629,
|
||||
"p95": 0.3500000071525573,
|
||||
"max": 2.7699999141693112
|
||||
},
|
||||
"c1": {
|
||||
"mean": 0.006259665157786696,
|
||||
"p95": 0.0225000038146973,
|
||||
"max": 0.10550000000000004
|
||||
}
|
||||
},
|
||||
"clean_seconds": 213.9659020539998,
|
||||
"c1_bound_seconds": [
|
||||
0.0,
|
||||
0.0
|
||||
],
|
||||
"source_sha256": {
|
||||
".cache/ford_route115/route.npz": "e0df32d9e80f1c6b7d56327b37cf07af60070ffc212b8d111e343306148bff23",
|
||||
".cache/ford_route115/model_paths.npz": "52f3ed9e188e4947618a57a3ed872fd1966a887fe1014999df741553b6c13bc0",
|
||||
".cache/ford_route115/metadata.json": "d3c73035bad8eb5059e07962fd274c19cb70c8952b6f1514835d312d08b87a16",
|
||||
"tools/ford_pscm_lab/proportional_replay.py": "0296fedbc134c3e11022c51e0b94cc16e1135b620fd278bf381ce040e953a7ed",
|
||||
"openpilot/selfdrive/controls/lib/ford_model_action.py": "0c07b85db9c5cb0f42433116aea5d270fb244d8840119b4d7e8a526ec5b090d1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"route": "116",
|
||||
"cycles": 68793,
|
||||
"can_round_trips": 137586,
|
||||
"baseline_difference_from_recorded_path": {
|
||||
"c0": {
|
||||
"mean": 0.1229211861673407,
|
||||
"p95": 0.40999998092651335,
|
||||
"max": 3.539999895095825
|
||||
},
|
||||
"c1": {
|
||||
"mean": 0.00492455679344663,
|
||||
"p95": 0.01600000357627862,
|
||||
"max": 0.12550000327825545
|
||||
}
|
||||
},
|
||||
"clean_seconds": 286.72210779700015,
|
||||
"c1_bound_seconds": [
|
||||
0.0,
|
||||
0.0
|
||||
],
|
||||
"source_sha256": {
|
||||
".cache/ford_route116/route.npz": "13df1d49edd0c92bd4a4236c766ee76baa6dad477824b9de4b79927c98410394",
|
||||
".cache/ford_route116/model_paths.npz": "85d1112b5e89340e8a955b7fe4040b3135cf1c91a54ff756826c6119cc9cc01d",
|
||||
".cache/ford_route116/metadata.json": "d52fe63d013fa546524ad9522a26943d9372730d397d92d75dfd9d75c63ce766",
|
||||
"tools/ford_pscm_lab/proportional_replay.py": "0296fedbc134c3e11022c51e0b94cc16e1135b620fd278bf381ce040e953a7ed",
|
||||
"openpilot/selfdrive/controls/lib/ford_model_action.py": "0c07b85db9c5cb0f42433116aea5d270fb244d8840119b4d7e8a526ec5b090d1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"route": "117",
|
||||
"cycles": 27301,
|
||||
"can_round_trips": 54602,
|
||||
"baseline_difference_from_recorded_path": {
|
||||
"c0": {
|
||||
"mean": 0.1466742382755,
|
||||
"p95": 0.6379999847412033,
|
||||
"max": 4.270000057220459
|
||||
},
|
||||
"c1": {
|
||||
"mean": 0.00582792986837905,
|
||||
"p95": 0.024499997889995594,
|
||||
"max": 0.05100000405311583
|
||||
}
|
||||
},
|
||||
"clean_seconds": 97.48647941900026,
|
||||
"c1_bound_seconds": [
|
||||
0.9928339700004472,
|
||||
1.080442829000276
|
||||
],
|
||||
"source_sha256": {
|
||||
".cache/ford_route117/route.npz": "65e4ea6c76dce73018b636479b3777116f1e58bb9813e6c1a7b79018b219140e",
|
||||
".cache/ford_route117/model_paths.npz": "079f6003968287530e1ed6f2243c727e80c2ef3d16fb8a7d945d7eca744d54e7",
|
||||
".cache/ford_route117/metadata.json": "348396b3059c5f54d7938cd3acf2bb268a813b47ff47561da7a94cb75c3f3114",
|
||||
"tools/ford_pscm_lab/proportional_replay.py": "0296fedbc134c3e11022c51e0b94cc16e1135b620fd278bf381ce040e953a7ed",
|
||||
"openpilot/selfdrive/controls/lib/ford_model_action.py": "0c07b85db9c5cb0f42433116aea5d270fb244d8840119b4d7e8a526ec5b090d1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"route": "119",
|
||||
"cycles": 171423,
|
||||
"can_round_trips": 342846,
|
||||
"baseline_difference_from_recorded_path": {
|
||||
"c0": {
|
||||
"mean": 0.06276063585086782,
|
||||
"p95": 0.22999999165535012,
|
||||
"max": 4.129999866485596
|
||||
},
|
||||
"c1": {
|
||||
"mean": 0.00042448693421451755,
|
||||
"p95": 0.0024999996423721305,
|
||||
"max": 0.035999999776482605
|
||||
}
|
||||
},
|
||||
"clean_seconds": 616.4493080259994,
|
||||
"c1_bound_seconds": [
|
||||
0.0,
|
||||
0.0
|
||||
],
|
||||
"source_sha256": {
|
||||
".cache/ford_route119/route.npz": "415a099935fef152123b7422870442d6cfe6302bab9b8983ce3b3ffc71a7702b",
|
||||
".cache/ford_route119/model_paths.npz": "dfd41383bea486ccd0a476e94e612921a44a099a0c1213ff132ab81df3aa94d7",
|
||||
".cache/ford_route119/metadata.json": "d61bd3351912f2f1a3c51f6061beffe89b430ebc50178c35abff8d1ecc1fc04d",
|
||||
"tools/ford_pscm_lab/proportional_replay.py": "0296fedbc134c3e11022c51e0b94cc16e1135b620fd278bf381ce040e953a7ed",
|
||||
"openpilot/selfdrive/controls/lib/ford_model_action.py": "0c07b85db9c5cb0f42433116aea5d270fb244d8840119b4d7e8a526ec5b090d1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"route": "11a",
|
||||
"cycles": 235771,
|
||||
"can_round_trips": 471542,
|
||||
"baseline_difference_from_recorded_path": {
|
||||
"c0": {
|
||||
"mean": 0.060649023224496834,
|
||||
"p95": 0.17000000953674288,
|
||||
"max": 3.7
|
||||
},
|
||||
"c1": {
|
||||
"mean": 0.00018421321567653392,
|
||||
"p95": 0.0005000000949949013,
|
||||
"max": 0.045000003814697265
|
||||
}
|
||||
},
|
||||
"clean_seconds": 1085.65972405699,
|
||||
"c1_bound_seconds": [
|
||||
0.0811529149996204,
|
||||
0.0811529149996204
|
||||
],
|
||||
"source_sha256": {
|
||||
".cache/ford_route11a/route.npz": "428c203d090731386f06bf9fdeefe608f1999c43c52ff39cab7ee86ec0ac804f",
|
||||
".cache/ford_route11a/model_paths.npz": "e2b827fd7c3dfbf66d7872a56b57eaec13c600a9a48e12dae59be81c31b0978f",
|
||||
".cache/ford_route11a/metadata.json": "b7b10ccb1c0fa740779a1cf977faa4ae6714c9f9d7a0ffc39f3684680c16ae2a",
|
||||
"tools/ford_pscm_lab/proportional_replay.py": "0296fedbc134c3e11022c51e0b94cc16e1135b620fd278bf381ce040e953a7ed",
|
||||
"openpilot/selfdrive/controls/lib/ford_model_action.py": "0c07b85db9c5cb0f42433116aea5d270fb244d8840119b4d7e8a526ec5b090d1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"route": "120",
|
||||
"cycles": 56704,
|
||||
"can_round_trips": 113408,
|
||||
"baseline_difference_from_recorded_path": {
|
||||
"c0": {
|
||||
"mean": 0.07882976760905562,
|
||||
"p95": 0.3099999999999996,
|
||||
"max": 3.3499999046325684
|
||||
},
|
||||
"c1": {
|
||||
"mean": 6.576444779324904e-10,
|
||||
"p95": 3.218650845848714e-09,
|
||||
"max": 1.478195188475695e-08
|
||||
}
|
||||
},
|
||||
"clean_seconds": 325.62925950599976,
|
||||
"c1_bound_seconds": [
|
||||
0.0,
|
||||
0.0
|
||||
],
|
||||
"source_sha256": {
|
||||
".cache/ford_route120/route.npz": "ca40b52257af313ed0abd1109a33115a0a3227bd3438367faf282467e807a34a",
|
||||
".cache/ford_route120/model_paths.npz": "9b4a21357de4076c555e8776ed36b1d5728da1307d739268a3f84e84593e12b4",
|
||||
".cache/ford_route120/metadata.json": "110fe2016561722743064f2619e32f1b69164cc86015ef01c0a9fd2dd7f24bba",
|
||||
"tools/ford_pscm_lab/proportional_replay.py": "0296fedbc134c3e11022c51e0b94cc16e1135b620fd278bf381ce040e953a7ed",
|
||||
"openpilot/selfdrive/controls/lib/ford_model_action.py": "0c07b85db9c5cb0f42433116aea5d270fb244d8840119b4d7e8a526ec5b090d1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"route": "124",
|
||||
"cycles": 5820,
|
||||
"can_round_trips": 11640,
|
||||
"baseline_difference_from_recorded_path": {
|
||||
"c0": {
|
||||
"mean": 2.909556870453723e-10,
|
||||
"p95": 6.70552502413102e-10,
|
||||
"max": 4.76837147544984e-09
|
||||
},
|
||||
"c1": {
|
||||
"mean": 2.2964119221745972e-10,
|
||||
"p95": 8.344650526126429e-10,
|
||||
"max": 3.3974647539736225e-09
|
||||
}
|
||||
},
|
||||
"clean_seconds": 22.732139372000063,
|
||||
"c1_bound_seconds": [
|
||||
0.0,
|
||||
0.0
|
||||
],
|
||||
"source_sha256": {
|
||||
".cache/ford_c0_response_124_125/route124/route.npz": "26f8cf284e54c5f80bc7414c1302c429d4c10d54147e137fc5b8a348249e4fe5",
|
||||
".cache/ford_c0_response_124_125/route124/model_paths.npz": "898d75cc80f29fc50a818c29614961ca8fdf56d1e8908993662c6e92e54ebf6f",
|
||||
".cache/ford_c0_response_124_125/route124/metadata.json": "8f93b186178c89a95e56177dda7f0aba740bc57dfb832cde6d5c3fd3f43a8126",
|
||||
"tools/ford_pscm_lab/proportional_replay.py": "0296fedbc134c3e11022c51e0b94cc16e1135b620fd278bf381ce040e953a7ed",
|
||||
"openpilot/selfdrive/controls/lib/ford_model_action.py": "0c07b85db9c5cb0f42433116aea5d270fb244d8840119b4d7e8a526ec5b090d1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"route": "125",
|
||||
"cycles": 41717,
|
||||
"can_round_trips": 83434,
|
||||
"baseline_difference_from_recorded_path": {
|
||||
"c0": {
|
||||
"mean": 3.143796501787139e-10,
|
||||
"p95": 1.3411041166477844e-09,
|
||||
"max": 4.76837147544984e-09
|
||||
},
|
||||
"c1": {
|
||||
"mean": 0.001590841778388488,
|
||||
"p95": 0.004000000216066835,
|
||||
"max": 0.0040000033974647575
|
||||
}
|
||||
},
|
||||
"clean_seconds": 400.6387703520005,
|
||||
"c1_bound_seconds": [
|
||||
0.0,
|
||||
0.0
|
||||
],
|
||||
"source_sha256": {
|
||||
".cache/ford_c0_response_124_125/route125/route.npz": "8dcf304871bf5c6bf1ce206c788d7034bf5f056522faded0aa7948ff1cccbb75",
|
||||
".cache/ford_c0_response_124_125/route125/model_paths.npz": "4be9c7809dabbcb52296ba93b4743146dc941e49a32aca07f008b75757703e49",
|
||||
".cache/ford_c0_response_124_125/route125/metadata.json": "1f2b812d3c2cc8e5a2246098756940d6f8989a2591daf4d74b1ef4c8555e2d57",
|
||||
"tools/ford_pscm_lab/proportional_replay.py": "0296fedbc134c3e11022c51e0b94cc16e1135b620fd278bf381ce040e953a7ed",
|
||||
"openpilot/selfdrive/controls/lib/ford_model_action.py": "0c07b85db9c5cb0f42433116aea5d270fb244d8840119b4d7e8a526ec5b090d1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"route": "146",
|
||||
"cycles": 53229,
|
||||
"can_round_trips": 106458,
|
||||
"baseline_difference_from_recorded_path": {
|
||||
"c0": {
|
||||
"mean": 3.47655752720815e-10,
|
||||
"p95": 1.3411041166477844e-09,
|
||||
"max": 2.8610229740877458e-08
|
||||
},
|
||||
"c1": {
|
||||
"mean": 2.782228297198912e-10,
|
||||
"p95": 1.19209286886246e-09,
|
||||
"max": 1.3351440397713077e-08
|
||||
}
|
||||
},
|
||||
"clean_seconds": 388.4780471659999,
|
||||
"c1_bound_seconds": [
|
||||
0.0,
|
||||
0.0
|
||||
],
|
||||
"source_sha256": {
|
||||
".cache/ford_model_replay_146/full/route.npz": "b01db5bdd1c3b9fb83418a298571c98e60b5eb0509800d86cdaf07bafcdc6d63",
|
||||
".cache/ford_model_replay_146/full/model_paths.npz": "11e0e9c8ce2959aac72d2f8a9cc4f6a025480034d5cd8b59b68a41b2ae3cf4a8",
|
||||
".cache/ford_model_replay_146/full/metadata.json": "2be1254dd1eee422f3b2c2011faa91a21ab431751998525105381c2886df15ca",
|
||||
"tools/ford_pscm_lab/proportional_replay.py": "0296fedbc134c3e11022c51e0b94cc16e1135b620fd278bf381ce040e953a7ed",
|
||||
"openpilot/selfdrive/controls/lib/ford_model_action.py": "0c07b85db9c5cb0f42433116aea5d270fb244d8840119b4d7e8a526ec5b090d1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"route": "149",
|
||||
"cycles": 132334,
|
||||
"can_round_trips": 264668,
|
||||
"baseline_difference_from_recorded_path": {
|
||||
"c0": {
|
||||
"mean": 1.604709350643841e-09,
|
||||
"p95": 7.152557657263969e-09,
|
||||
"max": 5.7220459037665705e-08
|
||||
},
|
||||
"c1": {
|
||||
"mean": 6.774543859790247e-10,
|
||||
"p95": 3.0994415922691587e-09,
|
||||
"max": 1.478195188475695e-08
|
||||
}
|
||||
},
|
||||
"clean_seconds": 868.9707798709996,
|
||||
"c1_bound_seconds": [
|
||||
0.2315784860001031,
|
||||
0.6131428530001415
|
||||
],
|
||||
"source_sha256": {
|
||||
".cache/ford_route149/full/route.npz": "aa5902877343cd033ee286b3668d91a85336ffbf740861849fb0b76b0ca24ade",
|
||||
".cache/ford_route149/full/model_paths.npz": "19827800b8fb5983f3d6b72fcfaf36e35a40170bb17cd7c1e49374744fb449fb",
|
||||
".cache/ford_route149/full/metadata.json": "624fff03c25eb298661cb7b25f3dbe6d214d863f799d93635c0f4f05fc0d2b32",
|
||||
"tools/ford_pscm_lab/proportional_replay.py": "0296fedbc134c3e11022c51e0b94cc16e1135b620fd278bf381ce040e953a7ed",
|
||||
"openpilot/selfdrive/controls/lib/ford_model_action.py": "0c07b85db9c5cb0f42433116aea5d270fb244d8840119b4d7e8a526ec5b090d1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"route": "a0",
|
||||
"cycles": 114582,
|
||||
"can_round_trips": 229164,
|
||||
"baseline_difference_from_recorded_path": {
|
||||
"c0": {
|
||||
"mean": 0.02570244338167474,
|
||||
"p95": 0.08000000059604684,
|
||||
"max": 1.4500001144409183
|
||||
},
|
||||
"c1": {
|
||||
"mean": 0.01759150615156611,
|
||||
"p95": 0.0444999985605478,
|
||||
"max": 0.1095000026226044
|
||||
}
|
||||
},
|
||||
"clean_seconds": 926.827608547,
|
||||
"c1_bound_seconds": [
|
||||
7.083908395000037,
|
||||
7.184738287000073
|
||||
],
|
||||
"source_sha256": {
|
||||
".cache/ford_routea0/route.npz": "01ce8b0361c3154d07d5449525a650baa2cc261675095cb1f04ec9e37a9174d3",
|
||||
".cache/ford_routea0/model_paths.npz": "5d0c69d10a6d13882b47d950ae64c4d98f7ca2b59eab27e4e9557eef3bc4917b",
|
||||
".cache/ford_routea0/metadata.json": "00496a11afbee475e9b5d66838a4d1663a3c0063cd562b4d1408cce6d71e586e",
|
||||
"tools/ford_pscm_lab/proportional_replay.py": "0296fedbc134c3e11022c51e0b94cc16e1135b620fd278bf381ce040e953a7ed",
|
||||
"openpilot/selfdrive/controls/lib/ford_model_action.py": "0c07b85db9c5cb0f42433116aea5d270fb244d8840119b4d7e8a526ec5b090d1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"route": "a2",
|
||||
"cycles": 71111,
|
||||
"can_round_trips": 142222,
|
||||
"baseline_difference_from_recorded_path": {
|
||||
"c0": {
|
||||
"mean": 0.04191847719972587,
|
||||
"p95": 0.11000001072883592,
|
||||
"max": 2.5599999904632567
|
||||
},
|
||||
"c1": {
|
||||
"mean": 0.018344418527189366,
|
||||
"p95": 0.06350000252127648,
|
||||
"max": 0.10950000333786014
|
||||
}
|
||||
},
|
||||
"clean_seconds": 362.6477764930001,
|
||||
"c1_bound_seconds": [
|
||||
4.459983808000175,
|
||||
4.459983808000175
|
||||
],
|
||||
"source_sha256": {
|
||||
".cache/ford_routea2/route.npz": "af4128d278be6be0042f72a02255ddbec96a8a5aa435b9852c8202e4a5cb39e5",
|
||||
".cache/ford_routea2/model_paths.npz": "5a5a7c4fc30c7b3bc7f2413daa8b11fe86b69dd3249e8df2d35b0a0636a49717",
|
||||
".cache/ford_routea2/metadata.json": "cfecd240216e409a1e2591c6c2e6f0bad002ecd2a50c8f6dbf22a8f9174708da",
|
||||
"tools/ford_pscm_lab/proportional_replay.py": "0296fedbc134c3e11022c51e0b94cc16e1135b620fd278bf381ce040e953a7ed",
|
||||
"openpilot/selfdrive/controls/lib/ford_model_action.py": "0c07b85db9c5cb0f42433116aea5d270fb244d8840119b4d7e8a526ec5b090d1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"route": "a5",
|
||||
"cycles": 38961,
|
||||
"can_round_trips": 77922,
|
||||
"baseline_difference_from_recorded_path": {
|
||||
"c0": {
|
||||
"mean": 0.0678814615486646,
|
||||
"p95": 0.23999999046325726,
|
||||
"max": 2.549999809265137
|
||||
},
|
||||
"c1": {
|
||||
"mean": 0.010050927104249109,
|
||||
"p95": 0.02549999953061341,
|
||||
"max": 0.11449999988079068
|
||||
}
|
||||
},
|
||||
"clean_seconds": 281.78537111799994,
|
||||
"c1_bound_seconds": [
|
||||
3.117821645999925,
|
||||
3.1221818889999327
|
||||
],
|
||||
"source_sha256": {
|
||||
".cache/ford_routea5/route.npz": "815d1e248ff5c3e5e5cfc11dfbd0690d0d13ddafe1436890e239975a72dae9b8",
|
||||
".cache/ford_routea5/model_paths.npz": "72e014b844322df6318f6af588c27175ce68d9a37bda62b70eafca5bdfe6dfe4",
|
||||
".cache/ford_routea5/metadata.json": "c533b06d1b7c598330b36b117bdb3624221bc4193ded8354246d5c86f36b2628",
|
||||
"tools/ford_pscm_lab/proportional_replay.py": "0296fedbc134c3e11022c51e0b94cc16e1135b620fd278bf381ce040e953a7ed",
|
||||
"openpilot/selfdrive/controls/lib/ford_model_action.py": "0c07b85db9c5cb0f42433116aea5d270fb244d8840119b4d7e8a526ec5b090d1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"route": "a9",
|
||||
"cycles": 286319,
|
||||
"can_round_trips": 572638,
|
||||
"baseline_difference_from_recorded_path": {
|
||||
"c0": {
|
||||
"mean": 0.0574873679806263,
|
||||
"p95": 0.24000002861022907,
|
||||
"max": 1.8600000095367433
|
||||
},
|
||||
"c1": {
|
||||
"mean": 0.012787646152435335,
|
||||
"p95": 0.04100000113248825,
|
||||
"max": 0.20099999380111694
|
||||
}
|
||||
},
|
||||
"clean_seconds": 1231.1594245750002,
|
||||
"c1_bound_seconds": [
|
||||
0.945385510000051,
|
||||
1.1985475469998619
|
||||
],
|
||||
"source_sha256": {
|
||||
".cache/ford_routea9/route.npz": "3fa8cabfb729dd42d689de38618d1d21c8965c9d60f914546f4d7cc58db8c975",
|
||||
".cache/ford_routea9/model_paths.npz": "7f3646ad24aa52de5982c65600813de5bbc5f7664d92a2516bbba35da6e7b240",
|
||||
".cache/ford_routea9/metadata.json": "403d35504bf596144845ac2060ce067d571f49a2955d16223af28a79a7a66e93",
|
||||
"tools/ford_pscm_lab/proportional_replay.py": "0296fedbc134c3e11022c51e0b94cc16e1135b620fd278bf381ce040e953a7ed",
|
||||
"openpilot/selfdrive/controls/lib/ford_model_action.py": "0c07b85db9c5cb0f42433116aea5d270fb244d8840119b4d7e8a526ec5b090d1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"route": "b8",
|
||||
"cycles": 160431,
|
||||
"can_round_trips": 320862,
|
||||
"baseline_difference_from_recorded_path": {
|
||||
"c0": {
|
||||
"mean": 0.09075655656311847,
|
||||
"p95": 0.3199999809265135,
|
||||
"max": 3.8600001049041746
|
||||
},
|
||||
"c1": {
|
||||
"mean": 0.014587774875948546,
|
||||
"p95": 0.047500000029802314,
|
||||
"max": 0.2005000000223518
|
||||
}
|
||||
},
|
||||
"clean_seconds": 465.3759713629988,
|
||||
"c1_bound_seconds": [
|
||||
1.233373995994043,
|
||||
1.536421163000341
|
||||
],
|
||||
"source_sha256": {
|
||||
".cache/ford_routeb8/route.npz": "6f5dd369b70eaed4b95b28c8b25c9f2e9b830fa07a334881a185505481667c8b",
|
||||
".cache/ford_routeb8/model_paths.npz": "939af6cf7e74251d8842581cc078d26d9fbfd22a0d7817cb0e368697d419b615",
|
||||
".cache/ford_routeb8/metadata.json": "73b439132d1de37ec187b544c04d2b05c80965065515a4b7dec29ba57ae37e7c",
|
||||
"tools/ford_pscm_lab/proportional_replay.py": "0296fedbc134c3e11022c51e0b94cc16e1135b620fd278bf381ce040e953a7ed",
|
||||
"openpilot/selfdrive/controls/lib/ford_model_action.py": "0c07b85db9c5cb0f42433116aea5d270fb244d8840119b4d7e8a526ec5b090d1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"route": "b9",
|
||||
"cycles": 90774,
|
||||
"can_round_trips": 181548,
|
||||
"baseline_difference_from_recorded_path": {
|
||||
"c0": {
|
||||
"mean": 0.0779309579021297,
|
||||
"p95": 0.3100000107288361,
|
||||
"max": 4.5400001907348635
|
||||
},
|
||||
"c1": {
|
||||
"mean": 0.007145982184135228,
|
||||
"p95": 0.020500003129243882,
|
||||
"max": 0.14649999999999996
|
||||
}
|
||||
},
|
||||
"clean_seconds": 680.5921192759997,
|
||||
"c1_bound_seconds": [
|
||||
0.7494592899999759,
|
||||
0.9690469479999706
|
||||
],
|
||||
"source_sha256": {
|
||||
".cache/ford_routeb9/route.npz": "b07c789d8155335f5d120d0262fced6e4d5803fe767b0ff49b6413dce4140b5c",
|
||||
".cache/ford_routeb9/model_paths.npz": "6b1f87897c050273fdc05af051307a049b6fc3a93072e7cda1721195ce7c3861",
|
||||
".cache/ford_routeb9/metadata.json": "9ce452220cab61b81883f32fc2fcaf5db6c78a674cb255a49cc77d5029580fee",
|
||||
"tools/ford_pscm_lab/proportional_replay.py": "0296fedbc134c3e11022c51e0b94cc16e1135b620fd278bf381ce040e953a7ed",
|
||||
"openpilot/selfdrive/controls/lib/ford_model_action.py": "0c07b85db9c5cb0f42433116aea5d270fb244d8840119b4d7e8a526ec5b090d1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"route": "ca",
|
||||
"cycles": 327448,
|
||||
"can_round_trips": 654896,
|
||||
"baseline_difference_from_recorded_path": {
|
||||
"c0": {
|
||||
"mean": 0.015992715429034462,
|
||||
"p95": 0.05000000029802365,
|
||||
"max": 1.0699999809265135
|
||||
},
|
||||
"c1": {
|
||||
"mean": 0.0030197859214003767,
|
||||
"p95": 0.00799999958276748,
|
||||
"max": 0.10800000715255731
|
||||
}
|
||||
},
|
||||
"clean_seconds": 999.6971771089911,
|
||||
"c1_bound_seconds": [
|
||||
0.871117996999601,
|
||||
0.871117996999601
|
||||
],
|
||||
"source_sha256": {
|
||||
".cache/ford_routeca/route.npz": "ae9d46770eaf0dbbac6af86aebc926320eed0cf114eb43d5f78b0676e8e0dbf9",
|
||||
".cache/ford_routeca/model_paths.npz": "bf17deb442383aaa79432566cd382df24a1bbbbd0521d0cafab956618f5bdd96",
|
||||
".cache/ford_routeca/metadata.json": "a759d5cdf878df8b05d91db637b1935b6b4bdd87af96f0f256b67e7d809b3525",
|
||||
"tools/ford_pscm_lab/proportional_replay.py": "0296fedbc134c3e11022c51e0b94cc16e1135b620fd278bf381ce040e953a7ed",
|
||||
"openpilot/selfdrive/controls/lib/ford_model_action.py": "0c07b85db9c5cb0f42433116aea5d270fb244d8840119b4d7e8a526ec5b090d1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"route": "raptor02",
|
||||
"cycles": 132881,
|
||||
"can_round_trips": 265762,
|
||||
"baseline_difference_from_recorded_path": {
|
||||
"c0": {
|
||||
"mean": 0.01159572992270899,
|
||||
"p95": 0.06918421261012586,
|
||||
"max": 0.39572428941726656
|
||||
},
|
||||
"c1": {
|
||||
"mean": 0.017327589689457763,
|
||||
"p95": 0.04501086750242392,
|
||||
"max": 0.10240508272871374
|
||||
}
|
||||
},
|
||||
"clean_seconds": 592.9243753840037,
|
||||
"c1_bound_seconds": [
|
||||
0.0,
|
||||
0.0
|
||||
],
|
||||
"source_sha256": {
|
||||
".cache/ford_raptor_route02/route.npz": "8a4cfe53994988d053b47d9caadf21ebd064fd20e598fa05ddc2da245bd0e980",
|
||||
".cache/ford_raptor_route02/model_paths.npz": "d5dc6f72d91472b8f2fb1add1680cdaea1e122774ddd6d8519fd7673daf71bb5",
|
||||
".cache/ford_raptor_route02/metadata.json": "9602e08efc2bd784133837c4156c075bb89bad3bedd6b309f15e884ed20e338a",
|
||||
"tools/ford_pscm_lab/proportional_replay.py": "0296fedbc134c3e11022c51e0b94cc16e1135b620fd278bf381ce040e953a7ed",
|
||||
"openpilot/selfdrive/controls/lib/ford_model_action.py": "0c07b85db9c5cb0f42433116aea5d270fb244d8840119b4d7e8a526ec5b090d1"
|
||||
}
|
||||
}
|
||||
],
|
||||
"selected_runtime_source_sha256": "a653b9c16ff8e5ac91e7c1751eaeb7a77b50701d185ce426fe1074d1a58a07b4",
|
||||
"opendbc_commit": "64aa61b9b3fd26e70a7caa915acab207ff3cd64a"
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
# Ford C1 proportional feedback trial
|
||||
|
||||
V6 adds **P = 0.25** to the selected-action controller. It responds to a steering
|
||||
shortfall immediately and subtracts demand immediately when the wheel exceeds
|
||||
the selected request. V5 accumulated correction over traveled distance. P has
|
||||
no stored correction to release when its error disappears.
|
||||
|
||||
This is an initial drive-trial gain, not an identified PSCM calibration or a
|
||||
claim of improved physical tracking. Six Lightning routes establish command
|
||||
behavior across recorded scenarios. They cannot identify the best stable gain
|
||||
without observing the vehicle responding to the changed commands.
|
||||
|
||||
## Command law
|
||||
|
||||
With curvature in inverse meters, speed in meters per second and heading in radians:
|
||||
|
||||
```text
|
||||
D = max(7 m, speed × 1 s)
|
||||
error = selected_limited_curvature - measured_steering_curvature
|
||||
base_C1 = clip(D × selected_limited_curvature, -0.5, +0.5)
|
||||
P = 0.25 × D × error
|
||||
I_increment = speed × error × fresh_measurement_elapsed_time
|
||||
C1 = amplitude_and_slew_limit(base_C1 + P + I)
|
||||
```
|
||||
|
||||
P is 25% of the heading-equivalent tracking error, not a 25% multiplier on the
|
||||
model request. At matched curvature it is zero. It is stateless and can change
|
||||
with a new request even if a steering publication repeats; repeated steering
|
||||
publications still cannot integrate I twice. Driver override and fresh PSCM
|
||||
denied/inactive states clear both feedback terms. Fresh `limit=2` inhibits
|
||||
outward I accumulation while permitting unwind; P remains available inside the
|
||||
existing combined output envelope.
|
||||
|
||||
The existing C1 amplitude limit (±0.5 rad) and slew (0.5 rad/s) apply to the sum.
|
||||
Anti-windup includes P when calculating I's available headroom. P can consume
|
||||
a slew interval that previously allowed I accumulation. The conditional I
|
||||
release rules, including [completed-unwind release](ford_unwind_catchup.md),
|
||||
remain. C0 retains the same 7 m mapping, base-heading overflow, cap and slew;
|
||||
neither P nor I spills into C0. C2/C3 stay zero. No plant, gain schedule or
|
||||
automatic gain learning is introduced.
|
||||
|
||||
Onroad selection explicitly supplies `C1_PROPORTIONAL_GAIN = 0.25`. Direct
|
||||
`FordModelActionController()` and `ModelActionController()` construction defaults
|
||||
to zero P for v5 reference/replay compatibility. The existing default-off
|
||||
Sunnylink toggle selects v6 on any Ford CAN FD. Toggle off still selects
|
||||
upstream Ford control. See [installation and selection](ford_model_action_drive_test.md).
|
||||
|
||||
## Lightning replay findings
|
||||
|
||||
Routes `112`, `113`, `114`, `115`, `b9` and `ca` supplied 677,871 control cycles.
|
||||
Each was replayed with P gains 0, 0.1, 0.25 and 0.5, paired with diagnostic
|
||||
feedback delays 0, 0.2 and 0.4 s: 12 combinations and 8,134,452 candidate updates.
|
||||
Recorded model, driver, steering and PSCM inputs stayed fixed.
|
||||
|
||||
Both command columns are replayed C1 in radians with left positive. The angle
|
||||
pair is the single recorded desired/actual wheel measurement, not a predicted
|
||||
outcome for either candidate.
|
||||
|
||||
| Example | Desired / actual angle | V5 C1 | P=0.25 C1 |
|
||||
| --- | ---: | ---: | ---: |
|
||||
| 115, 207.908 s: late left entry | 94.2° / 51.2° | +0.1685 | +0.1825 |
|
||||
| 115, 208.099 s: entry continues | 111.0° / 72.8° | +0.2105 | +0.2245 |
|
||||
| 114, 473.086 s: well-tracked bend | 57.3° / 56.3° | +0.1855 | +0.1850 |
|
||||
| 113, 481.567 s: hanging right exit | −7.6° / −94.4° | +0.0645 | +0.0875 |
|
||||
| 115, 133.595 s: completed unwind | 6.0° / 29.9° | +0.0105 | 0.0000 |
|
||||
|
||||
The completed-unwind example is excluded by the original quality/driver clean
|
||||
mask. It is useful for checking command release, not autonomous tracking
|
||||
attribution. Large-turn windows often contain interventions and require review
|
||||
of driver input before assigning a tracking result to the controller.
|
||||
|
||||
Across 2,740.44 seconds of valid, feedback-enabled, clean samples with desired
|
||||
wheel angle below 30°, the duration-weighted mean absolute C1 change is
|
||||
0.001001 rad at P=0.25, versus 0.001961 at P=0.5. Per-route 95th-percentile
|
||||
changes at P=0.25 are 0.0025–0.0070 rad; the largest ordinary-cohort change is
|
||||
0.0350 rad. Small average command changes do not establish unchanged centering
|
||||
or stability.
|
||||
|
||||
P=0.25 is an engineering choice between the tested smaller and larger responses,
|
||||
not an optimization result. At the late-entry example, adding a fixed 0.4 s
|
||||
feedback delay instead gives C1 +0.1420 rad. Across the ordinary cohort, that
|
||||
delayed P=0.25 variant changes C1 by 0.011255 rad on average. V6 therefore
|
||||
retains v5's feedback timing to isolate P. This does not identify or disprove
|
||||
the vehicle's physical delay. The diagnostic delay variants change only P and
|
||||
I integration targets; request-release decisions still use the current request.
|
||||
|
||||
## Tuning and next-drive evidence
|
||||
|
||||
Comma's [torque controller](https://github.com/commaai/openpilot/blob/master/openpilot/selfdrive/controls/lib/latcontrol_torque.py)
|
||||
separates feedforward, P and I and aligns its torque feedback reference with
|
||||
steering delay. Its [angle PID controller](https://github.com/commaai/openpilot/blob/master/openpilot/selfdrive/controls/lib/latcontrol_pid.py)
|
||||
uses desired-minus-measured steering angle directly. These different paths do
|
||||
not imply one delay setting should be copied into Ford C1.
|
||||
|
||||
Use the same discipline: explicit parameters, separate term logging, fixed
|
||||
request conditions and measured response. Comma's
|
||||
[lateral maneuver report](https://blog.comma.ai/0111release/#lateral-maneuver-report)
|
||||
uses repeatable step/sine maneuvers to assess response. C1 is a path-heading
|
||||
request to another controller, not normalized steering torque; numerical torque
|
||||
gains and torque calibration cannot be copied across.
|
||||
|
||||
For the next controlled evaluation, compare similar speeds and model requests:
|
||||
entry delay/shortfall, overshoot as the request relaxes, correction after catch-up,
|
||||
ordinary-bend centering and oscillation. Keep desired/actual tracking on original
|
||||
timestamps. Check C0/C1 caps, slew, driver input and fresh PSCM flags separately.
|
||||
More gain cannot remove hardware limits and can introduce oscillation. These
|
||||
logs all come from a Lightning; the gain is not yet validated across other
|
||||
PSCMs. No scripted maneuver mode is enabled by this change.
|
||||
|
||||
Periodic `Ford C2-free path tracking` events identify
|
||||
`hypothesis=model-action-c1-pi-v6` and expose `heading_proportional`,
|
||||
`proportional_gain`, `feedback_curvature` and `feedback_error` alongside
|
||||
`heading_feedforward`, `heading_correction`, command and release diagnostics.
|
||||
`calibration_approved=false` remains.
|
||||
|
||||
## Validation and reproduction
|
||||
|
||||
The final selected path exactly matches the sweep's P=0.25, zero-delay variant
|
||||
on all six routes, including C0/C1, P, I and activation. Zero-P/zero-delay matches
|
||||
v5 exactly on every cycle. All variants preserve C0 and activation. Another
|
||||
200,000 seeded stress cycles check PI arithmetic, anti-windup, mirror symmetry,
|
||||
driver/PSCM arbitration, resets, amplitude/slew and zero-P parity. Sweep,
|
||||
selected replay and stress total **9,012,323 Float32/CAN round trips**.
|
||||
Encoding checks do not test vehicle motion.
|
||||
|
||||
**776 tests and 9,145 subtests passed; 178 were skipped.** Coverage includes
|
||||
actual startup selection, controlsd request source/limiting, Float32 publication,
|
||||
100 Hz CAN encoding, checksums, both turn signs, integral release, Sunnylink
|
||||
persistence, toggle-off upstream behavior and Ford safety tests. Ruff,
|
||||
controller Ty and settings compilation passed. A hardware build/device boot
|
||||
and physical response tests have not been performed.
|
||||
|
||||
Use the project's Python environment and built cereal/opendbc dependencies:
|
||||
|
||||
```sh
|
||||
export PYTHONPATH=.:opendbc_repo:.cache/ford_v6/test_deps
|
||||
export PYTHONDONTWRITEBYTECODE=1
|
||||
export PARAMS_ROOT=/tmp/ford-pi-test-params
|
||||
export LOG_ROOT=/tmp/ford-pi-test-logs
|
||||
python -m tools.ford_pscm_lab.pi_replay .cache/ford_route115 --output .cache/ford_pi_sweep/route115
|
||||
python -m tools.ford_pscm_lab.feedback_replay route .cache/ford_route115 --baseline 22d188776cb557acea459a1fca70812bdb2df46c --output .cache/ford_pi_sweep/selected115
|
||||
python -m tools.ford_pscm_lab.pi_stress --cycles 200000 --gain .25 --output .cache/ford_pi_sweep/stress.json
|
||||
python -m pytest -q -p no:cacheprovider openpilot/selfdrive/controls/tests/test_ford_*.py tools/ford_pscm_lab openpilot/selfdrive/car/tests/test_ford_pscm_status.py openpilot/sunnypilot/sunnylink/tests openpilot/common/tests/test_params.py opendbc_repo/opendbc/car/ford/tests/test_ford.py opendbc_repo/opendbc/safety/tests/test_ford.py
|
||||
```
|
||||
|
||||
Repeat both replay commands for the other five extracts. The machine-readable
|
||||
[validation record](ford_c1_pi_validation.json) records counts, source hashes,
|
||||
cohort definitions and sampled command changes. Publication time proxies the
|
||||
computation clock; full SubMaster state and selected maneuver-plan publications
|
||||
are not reconstructed. Real maneuver source selection is exercised in integration
|
||||
tests. Historical controller reports retain their original version scope.
|
||||
@@ -0,0 +1,373 @@
|
||||
{
|
||||
"hypothesis": "model-action-c1-pi-v6",
|
||||
"baseline_revision": "22d188776cb557acea459a1fca70812bdb2df46c",
|
||||
"opendbc_revision": "64aa61b9b3fd26e70a7caa915acab207ff3cd64a",
|
||||
"selected_gain": 0.25,
|
||||
"selected_feedback_delay_s": 0.0,
|
||||
"calibration_approved": false,
|
||||
"scope": "Frozen recorded steering, model, driver and PSCM inputs. Command checks only; no predicted wheel response or physical tracking improvement score.",
|
||||
"controller_sha256": "8eaf242a8627c398b732e9c185527640b2ae39b4726cbb138c0a10490a4b890f",
|
||||
"routes": {
|
||||
"112": {
|
||||
"route": "84865544361f55cb_00000112--ec2edd4afc",
|
||||
"fingerprint": "FORD_F_150_LIGHTNING_MK1",
|
||||
"cycles": 108971,
|
||||
"candidate_updates": 1307652,
|
||||
"sweep_can_round_trips": 1307652,
|
||||
"selected_can_round_trips": 108971,
|
||||
"selected_matches_sweep_commands_p_i_valid_exactly": true,
|
||||
"zero_gain_zero_delay_matches_v5_exactly": true,
|
||||
"all_c0_and_activation_match_exactly": true,
|
||||
"ordinary_clean_seconds": 655.2228206790003,
|
||||
"ordinary_mean_abs_c1_change_rad": 0.0008885702173989647,
|
||||
"ordinary_p95_abs_c1_change_rad": 0.0025000000000000022,
|
||||
"ordinary_max_abs_c1_change_rad": 0.02400000000000002,
|
||||
"all_valid_max_abs_c1_change_rad": 0.054500000000000104,
|
||||
"source_sha256": {
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route112/route.npz": "2b08a2fb636f7d14556d7df4035eafc1d1b97932237955528562a16db2b31d3e",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route112/model_paths.npz": "9837afe78aab4cad288cad98a595a5777fa8a66bb235986b1272a7f7c54e559a",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route112/metadata.json": "726d78a7e7aa45307dcfe27cb00775c20ecc9d54538eb7f26a0166fc216226ce",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/feedback_replay.py": "c7de9d9d7aefb27221ad6d964df723fbf9f2b0f820cdd7de4799c83984d05485",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/openpilot/selfdrive/controls/lib/ford_model_action.py": "8eaf242a8627c398b732e9c185527640b2ae39b4726cbb138c0a10490a4b890f",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route112/angles.npz": "9429813237b4988dd99a1461d1a9bd6f6d3056693a93a5dd1ec63567d19f2aa1"
|
||||
},
|
||||
"sweep_report_sha256": "413e844d20d7591d32d95b1d48a14db11fa089800315cb899e9f5a32a3e630f5",
|
||||
"selected_report_sha256": "ab4ced7ec6b628ac4a941fb968e6e18d06f9c616f8704f1a6abd0081a4fb0e65"
|
||||
},
|
||||
"113": {
|
||||
"route": "84865544361f55cb_00000113--3947b0487c",
|
||||
"fingerprint": "FORD_F_150_LIGHTNING_MK1",
|
||||
"cycles": 49614,
|
||||
"candidate_updates": 595368,
|
||||
"sweep_can_round_trips": 595368,
|
||||
"selected_can_round_trips": 49614,
|
||||
"selected_matches_sweep_commands_p_i_valid_exactly": true,
|
||||
"zero_gain_zero_delay_matches_v5_exactly": true,
|
||||
"all_c0_and_activation_match_exactly": true,
|
||||
"ordinary_clean_seconds": 187.8263240149995,
|
||||
"ordinary_mean_abs_c1_change_rad": 0.0015004664599222238,
|
||||
"ordinary_p95_abs_c1_change_rad": 0.007000000000000006,
|
||||
"ordinary_max_abs_c1_change_rad": 0.025000000000000022,
|
||||
"all_valid_max_abs_c1_change_rad": 0.08250000000000002,
|
||||
"source_sha256": {
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route113/route.npz": "774ca4a21b7113c2706d6130bc180c3216ea4833155300ab01e75b3486e36327",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route113/model_paths.npz": "93c41761eb85263f534f5371b905482cf7c948582eb1e9149966594be1d3768f",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route113/metadata.json": "1c0ca74dd48b90ab9d5444c5ca7f8aa9361700bbdf98bd5e853be50ad2895d7f",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/feedback_replay.py": "c7de9d9d7aefb27221ad6d964df723fbf9f2b0f820cdd7de4799c83984d05485",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/openpilot/selfdrive/controls/lib/ford_model_action.py": "8eaf242a8627c398b732e9c185527640b2ae39b4726cbb138c0a10490a4b890f",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route113/angles.npz": "2f280a1b5973901d878f1e36564d6e7ae6d817cac176149f2cb898ab7591d65b"
|
||||
},
|
||||
"sweep_report_sha256": "a712fdbe0602404ade075b0ccb540d5f571e5a55d37723fc2674c79b5f62252f",
|
||||
"selected_report_sha256": "0098ef5b1dd0b8b1f0b742cfb5f8e80211eb60718a08e471927c78b9a7c740b3"
|
||||
},
|
||||
"114": {
|
||||
"route": "84865544361f55cb_00000114--03902c6e04",
|
||||
"fingerprint": "FORD_F_150_LIGHTNING_MK1",
|
||||
"cycles": 61027,
|
||||
"candidate_updates": 732324,
|
||||
"sweep_can_round_trips": 732324,
|
||||
"selected_can_round_trips": 61027,
|
||||
"selected_matches_sweep_commands_p_i_valid_exactly": true,
|
||||
"zero_gain_zero_delay_matches_v5_exactly": true,
|
||||
"all_c0_and_activation_match_exactly": true,
|
||||
"ordinary_clean_seconds": 263.004525574,
|
||||
"ordinary_mean_abs_c1_change_rad": 0.0010481495632417845,
|
||||
"ordinary_p95_abs_c1_change_rad": 0.003500000000000003,
|
||||
"ordinary_max_abs_c1_change_rad": 0.010999999999999954,
|
||||
"all_valid_max_abs_c1_change_rad": 0.0655,
|
||||
"source_sha256": {
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route114/route.npz": "83524f07d61104b84b004ddc46bb751ca7f61da67798aa47db56d307765f5b3e",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route114/model_paths.npz": "14d14362d1a3e46398edd8e22b7cc4e36277a7596a73f546192d0e14c6642b07",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route114/metadata.json": "ec677b9275c1707e477ebd0ffa235d49b5ec63a1ee717b16040c3acdbcd3bdc0",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/feedback_replay.py": "c7de9d9d7aefb27221ad6d964df723fbf9f2b0f820cdd7de4799c83984d05485",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/openpilot/selfdrive/controls/lib/ford_model_action.py": "8eaf242a8627c398b732e9c185527640b2ae39b4726cbb138c0a10490a4b890f",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route114/angles.npz": "63aa2ff6a21e8613519bda476690e2eda6f42cb5dcf108606bc6072c9e29408d"
|
||||
},
|
||||
"sweep_report_sha256": "138c522c4278d6827eb6192acc2052ba4832f652c3194e2ca330d3a4458a2f85",
|
||||
"selected_report_sha256": "52cdc595248505301501d815d7f6219c47e8cdcac2a059dc345498b3ef707875"
|
||||
},
|
||||
"115": {
|
||||
"route": "84865544361f55cb_00000115--899b9bf91d",
|
||||
"fingerprint": "FORD_F_150_LIGHTNING_MK1",
|
||||
"cycles": 40037,
|
||||
"candidate_updates": 480444,
|
||||
"sweep_can_round_trips": 480444,
|
||||
"selected_can_round_trips": 40037,
|
||||
"selected_matches_sweep_commands_p_i_valid_exactly": true,
|
||||
"zero_gain_zero_delay_matches_v5_exactly": true,
|
||||
"all_c0_and_activation_match_exactly": true,
|
||||
"ordinary_clean_seconds": 165.0304544679998,
|
||||
"ordinary_mean_abs_c1_change_rad": 0.0009869321832509364,
|
||||
"ordinary_p95_abs_c1_change_rad": 0.0030000000000000027,
|
||||
"ordinary_max_abs_c1_change_rad": 0.034999999999999976,
|
||||
"all_valid_max_abs_c1_change_rad": 0.09900000000000003,
|
||||
"source_sha256": {
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route115/route.npz": "e0df32d9e80f1c6b7d56327b37cf07af60070ffc212b8d111e343306148bff23",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route115/model_paths.npz": "52f3ed9e188e4947618a57a3ed872fd1966a887fe1014999df741553b6c13bc0",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route115/metadata.json": "d3c73035bad8eb5059e07962fd274c19cb70c8952b6f1514835d312d08b87a16",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/feedback_replay.py": "c7de9d9d7aefb27221ad6d964df723fbf9f2b0f820cdd7de4799c83984d05485",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/openpilot/selfdrive/controls/lib/ford_model_action.py": "8eaf242a8627c398b732e9c185527640b2ae39b4726cbb138c0a10490a4b890f",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route115/angles.npz": "71415b0f49efbfeda495ae52bde4beb7e16f31a1f5570af515a365e756056d3b"
|
||||
},
|
||||
"sweep_report_sha256": "cdb08fede1f86427174c1291e40f63a3ac18044dfb6276e98e0bc184bbff7dc5",
|
||||
"selected_report_sha256": "92d3e720d12018599a5883ecfb3c2dd1330e22235e07b3cd0dcc9bbc0aa7c3dd"
|
||||
},
|
||||
"b9": {
|
||||
"route": "84865544361f55cb_000000b9--5da7fe66ad",
|
||||
"fingerprint": "FORD_F_150_LIGHTNING_MK1",
|
||||
"cycles": 90774,
|
||||
"candidate_updates": 1089288,
|
||||
"sweep_can_round_trips": 1089288,
|
||||
"selected_can_round_trips": 90774,
|
||||
"selected_matches_sweep_commands_p_i_valid_exactly": true,
|
||||
"zero_gain_zero_delay_matches_v5_exactly": true,
|
||||
"all_c0_and_activation_match_exactly": true,
|
||||
"ordinary_clean_seconds": 577.3471252719997,
|
||||
"ordinary_mean_abs_c1_change_rad": 0.001220959786299961,
|
||||
"ordinary_p95_abs_c1_change_rad": 0.0050000000000000044,
|
||||
"ordinary_max_abs_c1_change_rad": 0.02350000000000002,
|
||||
"all_valid_max_abs_c1_change_rad": 0.10250000000000004,
|
||||
"source_sha256": {
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_routeb9/route.npz": "b07c789d8155335f5d120d0262fced6e4d5803fe767b0ff49b6413dce4140b5c",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_routeb9/model_paths.npz": "6b1f87897c050273fdc05af051307a049b6fc3a93072e7cda1721195ce7c3861",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_routeb9/metadata.json": "9ce452220cab61b81883f32fc2fcaf5db6c78a674cb255a49cc77d5029580fee",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/feedback_replay.py": "c7de9d9d7aefb27221ad6d964df723fbf9f2b0f820cdd7de4799c83984d05485",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/openpilot/selfdrive/controls/lib/ford_model_action.py": "8eaf242a8627c398b732e9c185527640b2ae39b4726cbb138c0a10490a4b890f",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_routeb9/angles.npz": "ce16212d9b13e41993d2c8df3246ddfe21324ad0cb213310e62eee1d685b197b"
|
||||
},
|
||||
"sweep_report_sha256": "13c9b2c4c0f89ec580e9c7a70c7556b4e40873d4d98561c9edfa1c12a0855669",
|
||||
"selected_report_sha256": "733c3eb657cbae29fcb809ca42b85ed6fc03ab44665c5fe31020e266717ce53c"
|
||||
},
|
||||
"ca": {
|
||||
"route": "84865544361f55cb_000000ca--1f70b49ec6",
|
||||
"fingerprint": "FORD_F_150_LIGHTNING_MK1",
|
||||
"cycles": 327448,
|
||||
"candidate_updates": 3929376,
|
||||
"sweep_can_round_trips": 3929376,
|
||||
"selected_can_round_trips": 327448,
|
||||
"selected_matches_sweep_commands_p_i_valid_exactly": true,
|
||||
"zero_gain_zero_delay_matches_v5_exactly": true,
|
||||
"all_c0_and_activation_match_exactly": true,
|
||||
"ordinary_clean_seconds": 892.0115341569954,
|
||||
"ordinary_mean_abs_c1_change_rad": 0.0008244438245156879,
|
||||
"ordinary_p95_abs_c1_change_rad": 0.0025000000000000022,
|
||||
"ordinary_max_abs_c1_change_rad": 0.01200000000000001,
|
||||
"all_valid_max_abs_c1_change_rad": 0.05149999999999999,
|
||||
"source_sha256": {
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_routeca/route.npz": "ae9d46770eaf0dbbac6af86aebc926320eed0cf114eb43d5f78b0676e8e0dbf9",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_routeca/model_paths.npz": "bf17deb442383aaa79432566cd382df24a1bbbbd0521d0cafab956618f5bdd96",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_routeca/metadata.json": "a759d5cdf878df8b05d91db637b1935b6b4bdd87af96f0f256b67e7d809b3525",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/feedback_replay.py": "c7de9d9d7aefb27221ad6d964df723fbf9f2b0f820cdd7de4799c83984d05485",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/openpilot/selfdrive/controls/lib/ford_model_action.py": "8eaf242a8627c398b732e9c185527640b2ae39b4726cbb138c0a10490a4b890f",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_routeca/angles.npz": "1681bda6db7dac332abf627d97e7452acd7e0faef0c196b11e55e22291cad073"
|
||||
},
|
||||
"sweep_report_sha256": "4ced4be50569d4025c9de7fb8085714deebfb6098238ab63e5a2e4c88460a94b",
|
||||
"selected_report_sha256": "db0edb05105600f211e7ff833a324887f4402dc64ddceb44ee8ddec911de7533"
|
||||
}
|
||||
},
|
||||
"examples": [
|
||||
{
|
||||
"description": "Late left entry",
|
||||
"route": "115",
|
||||
"time_s": 207.908066963,
|
||||
"desired_angle_deg": 94.1796646118164,
|
||||
"actual_angle_deg": 51.20000076293945,
|
||||
"speed_mph": 12.099885821086458,
|
||||
"strict_clean": true,
|
||||
"c0_m_left_positive": 0.96,
|
||||
"c1_rad_left_positive": {
|
||||
"v5": 0.16849999999999998,
|
||||
"p_025": 0.1825,
|
||||
"p_025_delay_04": 0.14200000000000002
|
||||
},
|
||||
"p_rad_left_positive": 0.018101743422448635,
|
||||
"i_rad_left_positive": 0.015372994845796784
|
||||
},
|
||||
{
|
||||
"description": "Entry continues",
|
||||
"route": "115",
|
||||
"time_s": 208.09921410900006,
|
||||
"desired_angle_deg": 111.01296997070312,
|
||||
"actual_angle_deg": 72.80000305175781,
|
||||
"speed_mph": 12.297558204224886,
|
||||
"strict_clean": true,
|
||||
"c0_m_left_positive": 1.13,
|
||||
"c1_rad_left_positive": {
|
||||
"v5": 0.21050000000000002,
|
||||
"p_025": 0.22450000000000003,
|
||||
"p_025_delay_04": 0.18100000000000005
|
||||
},
|
||||
"p_rad_left_positive": 0.016087211202830076,
|
||||
"i_rad_left_positive": 0.02176835685651445
|
||||
},
|
||||
{
|
||||
"description": "Completed right-turn unwind",
|
||||
"route": "115",
|
||||
"time_s": 133.59461515599992,
|
||||
"desired_angle_deg": 5.989600658416748,
|
||||
"actual_angle_deg": 29.899999618530273,
|
||||
"speed_mph": 5.890273288393662,
|
||||
"strict_clean": false,
|
||||
"c0_m_left_positive": 0.1900000000000004,
|
||||
"c1_rad_left_positive": {
|
||||
"v5": 0.010500000000000065,
|
||||
"p_025": 0.0,
|
||||
"p_025_delay_04": -0.0030000000000000027
|
||||
},
|
||||
"p_rad_left_positive": -0.010158478980883956,
|
||||
"i_rad_left_positive": -0.0015653052344988395
|
||||
},
|
||||
{
|
||||
"description": "Large left overshoot; nearby driver input",
|
||||
"route": "114",
|
||||
"time_s": 168.957705716,
|
||||
"desired_angle_deg": 278.3920593261719,
|
||||
"actual_angle_deg": 450.20001220703125,
|
||||
"speed_mph": 7.027778014508665,
|
||||
"strict_clean": true,
|
||||
"c0_m_left_positive": 5.11,
|
||||
"c1_rad_left_positive": {
|
||||
"v5": 0.34099999999999997,
|
||||
"p_025": 0.2875,
|
||||
"p_025_delay_04": 0.3125
|
||||
},
|
||||
"p_rad_left_positive": -0.07201755233108997,
|
||||
"i_rad_left_positive": -0.13527950258838367
|
||||
},
|
||||
{
|
||||
"description": "Well-tracked left bend",
|
||||
"route": "114",
|
||||
"time_s": 473.085523785,
|
||||
"desired_angle_deg": 57.32655334472656,
|
||||
"actual_angle_deg": 56.29999923706055,
|
||||
"speed_mph": 27.33261651794824,
|
||||
"strict_clean": true,
|
||||
"c0_m_left_positive": 0.6699999999999999,
|
||||
"c1_rad_left_positive": {
|
||||
"v5": 0.1855,
|
||||
"p_025": 0.18500000000000005,
|
||||
"p_025_delay_04": 0.15900000000000003
|
||||
},
|
||||
"p_rad_left_positive": 0.0007143015310955292,
|
||||
"i_rad_left_positive": 0.022011912629614844
|
||||
},
|
||||
{
|
||||
"description": "Hanging right exit",
|
||||
"route": "113",
|
||||
"time_s": 481.56693996600006,
|
||||
"desired_angle_deg": -7.566320896148682,
|
||||
"actual_angle_deg": -94.4000015258789,
|
||||
"speed_mph": 11.959057581279636,
|
||||
"strict_clean": true,
|
||||
"c0_m_left_positive": -0.5800000000000001,
|
||||
"c1_rad_left_positive": {
|
||||
"v5": 0.0645,
|
||||
"p_025": 0.08750000000000002,
|
||||
"p_025_delay_04": 0.034499999999999975
|
||||
},
|
||||
"p_rad_left_positive": 0.03597008844371885,
|
||||
"i_rad_left_positive": 0.06765882642510383
|
||||
}
|
||||
],
|
||||
"ordinary_cohort": "Existing interval-clean angle mask, valid replay and feedback enabled, absolute desired wheel angle <30 degrees.",
|
||||
"weighting": "Extracted interval duration weights for seconds and mean absolute command changes; percentiles are cycle-weighted.",
|
||||
"ordinary_clean_seconds": 2740.4427841649945,
|
||||
"ordinary_mean_abs_c1_change_by_setting_rad": [
|
||||
{
|
||||
"kp": 0.0,
|
||||
"delay_s": 0.0,
|
||||
"mean_abs_delta_rad": 0.0
|
||||
},
|
||||
{
|
||||
"kp": 0.1,
|
||||
"delay_s": 0.0,
|
||||
"mean_abs_delta_rad": 0.0003946011107110312
|
||||
},
|
||||
{
|
||||
"kp": 0.25,
|
||||
"delay_s": 0.0,
|
||||
"mean_abs_delta_rad": 0.0010009008647461168
|
||||
},
|
||||
{
|
||||
"kp": 0.5,
|
||||
"delay_s": 0.0,
|
||||
"mean_abs_delta_rad": 0.001961192031401653
|
||||
},
|
||||
{
|
||||
"kp": 0.0,
|
||||
"delay_s": 0.2,
|
||||
"mean_abs_delta_rad": 0.006131012841938232
|
||||
},
|
||||
{
|
||||
"kp": 0.1,
|
||||
"delay_s": 0.2,
|
||||
"mean_abs_delta_rad": 0.006179941022213607
|
||||
},
|
||||
{
|
||||
"kp": 0.25,
|
||||
"delay_s": 0.2,
|
||||
"mean_abs_delta_rad": 0.006305419932002006
|
||||
},
|
||||
{
|
||||
"kp": 0.5,
|
||||
"delay_s": 0.2,
|
||||
"mean_abs_delta_rad": 0.006643212063186747
|
||||
},
|
||||
{
|
||||
"kp": 0.0,
|
||||
"delay_s": 0.4,
|
||||
"mean_abs_delta_rad": 0.010964264260175235
|
||||
},
|
||||
{
|
||||
"kp": 0.1,
|
||||
"delay_s": 0.4,
|
||||
"mean_abs_delta_rad": 0.011070256870936306
|
||||
},
|
||||
{
|
||||
"kp": 0.25,
|
||||
"delay_s": 0.4,
|
||||
"mean_abs_delta_rad": 0.011255438766715855
|
||||
},
|
||||
{
|
||||
"kp": 0.5,
|
||||
"delay_s": 0.4,
|
||||
"mean_abs_delta_rad": 0.01162257561022028
|
||||
}
|
||||
],
|
||||
"totals": {
|
||||
"cycles": 677871,
|
||||
"candidate_updates": 8134452,
|
||||
"sweep_can_round_trips": 8134452,
|
||||
"selected_can_round_trips": 677871,
|
||||
"can_round_trips_including_stress": 9012323
|
||||
},
|
||||
"stress": {
|
||||
"cycles": 200000,
|
||||
"gain": 0.25,
|
||||
"seed": 20260913,
|
||||
"mirrored_updates": 200000,
|
||||
"zero_gain_exact_v5_comparisons": 200000,
|
||||
"can_round_trips": 200000,
|
||||
"baseline_revision": "22d188776cb557acea459a1fca70812bdb2df46c",
|
||||
"baseline_source_sha256": "2ceb4cd8717bb3325f9b22189c78605ad5d42a1061dec9ca2e4dbb90fd256d1e",
|
||||
"release_cycles": 91,
|
||||
"calibration_approved": false,
|
||||
"checks": "Independent scalar PI arithmetic, combined anti-windup, mirror symmetry, slew/amplitude, driver/PSCM gates, resets, zero-P v5 parity and CAN.",
|
||||
"scope": "Check PI arithmetic and CAN invariants without a model of vehicle response.",
|
||||
"source_sha256": {
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/pi_stress.py": "07f3bd99c56e2184ba3402d7b2f324506ac0b786c7a15e422e9e2392c934544a",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/openpilot/selfdrive/controls/lib/ford_model_action.py": "8eaf242a8627c398b732e9c185527640b2ae39b4726cbb138c0a10490a4b890f"
|
||||
},
|
||||
"formatting_only_final_source_ast_identical": true,
|
||||
"final_script_sha256": "cff014beda81b0bfbaabd7219b8e2848464c5c45ac779669f80084d5be347a06"
|
||||
},
|
||||
"tests": {
|
||||
"passed": 776,
|
||||
"skipped": 178,
|
||||
"subtests_passed": 9145,
|
||||
"log_sha256": "5b9274e2557f41b630ad285b0426c916835c2997fc7f5c4fcad91aa4c6f64243",
|
||||
"ruff": "passed",
|
||||
"controller_ty": "passed",
|
||||
"settings_compiler": "passed"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
# Ford changed-request correction release
|
||||
|
||||
The Chestnut/Tee Time routes 112 and 113 ran `17a86842f` (C1 feedback v3).
|
||||
In route 113, an increasing turn request remained below its base C1 because
|
||||
negative correction from an earlier oversteer episode took time to return to
|
||||
zero. The existing reversal release did not apply: requested and measured
|
||||
curvature were already in the same turn direction.
|
||||
|
||||
Version `model-action-c1-feedback-v4` retires a bounded amount of correction
|
||||
when a changed request and measured error both oppose that correction. This
|
||||
addresses software command delay. It does not establish improved wheel tracking
|
||||
or fix all the recorded hanging exits.
|
||||
|
||||
## Rule
|
||||
|
||||
On a fresh steering measurement, evaluate the previous selected curvature and
|
||||
the current selected curvature using today's existing heading reference:
|
||||
|
||||
```text
|
||||
distance = max(7 m, speed * 1 s)
|
||||
change = clip(distance * desired, -0.5, 0.5)
|
||||
- clip(distance * previous_desired, -0.5, 0.5)
|
||||
error = desired - measured
|
||||
```
|
||||
|
||||
Retirement requires all of:
|
||||
|
||||
- Feedback enabled and a previous feedback request available.
|
||||
- Heading change at least one existing C1 DBC step (0.0005 rad).
|
||||
- Change and current error agree in direction.
|
||||
- Stored correction opposes that direction.
|
||||
- The magnitude of `distance * error` is at least the correction magnitude.
|
||||
|
||||
Move the correction toward zero by at most the heading change, without crossing
|
||||
zero. Then run the existing reversal release, elapsed-distance integration,
|
||||
PSCM arbitration and final output slew. The mismatch requirement is an
|
||||
engineering guard using the existing reference distance; it is not a fitted
|
||||
PSCM response threshold or proof of stability. It protects a larger learned
|
||||
correction from small target/measurement noise. There is no new tunable strength
|
||||
multiplier, and the existing 1:1 feedback-strength choice remains.
|
||||
|
||||
The last selected curvature adds one control state. Duplicate steering samples
|
||||
do not advance this history or retire correction; the next fresh sample uses
|
||||
the net request change. A speed change alone cannot cause retirement because
|
||||
both requests are evaluated at the same current speed. Invalid input and
|
||||
disengagement reset the history. Driver override clears correction and prevents
|
||||
a pending request change from being applied later.
|
||||
|
||||
C0 mapping and overflow, C2/C3 zeroing, amplitude/slew limits, sender cadence and
|
||||
all input/driver/PSCM gates remain unchanged. Toggle off still selects upstream
|
||||
Ford control on every platform. The existing default-off toggle selects v4 on
|
||||
Ford CAN FD vehicles. `request_release` logs signed radians retired on that
|
||||
cycle; periodic diagnostics do not capture every individual retirement.
|
||||
|
||||
## Evidence and limits
|
||||
|
||||
The regression command `python -m pytest -q -p no:cacheprovider
|
||||
openpilot/selfdrive/controls/tests/test_ford_model_action_request_release.py`
|
||||
initially returned **4 failed** on v3. It checks that an obsolete correction no
|
||||
longer delays a changed same-direction turn or unwind after the output slew
|
||||
has time to respond. Expanded cases cover small noise, matched tracking,
|
||||
insufficient error, speed-only changes, clipped base requests, duplicate
|
||||
measurements, override and reset. Integration tests exercise actual controlsd
|
||||
selection/limiting, both model and maneuver references, Float32 publication
|
||||
and Ford CAN packing.
|
||||
|
||||
Frozen replay compares v4 with the deployed v3 on the same recorded model,
|
||||
measurement, driver and PSCM inputs:
|
||||
|
||||
| Route | Control cycles | Retirement cycles | Largest C1 difference |
|
||||
| --- | ---: | ---: | ---: |
|
||||
| 112 | 108,971 | 414 | 0.0235 rad |
|
||||
| 113 | 49,614 | 119 | 0.0275 rad |
|
||||
| Historical b9 | 90,774 | 440 | 0.0350 rad |
|
||||
|
||||
Activation and C0 are identical on every replay cycle. C2/C3 remain zero.
|
||||
Retirement changes subsequent correction history, so command differences can
|
||||
persist after a retirement cycle. In frozen measurements the vehicle cannot
|
||||
react to those differences. Command differences occur in ordinary bends too;
|
||||
these tests do not establish unchanged real-world centering or stability.
|
||||
|
||||
In route 113, segment 3, old opposing correction reaches zero at **3:18.630**
|
||||
instead of **3:19.253**: **0.623 s earlier**. At 3:18.649, C1 magnitude is
|
||||
0.319 rad instead of 0.2925 rad. The PSCM limit flag still inhibits additional
|
||||
outward integration; retiring opposing correction cannot create new stored
|
||||
outward demand through that gate.
|
||||
|
||||
The route 113 exit at 8:01.567 has **identical C1** in this replay. Route 112's
|
||||
11:40.555 overshoot changes C1 by only 0.0015 rad, slightly later in the unwind
|
||||
direction on the frozen history. These are material limits: the change does
|
||||
not solve those exits. C0's contribution and physical PSCM response remain
|
||||
unresolved. No counterfactual wheel-angle or tracking-error score is reported.
|
||||
|
||||
The combined suite passes **717 tests and 9,145 subtests**, with 178 inherited
|
||||
or unsupported safety-test skips. Feedback stress and zero-error stress cover
|
||||
200,000 cycles each; the latter also covers 18,138 field-boundary cases.
|
||||
Together with the three route replays, these verify **667,497 Float32/CAN round
|
||||
trips**, separately from the integration suite. Stress compares each step to
|
||||
v3 after only the declared retirement and checks sign symmetry, bounds, slew,
|
||||
resets, arbitration and correction direction. Ruff, the controller Ty check
|
||||
and settings compilation pass. Numerical records are in
|
||||
`ford_c1_request_release_validation.json`.
|
||||
|
||||
## Reproduction
|
||||
|
||||
Use the project's native Python dependencies and unchanged opendbc revision
|
||||
`64aa61b9b3fd26e70a7caa915acab207ff3cd64a`. Route commands require the full-rlog
|
||||
extracts (`route.npz`, `model_paths.npz`, `metadata.json`) identified by the
|
||||
validation hashes. No original logs are modified.
|
||||
|
||||
```sh
|
||||
export PYTHONDONTWRITEBYTECODE=1
|
||||
export PYTHONPATH=.:opendbc_repo
|
||||
# Optional writable roots for the tests' temporary parameter stores and logs:
|
||||
export PARAMS_ROOT=/tmp/ford-v4-test-params
|
||||
export LOG_ROOT=/tmp/ford-v4-test-logs
|
||||
python -m pytest -q -p no:cacheprovider openpilot/selfdrive/controls/tests/test_ford_*.py tools/ford_pscm_lab openpilot/selfdrive/car/tests/test_ford_pscm_status.py openpilot/sunnypilot/sunnylink/tests openpilot/common/tests/test_params.py opendbc_repo/opendbc/car/ford/tests/test_ford.py opendbc_repo/opendbc/safety/tests/test_ford.py
|
||||
python -m tools.ford_pscm_lab.feedback_replay stress --cycles 200000 --output .cache/ford_v4/stress.json
|
||||
python -m tools.ford_pscm_lab.stress_model_action --cycles 200000 --seed 20260912 --opendbc-revision 64aa61b9b3fd26e70a7caa915acab207ff3cd64a --output .cache/ford_v4/zero_error.json
|
||||
python -m tools.ford_pscm_lab.feedback_replay route .cache/ford_route112 --baseline 17a86842f --output .cache/ford_v4/route112
|
||||
python -m tools.ford_pscm_lab.feedback_replay route .cache/ford_route113 --baseline 17a86842f --output .cache/ford_v4/route113
|
||||
python -m tools.ford_pscm_lab.feedback_replay route .cache/ford_routeb9 --baseline 17a86842f --output .cache/ford_v4/routeb9
|
||||
```
|
||||
|
||||
Publication time approximates the computation clock; full SubMaster health is
|
||||
not reconstructable. These routes have no selected maneuver-plan publications;
|
||||
that source is covered by integration tests. No device build, boot, installation
|
||||
or physical steering test is performed offline.
|
||||
@@ -0,0 +1,214 @@
|
||||
{
|
||||
"hypothesis": "model-action-c1-feedback-v4",
|
||||
"baseline_revision": "17a86842f97f65216443a5d89648c8ace8518758",
|
||||
"scope": "Software command delay and invariants only; no counterfactual wheel motion or proven physical tracking improvement. Hanging exits remain unresolved.",
|
||||
"calibration_approved": false,
|
||||
"tests": {
|
||||
"passed": 717,
|
||||
"subtests_passed": 9145,
|
||||
"skipped": 178,
|
||||
"log_sha256": "1864f8618b9d788f67e57766cf7b9ab9eda8e98a2ad0caf1c668bb9936b6eb7d",
|
||||
"initial_regression": "4 failures on v3; same-turn command delay tests pass on v4",
|
||||
"environment": "PARAMS_ROOT and LOG_ROOT point to dedicated temporary directories; initial sandbox path failures resolved without changing tests."
|
||||
},
|
||||
"feedback_stress": {
|
||||
"cycles": 200000,
|
||||
"mirrored_updates": 200000,
|
||||
"can_round_trips": 200000,
|
||||
"carryover_release_count": 165,
|
||||
"baseline_revision": "17a86842f97f65216443a5d89648c8ace8518758",
|
||||
"baseline_source_sha256": "167ae5a01fdd7ea014e6ad3fe9d0b6e31c67de8ba057ec5ecf18ab38fc16353f",
|
||||
"request_release_cycles": 20454,
|
||||
"exact_unchanged_state_and_commands_without_request_release": 179546,
|
||||
"exact_v3_match_after_only_declared_retirement": 200000,
|
||||
"checks": "Mirror symmetry, reset/override, amplitude, slew, correction bounds, bounded request retirement, carryover direction/confirmation, integration, PSCM limits, CAN.",
|
||||
"scope": "Numerical software invariants only; no model of vehicle motion.",
|
||||
"calibration_approved": false,
|
||||
"controller_sha256": "5673630d31910fcfa5a3cc9a8d533b6b8fe9a76e2627bf1f67b52b3550ec7442"
|
||||
},
|
||||
"zero_error_stress": {
|
||||
"seed": 20260912,
|
||||
"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,
|
||||
"direct_raw_float32_packing_matches_host_output": true,
|
||||
"max_continuous_step_c0_c1": [
|
||||
0.4000000000000019,
|
||||
0.05000000000000002
|
||||
],
|
||||
"calibration_approved": false,
|
||||
"scope": "Zero-error numerical construction: measured equals requested curvature. No PSCM response claims.",
|
||||
"opendbc_import_head": "64aa61b9b3fd26e70a7caa915acab207ff3cd64a",
|
||||
"source_sha256": {
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/stress_model_action.py": "cec2619285dd41274562ac035ee8ea0a389269a0c4ef1b62efa6252ad1a714aa",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/openpilot/selfdrive/controls/lib/ford_model_action.py": "1a4ce5f5f63b4d2f1f6e0537c9b2ca7c463ca44b427349d71d28fb8a1138b00f",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/model_action_replay.py": "af97c665f342c66b1be2502e188c63e6f3ee106d0a0d5e80997bc3040373ff9f"
|
||||
}
|
||||
},
|
||||
"replays": {
|
||||
"112": {
|
||||
"baseline_revision": "17a86842f",
|
||||
"baseline_source_sha256": "167ae5a01fdd7ea014e6ad3fe9d0b6e31c67de8ba057ec5ecf18ab38fc16353f",
|
||||
"calibration_approved": false,
|
||||
"cycles": 108971,
|
||||
"active_cycles": 91414,
|
||||
"validity_matches_baseline_exactly": true,
|
||||
"status_counts": {
|
||||
"inactive": 17557,
|
||||
"active": 91414
|
||||
},
|
||||
"c0_matches_baseline_exactly": true,
|
||||
"c0_changed_cycles": 0,
|
||||
"max_abs_c0_change_m": 0.0,
|
||||
"offset_overflow_seconds": 1.43945091800002,
|
||||
"max_abs_offset_overflow_target_m": 0.5727187991142273,
|
||||
"request_release_cycles": 414,
|
||||
"request_release_seconds": 4.192443282002046,
|
||||
"max_abs_request_release_rad": 0.008723706007003784,
|
||||
"feedback_enabled_seconds": 840.7664582650004,
|
||||
"pscm_limit_2_seconds": 14.441855805999936,
|
||||
"c1_changed_cycles": 58152,
|
||||
"max_abs_c1_change_rad": 0.023500000000000076,
|
||||
"max_abs_correction_rad": 0.205313389369823,
|
||||
"can_round_trips": 108971,
|
||||
"source_sha256": {
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route112/route.npz": "2b08a2fb636f7d14556d7df4035eafc1d1b97932237955528562a16db2b31d3e",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route112/model_paths.npz": "9837afe78aab4cad288cad98a595a5777fa8a66bb235986b1272a7f7c54e559a",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route112/metadata.json": "726d78a7e7aa45307dcfe27cb00775c20ecc9d54538eb7f26a0166fc216226ce",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/feedback_replay.py": "2e9ea03d4947c7bb3a02c864e0dbc7c9bf031af51dba5e44a8aaa137c3aac6b2",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/openpilot/selfdrive/controls/lib/ford_model_action.py": "5673630d31910fcfa5a3cc9a8d533b6b8fe9a76e2627bf1f67b52b3550ec7442"
|
||||
},
|
||||
"timing_limit": "Controls publication time proxies the computation clock; full SubMaster checks are unavailable.",
|
||||
"reference_limit": "Uses exact consumed model publication as reference; selected maneuver-plan messages are not reconstructed.",
|
||||
"targeted_points": [
|
||||
{
|
||||
"time_s": 700.5553145380001,
|
||||
"baseline_c0_c1": [
|
||||
-0.34999999999999964,
|
||||
0.010000000000000009
|
||||
],
|
||||
"candidate_c0_c1": [
|
||||
-0.34999999999999964,
|
||||
0.008500000000000008
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"113": {
|
||||
"baseline_revision": "17a86842f",
|
||||
"baseline_source_sha256": "167ae5a01fdd7ea014e6ad3fe9d0b6e31c67de8ba057ec5ecf18ab38fc16353f",
|
||||
"calibration_approved": false,
|
||||
"cycles": 49614,
|
||||
"active_cycles": 27207,
|
||||
"validity_matches_baseline_exactly": true,
|
||||
"status_counts": {
|
||||
"inactive": 22407,
|
||||
"active": 27207
|
||||
},
|
||||
"c0_matches_baseline_exactly": true,
|
||||
"c0_changed_cycles": 0,
|
||||
"max_abs_c0_change_m": 0.0,
|
||||
"offset_overflow_seconds": 2.7607263189997866,
|
||||
"max_abs_offset_overflow_target_m": 0.6065444126725197,
|
||||
"request_release_cycles": 119,
|
||||
"request_release_seconds": 1.235422420998475,
|
||||
"max_abs_request_release_rad": 0.009946223348379135,
|
||||
"feedback_enabled_seconds": 243.3033761190004,
|
||||
"pscm_limit_2_seconds": 14.003248144999816,
|
||||
"c1_changed_cycles": 17825,
|
||||
"max_abs_c1_change_rad": 0.027500000000000024,
|
||||
"max_abs_correction_rad": 0.16966817302181283,
|
||||
"can_round_trips": 49614,
|
||||
"source_sha256": {
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route113/route.npz": "774ca4a21b7113c2706d6130bc180c3216ea4833155300ab01e75b3486e36327",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route113/model_paths.npz": "93c41761eb85263f534f5371b905482cf7c948582eb1e9149966594be1d3768f",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route113/metadata.json": "1c0ca74dd48b90ab9d5444c5ca7f8aa9361700bbdf98bd5e853be50ad2895d7f",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/feedback_replay.py": "2e9ea03d4947c7bb3a02c864e0dbc7c9bf031af51dba5e44a8aaa137c3aac6b2",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/openpilot/selfdrive/controls/lib/ford_model_action.py": "5673630d31910fcfa5a3cc9a8d533b6b8fe9a76e2627bf1f67b52b3550ec7442"
|
||||
},
|
||||
"timing_limit": "Controls publication time proxies the computation clock; full SubMaster checks are unavailable.",
|
||||
"reference_limit": "Uses exact consumed model publication as reference; selected maneuver-plan messages are not reconstructed.",
|
||||
"old_correction_zero_s": 199.2531750590001,
|
||||
"new_correction_zero_s": 198.62974550599984,
|
||||
"earlier_correction_zero_s": 0.6234295530002782,
|
||||
"targeted_points": [
|
||||
{
|
||||
"time_s": 198.64943081699994,
|
||||
"baseline_c0_c1": [
|
||||
2.16,
|
||||
0.2925
|
||||
],
|
||||
"candidate_c0_c1": [
|
||||
2.16,
|
||||
0.319
|
||||
]
|
||||
},
|
||||
{
|
||||
"time_s": 481.56693996600006,
|
||||
"baseline_c0_c1": [
|
||||
0.5800000000000001,
|
||||
-0.0645
|
||||
],
|
||||
"candidate_c0_c1": [
|
||||
0.5800000000000001,
|
||||
-0.0645
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"b9": {
|
||||
"baseline_revision": "17a86842f",
|
||||
"baseline_source_sha256": "167ae5a01fdd7ea014e6ad3fe9d0b6e31c67de8ba057ec5ecf18ab38fc16353f",
|
||||
"calibration_approved": false,
|
||||
"cycles": 90774,
|
||||
"active_cycles": 86474,
|
||||
"validity_matches_baseline_exactly": true,
|
||||
"status_counts": {
|
||||
"inactive": 4300,
|
||||
"active": 86474
|
||||
},
|
||||
"c0_matches_baseline_exactly": true,
|
||||
"c0_changed_cycles": 0,
|
||||
"max_abs_c0_change_m": 0.0,
|
||||
"offset_overflow_seconds": 5.703841178999909,
|
||||
"max_abs_offset_overflow_target_m": 4.280821338295937,
|
||||
"request_release_cycles": 440,
|
||||
"request_release_seconds": 4.520361682000512,
|
||||
"max_abs_request_release_rad": 0.017186015844345093,
|
||||
"feedback_enabled_seconds": 816.0284774219999,
|
||||
"pscm_limit_2_seconds": 9.308613716000167,
|
||||
"c1_changed_cycles": 48629,
|
||||
"max_abs_c1_change_rad": 0.03500000000000003,
|
||||
"max_abs_correction_rad": 0.18457476562660308,
|
||||
"can_round_trips": 90774,
|
||||
"source_sha256": {
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_routeb9/route.npz": "b07c789d8155335f5d120d0262fced6e4d5803fe767b0ff49b6413dce4140b5c",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_routeb9/model_paths.npz": "6b1f87897c050273fdc05af051307a049b6fc3a93072e7cda1721195ce7c3861",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_routeb9/metadata.json": "9ce452220cab61b81883f32fc2fcaf5db6c78a674cb255a49cc77d5029580fee",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/feedback_replay.py": "cd76c6106b2e41e905b752f1638d5b3e0feaa10ec21e038268df33183a91c640",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/openpilot/selfdrive/controls/lib/ford_model_action.py": "1a4ce5f5f63b4d2f1f6e0537c9b2ca7c463ca44b427349d71d28fb8a1138b00f"
|
||||
},
|
||||
"timing_limit": "Controls publication time proxies the computation clock; full SubMaster checks are unavailable.",
|
||||
"reference_limit": "Uses exact consumed model publication as reference; selected maneuver-plan messages are not reconstructed."
|
||||
}
|
||||
},
|
||||
"float32_can_round_trips_excluding_integration_tests": 667497,
|
||||
"checks": {
|
||||
"ruff": true,
|
||||
"controller_ty": true,
|
||||
"settings_compilation": true,
|
||||
"toggle_off_upstream_integration": true
|
||||
},
|
||||
"final_source_sha256": {
|
||||
"openpilot/selfdrive/controls/lib/ford_model_action.py": "1a4ce5f5f63b4d2f1f6e0537c9b2ca7c463ca44b427349d71d28fb8a1138b00f",
|
||||
"openpilot/selfdrive/controls/tests/test_ford_model_action_request_release.py": "4b4a69d67eba0ff9d5db0a50a4f868c5eb5f7c8c79d70832500554febc7d28fe",
|
||||
"openpilot/selfdrive/controls/tests/test_ford_model_action.py": "7b2429a5c40e5067b8edea4c11e9cdd4c6271d09f7982e30126eb42b93425a50",
|
||||
"openpilot/selfdrive/controls/tests/test_ford_model_action_adapter.py": "5943a37f6e3865297ac543fb922a3c8b6e016c5af3eff589f518bd9615bbdb4f",
|
||||
"openpilot/selfdrive/controls/tests/test_ford_controlsd_logging.py": "df883702a847465815feb6c4fbf88130c27e69d7e3e256c9962d99a9738ee353",
|
||||
"tools/ford_pscm_lab/feedback_replay.py": "cd76c6106b2e41e905b752f1638d5b3e0feaa10ec21e038268df33183a91c640"
|
||||
},
|
||||
"source_note": "Controller comment/docstring cleanup followed feedback stress and routes 112/113. The recorded tested hashes are retained; b9 and zero-error stress record the final controller source. No executable controller change followed those runs."
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
# Desired-curvature C0 on continuous PI
|
||||
|
||||
This local candidate changes C0's reference from the live model path's lateral
|
||||
position at 7 m to a circular arc of the selected, upstream-limited desired
|
||||
curvature. It is based on v7 (`08b3a14ad`), with the same P=0.50/I=0.25 controller.
|
||||
No new gain, state, release condition, reference delay or output limit is added.
|
||||
The candidate is on `codex/ford-curvature-c0-trial`; this evaluation does not
|
||||
publish it over the v7 onroad branch.
|
||||
|
||||
For selected curvature k and the existing 7 m reference distance:
|
||||
|
||||
```text
|
||||
arc C0 = (1 − cos(7 × k)) / k, or 0 when k = 0
|
||||
≈ 24.5 × k for small curvature
|
||||
C0 target = clip(arc C0 + 7 × clipped-away base C1, −5.11, +5.11)
|
||||
```
|
||||
|
||||
The implementation uses the equivalent squared-sinc expression to avoid
|
||||
subtracting nearly equal floating-point numbers near zero. C0 keeps its 4 m/s
|
||||
output slew and Float32/CAN quantization. C1 keeps the existing mapping and PI
|
||||
law, ±0.5 rad bound and 0.5 rad/s slew. C2 and C3 stay zero.
|
||||
|
||||
This is a geometric reference choice, not a model of the PSCM. The arc starts
|
||||
at zero lateral position and heading. Independent live model-path position and
|
||||
heading are omitted, while selected curvature can still include the model's
|
||||
centering decision. Valid live model geometry remains a health gate. Short valid
|
||||
paths do not shorten the synthetic 7 m arc. Both C0 and C1 use the selected
|
||||
request, including the maneuver source when selected by controlsd.
|
||||
|
||||
## What the recorded routes show
|
||||
|
||||
All fourteen previous routes were replayed with v7 and this C0 replacement:
|
||||
Lightning 112–117, a0, a2, a5, a9, b8, b9, ca and Raptor 02. On all 1,578,250
|
||||
control cycles, C1, P, I, activation, feedforward, overflow and feedback/PSCM
|
||||
gates match exactly. The v7 baseline also reproduces its archived commands
|
||||
exactly. C0 matches the earlier isolated geometry experiment, but C1 no longer
|
||||
has the release rules that coupled it to C0 in that experiment.
|
||||
|
||||
Duration-weighted clean samples, grouped by requested steering-wheel angle:
|
||||
|
||||
| Absolute requested wheel angle | v7 mean absolute C0 | Curvature C0 | Reduction |
|
||||
| --- | ---: | ---: | ---: |
|
||||
| Under 5° | 0.0136 m | 0.0072 m | 46.7% |
|
||||
| 5–30° | 0.0965 m | 0.0608 m | 37.0% |
|
||||
| 30–90° | 0.5064 m | 0.2916 m | 42.4% |
|
||||
| At least 90° | 2.6314 m | 1.5282 m | 41.9% |
|
||||
|
||||
The clean cohort is 6,654.52 s with the existing quality/driver mask and margins,
|
||||
speed at least 2 m/s and replay feedback enabled. Only 36.20 s have a requested
|
||||
wheel angle of at least 90°. These are command magnitudes, not torque or tracking
|
||||
scores, and do not classify driver interventions as controller failures.
|
||||
|
||||
At route 117, 128.272 s (right entry), C0 changes from −2.51 m to −1.35 m while
|
||||
C1 remains −0.427 rad. At 142.670 s (request relaxing/reversing), C0 changes from
|
||||
+0.18 m to −0.02 m while C1 remains −0.0425 rad. Both comparisons are replayed
|
||||
on the same recorded vehicle motion. The new C0 follows the selected request
|
||||
more directly, but it supplies less C0 during the entry as well as the exit.
|
||||
|
||||
## Validation and interpretation
|
||||
|
||||
- 694 Ford/controlsd, tracked PSCM lab, Sunnylink, params, sender and safety
|
||||
tests pass; 9,145 subtests pass and 178 platform tests skip. Historical
|
||||
untracked offline experiment tests are outside this deployment suite.
|
||||
- The isolated two-controller replay performs 3,156,500 Float32/CAN checks.
|
||||
- Production selection/adapter replay exactly reproduces the isolated candidate
|
||||
on every route cycle, adding 1,578,250 Float32/CAN checks.
|
||||
- A 20,000-cycle scalar geometry/PI stress with mirrored and unrelated model
|
||||
paths adds 60,000 checks. Total: 4,794,750 CAN round trips.
|
||||
- Tests cover zero/tiny curvature, signs, circular geometry, short/malformed
|
||||
paths, selected maneuver requests, overflow, unwind, duplicate measurements,
|
||||
model/driver/PSCM gates, caps/slew and toggle-off upstream Ford fallback.
|
||||
- Ruff, production Ty and diff whitespace checks pass.
|
||||
|
||||
Software C1 parity does not guarantee identical physical unwind: changing C0
|
||||
changes the PSCM's input and therefore the vehicle response and future feedback.
|
||||
Smaller C0 is not established as better or worse tracking. No device build,
|
||||
boot or drive of this candidate is claimed. Collecting the promising v7 drive's
|
||||
logs before replacing it would preserve a useful comparison.
|
||||
|
||||
## Reproduction
|
||||
|
||||
Use the built cereal/opendbc environment and the same local route extracts:
|
||||
|
||||
```sh
|
||||
export PYTHONPATH=.:opendbc_repo:.cache/ford_v6/test_deps
|
||||
export PYTHONDONTWRITEBYTECODE=1
|
||||
export PARAMS_ROOT=/tmp/ford-c0-params
|
||||
export LOG_ROOT=/tmp/ford-c0-logs
|
||||
python -m tools.ford_pscm_lab.curvature_c0_v7_replay .cache/ford_route117 --output .cache/ford_curvature_c0_v7/117
|
||||
python -m tools.ford_pscm_lab.curvature_c0_validate --output .cache/ford_curvature_c0_v7/production --workers 4
|
||||
python -m tools.ford_pscm_lab.curvature_c0_production_stress --cycles 20000 --output .cache/ford_curvature_c0_v7/production_stress.json
|
||||
```
|
||||
|
||||
Repeat the first command for each label before validating all routes. Raptor
|
||||
uses input `.cache/ford_raptor_route02` and output label `raptor02`. The first
|
||||
replay loads isolated copies of pinned v7; the second tests this checkout's
|
||||
actual selector and adapter. The validation JSON records source and extract
|
||||
hashes, settings, example points and both sets of route reports. Older v7-only
|
||||
production validation commands should run from the v7 commit.
|
||||
@@ -0,0 +1,890 @@
|
||||
{
|
||||
"scope": "Curvature-derived C0 candidate on the continuous PI controller; fixed recorded motion, no physical tracking prediction.",
|
||||
"baseline_revision": "08b3a14ad46260fda0f8d3a1c2cee2d272504153",
|
||||
"branch": "codex/ford-curvature-c0-trial",
|
||||
"deployment_at_evaluation": "local candidate; hiimisaac-dev remains v7",
|
||||
"hypothesis": "model-action-curvature-c0-pi-v8",
|
||||
"source_sha256": "6f8c4a6f7fa54fa2e23f32f1e83ace83da5f0b6f45011941f1902a1dae778ffc",
|
||||
"kp": 0.5,
|
||||
"ki": 0.25,
|
||||
"station_m": 7.0,
|
||||
"cycles": 1578250,
|
||||
"route_count": 14,
|
||||
"same_c1_and_integral_on_every_recorded_cycle": true,
|
||||
"baseline_exactly_matches_archived_v7": true,
|
||||
"c0_commands_match_prior_geometry_experiment": true,
|
||||
"can_round_trips": 4794750,
|
||||
"tests": {
|
||||
"passed": 694,
|
||||
"skipped": 178,
|
||||
"subtests_passed": 9145,
|
||||
"scope": "Same Ford controls, tracked PSCM lab, car status, Sunnylink, params, Ford car and safety suite as v7; historical untracked experiments excluded."
|
||||
},
|
||||
"ruff": "pass",
|
||||
"ty_production": "pass",
|
||||
"bins": [
|
||||
{
|
||||
"requested_wheel_angle_degrees": "0-5",
|
||||
"seconds": 4863.242382185041,
|
||||
"mean_abs_c0_m": [
|
||||
0.013572257514406704,
|
||||
0.007239040142159917
|
||||
],
|
||||
"reduction_pct": 46.662962042417725
|
||||
},
|
||||
{
|
||||
"requested_wheel_angle_degrees": "5-30",
|
||||
"seconds": 1576.952442509967,
|
||||
"mean_abs_c0_m": [
|
||||
0.09652124113032315,
|
||||
0.06077627429873566
|
||||
],
|
||||
"reduction_pct": 37.03326481610879
|
||||
},
|
||||
{
|
||||
"requested_wheel_angle_degrees": "30-90",
|
||||
"seconds": 178.12595563801986,
|
||||
"mean_abs_c0_m": [
|
||||
0.5064290157685561,
|
||||
0.2916269430470148
|
||||
],
|
||||
"reduction_pct": 42.4150406144399
|
||||
},
|
||||
{
|
||||
"requested_wheel_angle_degrees": "90-inf",
|
||||
"seconds": 36.19569558700505,
|
||||
"mean_abs_c0_m": [
|
||||
2.6314148997254545,
|
||||
1.528200796263899
|
||||
],
|
||||
"reduction_pct": 41.92474944094366
|
||||
}
|
||||
],
|
||||
"examples": [
|
||||
{
|
||||
"route": "116",
|
||||
"time": 112.00019806100002,
|
||||
"desired_angle": 59.83296203613281,
|
||||
"actual_angle": 60.900001525878906,
|
||||
"c0_left_positive": [
|
||||
0.7599999999999998,
|
||||
0.33999999999999986
|
||||
],
|
||||
"c1_both_left_positive": 0.13550000000000006,
|
||||
"clean": true
|
||||
},
|
||||
{
|
||||
"route": "117",
|
||||
"time": 128.2721161039999,
|
||||
"desired_angle": -226.72189331054688,
|
||||
"actual_angle": -185.1999969482422,
|
||||
"c0_left_positive": [
|
||||
-2.5100000000000002,
|
||||
-1.35
|
||||
],
|
||||
"c1_both_left_positive": -0.427,
|
||||
"clean": true
|
||||
},
|
||||
{
|
||||
"route": "116",
|
||||
"time": 673.6933639450001,
|
||||
"desired_angle": -152.83456420898438,
|
||||
"actual_angle": -272.1000061035156,
|
||||
"c0_left_positive": [
|
||||
-2.21,
|
||||
-0.9199999999999999
|
||||
],
|
||||
"c1_both_left_positive": -0.183,
|
||||
"clean": true
|
||||
},
|
||||
{
|
||||
"route": "117",
|
||||
"time": 142.52000677500018,
|
||||
"desired_angle": 6.6218461990356445,
|
||||
"actual_angle": 40.599998474121094,
|
||||
"c0_left_positive": [
|
||||
0.2599999999999998,
|
||||
0.040000000000000036
|
||||
],
|
||||
"c1_both_left_positive": -0.024499999999999966,
|
||||
"clean": true
|
||||
},
|
||||
{
|
||||
"route": "117",
|
||||
"time": 142.66993146999994,
|
||||
"desired_angle": -2.186957597732544,
|
||||
"actual_angle": 28.5,
|
||||
"c0_left_positive": [
|
||||
0.17999999999999972,
|
||||
-0.019999999999999574
|
||||
],
|
||||
"c1_both_left_positive": -0.04249999999999998,
|
||||
"clean": true
|
||||
}
|
||||
],
|
||||
"replay_reports": [
|
||||
{
|
||||
"scope": "Circular-arc C0 experiment against the pinned continuous PI v7 controller.\n\nBoth controllers run on identical recorded motion. Replace only the encoder's\nC0 target, retaining path validity, C1 overflow, continuous PI feedback and\noutput limits. No onroad selector or production module is modified.\n",
|
||||
"baseline_revision": "08b3a14ad46260fda0f8d3a1c2cee2d272504153",
|
||||
"baseline_source_sha256": "a899b8595e903d6fc4401392a8d123ef77725f6d6d7fa10392a7b3fe72e732b8",
|
||||
"opendbc_revision": "64aa61b9b3fd26e70a7caa915acab207ff3cd64a",
|
||||
"proportional_gain": 0.5,
|
||||
"integral_gain": 0.25,
|
||||
"station_m": 7.0,
|
||||
"cycles": 108971,
|
||||
"controller_updates": 217942,
|
||||
"active_cycles_per_controller": 91414,
|
||||
"can_round_trips": 217942,
|
||||
"same_activation_p_feedforward_overflow_and_feedback_gates": true,
|
||||
"changed_c1_cycles": 0,
|
||||
"max_abs_c1_change_rad": 0.0,
|
||||
"changed_integral_cycles": 0,
|
||||
"pscm_status_fresh_active_seconds": 919.4963447830002,
|
||||
"clean_seconds": 678.5721400560004,
|
||||
"source_sha256": {
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route112/route.npz": "2b08a2fb636f7d14556d7df4035eafc1d1b97932237955528562a16db2b31d3e",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route112/model_paths.npz": "9837afe78aab4cad288cad98a595a5777fa8a66bb235986b1272a7f7c54e559a",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route112/metadata.json": "726d78a7e7aa45307dcfe27cb00775c20ecc9d54538eb7f26a0166fc216226ce",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route112/angles.npz": "9429813237b4988dd99a1461d1a9bd6f6d3056693a93a5dd1ec63567d19f2aa1",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/curvature_c0_v7_replay.py": "fcd8a10db3d68024dbc836dce1b4c9a459912837b1a4234f61b2dd508c6d5e9f"
|
||||
},
|
||||
"limitations": [
|
||||
"No new steering trace or physical tracking score: recorded motion stays fixed.",
|
||||
"The baseline is v7 on every route, regardless of its recorded controller version.",
|
||||
"C1 and integral state match exactly on fixed recorded motion; physical feedback may differ.",
|
||||
"Publication time proxies computation time; full SubMaster state is unavailable.",
|
||||
"The synthetic arc keeps the original model-health gates for a controlled comparison."
|
||||
]
|
||||
},
|
||||
{
|
||||
"scope": "Circular-arc C0 experiment against the pinned continuous PI v7 controller.\n\nBoth controllers run on identical recorded motion. Replace only the encoder's\nC0 target, retaining path validity, C1 overflow, continuous PI feedback and\noutput limits. No onroad selector or production module is modified.\n",
|
||||
"baseline_revision": "08b3a14ad46260fda0f8d3a1c2cee2d272504153",
|
||||
"baseline_source_sha256": "a899b8595e903d6fc4401392a8d123ef77725f6d6d7fa10392a7b3fe72e732b8",
|
||||
"opendbc_revision": "64aa61b9b3fd26e70a7caa915acab207ff3cd64a",
|
||||
"proportional_gain": 0.5,
|
||||
"integral_gain": 0.25,
|
||||
"station_m": 7.0,
|
||||
"cycles": 49614,
|
||||
"controller_updates": 99228,
|
||||
"active_cycles_per_controller": 27207,
|
||||
"can_round_trips": 99228,
|
||||
"same_activation_p_feedforward_overflow_and_feedback_gates": true,
|
||||
"changed_c1_cycles": 0,
|
||||
"max_abs_c1_change_rad": 0.0,
|
||||
"changed_integral_cycles": 0,
|
||||
"pscm_status_fresh_active_seconds": 273.884368861,
|
||||
"clean_seconds": 195.66672679699968,
|
||||
"source_sha256": {
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route113/route.npz": "774ca4a21b7113c2706d6130bc180c3216ea4833155300ab01e75b3486e36327",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route113/model_paths.npz": "93c41761eb85263f534f5371b905482cf7c948582eb1e9149966594be1d3768f",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route113/metadata.json": "1c0ca74dd48b90ab9d5444c5ca7f8aa9361700bbdf98bd5e853be50ad2895d7f",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route113/angles.npz": "2f280a1b5973901d878f1e36564d6e7ae6d817cac176149f2cb898ab7591d65b",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/curvature_c0_v7_replay.py": "fcd8a10db3d68024dbc836dce1b4c9a459912837b1a4234f61b2dd508c6d5e9f"
|
||||
},
|
||||
"limitations": [
|
||||
"No new steering trace or physical tracking score: recorded motion stays fixed.",
|
||||
"The baseline is v7 on every route, regardless of its recorded controller version.",
|
||||
"C1 and integral state match exactly on fixed recorded motion; physical feedback may differ.",
|
||||
"Publication time proxies computation time; full SubMaster state is unavailable.",
|
||||
"The synthetic arc keeps the original model-health gates for a controlled comparison."
|
||||
]
|
||||
},
|
||||
{
|
||||
"scope": "Circular-arc C0 experiment against the pinned continuous PI v7 controller.\n\nBoth controllers run on identical recorded motion. Replace only the encoder's\nC0 target, retaining path validity, C1 overflow, continuous PI feedback and\noutput limits. No onroad selector or production module is modified.\n",
|
||||
"baseline_revision": "08b3a14ad46260fda0f8d3a1c2cee2d272504153",
|
||||
"baseline_source_sha256": "a899b8595e903d6fc4401392a8d123ef77725f6d6d7fa10392a7b3fe72e732b8",
|
||||
"opendbc_revision": "64aa61b9b3fd26e70a7caa915acab207ff3cd64a",
|
||||
"proportional_gain": 0.5,
|
||||
"integral_gain": 0.25,
|
||||
"station_m": 7.0,
|
||||
"cycles": 61027,
|
||||
"controller_updates": 122054,
|
||||
"active_cycles_per_controller": 48419,
|
||||
"can_round_trips": 122054,
|
||||
"same_activation_p_feedforward_overflow_and_feedback_gates": true,
|
||||
"changed_c1_cycles": 0,
|
||||
"max_abs_c1_change_rad": 0.0,
|
||||
"changed_integral_cycles": 0,
|
||||
"pscm_status_fresh_active_seconds": 487.557504183,
|
||||
"clean_seconds": 290.3177928490001,
|
||||
"source_sha256": {
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route114/route.npz": "83524f07d61104b84b004ddc46bb751ca7f61da67798aa47db56d307765f5b3e",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route114/model_paths.npz": "14d14362d1a3e46398edd8e22b7cc4e36277a7596a73f546192d0e14c6642b07",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route114/metadata.json": "ec677b9275c1707e477ebd0ffa235d49b5ec63a1ee717b16040c3acdbcd3bdc0",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route114/angles.npz": "63aa2ff6a21e8613519bda476690e2eda6f42cb5dcf108606bc6072c9e29408d",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/curvature_c0_v7_replay.py": "fcd8a10db3d68024dbc836dce1b4c9a459912837b1a4234f61b2dd508c6d5e9f"
|
||||
},
|
||||
"limitations": [
|
||||
"No new steering trace or physical tracking score: recorded motion stays fixed.",
|
||||
"The baseline is v7 on every route, regardless of its recorded controller version.",
|
||||
"C1 and integral state match exactly on fixed recorded motion; physical feedback may differ.",
|
||||
"Publication time proxies computation time; full SubMaster state is unavailable.",
|
||||
"The synthetic arc keeps the original model-health gates for a controlled comparison."
|
||||
]
|
||||
},
|
||||
{
|
||||
"scope": "Circular-arc C0 experiment against the pinned continuous PI v7 controller.\n\nBoth controllers run on identical recorded motion. Replace only the encoder's\nC0 target, retaining path validity, C1 overflow, continuous PI feedback and\noutput limits. No onroad selector or production module is modified.\n",
|
||||
"baseline_revision": "08b3a14ad46260fda0f8d3a1c2cee2d272504153",
|
||||
"baseline_source_sha256": "a899b8595e903d6fc4401392a8d123ef77725f6d6d7fa10392a7b3fe72e732b8",
|
||||
"opendbc_revision": "64aa61b9b3fd26e70a7caa915acab207ff3cd64a",
|
||||
"proportional_gain": 0.5,
|
||||
"integral_gain": 0.25,
|
||||
"station_m": 7.0,
|
||||
"cycles": 40037,
|
||||
"controller_updates": 80074,
|
||||
"active_cycles_per_controller": 33976,
|
||||
"can_round_trips": 80074,
|
||||
"same_activation_p_feedforward_overflow_and_feedback_gates": true,
|
||||
"changed_c1_cycles": 0,
|
||||
"max_abs_c1_change_rad": 0.0,
|
||||
"changed_integral_cycles": 0,
|
||||
"pscm_status_fresh_active_seconds": 341.89711117800005,
|
||||
"clean_seconds": 176.48340830000006,
|
||||
"source_sha256": {
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route115/route.npz": "e0df32d9e80f1c6b7d56327b37cf07af60070ffc212b8d111e343306148bff23",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route115/model_paths.npz": "52f3ed9e188e4947618a57a3ed872fd1966a887fe1014999df741553b6c13bc0",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route115/metadata.json": "d3c73035bad8eb5059e07962fd274c19cb70c8952b6f1514835d312d08b87a16",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route115/angles.npz": "71415b0f49efbfeda495ae52bde4beb7e16f31a1f5570af515a365e756056d3b",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/curvature_c0_v7_replay.py": "fcd8a10db3d68024dbc836dce1b4c9a459912837b1a4234f61b2dd508c6d5e9f"
|
||||
},
|
||||
"limitations": [
|
||||
"No new steering trace or physical tracking score: recorded motion stays fixed.",
|
||||
"The baseline is v7 on every route, regardless of its recorded controller version.",
|
||||
"C1 and integral state match exactly on fixed recorded motion; physical feedback may differ.",
|
||||
"Publication time proxies computation time; full SubMaster state is unavailable.",
|
||||
"The synthetic arc keeps the original model-health gates for a controlled comparison."
|
||||
]
|
||||
},
|
||||
{
|
||||
"scope": "Circular-arc C0 experiment against the pinned continuous PI v7 controller.\n\nBoth controllers run on identical recorded motion. Replace only the encoder's\nC0 target, retaining path validity, C1 overflow, continuous PI feedback and\noutput limits. No onroad selector or production module is modified.\n",
|
||||
"baseline_revision": "08b3a14ad46260fda0f8d3a1c2cee2d272504153",
|
||||
"baseline_source_sha256": "a899b8595e903d6fc4401392a8d123ef77725f6d6d7fa10392a7b3fe72e732b8",
|
||||
"opendbc_revision": "64aa61b9b3fd26e70a7caa915acab207ff3cd64a",
|
||||
"proportional_gain": 0.5,
|
||||
"integral_gain": 0.25,
|
||||
"station_m": 7.0,
|
||||
"cycles": 68793,
|
||||
"controller_updates": 137586,
|
||||
"active_cycles_per_controller": 52456,
|
||||
"can_round_trips": 137586,
|
||||
"same_activation_p_feedforward_overflow_and_feedback_gates": true,
|
||||
"changed_c1_cycles": 0,
|
||||
"max_abs_c1_change_rad": 0.0,
|
||||
"changed_integral_cycles": 0,
|
||||
"pscm_status_fresh_active_seconds": 527.966444073,
|
||||
"clean_seconds": 218.019997628,
|
||||
"source_sha256": {
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route116/route.npz": "13df1d49edd0c92bd4a4236c766ee76baa6dad477824b9de4b79927c98410394",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route116/model_paths.npz": "85d1112b5e89340e8a955b7fe4040b3135cf1c91a54ff756826c6119cc9cc01d",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route116/metadata.json": "d52fe63d013fa546524ad9522a26943d9372730d397d92d75dfd9d75c63ce766",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route116/angles.npz": "68e699e01c3ea291e6602ee53edf9cee27c0b461e16c57c3acc0ad12bf29b2aa",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/curvature_c0_v7_replay.py": "fcd8a10db3d68024dbc836dce1b4c9a459912837b1a4234f61b2dd508c6d5e9f"
|
||||
},
|
||||
"limitations": [
|
||||
"No new steering trace or physical tracking score: recorded motion stays fixed.",
|
||||
"The baseline is v7 on every route, regardless of its recorded controller version.",
|
||||
"C1 and integral state match exactly on fixed recorded motion; physical feedback may differ.",
|
||||
"Publication time proxies computation time; full SubMaster state is unavailable.",
|
||||
"The synthetic arc keeps the original model-health gates for a controlled comparison."
|
||||
]
|
||||
},
|
||||
{
|
||||
"scope": "Circular-arc C0 experiment against the pinned continuous PI v7 controller.\n\nBoth controllers run on identical recorded motion. Replace only the encoder's\nC0 target, retaining path validity, C1 overflow, continuous PI feedback and\noutput limits. No onroad selector or production module is modified.\n",
|
||||
"baseline_revision": "08b3a14ad46260fda0f8d3a1c2cee2d272504153",
|
||||
"baseline_source_sha256": "a899b8595e903d6fc4401392a8d123ef77725f6d6d7fa10392a7b3fe72e732b8",
|
||||
"opendbc_revision": "64aa61b9b3fd26e70a7caa915acab207ff3cd64a",
|
||||
"proportional_gain": 0.5,
|
||||
"integral_gain": 0.25,
|
||||
"station_m": 7.0,
|
||||
"cycles": 27301,
|
||||
"controller_updates": 54602,
|
||||
"active_cycles_per_controller": 15841,
|
||||
"can_round_trips": 54602,
|
||||
"same_activation_p_feedforward_overflow_and_feedback_gates": true,
|
||||
"changed_c1_cycles": 0,
|
||||
"max_abs_c1_change_rad": 0.0,
|
||||
"changed_integral_cycles": 0,
|
||||
"pscm_status_fresh_active_seconds": 159.4681704720001,
|
||||
"clean_seconds": 74.58932843500065,
|
||||
"source_sha256": {
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route117/route.npz": "65e4ea6c76dce73018b636479b3777116f1e58bb9813e6c1a7b79018b219140e",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route117/model_paths.npz": "079f6003968287530e1ed6f2243c727e80c2ef3d16fb8a7d945d7eca744d54e7",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route117/metadata.json": "348396b3059c5f54d7938cd3acf2bb268a813b47ff47561da7a94cb75c3f3114",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route117/angles.npz": "0575b2d888a6848649b9fb1f7df7c72140f0abbceaa1c451b6eecd034729ef7b",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/curvature_c0_v7_replay.py": "fcd8a10db3d68024dbc836dce1b4c9a459912837b1a4234f61b2dd508c6d5e9f"
|
||||
},
|
||||
"limitations": [
|
||||
"No new steering trace or physical tracking score: recorded motion stays fixed.",
|
||||
"The baseline is v7 on every route, regardless of its recorded controller version.",
|
||||
"C1 and integral state match exactly on fixed recorded motion; physical feedback may differ.",
|
||||
"Publication time proxies computation time; full SubMaster state is unavailable.",
|
||||
"The synthetic arc keeps the original model-health gates for a controlled comparison."
|
||||
]
|
||||
},
|
||||
{
|
||||
"scope": "Circular-arc C0 experiment against the pinned continuous PI v7 controller.\n\nBoth controllers run on identical recorded motion. Replace only the encoder's\nC0 target, retaining path validity, C1 overflow, continuous PI feedback and\noutput limits. No onroad selector or production module is modified.\n",
|
||||
"baseline_revision": "08b3a14ad46260fda0f8d3a1c2cee2d272504153",
|
||||
"baseline_source_sha256": "a899b8595e903d6fc4401392a8d123ef77725f6d6d7fa10392a7b3fe72e732b8",
|
||||
"opendbc_revision": "64aa61b9b3fd26e70a7caa915acab207ff3cd64a",
|
||||
"proportional_gain": 0.5,
|
||||
"integral_gain": 0.25,
|
||||
"station_m": 7.0,
|
||||
"cycles": 114582,
|
||||
"controller_updates": 229164,
|
||||
"active_cycles_per_controller": 105382,
|
||||
"can_round_trips": 229164,
|
||||
"same_activation_p_feedforward_overflow_and_feedback_gates": true,
|
||||
"changed_c1_cycles": 0,
|
||||
"max_abs_c1_change_rad": 0.0,
|
||||
"changed_integral_cycles": 0,
|
||||
"pscm_status_fresh_active_seconds": 1061.1945591,
|
||||
"clean_seconds": 884.2159145080002,
|
||||
"source_sha256": {
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_routea0/route.npz": "01ce8b0361c3154d07d5449525a650baa2cc261675095cb1f04ec9e37a9174d3",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_routea0/model_paths.npz": "5d0c69d10a6d13882b47d950ae64c4d98f7ca2b59eab27e4e9557eef3bc4917b",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_routea0/metadata.json": "00496a11afbee475e9b5d66838a4d1663a3c0063cd562b4d1408cce6d71e586e",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_routea0/angles.npz": "feb7b558d75292ab69ce8657540f3295bbc1b0c915d27f39d831bdece3fac534",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/curvature_c0_v7_replay.py": "fcd8a10db3d68024dbc836dce1b4c9a459912837b1a4234f61b2dd508c6d5e9f"
|
||||
},
|
||||
"limitations": [
|
||||
"No new steering trace or physical tracking score: recorded motion stays fixed.",
|
||||
"The baseline is v7 on every route, regardless of its recorded controller version.",
|
||||
"C1 and integral state match exactly on fixed recorded motion; physical feedback may differ.",
|
||||
"Publication time proxies computation time; full SubMaster state is unavailable.",
|
||||
"The synthetic arc keeps the original model-health gates for a controlled comparison."
|
||||
]
|
||||
},
|
||||
{
|
||||
"scope": "Circular-arc C0 experiment against the pinned continuous PI v7 controller.\n\nBoth controllers run on identical recorded motion. Replace only the encoder's\nC0 target, retaining path validity, C1 overflow, continuous PI feedback and\noutput limits. No onroad selector or production module is modified.\n",
|
||||
"baseline_revision": "08b3a14ad46260fda0f8d3a1c2cee2d272504153",
|
||||
"baseline_source_sha256": "a899b8595e903d6fc4401392a8d123ef77725f6d6d7fa10392a7b3fe72e732b8",
|
||||
"opendbc_revision": "64aa61b9b3fd26e70a7caa915acab207ff3cd64a",
|
||||
"proportional_gain": 0.5,
|
||||
"integral_gain": 0.25,
|
||||
"station_m": 7.0,
|
||||
"cycles": 71111,
|
||||
"controller_updates": 142222,
|
||||
"active_cycles_per_controller": 43430,
|
||||
"can_round_trips": 142222,
|
||||
"same_activation_p_feedforward_overflow_and_feedback_gates": true,
|
||||
"changed_c1_cycles": 0,
|
||||
"max_abs_c1_change_rad": 0.0,
|
||||
"changed_integral_cycles": 0,
|
||||
"pscm_status_fresh_active_seconds": 437.0644468180001,
|
||||
"clean_seconds": 332.6767952400001,
|
||||
"source_sha256": {
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_routea2/route.npz": "af4128d278be6be0042f72a02255ddbec96a8a5aa435b9852c8202e4a5cb39e5",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_routea2/model_paths.npz": "5a5a7c4fc30c7b3bc7f2413daa8b11fe86b69dd3249e8df2d35b0a0636a49717",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_routea2/metadata.json": "cfecd240216e409a1e2591c6c2e6f0bad002ecd2a50c8f6dbf22a8f9174708da",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_routea2/angles.npz": "2424b32f02f7e5079f8ae6ffdd30a811820d1d1bfa5b17636c5457eb67d64ecb",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/curvature_c0_v7_replay.py": "fcd8a10db3d68024dbc836dce1b4c9a459912837b1a4234f61b2dd508c6d5e9f"
|
||||
},
|
||||
"limitations": [
|
||||
"No new steering trace or physical tracking score: recorded motion stays fixed.",
|
||||
"The baseline is v7 on every route, regardless of its recorded controller version.",
|
||||
"C1 and integral state match exactly on fixed recorded motion; physical feedback may differ.",
|
||||
"Publication time proxies computation time; full SubMaster state is unavailable.",
|
||||
"The synthetic arc keeps the original model-health gates for a controlled comparison."
|
||||
]
|
||||
},
|
||||
{
|
||||
"scope": "Circular-arc C0 experiment against the pinned continuous PI v7 controller.\n\nBoth controllers run on identical recorded motion. Replace only the encoder's\nC0 target, retaining path validity, C1 overflow, continuous PI feedback and\noutput limits. No onroad selector or production module is modified.\n",
|
||||
"baseline_revision": "08b3a14ad46260fda0f8d3a1c2cee2d272504153",
|
||||
"baseline_source_sha256": "a899b8595e903d6fc4401392a8d123ef77725f6d6d7fa10392a7b3fe72e732b8",
|
||||
"opendbc_revision": "64aa61b9b3fd26e70a7caa915acab207ff3cd64a",
|
||||
"proportional_gain": 0.5,
|
||||
"integral_gain": 0.25,
|
||||
"station_m": 7.0,
|
||||
"cycles": 38961,
|
||||
"controller_updates": 77922,
|
||||
"active_cycles_per_controller": 35677,
|
||||
"can_round_trips": 77922,
|
||||
"same_activation_p_feedforward_overflow_and_feedback_gates": true,
|
||||
"changed_c1_cycles": 0,
|
||||
"max_abs_c1_change_rad": 0.0,
|
||||
"changed_integral_cycles": 0,
|
||||
"pscm_status_fresh_active_seconds": 359.186785903,
|
||||
"clean_seconds": 249.91415584299997,
|
||||
"source_sha256": {
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_routea5/route.npz": "815d1e248ff5c3e5e5cfc11dfbd0690d0d13ddafe1436890e239975a72dae9b8",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_routea5/model_paths.npz": "72e014b844322df6318f6af588c27175ce68d9a37bda62b70eafca5bdfe6dfe4",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_routea5/metadata.json": "c533b06d1b7c598330b36b117bdb3624221bc4193ded8354246d5c86f36b2628",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_routea5/angles.npz": "5466a674cae3f63eca8758f9f370a8c6cdee3dbb6bcf115f85e3b2e95cf5c1fb",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/curvature_c0_v7_replay.py": "fcd8a10db3d68024dbc836dce1b4c9a459912837b1a4234f61b2dd508c6d5e9f"
|
||||
},
|
||||
"limitations": [
|
||||
"No new steering trace or physical tracking score: recorded motion stays fixed.",
|
||||
"The baseline is v7 on every route, regardless of its recorded controller version.",
|
||||
"C1 and integral state match exactly on fixed recorded motion; physical feedback may differ.",
|
||||
"Publication time proxies computation time; full SubMaster state is unavailable.",
|
||||
"The synthetic arc keeps the original model-health gates for a controlled comparison."
|
||||
]
|
||||
},
|
||||
{
|
||||
"scope": "Circular-arc C0 experiment against the pinned continuous PI v7 controller.\n\nBoth controllers run on identical recorded motion. Replace only the encoder's\nC0 target, retaining path validity, C1 overflow, continuous PI feedback and\noutput limits. No onroad selector or production module is modified.\n",
|
||||
"baseline_revision": "08b3a14ad46260fda0f8d3a1c2cee2d272504153",
|
||||
"baseline_source_sha256": "a899b8595e903d6fc4401392a8d123ef77725f6d6d7fa10392a7b3fe72e732b8",
|
||||
"opendbc_revision": "64aa61b9b3fd26e70a7caa915acab207ff3cd64a",
|
||||
"proportional_gain": 0.5,
|
||||
"integral_gain": 0.25,
|
||||
"station_m": 7.0,
|
||||
"cycles": 286319,
|
||||
"controller_updates": 572638,
|
||||
"active_cycles_per_controller": 152043,
|
||||
"can_round_trips": 572638,
|
||||
"same_activation_p_feedforward_overflow_and_feedback_gates": true,
|
||||
"changed_c1_cycles": 0,
|
||||
"max_abs_c1_change_rad": 0.0,
|
||||
"changed_integral_cycles": 0,
|
||||
"pscm_status_fresh_active_seconds": 1530.3877326270015,
|
||||
"clean_seconds": 1093.2748892979992,
|
||||
"source_sha256": {
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_routea9/route.npz": "3fa8cabfb729dd42d689de38618d1d21c8965c9d60f914546f4d7cc58db8c975",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_routea9/model_paths.npz": "7f3646ad24aa52de5982c65600813de5bbc5f7664d92a2516bbba35da6e7b240",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_routea9/metadata.json": "403d35504bf596144845ac2060ce067d571f49a2955d16223af28a79a7a66e93",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_routea9/angles.npz": "cfcc9743aacfadc7be97e9ca130453c2ee6f7870edc3e98ccf9a873dac6d4897",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/curvature_c0_v7_replay.py": "fcd8a10db3d68024dbc836dce1b4c9a459912837b1a4234f61b2dd508c6d5e9f"
|
||||
},
|
||||
"limitations": [
|
||||
"No new steering trace or physical tracking score: recorded motion stays fixed.",
|
||||
"The baseline is v7 on every route, regardless of its recorded controller version.",
|
||||
"C1 and integral state match exactly on fixed recorded motion; physical feedback may differ.",
|
||||
"Publication time proxies computation time; full SubMaster state is unavailable.",
|
||||
"The synthetic arc keeps the original model-health gates for a controlled comparison."
|
||||
]
|
||||
},
|
||||
{
|
||||
"scope": "Circular-arc C0 experiment against the pinned continuous PI v7 controller.\n\nBoth controllers run on identical recorded motion. Replace only the encoder's\nC0 target, retaining path validity, C1 overflow, continuous PI feedback and\noutput limits. No onroad selector or production module is modified.\n",
|
||||
"baseline_revision": "08b3a14ad46260fda0f8d3a1c2cee2d272504153",
|
||||
"baseline_source_sha256": "a899b8595e903d6fc4401392a8d123ef77725f6d6d7fa10392a7b3fe72e732b8",
|
||||
"opendbc_revision": "64aa61b9b3fd26e70a7caa915acab207ff3cd64a",
|
||||
"proportional_gain": 0.5,
|
||||
"integral_gain": 0.25,
|
||||
"station_m": 7.0,
|
||||
"cycles": 160431,
|
||||
"controller_updates": 320862,
|
||||
"active_cycles_per_controller": 68217,
|
||||
"can_round_trips": 320862,
|
||||
"same_activation_p_feedforward_overflow_and_feedback_gates": true,
|
||||
"changed_c1_cycles": 0,
|
||||
"max_abs_c1_change_rad": 0.0,
|
||||
"changed_integral_cycles": 0,
|
||||
"pscm_status_fresh_active_seconds": 686.4382411050101,
|
||||
"clean_seconds": 390.78219015704235,
|
||||
"source_sha256": {
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_routeb8/route.npz": "6f5dd369b70eaed4b95b28c8b25c9f2e9b830fa07a334881a185505481667c8b",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_routeb8/model_paths.npz": "939af6cf7e74251d8842581cc078d26d9fbfd22a0d7817cb0e368697d419b615",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_routeb8/metadata.json": "73b439132d1de37ec187b544c04d2b05c80965065515a4b7dec29ba57ae37e7c",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_routeb8/angles.npz": "6f4ea54d101595a48d301a0914c940d6ad932249b14d4eaa4fd7e43a11dc6f92",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/curvature_c0_v7_replay.py": "fcd8a10db3d68024dbc836dce1b4c9a459912837b1a4234f61b2dd508c6d5e9f"
|
||||
},
|
||||
"limitations": [
|
||||
"No new steering trace or physical tracking score: recorded motion stays fixed.",
|
||||
"The baseline is v7 on every route, regardless of its recorded controller version.",
|
||||
"C1 and integral state match exactly on fixed recorded motion; physical feedback may differ.",
|
||||
"Publication time proxies computation time; full SubMaster state is unavailable.",
|
||||
"The synthetic arc keeps the original model-health gates for a controlled comparison."
|
||||
]
|
||||
},
|
||||
{
|
||||
"scope": "Circular-arc C0 experiment against the pinned continuous PI v7 controller.\n\nBoth controllers run on identical recorded motion. Replace only the encoder's\nC0 target, retaining path validity, C1 overflow, continuous PI feedback and\noutput limits. No onroad selector or production module is modified.\n",
|
||||
"baseline_revision": "08b3a14ad46260fda0f8d3a1c2cee2d272504153",
|
||||
"baseline_source_sha256": "a899b8595e903d6fc4401392a8d123ef77725f6d6d7fa10392a7b3fe72e732b8",
|
||||
"opendbc_revision": "64aa61b9b3fd26e70a7caa915acab207ff3cd64a",
|
||||
"proportional_gain": 0.5,
|
||||
"integral_gain": 0.25,
|
||||
"station_m": 7.0,
|
||||
"cycles": 90774,
|
||||
"controller_updates": 181548,
|
||||
"active_cycles_per_controller": 86474,
|
||||
"can_round_trips": 181548,
|
||||
"same_activation_p_feedforward_overflow_and_feedback_gates": true,
|
||||
"changed_c1_cycles": 0,
|
||||
"max_abs_c1_change_rad": 0.0,
|
||||
"changed_integral_cycles": 0,
|
||||
"pscm_status_fresh_active_seconds": 870.449417766,
|
||||
"clean_seconds": 604.9269013949997,
|
||||
"source_sha256": {
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_routeb9/route.npz": "b07c789d8155335f5d120d0262fced6e4d5803fe767b0ff49b6413dce4140b5c",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_routeb9/model_paths.npz": "6b1f87897c050273fdc05af051307a049b6fc3a93072e7cda1721195ce7c3861",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_routeb9/metadata.json": "9ce452220cab61b81883f32fc2fcaf5db6c78a674cb255a49cc77d5029580fee",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_routeb9/angles.npz": "ce16212d9b13e41993d2c8df3246ddfe21324ad0cb213310e62eee1d685b197b",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/curvature_c0_v7_replay.py": "fcd8a10db3d68024dbc836dce1b4c9a459912837b1a4234f61b2dd508c6d5e9f"
|
||||
},
|
||||
"limitations": [
|
||||
"No new steering trace or physical tracking score: recorded motion stays fixed.",
|
||||
"The baseline is v7 on every route, regardless of its recorded controller version.",
|
||||
"C1 and integral state match exactly on fixed recorded motion; physical feedback may differ.",
|
||||
"Publication time proxies computation time; full SubMaster state is unavailable.",
|
||||
"The synthetic arc keeps the original model-health gates for a controlled comparison."
|
||||
]
|
||||
},
|
||||
{
|
||||
"scope": "Circular-arc C0 experiment against the pinned continuous PI v7 controller.\n\nBoth controllers run on identical recorded motion. Replace only the encoder's\nC0 target, retaining path validity, C1 overflow, continuous PI feedback and\noutput limits. No onroad selector or production module is modified.\n",
|
||||
"baseline_revision": "08b3a14ad46260fda0f8d3a1c2cee2d272504153",
|
||||
"baseline_source_sha256": "a899b8595e903d6fc4401392a8d123ef77725f6d6d7fa10392a7b3fe72e732b8",
|
||||
"opendbc_revision": "64aa61b9b3fd26e70a7caa915acab207ff3cd64a",
|
||||
"proportional_gain": 0.5,
|
||||
"integral_gain": 0.25,
|
||||
"station_m": 7.0,
|
||||
"cycles": 327448,
|
||||
"controller_updates": 654896,
|
||||
"active_cycles_per_controller": 118756,
|
||||
"can_round_trips": 654896,
|
||||
"same_activation_p_feedforward_overflow_and_feedback_gates": true,
|
||||
"changed_c1_cycles": 0,
|
||||
"max_abs_c1_change_rad": 0.0,
|
||||
"changed_integral_cycles": 0,
|
||||
"pscm_status_fresh_active_seconds": 1194.5832199259985,
|
||||
"clean_seconds": 894.0687052669919,
|
||||
"source_sha256": {
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_routeca/route.npz": "ae9d46770eaf0dbbac6af86aebc926320eed0cf114eb43d5f78b0676e8e0dbf9",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_routeca/model_paths.npz": "bf17deb442383aaa79432566cd382df24a1bbbbd0521d0cafab956618f5bdd96",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_routeca/metadata.json": "a759d5cdf878df8b05d91db637b1935b6b4bdd87af96f0f256b67e7d809b3525",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_routeca/angles.npz": "1681bda6db7dac332abf627d97e7452acd7e0faef0c196b11e55e22291cad073",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/curvature_c0_v7_replay.py": "fcd8a10db3d68024dbc836dce1b4c9a459912837b1a4234f61b2dd508c6d5e9f"
|
||||
},
|
||||
"limitations": [
|
||||
"No new steering trace or physical tracking score: recorded motion stays fixed.",
|
||||
"The baseline is v7 on every route, regardless of its recorded controller version.",
|
||||
"C1 and integral state match exactly on fixed recorded motion; physical feedback may differ.",
|
||||
"Publication time proxies computation time; full SubMaster state is unavailable.",
|
||||
"The synthetic arc keeps the original model-health gates for a controlled comparison."
|
||||
]
|
||||
},
|
||||
{
|
||||
"scope": "Circular-arc C0 experiment against the pinned continuous PI v7 controller.\n\nBoth controllers run on identical recorded motion. Replace only the encoder's\nC0 target, retaining path validity, C1 overflow, continuous PI feedback and\noutput limits. No onroad selector or production module is modified.\n",
|
||||
"baseline_revision": "08b3a14ad46260fda0f8d3a1c2cee2d272504153",
|
||||
"baseline_source_sha256": "a899b8595e903d6fc4401392a8d123ef77725f6d6d7fa10392a7b3fe72e732b8",
|
||||
"opendbc_revision": "64aa61b9b3fd26e70a7caa915acab207ff3cd64a",
|
||||
"proportional_gain": 0.5,
|
||||
"integral_gain": 0.25,
|
||||
"station_m": 7.0,
|
||||
"cycles": 132881,
|
||||
"controller_updates": 265762,
|
||||
"active_cycles_per_controller": 64776,
|
||||
"can_round_trips": 265762,
|
||||
"same_activation_p_feedforward_overflow_and_feedback_gates": true,
|
||||
"changed_c1_cycles": 0,
|
||||
"max_abs_c1_change_rad": 0.0,
|
||||
"changed_integral_cycles": 0,
|
||||
"pscm_status_fresh_active_seconds": 651.3446633269996,
|
||||
"clean_seconds": 571.0075301469988,
|
||||
"source_sha256": {
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_raptor_route02/route.npz": "8a4cfe53994988d053b47d9caadf21ebd064fd20e598fa05ddc2da245bd0e980",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_raptor_route02/model_paths.npz": "d5dc6f72d91472b8f2fb1add1680cdaea1e122774ddd6d8519fd7673daf71bb5",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_raptor_route02/metadata.json": "9602e08efc2bd784133837c4156c075bb89bad3bedd6b309f15e884ed20e338a",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_raptor_route02/angles.npz": "82af2973ad40c1df392e11bdc5dd5895bfc6c170fc894735123af7704d95cd57",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/curvature_c0_v7_replay.py": "fcd8a10db3d68024dbc836dce1b4c9a459912837b1a4234f61b2dd508c6d5e9f"
|
||||
},
|
||||
"limitations": [
|
||||
"No new steering trace or physical tracking score: recorded motion stays fixed.",
|
||||
"The baseline is v7 on every route, regardless of its recorded controller version.",
|
||||
"C1 and integral state match exactly on fixed recorded motion; physical feedback may differ.",
|
||||
"Publication time proxies computation time; full SubMaster state is unavailable.",
|
||||
"The synthetic arc keeps the original model-health gates for a controlled comparison."
|
||||
]
|
||||
}
|
||||
],
|
||||
"production_parity": [
|
||||
{
|
||||
"scope": "Verify production commands against the archived curvature-C0 offline candidate.\n\nFixed recorded motion verifies integration parity, not physical tracking.\n",
|
||||
"route": "112",
|
||||
"cycles": 108971,
|
||||
"can_round_trips": 108971,
|
||||
"production_matches_archived_trial_exactly": true,
|
||||
"opendbc_revision": "64aa61b9b3fd26e70a7caa915acab207ff3cd64a",
|
||||
"hypothesis": "model-action-curvature-c0-pi-v8",
|
||||
"kp": 0.5,
|
||||
"ki": 0.25,
|
||||
"source_sha256": {
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route112/route.npz": "2b08a2fb636f7d14556d7df4035eafc1d1b97932237955528562a16db2b31d3e",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route112/model_paths.npz": "9837afe78aab4cad288cad98a595a5777fa8a66bb235986b1272a7f7c54e559a",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route112/metadata.json": "726d78a7e7aa45307dcfe27cb00775c20ecc9d54538eb7f26a0166fc216226ce",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_curvature_c0_v7/112/report.json": "1be0b52e00e8f67043ce35e19cc6612f788d6e3853b40d1bc2dd372b2ff4e70d",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_curvature_c0_v7/112/commands.npz": "3951aa6160295ee6c3835517b39c7c835074b93b1bda5fb013d4ef56f88b8f1e",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/curvature_c0_validate.py": "c91ead82b0320725276141b1bc9b84b132cd6345a144ea826ba2b7f069870932",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/openpilot/selfdrive/controls/lib/ford_model_action.py": "6f8c4a6f7fa54fa2e23f32f1e83ace83da5f0b6f45011941f1902a1dae778ffc"
|
||||
}
|
||||
},
|
||||
{
|
||||
"scope": "Verify production commands against the archived curvature-C0 offline candidate.\n\nFixed recorded motion verifies integration parity, not physical tracking.\n",
|
||||
"route": "113",
|
||||
"cycles": 49614,
|
||||
"can_round_trips": 49614,
|
||||
"production_matches_archived_trial_exactly": true,
|
||||
"opendbc_revision": "64aa61b9b3fd26e70a7caa915acab207ff3cd64a",
|
||||
"hypothesis": "model-action-curvature-c0-pi-v8",
|
||||
"kp": 0.5,
|
||||
"ki": 0.25,
|
||||
"source_sha256": {
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route113/route.npz": "774ca4a21b7113c2706d6130bc180c3216ea4833155300ab01e75b3486e36327",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route113/model_paths.npz": "93c41761eb85263f534f5371b905482cf7c948582eb1e9149966594be1d3768f",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route113/metadata.json": "1c0ca74dd48b90ab9d5444c5ca7f8aa9361700bbdf98bd5e853be50ad2895d7f",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_curvature_c0_v7/113/report.json": "c6e98c0f2e6f43f1ede8d8104b77606a91fb3e95efa8348f7f089e01a4f6c823",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_curvature_c0_v7/113/commands.npz": "0a7aa93e8e3e64659022dac254b476e06fb95649c4b97e1de66e4808fae73d6f",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/curvature_c0_validate.py": "c91ead82b0320725276141b1bc9b84b132cd6345a144ea826ba2b7f069870932",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/openpilot/selfdrive/controls/lib/ford_model_action.py": "6f8c4a6f7fa54fa2e23f32f1e83ace83da5f0b6f45011941f1902a1dae778ffc"
|
||||
}
|
||||
},
|
||||
{
|
||||
"scope": "Verify production commands against the archived curvature-C0 offline candidate.\n\nFixed recorded motion verifies integration parity, not physical tracking.\n",
|
||||
"route": "114",
|
||||
"cycles": 61027,
|
||||
"can_round_trips": 61027,
|
||||
"production_matches_archived_trial_exactly": true,
|
||||
"opendbc_revision": "64aa61b9b3fd26e70a7caa915acab207ff3cd64a",
|
||||
"hypothesis": "model-action-curvature-c0-pi-v8",
|
||||
"kp": 0.5,
|
||||
"ki": 0.25,
|
||||
"source_sha256": {
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route114/route.npz": "83524f07d61104b84b004ddc46bb751ca7f61da67798aa47db56d307765f5b3e",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route114/model_paths.npz": "14d14362d1a3e46398edd8e22b7cc4e36277a7596a73f546192d0e14c6642b07",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route114/metadata.json": "ec677b9275c1707e477ebd0ffa235d49b5ec63a1ee717b16040c3acdbcd3bdc0",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_curvature_c0_v7/114/report.json": "b7f1d341f4f33d2c16dcfa2694b36cafe0bf76405d0e7b61bc4e469caef6b1dc",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_curvature_c0_v7/114/commands.npz": "3b79d094ecb89d3f3554c492d83826ec60662c236d6854891feba95fffcd7bc7",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/curvature_c0_validate.py": "c91ead82b0320725276141b1bc9b84b132cd6345a144ea826ba2b7f069870932",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/openpilot/selfdrive/controls/lib/ford_model_action.py": "6f8c4a6f7fa54fa2e23f32f1e83ace83da5f0b6f45011941f1902a1dae778ffc"
|
||||
}
|
||||
},
|
||||
{
|
||||
"scope": "Verify production commands against the archived curvature-C0 offline candidate.\n\nFixed recorded motion verifies integration parity, not physical tracking.\n",
|
||||
"route": "115",
|
||||
"cycles": 40037,
|
||||
"can_round_trips": 40037,
|
||||
"production_matches_archived_trial_exactly": true,
|
||||
"opendbc_revision": "64aa61b9b3fd26e70a7caa915acab207ff3cd64a",
|
||||
"hypothesis": "model-action-curvature-c0-pi-v8",
|
||||
"kp": 0.5,
|
||||
"ki": 0.25,
|
||||
"source_sha256": {
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route115/route.npz": "e0df32d9e80f1c6b7d56327b37cf07af60070ffc212b8d111e343306148bff23",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route115/model_paths.npz": "52f3ed9e188e4947618a57a3ed872fd1966a887fe1014999df741553b6c13bc0",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route115/metadata.json": "d3c73035bad8eb5059e07962fd274c19cb70c8952b6f1514835d312d08b87a16",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_curvature_c0_v7/115/report.json": "e66d2be86136b0f6fdbcc2998673b2d1e8487f0543f99e4618e46a2f429538e4",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_curvature_c0_v7/115/commands.npz": "9a72eb6bab9116cc83210c92dfb12c5d63a2e4b3103ccdd3f5e70214f3f0e142",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/curvature_c0_validate.py": "c91ead82b0320725276141b1bc9b84b132cd6345a144ea826ba2b7f069870932",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/openpilot/selfdrive/controls/lib/ford_model_action.py": "6f8c4a6f7fa54fa2e23f32f1e83ace83da5f0b6f45011941f1902a1dae778ffc"
|
||||
}
|
||||
},
|
||||
{
|
||||
"scope": "Verify production commands against the archived curvature-C0 offline candidate.\n\nFixed recorded motion verifies integration parity, not physical tracking.\n",
|
||||
"route": "116",
|
||||
"cycles": 68793,
|
||||
"can_round_trips": 68793,
|
||||
"production_matches_archived_trial_exactly": true,
|
||||
"opendbc_revision": "64aa61b9b3fd26e70a7caa915acab207ff3cd64a",
|
||||
"hypothesis": "model-action-curvature-c0-pi-v8",
|
||||
"kp": 0.5,
|
||||
"ki": 0.25,
|
||||
"source_sha256": {
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route116/route.npz": "13df1d49edd0c92bd4a4236c766ee76baa6dad477824b9de4b79927c98410394",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route116/model_paths.npz": "85d1112b5e89340e8a955b7fe4040b3135cf1c91a54ff756826c6119cc9cc01d",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route116/metadata.json": "d52fe63d013fa546524ad9522a26943d9372730d397d92d75dfd9d75c63ce766",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_curvature_c0_v7/116/report.json": "501b944c65f41f6c1e05fccdc6c38f3a80450db6c4696adc35bf85640a04df94",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_curvature_c0_v7/116/commands.npz": "2469c545d0ce62894d59e07799942ebf40bec583693391aaa1dd9e75c218a396",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/curvature_c0_validate.py": "c91ead82b0320725276141b1bc9b84b132cd6345a144ea826ba2b7f069870932",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/openpilot/selfdrive/controls/lib/ford_model_action.py": "6f8c4a6f7fa54fa2e23f32f1e83ace83da5f0b6f45011941f1902a1dae778ffc"
|
||||
}
|
||||
},
|
||||
{
|
||||
"scope": "Verify production commands against the archived curvature-C0 offline candidate.\n\nFixed recorded motion verifies integration parity, not physical tracking.\n",
|
||||
"route": "117",
|
||||
"cycles": 27301,
|
||||
"can_round_trips": 27301,
|
||||
"production_matches_archived_trial_exactly": true,
|
||||
"opendbc_revision": "64aa61b9b3fd26e70a7caa915acab207ff3cd64a",
|
||||
"hypothesis": "model-action-curvature-c0-pi-v8",
|
||||
"kp": 0.5,
|
||||
"ki": 0.25,
|
||||
"source_sha256": {
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route117/route.npz": "65e4ea6c76dce73018b636479b3777116f1e58bb9813e6c1a7b79018b219140e",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route117/model_paths.npz": "079f6003968287530e1ed6f2243c727e80c2ef3d16fb8a7d945d7eca744d54e7",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route117/metadata.json": "348396b3059c5f54d7938cd3acf2bb268a813b47ff47561da7a94cb75c3f3114",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_curvature_c0_v7/117/report.json": "896b30e7ded613f1b05b693067fc2f19155016b6a33f26f881bbfff2b193019d",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_curvature_c0_v7/117/commands.npz": "6d88bfdb54fde00240954db9dbbf1f3e5059bfac9b6161c138f0069213ac35a8",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/curvature_c0_validate.py": "c91ead82b0320725276141b1bc9b84b132cd6345a144ea826ba2b7f069870932",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/openpilot/selfdrive/controls/lib/ford_model_action.py": "6f8c4a6f7fa54fa2e23f32f1e83ace83da5f0b6f45011941f1902a1dae778ffc"
|
||||
}
|
||||
},
|
||||
{
|
||||
"scope": "Verify production commands against the archived curvature-C0 offline candidate.\n\nFixed recorded motion verifies integration parity, not physical tracking.\n",
|
||||
"route": "a0",
|
||||
"cycles": 114582,
|
||||
"can_round_trips": 114582,
|
||||
"production_matches_archived_trial_exactly": true,
|
||||
"opendbc_revision": "64aa61b9b3fd26e70a7caa915acab207ff3cd64a",
|
||||
"hypothesis": "model-action-curvature-c0-pi-v8",
|
||||
"kp": 0.5,
|
||||
"ki": 0.25,
|
||||
"source_sha256": {
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_routea0/route.npz": "01ce8b0361c3154d07d5449525a650baa2cc261675095cb1f04ec9e37a9174d3",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_routea0/model_paths.npz": "5d0c69d10a6d13882b47d950ae64c4d98f7ca2b59eab27e4e9557eef3bc4917b",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_routea0/metadata.json": "00496a11afbee475e9b5d66838a4d1663a3c0063cd562b4d1408cce6d71e586e",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_curvature_c0_v7/a0/report.json": "52cd87929fbdc721450c735bd02ac049260cafa439af7dcdcd405e977dc9ea22",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_curvature_c0_v7/a0/commands.npz": "0f718892b0802cbf754ba33c9c9c6da8a3e915e88fa7a4e008b9b33d63a429e0",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/curvature_c0_validate.py": "c91ead82b0320725276141b1bc9b84b132cd6345a144ea826ba2b7f069870932",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/openpilot/selfdrive/controls/lib/ford_model_action.py": "6f8c4a6f7fa54fa2e23f32f1e83ace83da5f0b6f45011941f1902a1dae778ffc"
|
||||
}
|
||||
},
|
||||
{
|
||||
"scope": "Verify production commands against the archived curvature-C0 offline candidate.\n\nFixed recorded motion verifies integration parity, not physical tracking.\n",
|
||||
"route": "a2",
|
||||
"cycles": 71111,
|
||||
"can_round_trips": 71111,
|
||||
"production_matches_archived_trial_exactly": true,
|
||||
"opendbc_revision": "64aa61b9b3fd26e70a7caa915acab207ff3cd64a",
|
||||
"hypothesis": "model-action-curvature-c0-pi-v8",
|
||||
"kp": 0.5,
|
||||
"ki": 0.25,
|
||||
"source_sha256": {
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_routea2/route.npz": "af4128d278be6be0042f72a02255ddbec96a8a5aa435b9852c8202e4a5cb39e5",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_routea2/model_paths.npz": "5a5a7c4fc30c7b3bc7f2413daa8b11fe86b69dd3249e8df2d35b0a0636a49717",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_routea2/metadata.json": "cfecd240216e409a1e2591c6c2e6f0bad002ecd2a50c8f6dbf22a8f9174708da",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_curvature_c0_v7/a2/report.json": "fbaf59a1499df5dba0f790f76f211efefb3b68f327e9e49c763fb175f6f8e7a9",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_curvature_c0_v7/a2/commands.npz": "159f76d2d511af7f423941cc9d168c7112a8aec7a34e05bbd043f65a2324b325",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/curvature_c0_validate.py": "c91ead82b0320725276141b1bc9b84b132cd6345a144ea826ba2b7f069870932",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/openpilot/selfdrive/controls/lib/ford_model_action.py": "6f8c4a6f7fa54fa2e23f32f1e83ace83da5f0b6f45011941f1902a1dae778ffc"
|
||||
}
|
||||
},
|
||||
{
|
||||
"scope": "Verify production commands against the archived curvature-C0 offline candidate.\n\nFixed recorded motion verifies integration parity, not physical tracking.\n",
|
||||
"route": "a5",
|
||||
"cycles": 38961,
|
||||
"can_round_trips": 38961,
|
||||
"production_matches_archived_trial_exactly": true,
|
||||
"opendbc_revision": "64aa61b9b3fd26e70a7caa915acab207ff3cd64a",
|
||||
"hypothesis": "model-action-curvature-c0-pi-v8",
|
||||
"kp": 0.5,
|
||||
"ki": 0.25,
|
||||
"source_sha256": {
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_routea5/route.npz": "815d1e248ff5c3e5e5cfc11dfbd0690d0d13ddafe1436890e239975a72dae9b8",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_routea5/model_paths.npz": "72e014b844322df6318f6af588c27175ce68d9a37bda62b70eafca5bdfe6dfe4",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_routea5/metadata.json": "c533b06d1b7c598330b36b117bdb3624221bc4193ded8354246d5c86f36b2628",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_curvature_c0_v7/a5/report.json": "ba9cedd6d9f2bbe2caa3ad0eeb298fa3334750ad60086422d7d48f3b690480f7",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_curvature_c0_v7/a5/commands.npz": "8ac5c77a29824b8f621c3071309990d320a1a61cc968ad55f76408a530680913",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/curvature_c0_validate.py": "c91ead82b0320725276141b1bc9b84b132cd6345a144ea826ba2b7f069870932",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/openpilot/selfdrive/controls/lib/ford_model_action.py": "6f8c4a6f7fa54fa2e23f32f1e83ace83da5f0b6f45011941f1902a1dae778ffc"
|
||||
}
|
||||
},
|
||||
{
|
||||
"scope": "Verify production commands against the archived curvature-C0 offline candidate.\n\nFixed recorded motion verifies integration parity, not physical tracking.\n",
|
||||
"route": "a9",
|
||||
"cycles": 286319,
|
||||
"can_round_trips": 286319,
|
||||
"production_matches_archived_trial_exactly": true,
|
||||
"opendbc_revision": "64aa61b9b3fd26e70a7caa915acab207ff3cd64a",
|
||||
"hypothesis": "model-action-curvature-c0-pi-v8",
|
||||
"kp": 0.5,
|
||||
"ki": 0.25,
|
||||
"source_sha256": {
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_routea9/route.npz": "3fa8cabfb729dd42d689de38618d1d21c8965c9d60f914546f4d7cc58db8c975",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_routea9/model_paths.npz": "7f3646ad24aa52de5982c65600813de5bbc5f7664d92a2516bbba35da6e7b240",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_routea9/metadata.json": "403d35504bf596144845ac2060ce067d571f49a2955d16223af28a79a7a66e93",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_curvature_c0_v7/a9/report.json": "677a2f66ecc5696fa44190c5e3ee27cd3dc45f1deda9ffe3a2f330b4e1d0d20d",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_curvature_c0_v7/a9/commands.npz": "e109e77124dc2296a00f65563c1b1ec3c960b414473a3cc7c6caaafbe7e65037",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/curvature_c0_validate.py": "c91ead82b0320725276141b1bc9b84b132cd6345a144ea826ba2b7f069870932",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/openpilot/selfdrive/controls/lib/ford_model_action.py": "6f8c4a6f7fa54fa2e23f32f1e83ace83da5f0b6f45011941f1902a1dae778ffc"
|
||||
}
|
||||
},
|
||||
{
|
||||
"scope": "Verify production commands against the archived curvature-C0 offline candidate.\n\nFixed recorded motion verifies integration parity, not physical tracking.\n",
|
||||
"route": "b8",
|
||||
"cycles": 160431,
|
||||
"can_round_trips": 160431,
|
||||
"production_matches_archived_trial_exactly": true,
|
||||
"opendbc_revision": "64aa61b9b3fd26e70a7caa915acab207ff3cd64a",
|
||||
"hypothesis": "model-action-curvature-c0-pi-v8",
|
||||
"kp": 0.5,
|
||||
"ki": 0.25,
|
||||
"source_sha256": {
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_routeb8/route.npz": "6f5dd369b70eaed4b95b28c8b25c9f2e9b830fa07a334881a185505481667c8b",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_routeb8/model_paths.npz": "939af6cf7e74251d8842581cc078d26d9fbfd22a0d7817cb0e368697d419b615",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_routeb8/metadata.json": "73b439132d1de37ec187b544c04d2b05c80965065515a4b7dec29ba57ae37e7c",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_curvature_c0_v7/b8/report.json": "f48796acdfad87a0caaaddac2a41945c97311e85fa57f9fc73a1730d264ddba4",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_curvature_c0_v7/b8/commands.npz": "554891332a496498ff64e08e4eba4210125fd5f2da8307231dd6d9377becfe0a",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/curvature_c0_validate.py": "c91ead82b0320725276141b1bc9b84b132cd6345a144ea826ba2b7f069870932",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/openpilot/selfdrive/controls/lib/ford_model_action.py": "6f8c4a6f7fa54fa2e23f32f1e83ace83da5f0b6f45011941f1902a1dae778ffc"
|
||||
}
|
||||
},
|
||||
{
|
||||
"scope": "Verify production commands against the archived curvature-C0 offline candidate.\n\nFixed recorded motion verifies integration parity, not physical tracking.\n",
|
||||
"route": "b9",
|
||||
"cycles": 90774,
|
||||
"can_round_trips": 90774,
|
||||
"production_matches_archived_trial_exactly": true,
|
||||
"opendbc_revision": "64aa61b9b3fd26e70a7caa915acab207ff3cd64a",
|
||||
"hypothesis": "model-action-curvature-c0-pi-v8",
|
||||
"kp": 0.5,
|
||||
"ki": 0.25,
|
||||
"source_sha256": {
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_routeb9/route.npz": "b07c789d8155335f5d120d0262fced6e4d5803fe767b0ff49b6413dce4140b5c",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_routeb9/model_paths.npz": "6b1f87897c050273fdc05af051307a049b6fc3a93072e7cda1721195ce7c3861",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_routeb9/metadata.json": "9ce452220cab61b81883f32fc2fcaf5db6c78a674cb255a49cc77d5029580fee",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_curvature_c0_v7/b9/report.json": "3812983a690e5eafca93ea6af05620fe82e4e0cc8efc07e24505a63664444d66",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_curvature_c0_v7/b9/commands.npz": "2ffc685e29463a7b89d39bfb3583a37eb10a57d7201a24394550fcf9a89e3c4c",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/curvature_c0_validate.py": "c91ead82b0320725276141b1bc9b84b132cd6345a144ea826ba2b7f069870932",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/openpilot/selfdrive/controls/lib/ford_model_action.py": "6f8c4a6f7fa54fa2e23f32f1e83ace83da5f0b6f45011941f1902a1dae778ffc"
|
||||
}
|
||||
},
|
||||
{
|
||||
"scope": "Verify production commands against the archived curvature-C0 offline candidate.\n\nFixed recorded motion verifies integration parity, not physical tracking.\n",
|
||||
"route": "ca",
|
||||
"cycles": 327448,
|
||||
"can_round_trips": 327448,
|
||||
"production_matches_archived_trial_exactly": true,
|
||||
"opendbc_revision": "64aa61b9b3fd26e70a7caa915acab207ff3cd64a",
|
||||
"hypothesis": "model-action-curvature-c0-pi-v8",
|
||||
"kp": 0.5,
|
||||
"ki": 0.25,
|
||||
"source_sha256": {
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_routeca/route.npz": "ae9d46770eaf0dbbac6af86aebc926320eed0cf114eb43d5f78b0676e8e0dbf9",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_routeca/model_paths.npz": "bf17deb442383aaa79432566cd382df24a1bbbbd0521d0cafab956618f5bdd96",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_routeca/metadata.json": "a759d5cdf878df8b05d91db637b1935b6b4bdd87af96f0f256b67e7d809b3525",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_curvature_c0_v7/ca/report.json": "753760c726ef1b85daf83b7108f758f9304dab408b49018f303616b99c96b81f",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_curvature_c0_v7/ca/commands.npz": "3fa932ac86e14440d325a6cc4677b47cd75c5c6019b929d3a5d078a62c5590df",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/curvature_c0_validate.py": "c91ead82b0320725276141b1bc9b84b132cd6345a144ea826ba2b7f069870932",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/openpilot/selfdrive/controls/lib/ford_model_action.py": "6f8c4a6f7fa54fa2e23f32f1e83ace83da5f0b6f45011941f1902a1dae778ffc"
|
||||
}
|
||||
},
|
||||
{
|
||||
"scope": "Verify production commands against the archived curvature-C0 offline candidate.\n\nFixed recorded motion verifies integration parity, not physical tracking.\n",
|
||||
"route": "raptor02",
|
||||
"cycles": 132881,
|
||||
"can_round_trips": 132881,
|
||||
"production_matches_archived_trial_exactly": true,
|
||||
"opendbc_revision": "64aa61b9b3fd26e70a7caa915acab207ff3cd64a",
|
||||
"hypothesis": "model-action-curvature-c0-pi-v8",
|
||||
"kp": 0.5,
|
||||
"ki": 0.25,
|
||||
"source_sha256": {
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_raptor_route02/route.npz": "8a4cfe53994988d053b47d9caadf21ebd064fd20e598fa05ddc2da245bd0e980",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_raptor_route02/model_paths.npz": "d5dc6f72d91472b8f2fb1add1680cdaea1e122774ddd6d8519fd7673daf71bb5",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_raptor_route02/metadata.json": "9602e08efc2bd784133837c4156c075bb89bad3bedd6b309f15e884ed20e338a",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_curvature_c0_v7/raptor02/report.json": "af6b8fa4e4bfe3d5543794cdde4fbe4fd7560addf02717c2b6144c3987effafa",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_curvature_c0_v7/raptor02/commands.npz": "99ee65b866eda85e5c5d54f8896a7f952504826753eca31a01a29c7cf36c6482",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/curvature_c0_validate.py": "c91ead82b0320725276141b1bc9b84b132cd6345a144ea826ba2b7f069870932",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/openpilot/selfdrive/controls/lib/ford_model_action.py": "6f8c4a6f7fa54fa2e23f32f1e83ace83da5f0b6f45011941f1902a1dae778ffc"
|
||||
}
|
||||
}
|
||||
],
|
||||
"production_stress": {
|
||||
"cycles": 20000,
|
||||
"kp": 0.5,
|
||||
"ki": 0.25,
|
||||
"controller_updates": 60000,
|
||||
"can_round_trips": 60000,
|
||||
"seed": 20260913,
|
||||
"independent_c0_comparisons": 20000,
|
||||
"checks": "Independent scalar PI/unwind-first/anti-windup arithmetic, mirror symmetry, exact C0 independence, limits, slew, resets, CAN.",
|
||||
"source_sha256": {
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/curvature_c0_production_stress.py": "4bf031a793efe9ebc066e5b6db6897ca8e844d5e34c6faecd4701a4d8f04132a",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/openpilot/selfdrive/controls/lib/ford_model_action.py": "6f8c4a6f7fa54fa2e23f32f1e83ace83da5f0b6f45011941f1902a1dae778ffc"
|
||||
}
|
||||
},
|
||||
"limitations": [
|
||||
"C1 parity applies only to fixed recorded inputs; changing C0 changes physical response and subsequent feedback.",
|
||||
"Curvature arc omits independent live model position/heading; seven meters remains an engineering reference choice.",
|
||||
"Smaller C0 is not proof of better or worse tracking; no candidate hardware drive has been performed."
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
# Direct model-path C0/C1 trial
|
||||
|
||||
This trial takes priority over the untested filtered-driver change. Driver
|
||||
arbitration is restored to the last driven baseline, `18ded0380`: a raw torque
|
||||
crossing above 1 Nm, filtered driver input, or fresh PSCM driver override still
|
||||
clears feedback. The filtered-driver experiment remains available in history at
|
||||
`4f7d2b8d2` and is not included in this trial.
|
||||
|
||||
Keep **Selected-Action Path Tracking** and **Model Geometry Reference** enabled
|
||||
in Sunnylink. The existing geometry toggle now selects direct-path mapping at
|
||||
controlsd startup. Keep **C0 one-second distance** off for fixed 7 m C0. Apply an
|
||||
offroad-to-onroad cycle after updating. Geometry off restores the original
|
||||
model-action controller; the master controller toggle off restores upstream
|
||||
Ford control. Joystick and lateral-maneuver overrides retain their priority.
|
||||
|
||||
## Mapping
|
||||
|
||||
The path is parameterized by accumulated arc length in the model frame. C0
|
||||
samples model lateral position at 7 m, or max(7 m, speed × 1 s) when the existing
|
||||
C0 distance setting is enabled. C1 samples model orientation at max(7 m,
|
||||
speed × 1 s). A shorter path holds its endpoint; invalid paths disable the
|
||||
experimental command. Both distances are 7 m below about 15.7 mph. These are
|
||||
engineering choices, not identified Ford reference points.
|
||||
|
||||
C0 and C1 are independent base requests. C0 is no longer reconstructed as a
|
||||
circular arc from the curvature used for C1. C2/C3 remain zero, so this still
|
||||
does not transmit the model's entire curved path to the PSCM.
|
||||
|
||||
For the existing request limits and feedback, heading is divided by its sample
|
||||
distance to form a heading-equivalent curvature. controlsd applies its normal
|
||||
curvature limits to that value, logs it, and uses it for desired steering angle
|
||||
and feedback. Multiplication by the same distance recovers the model heading
|
||||
when unconstrained. C0 independently uses 2 × offset / distance² to apply the
|
||||
same request-limit function, then reverses that normalization. This does not
|
||||
infer C0 from C1. These envelopes constrain base requests; they are not proof of
|
||||
physical lateral acceleration or jerk under the unknown PSCM response.
|
||||
|
||||
P/I gains, measured-steering feedback, C1 anti-windup, C1-overflow allocation to
|
||||
C0, field bounds, driver arbitration, and CAN cadence retain the driven
|
||||
baseline behavior. There is no new C0/C1 actuator slew limiter. The independent
|
||||
C0 envelope adds one previous-reference state, reset with the controller.
|
||||
|
||||
This changes preview semantics: direct commands use the distance stations
|
||||
above, rather than the prior modeld curvature preview (about 0.744 s at low
|
||||
speed on these drives). The raw model points do not pass through the old
|
||||
curvature reference's 0.1 s smoother. Existing request limits still apply.
|
||||
|
||||
## Logging
|
||||
|
||||
Controller diagnostics identify `model-path-direct-feedback-v17`, with
|
||||
`direct_path=true` only when the model path is in control. A valid maneuver
|
||||
reference uses the original curvature-to-arc mapping and logs
|
||||
`direct_path=false`. `offset_reference` records the independently limited C0
|
||||
normalization. Desired curvature and desired steering angle describe the C1
|
||||
feedback target, not an independent measure of lateral path position.
|
||||
|
||||
modeld's geometry-reference telemetry is retained for comparison. Its
|
||||
`selectedCurvature` and published model action are not the direct controller's
|
||||
selected reference; use controlsState and the Ford controller diagnostics.
|
||||
|
||||
## Offline results
|
||||
|
||||
The replay compares the driven baseline against the direct-path candidate on
|
||||
identical recorded model frames and frozen vehicle measurements from full
|
||||
rlogs 15a and 15b. It covers 44,145 control cycles and 4,415 candidate CAN
|
||||
serialization checks. All commands remain finite and within field bounds;
|
||||
C2/C3 remain zero and driver/PSCM override behavior matches the baseline.
|
||||
The baseline reproduces recorded C1 to Float32 precision; C0 differs by at
|
||||
most one 0.01 m command quantum on 15b.
|
||||
|
||||
| Metric | 15a baseline → direct | 15b baseline → direct |
|
||||
|---|---:|---:|
|
||||
| C1 field-bound time | 2.57 → 6.77 s | 6.70 → 7.54 s |
|
||||
| Low-speed C0 steps > 0.25 m | 52 → 52 | 85 → 129 |
|
||||
| Low-speed C1 steps > 0.05 rad | 36 → 46 | 32 → 57 |
|
||||
|
||||
Low-speed counts use valid consecutive commands below 15 mph, normally 10 ms
|
||||
apart. They are descriptive command changes, not physical wheel jerk. The
|
||||
direct mapping changes requests substantially and does **not** establish a
|
||||
smoothness improvement: abrupt command changes and C1 field-bound time can
|
||||
increase. Fixed recorded measurements cannot predict the resulting wheel
|
||||
response, centering, or unwind. No on-road improvement is claimed.
|
||||
|
||||
**593 tests pass** (547 controller/path/geometry tests and 46 settings tests).
|
||||
The tests cover independent position/heading commands, distances, endpoint
|
||||
handling, invalid input resets, request limits through reversals, feedback,
|
||||
baseline driver overrides, toggle selection, maneuver priority, publication,
|
||||
and CAN encoding. Reproduce replay with:
|
||||
|
||||
```sh
|
||||
PYTHONPATH=.:opendbc_repo python tools/ford_pscm_lab/direct_model_path_replay.py \
|
||||
--source .cache/ford_route15a/rlog_full --output .cache/ford_direct_path/15a
|
||||
```
|
||||
|
||||
Detailed measurements and source hashes: `ford_direct_path_validation.json`.
|
||||
@@ -0,0 +1,70 @@
|
||||
# Ford direct C0/C1 requests — v9
|
||||
|
||||
The v8 maneuver route `84865544361f55cb/0000011c--99f4537696` showed
|
||||
commands being delayed by our extra 4 m/s C0 and 0.5 rad/s C1 slews.
|
||||
Those values were controller choices, not measurements of the PSCM's limits.
|
||||
For example, a C1 target reversal from +0.10 to -0.10 rad required at least
|
||||
0.4 seconds in our output stage alone.
|
||||
|
||||
V9 sends the current bounded C0 and C1 requests on each update. There is no
|
||||
additional actuator ramp, zero hold, reversal mode, or initial engagement ramp.
|
||||
C0 remains the selected-curvature arc at 7 m plus base-C1 overflow. C1 remains
|
||||
base heading plus proportional and integral feedback, with P=0.50 and I=0.25.
|
||||
These are calculated commands, not a known inverse of the PSCM's response.
|
||||
|
||||
Integral cancellation happens first. New integral accumulation fits the
|
||||
combined command's magnitude headroom, rather than the old slew headroom.
|
||||
This removes a second dependence on the old ramp. Fresh-measurement cadence,
|
||||
driver override, PSCM arbitration, stale/invalid input reset, and C2/C3=0 remain.
|
||||
|
||||
The default-off Sunnylink `FordModelActionController` selector still applies
|
||||
to any Ford CAN FD vehicle. Toggle off restores upstream Ford control.
|
||||
Logs identify `model-action-direct-c0-c1-pi-v9`.
|
||||
|
||||
## Remaining bounds
|
||||
|
||||
- Selected desired curvature still passes through upstream `clip_curvature`:
|
||||
3 m/s² lateral acceleration adjusted for roll, 5 m/s³ lateral jerk, and
|
||||
absolute curvature 0.2 m⁻¹. These bound the reference; they are not proof of
|
||||
a physical acceleration or jerk bound under custom C0/C1 feedback.
|
||||
- C0 remains within ±5.11 m and C1 within ±0.50 rad. The downstream packer
|
||||
retains the actual wire ranges, preventing out-of-range values wrapping.
|
||||
- Driver, CAN, timing, service health, and fault gates remain intact.
|
||||
- Panda safety, the 100 Hz sender, and ramp-type selection are unchanged.
|
||||
|
||||
No assumption is made that extended path mode independently enforces ISO
|
||||
limits. The absence of `LimitReached` is not evidence of unrestricted authority.
|
||||
|
||||
## Offline validation
|
||||
|
||||
The same-cycle reversal regression failed on v8 in both directions at 2, 10,
|
||||
and 100 ms timesteps, then passed after the change. Zero-error release reaches
|
||||
zero immediately. Full controlsd/publication/CAN tests check current-request
|
||||
output, upstream reference selection, field packing, driver and PSCM gates,
|
||||
integral cancellation and anti-windup, invalid input, and toggle-off fallback.
|
||||
|
||||
- 358 tests and 23 subtests passed across controller and Ford sender suites.
|
||||
- 20,000 seeded randomized cases produced 60,000 controller updates and CAN
|
||||
round trips, checking independent arithmetic, symmetry, bounds and resets.
|
||||
- Routes 11c, 119 and 11a supplied 461,340 input cycles: 922,680 baseline/v9
|
||||
controller updates and CAN round trips. V8 and v9 activation, feedforward,
|
||||
proportional feedback and feedback gates matched. Every active v9 output
|
||||
matched its current bounded request within wire quantization.
|
||||
- Ruff passed for changed production/tests and the new replay/stress tools.
|
||||
|
||||
| Route | Input cycles | Maximum C0 difference | Maximum C1 difference |
|
||||
|---|---:|---:|---:|
|
||||
| 11c maneuver suite | 54,146 | 0.09 m | 0.1605 rad |
|
||||
| 119 | 171,423 | 2.42 m | 0.4950 rad |
|
||||
| 11a | 235,771 | 3.36 m | 0.3710 rad |
|
||||
|
||||
The maximum differences include engagement and other transitions. Removing
|
||||
slews permits abrupt changes; these are not predictions of wheel motion.
|
||||
Recorded motion, driver input and PSCM feedback stay fixed in replay.
|
||||
Publication timestamps approximate computation time; replay is not a claim of
|
||||
exact onroad command parity. Synthetic reference freshness is approximated
|
||||
from valid maneuver publications. Physical tracking and stability are unvalidated.
|
||||
|
||||
The reproducible tools are `tools/ford_pscm_lab/direct_path_replay.py` and
|
||||
`tools/ford_pscm_lab/direct_path_production_stress.py`. Machine-readable checks
|
||||
are collected in `ford_direct_path_v9_validation.json`.
|
||||
@@ -0,0 +1,125 @@
|
||||
{
|
||||
"version": "model-action-direct-c0-c1-pi-v9",
|
||||
"baseline": "57ae29f257498f58170e2beec140c0f8103c1b43",
|
||||
"tests": {
|
||||
"passed": 358,
|
||||
"subtests_passed": 23
|
||||
},
|
||||
"stress": {
|
||||
"cycles": 20000,
|
||||
"kp": 0.5,
|
||||
"ki": 0.25,
|
||||
"controller_updates": 60000,
|
||||
"can_round_trips": 60000,
|
||||
"seed": 20260913,
|
||||
"independent_c0_comparisons": 20000,
|
||||
"checks": "Independent scalar PI/unwind-first/anti-windup arithmetic, mirror symmetry, exact C0 independence, amplitude bounds, current commands, resets, CAN.",
|
||||
"source_sha256": {
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/direct_path_production_stress.py": "ae0d0e3a32cddefc4072a58664f5afca84019067c8c5d9fa24e89dd1cbef2b99",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/openpilot/selfdrive/controls/lib/ford_model_action.py": "fbc2309c15289db184aa54bda697feb7cfc26a218998b5b21f063381f47aad45"
|
||||
}
|
||||
},
|
||||
"routes": {
|
||||
"11c": {
|
||||
"scope": "Compare v8 and direct C0/C1 on fixed logged inputs, without predicting motion.",
|
||||
"baseline": "57ae29f257498f58170e2beec140c0f8103c1b43",
|
||||
"baseline_source_sha256": "6f8c4a6f7fa54fa2e23f32f1e83ace83da5f0b6f45011941f1902a1dae778ffc",
|
||||
"cycles": 54146,
|
||||
"controller_updates": 108292,
|
||||
"can_round_trips": 108292,
|
||||
"status_counts": {
|
||||
"inactive": 5066,
|
||||
"active": 49080
|
||||
},
|
||||
"identical_validity_feedforward_p_and_feedback_gates": true,
|
||||
"all_candidate_outputs_match_current_bounded_request": true,
|
||||
"changed_c0_cycles": 73,
|
||||
"changed_c1_cycles": 17133,
|
||||
"max_abs_c0_change_m": 0.08999999999999986,
|
||||
"max_abs_c1_change_rad": 0.16049999999999998,
|
||||
"max_abs_integral_rad": 0.02639216769448115,
|
||||
"limitations": [
|
||||
"Recorded motion remains fixed; this cannot establish improved tracking or stability.",
|
||||
"Publication time proxies computation time; reconstructed baseline is not exact onroad parity.",
|
||||
"Synthetic reference freshness is approximated from valid publications; upstream selection itself is unchanged."
|
||||
],
|
||||
"source_sha256": {
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_maneuver_11c/analysis/route.npz": "1f3c37a7c7ae85ba69e9958435a9568f8244fa862f43ad1ee2a0192d1df13966",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_maneuver_11c/analysis/model_paths.npz": "0ff433da3dde1d4d7f10596cd53558ec5de9d56fbc49aec3e8afb4c9872377d5",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/direct_path_replay.py": "2a1baff08152230929d56b23b8f7fb03e1ae9c2b30c5e152a2be431188fc20e7",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/openpilot/selfdrive/controls/lib/ford_model_action.py": "fbc2309c15289db184aa54bda697feb7cfc26a218998b5b21f063381f47aad45"
|
||||
}
|
||||
},
|
||||
"119": {
|
||||
"scope": "Compare v8 and direct C0/C1 on fixed logged inputs, without predicting motion.",
|
||||
"baseline": "57ae29f257498f58170e2beec140c0f8103c1b43",
|
||||
"baseline_source_sha256": "6f8c4a6f7fa54fa2e23f32f1e83ace83da5f0b6f45011941f1902a1dae778ffc",
|
||||
"cycles": 171423,
|
||||
"controller_updates": 342846,
|
||||
"can_round_trips": 342846,
|
||||
"status_counts": {
|
||||
"inactive": 87482,
|
||||
"active": 83941
|
||||
},
|
||||
"identical_validity_feedforward_p_and_feedback_gates": true,
|
||||
"all_candidate_outputs_match_current_bounded_request": true,
|
||||
"changed_c0_cycles": 199,
|
||||
"changed_c1_cycles": 16426,
|
||||
"max_abs_c0_change_m": 2.42,
|
||||
"max_abs_c1_change_rad": 0.495,
|
||||
"max_abs_integral_rad": 0.05010140673563736,
|
||||
"limitations": [
|
||||
"Recorded motion remains fixed; this cannot establish improved tracking or stability.",
|
||||
"Publication time proxies computation time; reconstructed baseline is not exact onroad parity.",
|
||||
"Synthetic reference freshness is approximated from valid publications; upstream selection itself is unchanged."
|
||||
],
|
||||
"source_sha256": {
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route119/route.npz": "415a099935fef152123b7422870442d6cfe6302bab9b8983ce3b3ffc71a7702b",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route119/model_paths.npz": "dfd41383bea486ccd0a476e94e612921a44a099a0c1213ff132ab81df3aa94d7",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/direct_path_replay.py": "2a1baff08152230929d56b23b8f7fb03e1ae9c2b30c5e152a2be431188fc20e7",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/openpilot/selfdrive/controls/lib/ford_model_action.py": "fbc2309c15289db184aa54bda697feb7cfc26a218998b5b21f063381f47aad45"
|
||||
}
|
||||
},
|
||||
"11a": {
|
||||
"scope": "Compare v8 and direct C0/C1 on fixed logged inputs, without predicting motion.",
|
||||
"baseline": "57ae29f257498f58170e2beec140c0f8103c1b43",
|
||||
"baseline_source_sha256": "6f8c4a6f7fa54fa2e23f32f1e83ace83da5f0b6f45011941f1902a1dae778ffc",
|
||||
"cycles": 235771,
|
||||
"controller_updates": 471542,
|
||||
"can_round_trips": 471542,
|
||||
"status_counts": {
|
||||
"inactive": 103039,
|
||||
"active": 132732
|
||||
},
|
||||
"identical_validity_feedforward_p_and_feedback_gates": true,
|
||||
"all_candidate_outputs_match_current_bounded_request": true,
|
||||
"changed_c0_cycles": 689,
|
||||
"changed_c1_cycles": 16106,
|
||||
"max_abs_c0_change_m": 3.3600000000000003,
|
||||
"max_abs_c1_change_rad": 0.371,
|
||||
"max_abs_integral_rad": 0.05163860736233724,
|
||||
"limitations": [
|
||||
"Recorded motion remains fixed; this cannot establish improved tracking or stability.",
|
||||
"Publication time proxies computation time; reconstructed baseline is not exact onroad parity.",
|
||||
"Synthetic reference freshness is approximated from valid publications; upstream selection itself is unchanged."
|
||||
],
|
||||
"source_sha256": {
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route11a/route.npz": "428c203d090731386f06bf9fdeefe608f1999c43c52ff39cab7ee86ec0ac804f",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route11a/model_paths.npz": "e2b827fd7c3dfbf66d7872a56b57eaec13c600a9a48e12dae59be81c31b0978f",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/direct_path_replay.py": "2a1baff08152230929d56b23b8f7fb03e1ae9c2b30c5e152a2be431188fc20e7",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/openpilot/selfdrive/controls/lib/ford_model_action.py": "fbc2309c15289db184aa54bda697feb7cfc26a218998b5b21f063381f47aad45"
|
||||
}
|
||||
}
|
||||
},
|
||||
"limits_removed": [
|
||||
"C0 4 m/s extra slew",
|
||||
"C1 0.5 rad/s extra slew and associated integral slew headroom"
|
||||
],
|
||||
"limits_preserved": [
|
||||
"upstream selected-curvature acceleration/jerk/curvature bounds",
|
||||
"C0/C1 magnitude and wire packing bounds",
|
||||
"driver/fault/validity/freshness/PSCM feedback gates"
|
||||
],
|
||||
"physical_tracking_validated": false,
|
||||
"note": "Replay/stress script hashes identify their execution versions before formatting-only lint cleanup; production controller is unchanged after validation."
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
{
|
||||
"15a": {
|
||||
"cycles": 21867,
|
||||
"wire_checks": 2187,
|
||||
"baseline_recorded_error_p50_p95_p99_max": {
|
||||
"c0": [
|
||||
4.172324707951702e-09,
|
||||
5.2452087118126656e-08,
|
||||
8.583068833445395e-08,
|
||||
1.1444091807533141e-07
|
||||
],
|
||||
"c1": [
|
||||
8.791685157660822e-10,
|
||||
1.1682510403510094e-08,
|
||||
1.4305114759416426e-08,
|
||||
1.478195188475695e-08
|
||||
]
|
||||
},
|
||||
"latched_angle_match_fraction": 1.0,
|
||||
"command_changes": {
|
||||
"c0": [
|
||||
0.2799999999999998,
|
||||
1.3800000000000003,
|
||||
1.8000000000000007,
|
||||
2.51
|
||||
],
|
||||
"c1": [
|
||||
0.01749999999999996,
|
||||
0.124,
|
||||
0.2940450000000001,
|
||||
0.45200000000000007
|
||||
]
|
||||
},
|
||||
"reference_change_p50_p95_max": [
|
||||
0.0015834828704371431,
|
||||
0.012526763883651669,
|
||||
0.03905121179470369
|
||||
],
|
||||
"variants": {
|
||||
"old": {
|
||||
"feedback_switches_active": 112,
|
||||
"feedback_switches_low_speed": 61,
|
||||
"low_speed_c0_steps_over_025m": 52,
|
||||
"low_speed_c1_steps_over_005rad": 36,
|
||||
"low_speed_step_p99": {
|
||||
"c0": 0.4996000000000004,
|
||||
"c1": 0.05668000000000003
|
||||
},
|
||||
"c0_bound_active_s": 0.0,
|
||||
"c1_bound_active_s": 2.5702606670000137
|
||||
},
|
||||
"new": {
|
||||
"feedback_switches_active": 112,
|
||||
"feedback_switches_low_speed": 61,
|
||||
"low_speed_c0_steps_over_025m": 52,
|
||||
"low_speed_c1_steps_over_005rad": 46,
|
||||
"low_speed_step_p99": {
|
||||
"c0": 0.8499999999999996,
|
||||
"c1": 0.08540000000000011
|
||||
},
|
||||
"c0_bound_active_s": 0.0,
|
||||
"c1_bound_active_s": 6.774772933000349
|
||||
}
|
||||
},
|
||||
"method": "Compare direct model-path commands against the driven geometry-curvature controller.\n\nInput: extract.py route.npz/model_paths.npz/metadata.json directories. This checks\nreference selection, command continuity and overrides; it does not predict a changed wheel response.\n",
|
||||
"provenance": {
|
||||
"baseline_commit": "18ded0380045c29520cf1f690688e48ebad043d8",
|
||||
"baseline_controller_sha256": "8bc8554ed6743c5faf433deaa6de3f07fae27d183f62b04f49f9c3b9f1e8dcca",
|
||||
"candidate_controller_sha256": "02b64f40a56cadefb181afbf7c3720affa3c7fe4ac18a2902dc9c546fd6b395d",
|
||||
"fixed_c0_distance_m": 7.0
|
||||
},
|
||||
"sources_sha256": {
|
||||
".cache/ford_route15a/rlog_full/route.npz": "057ce69ce028b2c9c1b42ab8110f35c888a6c398ddcc4156e74afc98c83d119d",
|
||||
".cache/ford_route15a/rlog_full/model_paths.npz": "ef820d9df14afa308bf46f95463b636a599ff24fd08bffaff1ae671b3932c24a",
|
||||
".cache/ford_route15a/rlog_full/metadata.json": "bea47c7f0c0b583964ced3484fe814ec9a7a24abbfe6272e70f9dde12580a756"
|
||||
}
|
||||
},
|
||||
"15b": {
|
||||
"cycles": 22278,
|
||||
"wire_checks": 2228,
|
||||
"baseline_recorded_error_p50_p95_p99_max": {
|
||||
"c0": [
|
||||
6.70552502413102e-10,
|
||||
3.814697269177714e-08,
|
||||
1.0490417512443173e-07,
|
||||
0.009999947547912669
|
||||
],
|
||||
"c1": [
|
||||
1.9371515502797365e-10,
|
||||
5.4836273299940785e-09,
|
||||
1.2159347528850617e-08,
|
||||
1.478195188475695e-08
|
||||
]
|
||||
},
|
||||
"latched_angle_match_fraction": 0.9999321895978843,
|
||||
"command_changes": {
|
||||
"c0": [
|
||||
0.040000000000000036,
|
||||
1.08,
|
||||
1.7699999999999996,
|
||||
2.46
|
||||
],
|
||||
"c1": [
|
||||
0.003500000000000003,
|
||||
0.12449999999999994,
|
||||
0.22634999999999764,
|
||||
0.406
|
||||
]
|
||||
},
|
||||
"reference_change_p50_p95_max": [
|
||||
0.00021085212911765025,
|
||||
0.013382994089416386,
|
||||
0.03945627628640698
|
||||
],
|
||||
"variants": {
|
||||
"old": {
|
||||
"feedback_switches_active": 238,
|
||||
"feedback_switches_low_speed": 107,
|
||||
"low_speed_c0_steps_over_025m": 85,
|
||||
"low_speed_c1_steps_over_005rad": 32,
|
||||
"low_speed_step_p99": {
|
||||
"c0": 0.36999999999999944,
|
||||
"c1": 0.03771999999999984
|
||||
},
|
||||
"c0_bound_active_s": 0.0,
|
||||
"c1_bound_active_s": 6.701652654999748
|
||||
},
|
||||
"new": {
|
||||
"feedback_switches_active": 238,
|
||||
"feedback_switches_low_speed": 107,
|
||||
"low_speed_c0_steps_over_025m": 129,
|
||||
"low_speed_c1_steps_over_005rad": 57,
|
||||
"low_speed_step_p99": {
|
||||
"c0": 0.6187999999999921,
|
||||
"c1": 0.05121999999999982
|
||||
},
|
||||
"c0_bound_active_s": 0.0,
|
||||
"c1_bound_active_s": 7.542629173000023
|
||||
}
|
||||
},
|
||||
"method": "Compare direct model-path commands against the driven geometry-curvature controller.\n\nInput: extract.py route.npz/model_paths.npz/metadata.json directories. This checks\nreference selection, command continuity and overrides; it does not predict a changed wheel response.\n",
|
||||
"provenance": {
|
||||
"baseline_commit": "18ded0380045c29520cf1f690688e48ebad043d8",
|
||||
"baseline_controller_sha256": "8bc8554ed6743c5faf433deaa6de3f07fae27d183f62b04f49f9c3b9f1e8dcca",
|
||||
"candidate_controller_sha256": "02b64f40a56cadefb181afbf7c3720affa3c7fe4ac18a2902dc9c546fd6b395d",
|
||||
"fixed_c0_distance_m": 7.0
|
||||
},
|
||||
"sources_sha256": {
|
||||
".cache/ford_route15b/rlog_full/route.npz": "f95e1c16a4677fee0ab57002d714e22716bf11873c308280553312fa9d18aab0",
|
||||
".cache/ford_route15b/rlog_full/model_paths.npz": "a90be55943ce0009afbf38ef2035b3fff4de8ab00ea266636247bcc5a5012eae",
|
||||
".cache/ford_route15b/rlog_full/metadata.json": "545ef8d6740e57237f35b2ca684111fc2d774e83aa1d6c8a5a0fff1b71270e85"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
# Ford feedback timing follows comma's request-history pattern
|
||||
|
||||
The base C0/C1 mapping continues to use the latest selected curvature. C0 P,
|
||||
C1 P and C1 I now calculate error from an earlier selected curvature, using
|
||||
the same one-second request buffer and frame-index expression as comma's torque
|
||||
controller. The buffer advances at the 100 Hz controls rate, including while
|
||||
disengaged. The outgoing base request is never buffered for later transmission.
|
||||
|
||||
`controlsd` passes the same `lat_delay` value it passes to the upstream lateral
|
||||
controller: `lateralDelay.lateralDelay + LAT_SMOOTH_SECONDS`. The existing extra
|
||||
Ford model preview remains model-side; this change does not add it to feedback
|
||||
delay or infer hardware delay from a turn's angle crossing. In the four replayed
|
||||
routes the published delay was 0.1689463705 s and `LAT_SMOOTH_SECONDS` was zero.
|
||||
Upstream's integer indexing selects the request 16 control cycles earlier,
|
||||
nominally 0.16 s. The maximum lookback is 99 cycles, nominally 0.99 s.
|
||||
|
||||
The history is in curvature units because that is this controller's feedback
|
||||
quantity. This adopts upstream's timing pattern, not its torque conversion,
|
||||
acceleration gain schedule, friction/jerk feedforward or low-speed integral
|
||||
freeze. Ford gains, formulas, anti-windup, integration cadence, field bounds,
|
||||
upstream curvature limiting and driver/PSCM arbitration remain unchanged.
|
||||
Action C0 P remains 1.0; direct-path C0 P remains 0.5; C1 P=0.75 and I=1.0.
|
||||
Both Ford reference modes use the new timing.
|
||||
|
||||
The history starts at zero, like upstream. Ordinary disengagement clears P/I
|
||||
while continuing to record requests. Existing invalid-input, timing and path
|
||||
resets also clear request history, preventing invalid retained references from
|
||||
surviving recovery. A nonfinite delay rejects the active command; finite delay
|
||||
values use upstream's bounded frame selection. An explicit zero delay recovers
|
||||
the previous command law exactly. Existing offline feedback-reference overrides
|
||||
remain available; production supplies delay rather than an override.
|
||||
|
||||
Diagnostics identify `v21-delayed-feedback` and report the requested delay,
|
||||
nominal frame delay, delayed feedback curvature/error and latest requested
|
||||
curvature separately. The reported frame delay is not a measured physical delay.
|
||||
|
||||
## Verification and limitations
|
||||
|
||||
The delay-tracking regression failed against the old feedback behavior: a wheel
|
||||
following a request with the configured delay still received correction. It
|
||||
passes with delayed feedback through entry, reversal and release, in both turn
|
||||
directions and both reference modes. A separate test executes the actual
|
||||
upstream torque controller's buffer-selection expressions as an oracle.
|
||||
|
||||
Integration tests execute controlsd's actual delay wiring, source selection,
|
||||
limiter, adapter and Float32 publication through the Ford CAN sender. A
|
||||
delay-matched wheel trace generates zero P/I while the current base commands
|
||||
reach CAN immediately. Persistent error still integrates; repeated measurements
|
||||
do not integrate twice; driver and fresh PSCM override still clear correction
|
||||
immediately. Exact suite counts are in the validation JSON.
|
||||
|
||||
Four native-time route replays cover 583,599 control cycles and 58,362 CAN
|
||||
pack/decode checks. The baseline is the filtered-driver controller at
|
||||
`baeabfaa807c7a07baf183968e570f2c1d3fd665`. Baseline commands match the archived
|
||||
v20 replay exactly on all four routes. A third controller with zero delay
|
||||
matches that baseline's commands and integral on every cycle. With delay,
|
||||
command validity, base heading/overflow and feedback arbitration remain equal.
|
||||
All commands remain bounded with C2=C3=0.
|
||||
|
||||
| Route | Low-speed C0 changes >0.25 m, v20 → v21 | Low-speed C1 changes >0.05 rad, v20 → v21 | C1 at field bound, v20 → v21 |
|
||||
| --- | --- | --- | --- |
|
||||
| 162 | 70 → 68 | 50 → 44 | 1.06 → 0.92 s |
|
||||
| 157 | 137 → 110 | 59 → 51 | 6.52 → 5.94 s |
|
||||
| 151 | 77 → 65 | 42 → 36 | 4.04 → 3.89 s |
|
||||
| 149 | 197 → 159 | 111 → 95 | 16.08 → 13.23 s |
|
||||
|
||||
Low speed means below 15 mph; these counts include valid driver-interaction
|
||||
periods and are command-continuity measurements, not autonomous tracking scores.
|
||||
Neither baseline nor candidate was driven in these recordings; both command
|
||||
streams use the same recorded requests and vehicle motion.
|
||||
|
||||
The change reduces transient correction. At route-162 time 302.649 s, requesting
|
||||
141.4 degrees with the wheel at 87.3 degrees, replay C0 changes from -2.19 to
|
||||
-1.73 m and C1 from -0.3695 to -0.3135 rad. At 307.502 s, after driver input,
|
||||
opposite-direction release C0 falls from +1.52 to +1.04 m and C1 from +0.0655
|
||||
to +0.0355 rad. The base request is unchanged in each example.
|
||||
|
||||
This can mean less correction during both entry and unwind. Fewer large command
|
||||
steps do not prove reduced physical oscillation, maintained turn authority or
|
||||
better release. These are frozen-motion replays, not a validated PSCM simulator.
|
||||
The supplied delay has not been identified specifically for the combined C0/C1
|
||||
response. The next physical evaluation must distinguish these effects; this is
|
||||
not a demonstrated death-wobble fix.
|
||||
|
||||
Reproduce with the project's Python environment and built dependencies:
|
||||
|
||||
```sh
|
||||
PYTHONPATH=.:opendbc_repo:.cache/ford_geometry_deps python \
|
||||
tools/ford_pscm_lab/filtered_driver_replay.py \
|
||||
--source .cache/ford_route162/full \
|
||||
--delay-intake .cache/ford_route162/intake.npz \
|
||||
--baseline baeabfaa807c7a07baf183968e570f2c1d3fd665 \
|
||||
--output .cache/ford_feedback_delay_v21/162
|
||||
```
|
||||
|
||||
Pull and restart the software while offroad. The existing master toggle still
|
||||
selects upstream Ford control when disabled. For the current action trial,
|
||||
leave Model Geometry Reference and C0 one-second distance disabled.
|
||||
@@ -0,0 +1,391 @@
|
||||
{
|
||||
"baseline_commit": "baeabfaa807c7a07baf183968e570f2c1d3fd665",
|
||||
"tests": {
|
||||
"passed": 722,
|
||||
"subtests_passed": 2
|
||||
},
|
||||
"total_cycles": 583599,
|
||||
"total_wire_checks": 58362,
|
||||
"code_sha256": {
|
||||
"openpilot/selfdrive/controls/lib/ford_model_action.py": "d1730a79307297b32b44ba18e9792df216a1961a3f32927c5e6acf28014d288d",
|
||||
"openpilot/selfdrive/controls/controlsd.py": "3427212538fcb1cf4d8a44f6be62c54b31ab5ba1a3eb16d958bb58f9c52abecf",
|
||||
"openpilot/selfdrive/controls/lib/latcontrol_torque.py": "83ea33d37c349b10eca994150dae2a5841ec56b7038402432205a74054c917ee",
|
||||
"tools/ford_pscm_lab/filtered_driver_replay.py": "9c210b31df62cb6f41e6cf10d76e2e7ba15eefbbfcb30a8ea0244e095365543a"
|
||||
},
|
||||
"method": "Same frozen recorded inputs for v20, v21 using logged lateralDelay, and v21 with zero delay. The latter must exactly match v20 at every cycle. No counterfactual wheel response is predicted.",
|
||||
"limitations": "Supplied delay is not an identified C0/C1-specific response model. Lower command steps do not establish physical stability. Feedback demand is lower on rising and falling requests; entry and unwind can weaken.",
|
||||
"points": [
|
||||
{
|
||||
"meaning": "Clean entry shortfall",
|
||||
"t": 302.64934647200005,
|
||||
"desired_angle": 141.37510681152344,
|
||||
"old_c0": -2.1900000000000004,
|
||||
"new_c0": -1.7300000000000004,
|
||||
"old_c1": -0.36950000000000005,
|
||||
"new_c1": -0.3135,
|
||||
"old_i": -0.05458394920016066,
|
||||
"new_i": -0.02249363859059498,
|
||||
"old_reference": -0.03498964384198189,
|
||||
"new_reference": -0.030434442684054375
|
||||
},
|
||||
{
|
||||
"meaning": "Release following driver input; command comparison only",
|
||||
"t": 307.502298498,
|
||||
"desired_angle": 22.443912506103516,
|
||||
"old_c0": 1.52,
|
||||
"new_c0": 1.04,
|
||||
"old_c1": 0.0655,
|
||||
"new_c1": 0.035499999999999976,
|
||||
"old_i": 0.018708751669361232,
|
||||
"new_i": 0.01363232660438634,
|
||||
"old_reference": -0.005546241067349911,
|
||||
"new_reference": -0.010331000201404095
|
||||
}
|
||||
],
|
||||
"routes": {
|
||||
"162": {
|
||||
"cycles": 49003,
|
||||
"wire_checks": 4901,
|
||||
"zero_delay_matches_baseline": true,
|
||||
"feedback_delay_requested_min_median_max": [
|
||||
0.16894637048244476,
|
||||
0.16894637048244476,
|
||||
0.16894637048244476
|
||||
],
|
||||
"feedback_delay_used_min_median_max": [
|
||||
0.16,
|
||||
0.16,
|
||||
0.16
|
||||
],
|
||||
"baseline_recorded_error_p50_p95_p99_max": {
|
||||
"c0": [
|
||||
2.3841861818141297e-09,
|
||||
5.2452087118126656e-08,
|
||||
0.39000000458657785,
|
||||
2.5800000023841863
|
||||
],
|
||||
"c1": [
|
||||
0.001499999597668611,
|
||||
0.07550000331401825,
|
||||
0.11149999844551088,
|
||||
0.24500000381469733
|
||||
]
|
||||
},
|
||||
"latched_angle_match_fraction": 1.0,
|
||||
"command_changes": {
|
||||
"c0": [
|
||||
0.019999999999999574,
|
||||
0.22000000000000064,
|
||||
0.4300000000000006,
|
||||
0.839999999999999
|
||||
],
|
||||
"c1": [
|
||||
0.003500000000000003,
|
||||
0.02899999999999997,
|
||||
0.04049999999999998,
|
||||
0.0635
|
||||
]
|
||||
},
|
||||
"variants": {
|
||||
"old": {
|
||||
"feedback_switches_active": 218,
|
||||
"feedback_switches_low_speed": 95,
|
||||
"low_speed_c0_steps_over_025m": 70,
|
||||
"low_speed_c1_steps_over_005rad": 50,
|
||||
"low_speed_step_p99": {
|
||||
"c0": 0.16000000000000014,
|
||||
"c1": 0.019949999999999843
|
||||
},
|
||||
"raw_crossing_c0_steps_over_025m": 0,
|
||||
"c0_bound_active_s": 0.0,
|
||||
"c1_bound_active_s": 1.0580744659999937
|
||||
},
|
||||
"new": {
|
||||
"feedback_switches_active": 218,
|
||||
"feedback_switches_low_speed": 95,
|
||||
"low_speed_c0_steps_over_025m": 68,
|
||||
"low_speed_c1_steps_over_005rad": 44,
|
||||
"low_speed_step_p99": {
|
||||
"c0": 0.13999999999999968,
|
||||
"c1": 0.018449999999999786
|
||||
},
|
||||
"raw_crossing_c0_steps_over_025m": 0,
|
||||
"c0_bound_active_s": 0.0,
|
||||
"c1_bound_active_s": 0.9192208439999945
|
||||
}
|
||||
},
|
||||
"method": "Compare Ford feedback variants on recorded references and frozen vehicle motion.\n\nInput: extract.py route.npz/model_paths.npz/metadata.json directories. This checks\ncommand continuity and overrides; it does not predict a changed wheel response.\nBoth controllers receive the logged selected curvature. Recorded-command agreement\nis expected only for routes driven with the selected baseline and fixed 7 m C0.\n",
|
||||
"provenance": {
|
||||
"baseline_commit": "baeabfaa807c7a07baf183968e570f2c1d3fd665",
|
||||
"baseline_controller_sha256": "b34a55f84fdbf6c432ce89cdc378aaaf9bbd765ca4e6934845059f586e6f0773",
|
||||
"candidate_controller_sha256": "d1730a79307297b32b44ba18e9792df216a1961a3f32927c5e6acf28014d288d",
|
||||
"fixed_c0_distance_m": 7.0
|
||||
},
|
||||
"sources_sha256": {
|
||||
".cache/ford_route162/full/route.npz": "a725f9ca309b90a2d750d51a9b9d5b459d99119a54ab048631bc0fce07348af8",
|
||||
".cache/ford_route162/full/model_paths.npz": "cffe310b122d9ae26a53ee504608156b4242a6bba5f681898d5b661951ac5ce5",
|
||||
".cache/ford_route162/full/metadata.json": "ef3ac1caca0eaec31f7140a0743bd3b98885ce143cb0f97a8eee615b33943d7f",
|
||||
".cache/ford_route162/intake.npz": "9455b01e1d1d3dcba08276b382a8c6cd54ff6fb19cb417b72d0ac89cf193034b"
|
||||
},
|
||||
"baseline_matches_archived_v20_commands_and_integral": true
|
||||
},
|
||||
"157": {
|
||||
"cycles": 76554,
|
||||
"wire_checks": 7656,
|
||||
"zero_delay_matches_baseline": true,
|
||||
"feedback_delay_requested_min_median_max": [
|
||||
0.0,
|
||||
0.16894637048244476,
|
||||
0.16894637048244476
|
||||
],
|
||||
"feedback_delay_used_min_median_max": [
|
||||
0.16,
|
||||
0.16,
|
||||
0.16
|
||||
],
|
||||
"baseline_recorded_error_p50_p95_p99_max": {
|
||||
"c0": [
|
||||
0.009999999999999787,
|
||||
0.41999999010562905,
|
||||
1.3956999619007082,
|
||||
5.39999999165535
|
||||
],
|
||||
"c1": [
|
||||
4.291534405620467e-09,
|
||||
0.03549999962002037,
|
||||
0.06600000168576836,
|
||||
0.4085000002384186
|
||||
]
|
||||
},
|
||||
"latched_angle_match_fraction": 1.0,
|
||||
"command_changes": {
|
||||
"c0": [
|
||||
0.009999999999999787,
|
||||
0.21999999999999975,
|
||||
0.4900000000000002,
|
||||
1.5100000000000007
|
||||
],
|
||||
"c1": [
|
||||
0.0030000000000000027,
|
||||
0.028999999999999915,
|
||||
0.039000000000000035,
|
||||
0.09049999999999997
|
||||
]
|
||||
},
|
||||
"variants": {
|
||||
"old": {
|
||||
"feedback_switches_active": 224,
|
||||
"feedback_switches_low_speed": 148,
|
||||
"low_speed_c0_steps_over_025m": 137,
|
||||
"low_speed_c1_steps_over_005rad": 59,
|
||||
"low_speed_step_p99": {
|
||||
"c0": 0.20000000000000018,
|
||||
"c1": 0.022499999999999964
|
||||
},
|
||||
"raw_crossing_c0_steps_over_025m": 0,
|
||||
"c0_bound_active_s": 0.2999506119999751,
|
||||
"c1_bound_active_s": 6.521285587999955
|
||||
},
|
||||
"new": {
|
||||
"feedback_switches_active": 224,
|
||||
"feedback_switches_low_speed": 148,
|
||||
"low_speed_c0_steps_over_025m": 110,
|
||||
"low_speed_c1_steps_over_005rad": 51,
|
||||
"low_speed_step_p99": {
|
||||
"c0": 0.16000000000000014,
|
||||
"c1": 0.01750000000000007
|
||||
},
|
||||
"raw_crossing_c0_steps_over_025m": 0,
|
||||
"c0_bound_active_s": 0.2999506119999751,
|
||||
"c1_bound_active_s": 5.943273473999852
|
||||
}
|
||||
},
|
||||
"method": "Compare Ford feedback variants on recorded references and frozen vehicle motion.\n\nInput: extract.py route.npz/model_paths.npz/metadata.json directories. This checks\ncommand continuity and overrides; it does not predict a changed wheel response.\nBoth controllers receive the logged selected curvature. Recorded-command agreement\nis expected only for routes driven with the selected baseline and fixed 7 m C0.\n",
|
||||
"provenance": {
|
||||
"baseline_commit": "baeabfaa807c7a07baf183968e570f2c1d3fd665",
|
||||
"baseline_controller_sha256": "b34a55f84fdbf6c432ce89cdc378aaaf9bbd765ca4e6934845059f586e6f0773",
|
||||
"candidate_controller_sha256": "d1730a79307297b32b44ba18e9792df216a1961a3f32927c5e6acf28014d288d",
|
||||
"fixed_c0_distance_m": 7.0
|
||||
},
|
||||
"sources_sha256": {
|
||||
".cache/ford_route157/full/route.npz": "e2e2573f904aa11ee9e10450e7f5b965d475657b61127e827a67eadcd6b857fb",
|
||||
".cache/ford_route157/full/model_paths.npz": "25fc2526e092fde5ceb5aab63a5aa4f73a2c989a3263cedb0a26faf8da61c47b",
|
||||
".cache/ford_route157/full/metadata.json": "f2d72c9a877ed6694e4da6831841113dc0c05987e2c76cca51f358d12411b235",
|
||||
".cache/ford_route157/intake.npz": "5dffd86fde68197727d6c6b9c5eae82320a456fa1206c4cf18e5e9c69419dde4"
|
||||
},
|
||||
"baseline_matches_archived_v20_commands_and_integral": true
|
||||
},
|
||||
"151": {
|
||||
"cycles": 325708,
|
||||
"wire_checks": 32571,
|
||||
"zero_delay_matches_baseline": true,
|
||||
"feedback_delay_requested_min_median_max": [
|
||||
0.16894637048244476,
|
||||
0.16894637048244476,
|
||||
0.16894637048244476
|
||||
],
|
||||
"feedback_delay_used_min_median_max": [
|
||||
0.16,
|
||||
0.16,
|
||||
0.16
|
||||
],
|
||||
"baseline_recorded_error_p50_p95_p99_max": {
|
||||
"c0": [
|
||||
0.010000000223516992,
|
||||
0.2000000013411043,
|
||||
1.010000009727478,
|
||||
4.550000023841858
|
||||
],
|
||||
"c1": [
|
||||
0.004999999240040742,
|
||||
0.02900000113248826,
|
||||
0.07250000010803342,
|
||||
0.28449999523162844
|
||||
]
|
||||
},
|
||||
"latched_angle_match_fraction": 1.0,
|
||||
"command_changes": {
|
||||
"c0": [
|
||||
0.009999999999999787,
|
||||
0.07999999999999999,
|
||||
0.2900000000000002,
|
||||
1.2599999999999998
|
||||
],
|
||||
"c1": [
|
||||
0.0025000000000000022,
|
||||
0.02250000000000002,
|
||||
0.038500000000000034,
|
||||
0.07650000000000001
|
||||
]
|
||||
},
|
||||
"variants": {
|
||||
"old": {
|
||||
"feedback_switches_active": 259,
|
||||
"feedback_switches_low_speed": 101,
|
||||
"low_speed_c0_steps_over_025m": 77,
|
||||
"low_speed_c1_steps_over_005rad": 42,
|
||||
"low_speed_step_p99": {
|
||||
"c0": 0.13999999999999968,
|
||||
"c1": 0.014499999999999957
|
||||
},
|
||||
"raw_crossing_c0_steps_over_025m": 0,
|
||||
"c0_bound_active_s": 0.029249633000290487,
|
||||
"c1_bound_active_s": 4.042117826999856
|
||||
},
|
||||
"new": {
|
||||
"feedback_switches_active": 259,
|
||||
"feedback_switches_low_speed": 101,
|
||||
"low_speed_c0_steps_over_025m": 65,
|
||||
"low_speed_c1_steps_over_005rad": 36,
|
||||
"low_speed_step_p99": {
|
||||
"c0": 0.10000000000000053,
|
||||
"c1": 0.01050000000000001
|
||||
},
|
||||
"raw_crossing_c0_steps_over_025m": 0,
|
||||
"c0_bound_active_s": 0.0,
|
||||
"c1_bound_active_s": 3.8897953380001127
|
||||
}
|
||||
},
|
||||
"method": "Compare Ford feedback variants on recorded references and frozen vehicle motion.\n\nInput: extract.py route.npz/model_paths.npz/metadata.json directories. This checks\ncommand continuity and overrides; it does not predict a changed wheel response.\nBoth controllers receive the logged selected curvature. Recorded-command agreement\nis expected only for routes driven with the selected baseline and fixed 7 m C0.\n",
|
||||
"provenance": {
|
||||
"baseline_commit": "baeabfaa807c7a07baf183968e570f2c1d3fd665",
|
||||
"baseline_controller_sha256": "b34a55f84fdbf6c432ce89cdc378aaaf9bbd765ca4e6934845059f586e6f0773",
|
||||
"candidate_controller_sha256": "d1730a79307297b32b44ba18e9792df216a1961a3f32927c5e6acf28014d288d",
|
||||
"fixed_c0_distance_m": 7.0
|
||||
},
|
||||
"sources_sha256": {
|
||||
".cache/ford_route151/full/route.npz": "41a5b8bd388cf5a3d553f784542376ac9355fcdc5be4f427053d0504537babe1",
|
||||
".cache/ford_route151/full/model_paths.npz": "980b3843cec04254280d744a5801cee1bf0f8ef371052398327ff245f9eee01b",
|
||||
".cache/ford_route151/full/metadata.json": "937825317a0edd470c54647240b922be8f79dda5b3365ffdd61281f0aca877a1",
|
||||
".cache/ford_route151/intake.npz": "18bbefb7738cebd0071dc987e90f74469a0c8ed456f9412324030592cafd68c9"
|
||||
},
|
||||
"baseline_matches_archived_v20_commands_and_integral": true
|
||||
},
|
||||
"149": {
|
||||
"cycles": 132334,
|
||||
"wire_checks": 13234,
|
||||
"zero_delay_matches_baseline": true,
|
||||
"feedback_delay_requested_min_median_max": [
|
||||
0.16894637048244476,
|
||||
0.16894637048244476,
|
||||
0.16894637048244476
|
||||
],
|
||||
"feedback_delay_used_min_median_max": [
|
||||
0.16,
|
||||
0.16,
|
||||
0.16
|
||||
],
|
||||
"baseline_recorded_error_p50_p95_p99_max": {
|
||||
"c0": [
|
||||
0.02999999910593054,
|
||||
0.8400000008046626,
|
||||
2.1400000276565554,
|
||||
4.560000002980233
|
||||
],
|
||||
"c1": [
|
||||
0.015500000141560999,
|
||||
0.10499999982118613,
|
||||
0.32499999746203423,
|
||||
0.43149999833107
|
||||
]
|
||||
},
|
||||
"latched_angle_match_fraction": 1.0,
|
||||
"command_changes": {
|
||||
"c0": [
|
||||
0.009999999999999787,
|
||||
0.1999999999999993,
|
||||
0.4900000000000002,
|
||||
1.3100000000000005
|
||||
],
|
||||
"c1": [
|
||||
0.0040000000000000036,
|
||||
0.03200000000000003,
|
||||
0.04999999999999999,
|
||||
0.08850000000000002
|
||||
]
|
||||
},
|
||||
"variants": {
|
||||
"old": {
|
||||
"feedback_switches_active": 354,
|
||||
"feedback_switches_low_speed": 171,
|
||||
"low_speed_c0_steps_over_025m": 197,
|
||||
"low_speed_c1_steps_over_005rad": 111,
|
||||
"low_speed_step_p99": {
|
||||
"c0": 0.1999999999999993,
|
||||
"c1": 0.02150000000000002
|
||||
},
|
||||
"raw_crossing_c0_steps_over_025m": 0,
|
||||
"c0_bound_active_s": 0.0,
|
||||
"c1_bound_active_s": 16.08094779300012
|
||||
},
|
||||
"new": {
|
||||
"feedback_switches_active": 354,
|
||||
"feedback_switches_low_speed": 171,
|
||||
"low_speed_c0_steps_over_025m": 159,
|
||||
"low_speed_c1_steps_over_005rad": 95,
|
||||
"low_speed_step_p99": {
|
||||
"c0": 0.16000000000000014,
|
||||
"c1": 0.017500000000000016
|
||||
},
|
||||
"raw_crossing_c0_steps_over_025m": 0,
|
||||
"c0_bound_active_s": 0.0,
|
||||
"c1_bound_active_s": 13.22902962499984
|
||||
}
|
||||
},
|
||||
"method": "Compare Ford feedback variants on recorded references and frozen vehicle motion.\n\nInput: extract.py route.npz/model_paths.npz/metadata.json directories. This checks\ncommand continuity and overrides; it does not predict a changed wheel response.\nBoth controllers receive the logged selected curvature. Recorded-command agreement\nis expected only for routes driven with the selected baseline and fixed 7 m C0.\n",
|
||||
"provenance": {
|
||||
"baseline_commit": "baeabfaa807c7a07baf183968e570f2c1d3fd665",
|
||||
"baseline_controller_sha256": "b34a55f84fdbf6c432ce89cdc378aaaf9bbd765ca4e6934845059f586e6f0773",
|
||||
"candidate_controller_sha256": "d1730a79307297b32b44ba18e9792df216a1961a3f32927c5e6acf28014d288d",
|
||||
"fixed_c0_distance_m": 7.0
|
||||
},
|
||||
"sources_sha256": {
|
||||
".cache/ford_route149/full/route.npz": "aa5902877343cd033ee286b3668d91a85336ffbf740861849fb0b76b0ca24ade",
|
||||
".cache/ford_route149/full/model_paths.npz": "19827800b8fb5983f3d6b72fcfaf36e35a40170bb17cd7c1e49374744fb449fb",
|
||||
".cache/ford_route149/full/metadata.json": "624fff03c25eb298661cb7b25f3dbe6d214d863f799d93635c0f4f05fc0d2b32",
|
||||
".cache/ford_route149/intake.npz": "503919c4f1566ef850007c59f16ce074697ba57732f21beb7b158c1ca7be3a9d"
|
||||
},
|
||||
"baseline_matches_archived_v20_commands_and_integral": true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
# Ford feedback: use filtered driver input
|
||||
|
||||
Route 162 ran action mode at C0 P=1.0. Raw torque crossings above 1 Nm
|
||||
repeatedly removed C0/C1 feedback even while Ford's `steeringPressed` remained
|
||||
false. The controller duplicated the torque threshold without Ford's existing
|
||||
filter, so short crossings discarded the integral and abruptly removed P.
|
||||
|
||||
The adapter now uses `steeringPressed` for this decision. The existing Ford
|
||||
threshold and filter are unchanged. From a zero counter, sustained torque
|
||||
crosses that filter on the sixth sample. Fresh PSCM driver override (limit=3)
|
||||
and invalid torque still clear feedback immediately. PSCM denial/inactive
|
||||
status, freshness checks and input validity retain their previous behavior.
|
||||
Clearing feedback removes correction; the base path request remains.
|
||||
|
||||
This applies to action and direct-path modes. Gains, reference selection,
|
||||
distances, bounds, integration/unwind math, upstream request limits and CAN
|
||||
cadence are unchanged. Action C0 P remains 1.0; direct-path C0 P remains 0.5.
|
||||
Diagnostic names end in `v20-filtered-driver` so the next drive can confirm the
|
||||
new arbitration actually ran.
|
||||
|
||||
## Validation
|
||||
|
||||
The short-torque-pulse regression failed against the old adapter before the
|
||||
change. Afterward, the Ford controller, integration/CAN, geometry and Sunnylink
|
||||
suites passed: 658 tests and 2 subtests. Tests exercise the actual shared Ford
|
||||
filter, both torque signs, both reference modes, immediate PSCM override and
|
||||
invalid torque. Ruff and `git diff --check` passed.
|
||||
|
||||
Native-time replay compares the new adapter with
|
||||
`4900c0a40c87c72000b7168a6cf4fe6dd98ea6d0`, supplying identical recorded selected
|
||||
curvature, vehicle measurements, driver flags and PSCM status. Four routes
|
||||
cover 583,599 control cycles and 58,362 CAN pack/decode checks. Every command
|
||||
stays in its field bounds with C2=C3=0, and filtered driver input or fresh PSCM
|
||||
override clears all feedback.
|
||||
|
||||
Route 162's baseline reproduces recorded C0/C1 to Float32 precision (maximum
|
||||
errors 1.15e-7 m and 1.48e-8 rad). Older routes were driven with earlier gains;
|
||||
their comparisons below are two counterfactual command streams on the same
|
||||
recorded motion, not a reproduction of those older deployed controllers.
|
||||
|
||||
| Route | C0 changes >0.25 m below 15 mph, old → new | C1 changes >0.05 rad below 15 mph, old → new | C1 at field bound, old → new |
|
||||
| --- | --- | --- | --- |
|
||||
| 162 | 152 → 70 | 83 → 50 | 0.97 → 1.06 s |
|
||||
| 157 | 229 → 137 | 105 → 59 | 5.83 → 6.52 s |
|
||||
| 151 | 142 → 77 | 86 → 42 | 3.39 → 4.04 s |
|
||||
| 149 | 375 → 197 | 226 → 111 | 10.44 → 16.08 s |
|
||||
|
||||
For route 162, feedback on/off transitions fall from 538 to 218. At raw-torque
|
||||
threshold crossings with unchanged model frame, nearly unchanged target and
|
||||
no filtered driver/PSCM override, large C0 changes fall from 99 to zero.
|
||||
Remaining transitions include legitimate driver input and PSCM status changes.
|
||||
|
||||
Retained correction changes C1 after a brief torque crossing has ended. This
|
||||
can increase holding and time at the field limit, especially on route 149.
|
||||
Replay proves command continuity and override behavior on frozen measurements;
|
||||
it cannot establish improved physical tracking, stability or unwinding. Route
|
||||
162 also had lag without any torque-triggered reset, which this change alone
|
||||
does not explain.
|
||||
|
||||
Detailed metrics, source hashes and controller provenance are in
|
||||
`ford_filtered_driver_v20_validation.json`. Reproduce a route with:
|
||||
|
||||
```sh
|
||||
PYTHONPATH=.:opendbc_repo:.cache/ford_geometry_deps \
|
||||
/Users/ibpersonal/dev/sunnypilot/.venv/bin/python \
|
||||
tools/ford_pscm_lab/filtered_driver_replay.py \
|
||||
--source .cache/ford_route162/full --output .cache/ford_filtered_driver_v20/162
|
||||
```
|
||||
|
||||
## Deployment
|
||||
|
||||
Pull and restart the updated software while offroad. Keep Selected-Action Path
|
||||
Tracking enabled, Model Geometry Reference disabled and C0 one-second distance
|
||||
disabled for the current action/fixed-7-m trial. Turning the master controller
|
||||
toggle off continues to select upstream Ford control.
|
||||
@@ -0,0 +1,308 @@
|
||||
{
|
||||
"baseline_commit": "4900c0a40c87c72000b7168a6cf4fe6dd98ea6d0",
|
||||
"tests": {
|
||||
"passed": 658,
|
||||
"subtests_passed": 2
|
||||
},
|
||||
"total_cycles": 583599,
|
||||
"total_wire_checks": 58362,
|
||||
"limitations": "Frozen recorded motion; no predicted wheel response. Only route 162 ran the baseline gain/version. Older-route baseline differences from recorded commands are expected.",
|
||||
"routes": {
|
||||
"162": {
|
||||
"cycles": 49003,
|
||||
"wire_checks": 4901,
|
||||
"baseline_recorded_error_p50_p95_p99_max": {
|
||||
"c0": [
|
||||
2.3841861818141297e-09,
|
||||
3.33786012163273e-08,
|
||||
5.7220459037665705e-08,
|
||||
1.1444091807533141e-07
|
||||
],
|
||||
"c1": [
|
||||
4.6193593394860955e-10,
|
||||
7.152557435219364e-09,
|
||||
1.2874603272372553e-08,
|
||||
1.478195188475695e-08
|
||||
]
|
||||
},
|
||||
"latched_angle_match_fraction": 1.0,
|
||||
"command_changes": {
|
||||
"c0": [
|
||||
0.0,
|
||||
0.0,
|
||||
0.3899999999999997,
|
||||
2.58
|
||||
],
|
||||
"c1": [
|
||||
0.0014999999999999458,
|
||||
0.07550000000000001,
|
||||
0.11149999999999999,
|
||||
0.2450000000000001
|
||||
]
|
||||
},
|
||||
"variants": {
|
||||
"old": {
|
||||
"feedback_switches_active": 538,
|
||||
"feedback_switches_low_speed": 219,
|
||||
"low_speed_c0_steps_over_025m": 152,
|
||||
"low_speed_c1_steps_over_005rad": 83,
|
||||
"low_speed_step_p99": {
|
||||
"c0": 0.5400000000000005,
|
||||
"c1": 0.033499999999999974
|
||||
},
|
||||
"raw_crossing_c0_steps_over_025m": 99,
|
||||
"c0_bound_active_s": 0.0,
|
||||
"c1_bound_active_s": 0.9656279909999625
|
||||
},
|
||||
"new": {
|
||||
"feedback_switches_active": 218,
|
||||
"feedback_switches_low_speed": 95,
|
||||
"low_speed_c0_steps_over_025m": 70,
|
||||
"low_speed_c1_steps_over_005rad": 50,
|
||||
"low_speed_step_p99": {
|
||||
"c0": 0.16000000000000014,
|
||||
"c1": 0.019949999999999843
|
||||
},
|
||||
"raw_crossing_c0_steps_over_025m": 0,
|
||||
"c0_bound_active_s": 0.0,
|
||||
"c1_bound_active_s": 1.0580744659999937
|
||||
}
|
||||
},
|
||||
"method": "Compare driver-input arbitration on recorded references and frozen vehicle motion.\n\nInput: extract.py route.npz/model_paths.npz/metadata.json directories. This checks\ncommand continuity and overrides; it does not predict a changed wheel response.\nBoth controllers receive the logged selected curvature. Recorded-command agreement\nis expected only for routes driven with the selected baseline and fixed 7 m C0.\n",
|
||||
"provenance": {
|
||||
"baseline_commit": "4900c0a40c87c72000b7168a6cf4fe6dd98ea6d0",
|
||||
"baseline_controller_sha256": "d5841c1f6a4368de20ff14de89a48d7dce737c6a41d79628ccb4f8b19f28f91b",
|
||||
"candidate_controller_sha256": "b34a55f84fdbf6c432ce89cdc378aaaf9bbd765ca4e6934845059f586e6f0773",
|
||||
"fixed_c0_distance_m": 7.0
|
||||
},
|
||||
"sources_sha256": {
|
||||
".cache/ford_route162/full/route.npz": "a725f9ca309b90a2d750d51a9b9d5b459d99119a54ab048631bc0fce07348af8",
|
||||
".cache/ford_route162/full/model_paths.npz": "cffe310b122d9ae26a53ee504608156b4242a6bba5f681898d5b661951ac5ce5",
|
||||
".cache/ford_route162/full/metadata.json": "ef3ac1caca0eaec31f7140a0743bd3b98885ce143cb0f97a8eee615b33943d7f"
|
||||
},
|
||||
"valid_active_seconds": 319.0762965260001
|
||||
},
|
||||
"157": {
|
||||
"cycles": 76554,
|
||||
"wire_checks": 7656,
|
||||
"baseline_recorded_error_p50_p95_p99_max": {
|
||||
"c0": [
|
||||
0.009999999999999787,
|
||||
0.3699999867081644,
|
||||
1.1900000762939458,
|
||||
2.7099999666213987
|
||||
],
|
||||
"c1": [
|
||||
2.5331975406217566e-10,
|
||||
7.152557379708213e-09,
|
||||
1.3113021890553966e-08,
|
||||
1.478195188475695e-08
|
||||
]
|
||||
},
|
||||
"latched_angle_match_fraction": 1.0,
|
||||
"command_changes": {
|
||||
"c0": [
|
||||
0.0,
|
||||
0.0,
|
||||
0.18569999999999706,
|
||||
5.4
|
||||
],
|
||||
"c1": [
|
||||
0.0,
|
||||
0.035499999999999976,
|
||||
0.066,
|
||||
0.40850000000000003
|
||||
]
|
||||
},
|
||||
"variants": {
|
||||
"old": {
|
||||
"feedback_switches_active": 484,
|
||||
"feedback_switches_low_speed": 312,
|
||||
"low_speed_c0_steps_over_025m": 229,
|
||||
"low_speed_c1_steps_over_005rad": 105,
|
||||
"low_speed_step_p99": {
|
||||
"c0": 0.3100000000000005,
|
||||
"c1": 0.028000000000000025
|
||||
},
|
||||
"raw_crossing_c0_steps_over_025m": 99,
|
||||
"c0_bound_active_s": 0.25064877399995567,
|
||||
"c1_bound_active_s": 5.832844185000681
|
||||
},
|
||||
"new": {
|
||||
"feedback_switches_active": 224,
|
||||
"feedback_switches_low_speed": 148,
|
||||
"low_speed_c0_steps_over_025m": 137,
|
||||
"low_speed_c1_steps_over_005rad": 59,
|
||||
"low_speed_step_p99": {
|
||||
"c0": 0.20000000000000018,
|
||||
"c1": 0.022499999999999964
|
||||
},
|
||||
"raw_crossing_c0_steps_over_025m": 0,
|
||||
"c0_bound_active_s": 0.2999506119999751,
|
||||
"c1_bound_active_s": 6.521285587999955
|
||||
}
|
||||
},
|
||||
"method": "Compare driver-input arbitration on recorded references and frozen vehicle motion.\n\nInput: extract.py route.npz/model_paths.npz/metadata.json directories. This checks\ncommand continuity and overrides; it does not predict a changed wheel response.\nBoth controllers receive the logged selected curvature. Recorded-command agreement\nis expected only for routes driven with the selected baseline and fixed 7 m C0.\n",
|
||||
"provenance": {
|
||||
"baseline_commit": "4900c0a40c87c72000b7168a6cf4fe6dd98ea6d0",
|
||||
"baseline_controller_sha256": "d5841c1f6a4368de20ff14de89a48d7dce737c6a41d79628ccb4f8b19f28f91b",
|
||||
"candidate_controller_sha256": "b34a55f84fdbf6c432ce89cdc378aaaf9bbd765ca4e6934845059f586e6f0773",
|
||||
"fixed_c0_distance_m": 7.0
|
||||
},
|
||||
"sources_sha256": {
|
||||
".cache/ford_route157/full/route.npz": "e2e2573f904aa11ee9e10450e7f5b965d475657b61127e827a67eadcd6b857fb",
|
||||
".cache/ford_route157/full/model_paths.npz": "25fc2526e092fde5ceb5aab63a5aa4f73a2c989a3263cedb0a26faf8da61c47b",
|
||||
".cache/ford_route157/full/metadata.json": "f2d72c9a877ed6694e4da6831841113dc0c05987e2c76cca51f358d12411b235"
|
||||
},
|
||||
"valid_active_seconds": 527.940320743
|
||||
},
|
||||
"151": {
|
||||
"cycles": 325708,
|
||||
"wire_checks": 32571,
|
||||
"baseline_recorded_error_p50_p95_p99_max": {
|
||||
"c0": [
|
||||
0.010000000223516992,
|
||||
0.18000000834465002,
|
||||
0.9099999795913697,
|
||||
4.419999957084657
|
||||
],
|
||||
"c1": [
|
||||
0.002999999523162822,
|
||||
0.016500000025611382,
|
||||
0.04399999969661239,
|
||||
0.09699999737739562
|
||||
]
|
||||
},
|
||||
"latched_angle_match_fraction": 1.0,
|
||||
"command_changes": {
|
||||
"c0": [
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
4.55
|
||||
],
|
||||
"c1": [
|
||||
0.0,
|
||||
0.020500000000000018,
|
||||
0.050000000000000044,
|
||||
0.28450000000000003
|
||||
]
|
||||
},
|
||||
"variants": {
|
||||
"old": {
|
||||
"feedback_switches_active": 727,
|
||||
"feedback_switches_low_speed": 225,
|
||||
"low_speed_c0_steps_over_025m": 142,
|
||||
"low_speed_c1_steps_over_005rad": 86,
|
||||
"low_speed_step_p99": {
|
||||
"c0": 0.17999999999999972,
|
||||
"c1": 0.016500000000000015
|
||||
},
|
||||
"raw_crossing_c0_steps_over_025m": 80,
|
||||
"c0_bound_active_s": 0.0,
|
||||
"c1_bound_active_s": 3.3938434989995585
|
||||
},
|
||||
"new": {
|
||||
"feedback_switches_active": 259,
|
||||
"feedback_switches_low_speed": 101,
|
||||
"low_speed_c0_steps_over_025m": 77,
|
||||
"low_speed_c1_steps_over_005rad": 42,
|
||||
"low_speed_step_p99": {
|
||||
"c0": 0.13999999999999968,
|
||||
"c1": 0.014499999999999957
|
||||
},
|
||||
"raw_crossing_c0_steps_over_025m": 0,
|
||||
"c0_bound_active_s": 0.029249633000290487,
|
||||
"c1_bound_active_s": 4.042117826999856
|
||||
}
|
||||
},
|
||||
"method": "Compare driver-input arbitration on recorded references and frozen vehicle motion.\n\nInput: extract.py route.npz/model_paths.npz/metadata.json directories. This checks\ncommand continuity and overrides; it does not predict a changed wheel response.\nBoth controllers receive the logged selected curvature. Recorded-command agreement\nis expected only for routes driven with the selected baseline and fixed 7 m C0.\n",
|
||||
"provenance": {
|
||||
"baseline_commit": "4900c0a40c87c72000b7168a6cf4fe6dd98ea6d0",
|
||||
"baseline_controller_sha256": "d5841c1f6a4368de20ff14de89a48d7dce737c6a41d79628ccb4f8b19f28f91b",
|
||||
"candidate_controller_sha256": "b34a55f84fdbf6c432ce89cdc378aaaf9bbd765ca4e6934845059f586e6f0773",
|
||||
"fixed_c0_distance_m": 7.0
|
||||
},
|
||||
"sources_sha256": {
|
||||
".cache/ford_route151/full/route.npz": "41a5b8bd388cf5a3d553f784542376ac9355fcdc5be4f427053d0504537babe1",
|
||||
".cache/ford_route151/full/model_paths.npz": "980b3843cec04254280d744a5801cee1bf0f8ef371052398327ff245f9eee01b",
|
||||
".cache/ford_route151/full/metadata.json": "937825317a0edd470c54647240b922be8f79dda5b3365ffdd61281f0aca877a1"
|
||||
},
|
||||
"valid_active_seconds": 1355.493226190003
|
||||
},
|
||||
"149": {
|
||||
"cycles": 132334,
|
||||
"wire_checks": 13234,
|
||||
"baseline_recorded_error_p50_p95_p99_max": {
|
||||
"c0": [
|
||||
0.020000001788138988,
|
||||
0.7800000047683717,
|
||||
2.070000008535385,
|
||||
4.560000002980233
|
||||
],
|
||||
"c1": [
|
||||
0.005500000000000005,
|
||||
0.048999999627471036,
|
||||
0.09199999963641169,
|
||||
0.1995000131130219
|
||||
]
|
||||
},
|
||||
"latched_angle_match_fraction": 1.0,
|
||||
"command_changes": {
|
||||
"c0": [
|
||||
0.0,
|
||||
0.0,
|
||||
0.10000000000000053,
|
||||
4.499999999999999
|
||||
],
|
||||
"c1": [
|
||||
0.0,
|
||||
0.07950000000000002,
|
||||
0.3125,
|
||||
0.4315
|
||||
]
|
||||
},
|
||||
"variants": {
|
||||
"old": {
|
||||
"feedback_switches_active": 1024,
|
||||
"feedback_switches_low_speed": 393,
|
||||
"low_speed_c0_steps_over_025m": 375,
|
||||
"low_speed_c1_steps_over_005rad": 226,
|
||||
"low_speed_step_p99": {
|
||||
"c0": 0.44079999999998143,
|
||||
"c1": 0.031500000000000035
|
||||
},
|
||||
"raw_crossing_c0_steps_over_025m": 217,
|
||||
"c0_bound_active_s": 0.0,
|
||||
"c1_bound_active_s": 10.436320454000196
|
||||
},
|
||||
"new": {
|
||||
"feedback_switches_active": 354,
|
||||
"feedback_switches_low_speed": 171,
|
||||
"low_speed_c0_steps_over_025m": 197,
|
||||
"low_speed_c1_steps_over_005rad": 111,
|
||||
"low_speed_step_p99": {
|
||||
"c0": 0.1999999999999993,
|
||||
"c1": 0.02150000000000002
|
||||
},
|
||||
"raw_crossing_c0_steps_over_025m": 0,
|
||||
"c0_bound_active_s": 0.0,
|
||||
"c1_bound_active_s": 16.08094779300012
|
||||
}
|
||||
},
|
||||
"method": "Compare driver-input arbitration on recorded references and frozen vehicle motion.\n\nInput: extract.py route.npz/model_paths.npz/metadata.json directories. This checks\ncommand continuity and overrides; it does not predict a changed wheel response.\nBoth controllers receive the logged selected curvature. Recorded-command agreement\nis expected only for routes driven with the selected baseline and fixed 7 m C0.\n",
|
||||
"provenance": {
|
||||
"baseline_commit": "4900c0a40c87c72000b7168a6cf4fe6dd98ea6d0",
|
||||
"baseline_controller_sha256": "d5841c1f6a4368de20ff14de89a48d7dce737c6a41d79628ccb4f8b19f28f91b",
|
||||
"candidate_controller_sha256": "b34a55f84fdbf6c432ce89cdc378aaaf9bbd765ca4e6934845059f586e6f0773",
|
||||
"fixed_c0_distance_m": 7.0
|
||||
},
|
||||
"sources_sha256": {
|
||||
".cache/ford_route149/full/route.npz": "aa5902877343cd033ee286b3668d91a85336ffbf740861849fb0b76b0ca24ade",
|
||||
".cache/ford_route149/full/model_paths.npz": "19827800b8fb5983f3d6b72fcfaf36e35a40170bb17cd7c1e49374744fb449fb",
|
||||
".cache/ford_route149/full/metadata.json": "624fff03c25eb298661cb7b25f3dbe6d214d863f799d93635c0f4f05fc0d2b32"
|
||||
},
|
||||
"valid_active_seconds": 1247.577060015
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
# Ford model geometry reference trial
|
||||
|
||||
This documents the initial curvature-reference trial at `18ded0380`. The
|
||||
subsequent [direct model-path trial](ford_direct_path.md) reuses the geometry
|
||||
toggle and changes controlsd's reference and C0/C1 mapping. modeld telemetry
|
||||
described here remains available for comparison.
|
||||
|
||||
`FordGeometryReference` changes the steering reference used by the existing
|
||||
Ford C0/C1 feedback controller. It is default off and requires
|
||||
`FordModelActionController` and a Ford CAN FD vehicle.
|
||||
|
||||
In Sunnylink's Ford settings, enable **Model Geometry Reference (Experimental)**
|
||||
while offroad, then cycle offroad to onroad. Keep **Selected-Action Path Tracking**
|
||||
enabled. Disable only the geometry toggle to compare with the previous model
|
||||
action. Disable the controller toggle to restore upstream Ford control.
|
||||
|
||||
## Reference calculation
|
||||
|
||||
After modeld publishes the plan into the message builder, use its orientation
|
||||
and orientation rate with the existing `get_curvature_from_plan` function:
|
||||
|
||||
```
|
||||
curvature = 2 * heading_at_preview / (max(speed, 1) * preview)
|
||||
- initial_heading_rate / max(speed, 1)
|
||||
```
|
||||
|
||||
The existing function stabilizes previews shorter than 0.3 seconds using the
|
||||
0.3-second heading. The experiment uses the exact `lat_action_t` already supplied
|
||||
to inference, including learned/fixed delay, model-specific smoothing allowance,
|
||||
the Ford speed-dependent 0.4-second preview addition, and 75 ms frame/action
|
||||
timing compensation. CTMV2 on the recent route has roughly 0.744 seconds of
|
||||
preview below 15 mph. This is a time preview, not direct sampling at seven metres.
|
||||
|
||||
Apply the same model-specific lateral smoothing and standstill hold as the
|
||||
original action, once per published model frame. Use the published plan itself,
|
||||
without a separate Planplus multiplier. Both `modelV2.action` and
|
||||
`drivingModelData.action` receive the selected curvature. Preserve the original
|
||||
action's independent smoothing history and its acceleration/stop fields.
|
||||
|
||||
controlsd still applies its existing curvature/acceleration/jerk limits and
|
||||
lateral maneuver override. Its desired curvature and desired steering angle now
|
||||
describe the selected reference. The unchanged controller converts that same
|
||||
reference into C0/C1 and closes the loop on measured steering. C0 distance,
|
||||
C1 distance, P/I gains, driver arbitration, PSCM limit behavior, CAN cadence,
|
||||
and field bounds are unchanged; C2/C3 stay zero.
|
||||
|
||||
Malformed/nonfinite/incomplete orientation data retains the original action
|
||||
for that frame and resets geometry smoothing history. Existing model validity
|
||||
and freshness gates still apply. There is no runtime mode swap during engagement;
|
||||
the toggle is read at modeld startup.
|
||||
|
||||
## Logging
|
||||
|
||||
`modelDataV2SP.fordGeometryReference` records enabled/valid, associated model
|
||||
publication time, original action curvature, raw geometric curvature, selected
|
||||
curvature, preview, and smoothing time. `valid=false` while enabled identifies
|
||||
fallback. A startup log also identifies action or geometry mode. The existing
|
||||
Ford diagnostics continue to identify the unchanged v15 feedback controller.
|
||||
|
||||
## Offline validation
|
||||
|
||||
The tests cover signs/units across speed and preview, smoothing, release and
|
||||
reversal, standstill, invalid geometry, message serialization, preservation of
|
||||
the original action/longitudinal control, selection through actual controlsd
|
||||
code, desired-angle logging, maneuver priority, and upstream fallback.
|
||||
|
||||
Three Lightning routes (149, 151, 157; CTMV2 and Tee Time) were replayed through
|
||||
the current reference selection, upstream limiter, and controller using frozen
|
||||
recorded steering/driver/PSCM measurements. Native controller cadence is retained;
|
||||
every tenth command gets a CAN pack/decode check. Reference timing uses recorded
|
||||
delay and the speed of the first control sample consuming each model. That speed
|
||||
can differ slightly from modeld's original inference input. This is not a neural
|
||||
replay or a prediction of wheel motion under modified commands.
|
||||
|
||||
Results and input hashes: `ford_geometry_reference_validation.json`.
|
||||
|
||||
```
|
||||
PYTHONPATH=.:opendbc_repo python tools/ford_pscm_lab/geometry_reference_validate.py \
|
||||
--output .cache/ford_geometry
|
||||
```
|
||||
|
||||
Geometry is not uniformly earlier or stronger. The route 157 right-turn example
|
||||
requests a large angle earlier; the problematic segment 9 left initially asks
|
||||
for less and later asks for more. It hits command field bounds more often in
|
||||
the frozen-measurement replay. Straight cohorts are defined by the original
|
||||
action, so they also include disagreements about turn entry/exit. No claim of
|
||||
improved physical tracking or stability follows from these offline results.
|
||||
@@ -0,0 +1,212 @@
|
||||
{
|
||||
"scope": "Replay the geometry reference and current C0/C1 controller on frozen route measurements.\n\nRequires cached full/route.npz, full/metadata.json, intake.npz and intake.json.\nThis is command validation, not a simulation of the truck following new requests.\n",
|
||||
"routes": [
|
||||
{
|
||||
"route": "157",
|
||||
"model": "CTMV2",
|
||||
"cycles": 76554,
|
||||
"model_frames": 14892,
|
||||
"fallback_frames": 0,
|
||||
"wire_round_trips": 15312,
|
||||
"metrics": {
|
||||
"all": {
|
||||
"samples": 44971,
|
||||
"angle_reference_difference_deg_p50_p95_max": [
|
||||
0.7096582083526739,
|
||||
37.323791775018584,
|
||||
473.2113566966026
|
||||
],
|
||||
"action_c0_bound_samples": 0,
|
||||
"geometry_c0_bound_samples": 26,
|
||||
"action_c1_bound_samples": 433,
|
||||
"geometry_c1_bound_samples": 1393
|
||||
},
|
||||
"straight": {
|
||||
"samples": 29296,
|
||||
"angle_reference_difference_deg_p50_p95_max": [
|
||||
0.3907433332260141,
|
||||
10.800464754067871,
|
||||
172.34883032860316
|
||||
],
|
||||
"action_c0_bound_samples": 0,
|
||||
"geometry_c0_bound_samples": 0,
|
||||
"action_c1_bound_samples": 0,
|
||||
"geometry_c1_bound_samples": 0
|
||||
},
|
||||
"turn": {
|
||||
"samples": 5773,
|
||||
"angle_reference_difference_deg_p50_p95_max": [
|
||||
18.222567807080637,
|
||||
104.0235491521133,
|
||||
473.2113566966026
|
||||
],
|
||||
"action_c0_bound_samples": 0,
|
||||
"geometry_c0_bound_samples": 26,
|
||||
"action_c1_bound_samples": 433,
|
||||
"geometry_c1_bound_samples": 1393
|
||||
}
|
||||
},
|
||||
"sources_sha256": {
|
||||
".cache/ford_route157/full/route.npz": "e2e2573f904aa11ee9e10450e7f5b965d475657b61127e827a67eadcd6b857fb",
|
||||
".cache/ford_route157/full/metadata.json": "f2d72c9a877ed6694e4da6831841113dc0c05987e2c76cca51f358d12411b235",
|
||||
".cache/ford_route157/intake.npz": "5dffd86fde68197727d6c6b9c5eae82320a456fa1206c4cf18e5e9c69419dde4",
|
||||
".cache/ford_route157/intake.json": "1d82c7f87d42229b96771bce3dd43c6b1651aa4fb8bb0ca3b1886d5a1a1c1a3c",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/geometry_reference_validate.py": "84dae09c158f5aaa3b88b5141545cd16829022437027d3d86c60a0acf7d78f56",
|
||||
"openpilot/sunnypilot/modeld_v2/ford_geometry.py": "9a51519533b7e4a6b5aa5ff58ad5b646afb2267c943d5a790e86d8b20324d84b",
|
||||
"openpilot/selfdrive/controls/lib/ford_model_action.py": "8bc8554ed6743c5faf433deaa6de3f07fae27d183f62b04f49f9c3b9f1e8dcca"
|
||||
}
|
||||
},
|
||||
{
|
||||
"route": "149",
|
||||
"model": "CTMV2",
|
||||
"cycles": 132334,
|
||||
"model_frames": 26104,
|
||||
"fallback_frames": 0,
|
||||
"wire_round_trips": 26468,
|
||||
"metrics": {
|
||||
"all": {
|
||||
"samples": 109224,
|
||||
"angle_reference_difference_deg_p50_p95_max": [
|
||||
1.0809205342304384,
|
||||
21.80654129599715,
|
||||
180.71838950537807
|
||||
],
|
||||
"action_c0_bound_samples": 0,
|
||||
"geometry_c0_bound_samples": 142,
|
||||
"action_c1_bound_samples": 845,
|
||||
"geometry_c1_bound_samples": 2430
|
||||
},
|
||||
"straight": {
|
||||
"samples": 67435,
|
||||
"angle_reference_difference_deg_p50_p95_max": [
|
||||
0.6422363701463203,
|
||||
6.246569735165232,
|
||||
62.050131143674285
|
||||
],
|
||||
"action_c0_bound_samples": 0,
|
||||
"geometry_c0_bound_samples": 0,
|
||||
"action_c1_bound_samples": 0,
|
||||
"geometry_c1_bound_samples": 0
|
||||
},
|
||||
"turn": {
|
||||
"samples": 15161,
|
||||
"angle_reference_difference_deg_p50_p95_max": [
|
||||
6.8859610951543,
|
||||
87.51133149734092,
|
||||
180.71838950537807
|
||||
],
|
||||
"action_c0_bound_samples": 0,
|
||||
"geometry_c0_bound_samples": 142,
|
||||
"action_c1_bound_samples": 845,
|
||||
"geometry_c1_bound_samples": 2430
|
||||
}
|
||||
},
|
||||
"sources_sha256": {
|
||||
".cache/ford_route149/full/route.npz": "aa5902877343cd033ee286b3668d91a85336ffbf740861849fb0b76b0ca24ade",
|
||||
".cache/ford_route149/full/metadata.json": "624fff03c25eb298661cb7b25f3dbe6d214d863f799d93635c0f4f05fc0d2b32",
|
||||
".cache/ford_route149/intake.npz": "503919c4f1566ef850007c59f16ce074697ba57732f21beb7b158c1ca7be3a9d",
|
||||
".cache/ford_route149/intake.json": "80f8c4ba69b4259ed308e0db110123063c6917357d26b49c9b6585695c9b24ae",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/geometry_reference_validate.py": "84dae09c158f5aaa3b88b5141545cd16829022437027d3d86c60a0acf7d78f56",
|
||||
"openpilot/sunnypilot/modeld_v2/ford_geometry.py": "9a51519533b7e4a6b5aa5ff58ad5b646afb2267c943d5a790e86d8b20324d84b",
|
||||
"openpilot/selfdrive/controls/lib/ford_model_action.py": "8bc8554ed6743c5faf433deaa6de3f07fae27d183f62b04f49f9c3b9f1e8dcca"
|
||||
}
|
||||
},
|
||||
{
|
||||
"route": "151",
|
||||
"model": "TT",
|
||||
"cycles": 325708,
|
||||
"model_frames": 64837,
|
||||
"fallback_frames": 0,
|
||||
"wire_round_trips": 65142,
|
||||
"metrics": {
|
||||
"all": {
|
||||
"samples": 124806,
|
||||
"angle_reference_difference_deg_p50_p95_max": [
|
||||
0.6014079626170221,
|
||||
8.687564671100564,
|
||||
233.5485828527104
|
||||
],
|
||||
"action_c0_bound_samples": 0,
|
||||
"geometry_c0_bound_samples": 6,
|
||||
"action_c1_bound_samples": 162,
|
||||
"geometry_c1_bound_samples": 588
|
||||
},
|
||||
"straight": {
|
||||
"samples": 105881,
|
||||
"angle_reference_difference_deg_p50_p95_max": [
|
||||
0.5210065989727145,
|
||||
4.103656620119354,
|
||||
143.66919002099516
|
||||
],
|
||||
"action_c0_bound_samples": 0,
|
||||
"geometry_c0_bound_samples": 0,
|
||||
"action_c1_bound_samples": 0,
|
||||
"geometry_c1_bound_samples": 0
|
||||
},
|
||||
"turn": {
|
||||
"samples": 3715,
|
||||
"angle_reference_difference_deg_p50_p95_max": [
|
||||
9.423421334227854,
|
||||
66.85200116463068,
|
||||
233.5485828527104
|
||||
],
|
||||
"action_c0_bound_samples": 0,
|
||||
"geometry_c0_bound_samples": 6,
|
||||
"action_c1_bound_samples": 162,
|
||||
"geometry_c1_bound_samples": 588
|
||||
}
|
||||
},
|
||||
"sources_sha256": {
|
||||
".cache/ford_route151/full/route.npz": "41a5b8bd388cf5a3d553f784542376ac9355fcdc5be4f427053d0504537babe1",
|
||||
".cache/ford_route151/full/metadata.json": "937825317a0edd470c54647240b922be8f79dda5b3365ffdd61281f0aca877a1",
|
||||
".cache/ford_route151/intake.npz": "18bbefb7738cebd0071dc987e90f74469a0c8ed456f9412324030592cafd68c9",
|
||||
".cache/ford_route151/intake.json": "0cfe327b5889f2603d902d2a01e915bf9a27b7a312137111d892a5e8540e03aa",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/geometry_reference_validate.py": "84dae09c158f5aaa3b88b5141545cd16829022437027d3d86c60a0acf7d78f56",
|
||||
"openpilot/sunnypilot/modeld_v2/ford_geometry.py": "9a51519533b7e4a6b5aa5ff58ad5b646afb2267c943d5a790e86d8b20324d84b",
|
||||
"openpilot/selfdrive/controls/lib/ford_model_action.py": "8bc8554ed6743c5faf433deaa6de3f07fae27d183f62b04f49f9c3b9f1e8dcca"
|
||||
}
|
||||
}
|
||||
],
|
||||
"tests_passed": 605,
|
||||
"route157_timing": [
|
||||
{
|
||||
"event": 9,
|
||||
"action": {
|
||||
"entry_75deg": 509.71654137700006,
|
||||
"release_below_25deg": 515.628442615
|
||||
},
|
||||
"geometry": {
|
||||
"entry_75deg": 509.26702728900005,
|
||||
"release_below_25deg": 515.7717539590001
|
||||
}
|
||||
},
|
||||
{
|
||||
"event": 11,
|
||||
"action": {
|
||||
"entry_75deg": 595.0217840170001,
|
||||
"release_below_25deg": 601.3182963449999
|
||||
},
|
||||
"geometry": {
|
||||
"entry_75deg": 595.2721063070001,
|
||||
"release_below_25deg": 601.874657156
|
||||
}
|
||||
},
|
||||
{
|
||||
"event": 16,
|
||||
"action": {
|
||||
"entry_75deg": 700.319324972,
|
||||
"release_below_25deg": 708.5712472329999
|
||||
},
|
||||
"geometry": {
|
||||
"entry_75deg": 700.2793086730001,
|
||||
"release_below_25deg": 709.1140404539999
|
||||
}
|
||||
}
|
||||
],
|
||||
"totals": {
|
||||
"cycles": 534596,
|
||||
"model_frames": 105833,
|
||||
"fallback_frames": 0,
|
||||
"wire_round_trips": 106922
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
# Route 18d: memory growth and health-message stalls
|
||||
|
||||
Route: `84865544361f55cb/0000018d--edff60065e`, running clean
|
||||
`07affcf9340623d759f31915477b5176d21c0b14`. Examined 37 uploaded qlogs.
|
||||
|
||||
## Observed faults
|
||||
|
||||
- `card` RSS rose from 68.7 MiB to 871.2 MiB over about 40 minutes.
|
||||
PSS also grew substantially, identifying retained process memory rather than
|
||||
just shared mappings. `controlsd` remained comparatively stable.
|
||||
- Communication-error diagnostics identified `deviceState` as below frequency.
|
||||
A 4.005-second `gpu1-usr` temperature read coincided with a 4.436-second gap
|
||||
between health messages. A separate SOM power read took 1.577 seconds.
|
||||
|
||||
## Memory fix
|
||||
|
||||
The NumPy `ndpointer` argument conversion reaches `ctypes.cast`, which creates
|
||||
cyclic pointer references. `card` disables cyclic garbage collection through
|
||||
`config_realtime_process`. Consequently, every native encoder call retained
|
||||
small allocations indefinitely.
|
||||
|
||||
Keep NumPy's dtype/contiguity validation, but construct an acyclic pointer that
|
||||
owns the input array until the synchronous native call finishes. No forced GC,
|
||||
controller equations, gains, candidate search, bounds, or packet changes.
|
||||
|
||||
The regression test reproduced 3,000 retained pointers in 500 ordinary encoder
|
||||
updates and 5,500 with preview. Both now retain zero. Before the fix, an
|
||||
encoder-only run grew by 51.0 MB in 12,000 updates. The fixed full controller
|
||||
stayed near 72.3 MB for 300,000 updates with GC disabled (50 minutes of updates
|
||||
at 100 Hz, executed faster than real time).
|
||||
|
||||
## Communication fault: separate work
|
||||
|
||||
The temperature and power reads above are in shared upstream `hardwared`, not
|
||||
in the Ford controller. A tested background-reader candidate is preserved on
|
||||
local branch `codex/ford-health-18d-sensor-candidate`. It is deliberately
|
||||
excluded from this controller fix because it changes shared sensor sampling,
|
||||
freshness handling and thermal behavior and needs its own review.
|
||||
|
||||
This fix does not claim to eliminate the observed health-message stalls.
|
||||
Reducing memory pressure may help general resource pressure, but is not proof
|
||||
that the blocking sensor I/O will stop.
|
||||
|
||||
## Validation
|
||||
|
||||
- 156 focused Ford tests passed, including native argument lifetime, preview,
|
||||
turn-entry assistance and the controller/transmit path.
|
||||
- Nine frozen routes, 556,776 input samples: before/after C0/C1 and observer
|
||||
results match exactly, with entry assist both enabled and disabled.
|
||||
- Ruff and whitespace checks passed.
|
||||
|
||||
Frozen replays establish unchanged commands, not a new claim about vehicle
|
||||
tracking. The memory reproduction was local; on-device memory still needs
|
||||
confirmation after updating. Other logged processes also used memory, so this
|
||||
identifies and fixes the dominant confirmed leak rather than proving the whole
|
||||
system cannot grow.
|
||||
@@ -0,0 +1,51 @@
|
||||
# Coordinated controller: residual wheel-angle correction
|
||||
|
||||
Route 17a contains sustained small wheel-angle errors without PSCM limitReached. At 230–250 seconds the request is about -1.55 degrees while the wheel remains near zero. Other steady bends miss in either direction. The nominal inverse/encoder already closely meets its own internal curvature target, so precise encoding alone does not eliminate the observed residual. Actual PSCM calibration, load and unobserved state remain possible sources; a fixed directional offset would not fit the observations.
|
||||
|
||||
The coordinated controller now adds a bounded integral trim to the angle passed into its inverse. The model/action target remains the reference and remains unchanged in carControl. Both C0 and C1 are still allocated together. This is steering-angle feedback, not a lateral-position measurement, and cannot correct a model target that places the truck on a line.
|
||||
|
||||
## Tuning and lifecycle
|
||||
|
||||
- Integral gain: **0.2 / second**. This is new tuning, not a recovered firmware constant and not gainless control.
|
||||
- Trim limit: **±2 steering-wheel degrees**. Clipping the integration error to ±2 degrees limits normal accumulation to **0.4 degrees/second**.
|
||||
- Learn only with error ≤5 degrees, wheel speed ≤5 degrees/second, and vehicle speed ≥2 m/s. These trial thresholds focus adaptation on small, slow tracking instead of turn-entry lag.
|
||||
- Ford does not populate `steeringRateDeg`. Wheel speed is derived from successive measured angles with a 0.1-second filter to suppress quantization. This filter guards trim learning; it does not filter the normal angle request.
|
||||
- Freeze growth at the inverse acceleration limit, either command field bound, or fresh PSCM limitReached. Opposing error may reduce trim toward zero.
|
||||
- A large opposing error (>5 degrees) or a fast-changing requested angle (>5 degrees/second) clears opposing trim **before** allocation. Requested-angle rate uses the same 0.1-second derivative filtering, with its history reset on inactivity. The angle request itself is unchanged. Fast wheel motion alone freezes learning and permits gradual relief, rather than discarding the learned correction. Resetting can still remove up to 2 degrees immediately; the accumulation-rate limit does not apply to this reset.
|
||||
- steeringPressed freezes trim without cutting the base request. Actual disengagement, invalid inputs, faults, or fresh PSCM override/denial reset it with the normal inactive request.
|
||||
- No new proportional correction. No persistent learning across engagements. Diagnostics include requested angle, trimmed inverse input, applied trim, measured-wheel and requested-angle rates, and whether learning is permitted.
|
||||
|
||||
The 2-degree bound covers the roughly 1–1.6-degree steady residuals motivating this experiment. The slow gain and small-error/motion conditions deliberately limit adaptation during transients. They are an initial experimental calibration, not universal Ford tuning or a proof of stability.
|
||||
|
||||
The existing default-off Coordinated C0/C1 gate remains the selector. Upstream fallback, v23, geometry and joystick selections are unchanged. Existing field bounds, acceleration allowance, mode, packet cadence, driver override and fault handling remain in force.
|
||||
|
||||
## Validation and limitations
|
||||
|
||||
The new regression drives the production adapter and Ford CAN packer with a persistent ±1.5-degree error. It fails on baseline `6aa35bc1f` because the command never builds residual correction, and passes after this change. A synthetic first-order wheel response with a known additive mismatch also tests convergence through the packed-command observer and nominal angle stage. That deliberately simple plant is a software regression, not an identified PSCM model or evidence of onroad smoothness.
|
||||
|
||||
538 focused controller tests pass. They cover selection and upstream fallback, corrected yaw, message-reader integration, actual packet observation, command bounds, persistent errors of both signs, no learning from large lag/fast motion/crawl, trim bounds, driver touch, override/disengagement reset, acceleration/PSCM anti-windup, and prompt removal of opposing trim during reversal/unwind. Ruff and whitespace checks pass.
|
||||
|
||||
Frozen replays compare this change with `6aa35bc1f` on routes 172, 175, 177 and 17a using their logged calibrated carControl yaw. Additional archived 166/16a wobble/turn sensitivity runs use retained raw CAN yaw in **both** variants: their original calibrated vectors were unavailable locally, so these are not exact replays of the current yaw-corrected controller. No production raw-yaw fallback is introduced. Missing history starts an independent replay interval; it is not bridged or represented as a vehicle restart. PSCM timestamps are converted from extracted seconds back into nanoseconds before freshness checks.
|
||||
|
||||
The four calibrated-yaw routes cover 206,019 adapter/packer updates per variant; the two sensitivity routes bring the total to 372,927 per variant. Activation matched baseline throughout, with no latched prediction faults, nonzero C2/C3, field-bound violations, or added acceleration-limit clipping. All fresh override/denial samples remained inactive. Across ten archived wobble windows the largest increase in fitted nominal internal-angle oscillation was 0.021 degrees peak-to-peak; this is a command-model proxy, not measured new wheel motion. Across 141 nominal 25/50/100-degree crossing comparisons, the largest later entry was 11.4 ms and later exit 19.8 ms. Changed angle levels can move threshold crossings; these are not measurements of a new plant delay. Candidate pipeline p95 on the development Mac was 0.125–0.140 ms across the four full routes, about 0.001–0.002 ms above their paired baselines. Comma timing is unmeasured.
|
||||
|
||||
The first candidate was rejected for retaining correction through reversal. A subsequent replay exposed that the Ford steering-rate field was unpopulated; the final code and regression derive rate from actual angle samples. All metrics above use that final implementation.
|
||||
|
||||
Artifacts and runnable local scripts are under `.cache/ford_joint_trim`. Measurements and model outputs stay frozen. A trim reaching its bound on that tape does not demonstrate that the truck would need the full trim: the recorded wheel cannot respond to the changed commands. Command/proxy comparisons can reject obvious regressions; they do not predict a new lane position, steering feel, or closed-loop stability. The next drive must establish whether residual wheel error and lane placement improve without reintroducing wobble or hanging exits.
|
||||
|
||||
## Route 17c: distinguish wheel motion from a requested release
|
||||
|
||||
Build `bd9c0a8b4` improved the measured small-request precision on route 17c, but at 1461.084 and 1469.555 seconds it discarded approximately 0.98 and 0.87 degrees of trim during brief wheel movements at 53–59 mph. The requested angles remained near -1.3 and +1.6 degrees. Both events had continuous mode-2 commands, no detected driver torque, and no PSCM limit or override. The previous reset condition reused the learning gate: fast **wheel** motion plus an opposing small error therefore erased a learned bias without a fast **request**.
|
||||
|
||||
The release condition now uses requested-angle motion instead. The gain, bound, learning conditions, inverse/encoder, and upstream target are unchanged. The two-sign production-pipeline regression fails on `bd9c0a8b4` with applied trim zero instead of ±0.985 degrees, then passes with this change. Reversal tests retain immediate release for a fast request, including before the wheel moves. Inactivity clears the new request-rate history.
|
||||
|
||||
Validation of this refinement:
|
||||
|
||||
- **707 focused tests and 25 subtests pass**, including the Ford packer, gates/fallbacks, trim regression, unwind, override and bounds. Ruff and whitespace checks pass.
|
||||
- Frozen baseline/candidate comparisons cover **527,215 updates per variant** on seven routes. Routes 172, 175, 177, 17a and 17c supply 360,307 updates using recorded calibrated yaw; 166/16a remain the explicitly labelled archived raw-yaw sensitivity checks described above.
|
||||
- Both highway trim losses disappear in the replay. Candidate trim can subsequently reach its unchanged 2-degree bound because the recorded wheel cannot respond; this is not an onroad trim requirement.
|
||||
- Route 17c's two large roundabout-entry windows have identical packed C0/C1 commands. Its bookmarked release has unchanged 25/50/100-degree nominal exit crossings. This preserves the replayed behavior; it does not speed up the actual roundabout response.
|
||||
- Across 150 nominal-angle crossing comparisons requiring 50 ms continuously beyond the threshold, the largest later entry is 11.2 ms and later exit 8.7 ms. A naive first-sample crossing flagged 119 ms on route 177 because baseline briefly reached 100.0005 degrees while the candidate reached 99.9999; that approximately 0.0006-degree difference is not a new physical delay.
|
||||
- Across ten wobble windows, the largest increase in the fitted nominal-angle oscillation is **0.0157 degrees peak-to-peak**. It is small but not zero, and is a command-model proxy rather than measured wheel motion. Maximum consecutive-active C0/C1 packet steps do not increase; activation matches, no acceleration clipping is added, and all bounds and fresh override/denial checks pass.
|
||||
|
||||
Artifacts and runnable comparisons are under `.cache/ford_trim_release`. Offline results support a focused trim-release experiment, not a claim that centering or wobble is fixed on the truck.
|
||||
@@ -0,0 +1,134 @@
|
||||
# Coordinated C0/C1 steering trial (v24)
|
||||
|
||||
This default-off trial turns the normal action-derived desired steering angle into a jointly selected C0/C1 pair. It estimates the held fields and curvature filter of an older Ford PSCM and chooses one command whose predicted next-update error is no worse than a C1-anchored alternative. Among those candidates it scores a short action-trend forecast followed by the complete return to its steady pair, including the filter tail. It does not retain a future command plan or classify turns into maneuver states.
|
||||
|
||||
The later [residual wheel-angle trim](ford_joint_centering_trim.md) adds a small, bounded integral correction around this nominal encoder. The original validation below describes the earlier versions; the trim has separate validation and is not gainless.
|
||||
|
||||
## 0.10-second action-trend preview
|
||||
|
||||
The coordinated trial now extrapolates the existing filtered requested-angle rate for 0.10 seconds, then assumes that target holds. This changes allocation planning; the action-derived steering target itself is unchanged. Each forecast point uses the current inverse conversion, acceleration allowance and bounded/quantized C0/C1 mapping. Speed and measurement-driven states remain fixed within the forecast. The current-target immediate-error inequality is retained. The 0.10-second duration is a tuning parameter, not a firmware constant or additional model steerDelay.
|
||||
|
||||
The preview is included under the existing default-off coordinated-controller toggle; there is no new setting. Master-off still selects upstream Ford. Diagnostics add `target_preview_seconds`, `target_preview_delta` (unclipped extrapolated angle change, degrees), and `target_preview_limited` (forecast curvature clipped by the acceleration allowance). Disengagement resets the requested-rate history as before.
|
||||
|
||||
Frozen replays compared 0, 0.05, 0.10 and 0.15 seconds across routes 17c, 172, 177, 166 and 16a: 438,519 updates per variant through the real adapter and CAN packer. Under the nominal older-firmware model, the selected 0.10-second candidate changes internal-target MAE from 53.53 to 50.45 degrees and 33.00 to 30.36 degrees on two roundabout entries, and 8.00 to 4.94 degrees on the good unwind. Estimated release threshold crossings occur 19–152 ms earlier. These are internal target estimates with recorded vehicle motion frozen, not new wheel trajectories.
|
||||
|
||||
This candidate did **not** pass every offline reject check. In an older raw-yaw sensitivity rocking window, periodic tracking-error amplitude increases from 4.94 to 5.90 degrees peak-to-peak; a calibrated-yaw route-172 bookmark increases from 0.694 to 0.815 degrees. Whole-route averages improve, but local oscillation and response-rate sensitivity regressions remain. The user explicitly selected implementation and push of the 0.10-second candidate after reviewing this tradeoff. No road improvement or closed-loop stability is established. The report is `pscm-trend-preview.html`; local research artifacts are under `.cache/ford_trend_preview`.
|
||||
|
||||
Numerical checks compare the native score with an independent 1,600-tick forward calculation, preserve exact zero-preview/zero-trend behavior, and exercise timing jitter, duplicate timestamps, abrupt reversals, inactivity, touches, and PSCM override/denial through the real sender. Production exactly reproduces all 438,519 command/observer updates of the tested 0.10-second replay tapes. All 611 focused controller/settings tests pass, as do Ruff, diff checks and the native library's real SConscript build. No comma-device timing is claimed.
|
||||
|
||||
### Route 182 communication warnings
|
||||
|
||||
The next drive ran `2fe855920` with preview enabled. Its first six quick-log segments contain communication warnings identifying `deviceState`, followed by `managerState`. Device-state publication gaps are 5.23, 8.76 and 6.35 seconds. The unrelated absent `alertDebug` and `lateralManeuverPlan` topics listed in diagnostic dictionaries are ignored by the health checks. The warnings do not identify a PSCM communication failure. Quick logs alone do not identify the blocking health-loop operation; full segments were requested. The same logs show card/controlsd/selfdrived approaching one CPU core in several sampled intervals. These are distinct observations, not proof of a single cause.
|
||||
|
||||
Preview allocation no longer computes the entire ordinary C0/C1 search before computing its forecast search. It computes only the ordinary C1-anchored accuracy bound needed by the forecast. The skipped ordinary output already belonged to the original candidate grid, so removing it does not remove a candidate. All 3,600 deterministic before/after comparisons match commands, score, state, grid count and immediate-error bound exactly. All 438,519 updates of the five-route replay still match the released preview candidate. Local selection p95 falls from 0.273 to 0.180 ms, with 1.18× aggregate speedup; comma-device scheduling improvement remains unmeasured.
|
||||
|
||||
Existing hardware-loop stage records now enter quick logs when a stage exceeds one second; 100 ms–one-second timings remain in full logs. This changes logging severity only, not watchdog thresholds, UI alerts, process scheduling or hardware operations. It enables diagnosis without requiring full logs for every subsequent warning. The optimization and instrumentation are not a claim that the observed health-process stalls are fixed. Validation: 612 controller/settings tests and six hardware timing/alert tests pass.
|
||||
|
||||
## Select and revert
|
||||
|
||||
In sunnylink → Vehicle → Ford Settings, while offroad:
|
||||
|
||||
- **Selected-Action Path Tracking (Experimental): ON**
|
||||
- **Model Geometry Reference: OFF**
|
||||
- **Coordinated C0/C1 Steering (Experimental): ON**
|
||||
|
||||
Apply with an offroad-to-onroad cycle. The new `FordPscmJointControl` parameter defaults to false. Turning it off restores v23 selected-action control. Turning the master Selected-Action setting off restores upstream Ford control, regardless of this stored setting. Geometry mode and Joystick Debug Mode keep their existing paths. C0 distance selection does not affect this trial.
|
||||
|
||||
The integration accepts Ford CAN FD platforms, following the existing master gate. Compatibility with each PSCM calibration is not established.
|
||||
|
||||
## Live integration
|
||||
|
||||
`controlsd` retains the normal action source, curvature limits, vehicle-model conversion, input freshness checks and steering-angle target. With the trial selected, its existing Ford adapter is a validity carrier with zero P/I gains; it supplies no additional path correction. `card` performs the joint allocation immediately before the existing Ford CAN packer. It negates the encoder's CAN-coordinate output into the OP path interface; the packer negates it back on transmission.
|
||||
|
||||
C2/C3 remain zero. Existing field bounds, mode 2 and immediate ramp remain unchanged. The observer advances at an estimated 8 ms firmware cadence using the decoded packets actually queued to panda. Queueing is not a PSCM acknowledgment. It retains held/filter estimates while disengaged or overridden and applies the recovered inactive slew to mode-0 packets. A fresh limitReached status does not freeze the target; the PSCM's explicit override or denial inhibits this trial's lateral command. Like upstream Ford, steeringPressed alone does not zero the path. Longitudinal controls are untouched.
|
||||
|
||||
A command-history gap over 100 ms, nonmonotonic clock or invalid prediction latches the trial inactive until the next onroad process start. The normal steering-fault alert reports the latched fault. There is no mid-turn switch to another controller. Diagnostics use `Ford joint path tracking` / `ford-joint-v24` and distinguish requested angle, reachable angle, estimated held state, proposed pair and packed pair.
|
||||
|
||||
## What the estimate does and does not know
|
||||
|
||||
Packaged numerical calibration comes from ML3V-14D003-BD / ML34-14D007-EDL. No executable firmware or private firmware image is shipped. Equivalence to the Lightning's RL38 firmware remains unverified. Internal integral is assumed initially zero and frozen; interaction is nominally 1.0; acceleration uses measured yaw × speed (no measured bank input); vehicle geometry comes from CarParams. These are explicit assumptions, not measured PSCM RAM. The model's 3 m/s² acceleration allowance is retained. It estimates an internal angle target before subsequent PSCM filtering/torque stages, not the future wheel angle.
|
||||
|
||||
## Validation
|
||||
|
||||
The production Python/C++ port reproduces every C0/C1 sample of the audited prototype in seven frozen windows from routes 166 and 16a. Held fields and filtered curvature also match; the largest angle-stage numerical difference is below 6e-14 degrees.
|
||||
|
||||
A separate replay executes the actual 100 Hz adapter and Ford CAN packer with the reconstructed response ticking at 125 Hz. Mean internal-target error, degrees:
|
||||
|
||||
| Window | Audited prototype | Actual 100 Hz adapter |
|
||||
|---|---:|---:|
|
||||
| 16a wobble 1 | 0.520 | 0.521 |
|
||||
| 16a wobble 2 | 0.240 | 0.248 |
|
||||
| 16a exit | 3.509 | 3.503 |
|
||||
| 16a turn | 9.762 | 9.981 |
|
||||
| 166 wobble 1 | 3.491 | 3.545 |
|
||||
| 166 wobble 2 | 0.530 | 0.587 |
|
||||
| 166 good left | 4.124 | 4.222 |
|
||||
|
||||
These are frozen measurements and nominal active-mode reconstruction, not observed improvements on the truck. The actual adapter made 12,031 packed command updates without a latched fault. The slowest per-window 99th-percentile cycle was 0.95 ms on the development Mac; comma hardware timing is unmeasured. Firmware-rate/calibration uncertainty remains material.
|
||||
|
||||
The focused suite exercises default-off selection, rollback, zero second PI, CAN signs/bounds/cadence, inactive state retention, interventions, limitReached, stale/nonfinite inputs, timing faults, real card hooks, parameter/schema registration, and inverse/forward numerical agreement.
|
||||
|
||||
The first enabled on-device trial exposed a message-type crash: the adapter returned a Cap'n Proto builder, while Ford's sender expects a reader and copies its actuator fields with `as_builder()`. The adapter now returns a reader. Regression coverage passes its output unchanged through `CarInterfaceBase.apply` and the production Ford sender, including the actual card hook with active, inactive and driver-override inputs. The earlier harness converted the output to a reader itself and hid this integration error. The regression also checks that the incoming message is not mutated and all fields other than the selected lateral-active state are preserved.
|
||||
|
||||
Run:
|
||||
|
||||
```sh
|
||||
python -m pytest openpilot/selfdrive/car/tests/test_ford_joint_control.py \
|
||||
openpilot/selfdrive/car/tests/test_ford_pscm_status.py \
|
||||
openpilot/selfdrive/controls/tests/test_ford_model_action*.py \
|
||||
openpilot/sunnypilot/sunnylink/tests/test_settings_schema.py
|
||||
```
|
||||
|
||||
Build the native kernel through SCons. On the development checkout, its real SConscript and native parameter library were compiled successfully. A complete root build could not run because the checkout lacks the msgq/rednose SCons tool submodules. No on-device build or road validation is claimed.
|
||||
|
||||
The trial's question is whether coordinating both channels preserves entry while reducing unnecessary correction during release. Offline results justify the opt-in comparison; they cannot establish smoothness or closed-loop stability.
|
||||
|
||||
## Runtime optimization after route 174
|
||||
|
||||
Route 174 ran v24 on `528ed3615` and recorded System Lagging during turns. Full rlogs show card using about 63% of one CPU core during the first alert; card, controlsd and selfdrived share that core and priority, and their combined measured load was about 101%. This supports CPU contention, not a PSCM limit, as the explanation for this alert. Comma timing must still be checked after the optimization.
|
||||
|
||||
The search now rejects candidates that fail its existing immediate-error constraint before evaluating their full return. Within each selection it also reuses return costs for identical predicted states and prefix costs. No cache survives the selection, and the candidate set, full-return policy, scoring, tie breaks and command cadence are unchanged.
|
||||
|
||||
Compared with the original native library built at the same optimization level, all nine outputs matched exactly in 600 deterministic stress cases and 720 comparisons using the 360 active logged states from route 174 at both firmware tick counts. On the development Mac, the recorded-state benchmark's 95th-percentile selection time fell from 0.317 ms to 0.090 ms; aggregate speedup was 1.47×. The broader stress benchmark improved 5.41× in aggregate. These are local encoder timings, not measured post-fix comma CPU utilization or a guarantee of system scheduling latency. Frozen pre-optimization commands and costs are also covered by the regression suite.
|
||||
|
||||
## Route 175 follow-up
|
||||
|
||||
Route `00000175--9529c33e36` ran `99b3fb03e` with v24 enabled. All ten full rlogs contain no System Lagging alert. Remaining communication warnings identify deviceState and, sometimes, managerState. DeviceState publication timestamps contain gaps up to 8.06 seconds; manager polls that service and falls back to its one-second timeout. The recorded CPU samples still show card/controlsd/selfdrived close to one core in sustained sections, so further optimization is useful independently of the health-process stalls.
|
||||
|
||||
Two wheel pauses near 100 and 115 degrees follow short steeringPressed detections. At about 175.35 and 176.94 seconds, the existing override handling transmits mode 0 / zero C0/C1 for approximately 87 and 117 ms, then rebuilds the request. Those pauses do not show limitReached. None of the 1,710 active v24 diagnostic samples reports the inverse acceleration allowance clipping the target. The user reported small left corrections against a rightward tendency, so these detections cannot be classified as false driver input. Override behavior is unchanged.
|
||||
|
||||
The bookmark marks a moderately good but slightly wide left turn. It contains brief limitReached states with continued mode-2 commands, rather than a frozen command. Wheel-tracking error remains; this route is not evidence of closed-loop equivalence to another steering platform. An 8.71-second carState recording gap around 290.55–299.26 seconds, with other card streams continuing and buffered diagnostics arriving later, is excluded from tracking statistics.
|
||||
|
||||
The additional encoder optimization reuses the already-computed C0/C1 calibration gains, constructs the same sorted candidate grid with fewer NumPy temporaries, and decodes fixed calibration entries once. Relative to `99b3fb03e`, all nine search outputs matched exactly in 600 deterministic stress cases and 3,420 comparisons using active route-175 states at both tick counts. Another 20,000 grid-boundary comparisons matched. A 6,000-cycle adapter/CAN/observer exercise, including stop, override, reengagement, steps and reversals, produced exactly the same packed commands and observer states.
|
||||
|
||||
On the development Mac, recorded-state selection p95 decreased from 0.174 to 0.102 ms (1.63× aggregate speedup). Full pipeline p95 decreased from 0.199 to 0.162 ms (1.24× aggregate speedup). These local measurements do not establish comma scheduling latency, and there are scheduler outliers in both runs. No target, gain, bound, override rule, or steering command was intentionally changed.
|
||||
|
||||
The hardwared patch avoids redundant synchronous Params removal for hidden alerts whose unused extra text changes, and routes the Tici-support alert through the same change-only helper. Initial cleanup, visible text updates, and clearing remain synchronous; failed writes are retried. An unchanged hidden-temperature alert reproduces the unnecessary blocking operation on the previous implementation. The logs do not identify which blocking call caused every on-device stall. Slow-stage timing now records operations exceeding 100 ms to distinguish thermal/system reads, Chestnut status, startup/engagement parameters, power reads, publication, and persistence. Watchdog thresholds and health checks remain unchanged.
|
||||
|
||||
Validation: 524 focused controller/hardware tests passed; Ruff and `git diff --check` passed. Diagnostic artifacts, route data, and benchmarks are local under `.cache/ford_v24_followup`; the visual report is `route-175.html` in the existing report directory. No truck validation of this follow-up optimization is claimed.
|
||||
|
||||
## Route 175 yaw bias and touch response
|
||||
|
||||
The previous inverse consumed raw Ford CAN yaw. In clean near-steady small-request samples, it differs from sign-normalized calibrated `carControl.angularVelocity[2]` by a median -0.00767 rad/s. Through the recovered steady angle equation, the difference corresponds to about -3.08 degrees of steering; the observed median actual-minus-requested error was -3.36 degrees. Four continuous steady windows show observed errors of -2.09, -2.39, -3.17 and -2.95 degrees, compared with nominal yaw-source effects of -1.86, -1.90, -2.97 and -3.45 degrees. This supports an input-bias explanation; it does not establish which internal yaw signal the actual PSCM uses or prove the onroad correction.
|
||||
|
||||
The coordinated controller now uses negative calibrated car-frame yaw Z, matching CAN/pinion coordinates. This is the existing pose published by controlsd, without a fitted trim or additional feedback gain. The v24 validity carrier requires calibrated, valid, healthy and fresh deviceMotion; missing or invalid yaw disables the request rather than falling back to the biased raw signal. Diagnostics record both yaw inputs. Other controller selections retain their existing input behavior.
|
||||
|
||||
The extra v24 steeringPressed gate is removed. A light touch no longer directly forces zero C0/C1 and mode 0, and the observer retains its held state. Actual disengagement, steering faults, freshness failures, and a fresh PSCM override (limit 3) or denial still inhibit the command. This changes the software request continuity, not the PSCM's internal torque blending or safety permissions.
|
||||
|
||||
Regression coverage includes biased raw yaw with unchanged calibrated motion, yaw sign, missing/nonfinite/out-of-range yaw, touch continuity, retained PSCM override/denial, and execution of controlsd's motion-health gate with v24 enabled and disabled. The original eight bias/touch cases failed before the fix. All 524 focused controller tests now pass. Frozen replay compares yaw-only, touch-only, combined and baseline commands through the actual adapter and CAN packer; measurements remain recorded, so it cannot demonstrate the resulting wheel motion or smoothness. Artifacts are under `.cache/ford_v25_bias`.
|
||||
|
||||
All four variants completed 52,526 recorded updates each without latched prediction faults, nonzero C2/C3 or field-bound violations. The missing carState interval is treated as an independent replay boundary, not bridged or claimed as a vehicle restart. All recorded active samples had the required calibrated yaw and recorded motion-health fields. Yaw-only preserved the baseline active count; the combined change added 5,988 active updates, all during steeringPressed with lateral control otherwise permitted. Fresh PSCM override/denial remained inactive. In the 174–178 second wheel-pause window, the extra 21 mode-0 updates disappeared. No new acceleration-allowance clipping occurred. The four steady windows shifted the requested curvature leftward, consistent with correcting the measured right bias. Closed-loop bias and touch feel still require truck validation.
|
||||
|
||||
Replay-audit correction: the original four-variant `.cache/ford_v25_bias/replay.py` passed an extracted PSCM timestamp in seconds where the adapter expects nanoseconds. Its fresh-status branch therefore did not substantiate the override/denial claim above. The production freshness code and explicit-status unit tests were unaffected. The later [centering-trim validation](ford_joint_centering_trim.md) converts timestamps correctly and asserts inhibition on every fresh recorded override/denial sample.
|
||||
|
||||
## Route 183 health-persistence fix
|
||||
|
||||
Route `00000183--892e38ca21` ran clean `ee3d0d33a` with the joint controller enabled. Quick logs identify deviceState communication failures alongside hardware-loop section durations of 3.604 seconds in engagement handling, 7.656 seconds in thermal reads, and 5.463 seconds in statistics/persistence. These section timings do not identify individual blocking syscalls. Controls CPU contention and other-core I/O wait are also present; the device did not report overheating.
|
||||
|
||||
Routine `IsEngaged`, uptime, and last-offroad-status writes now use the existing ordered asynchronous Params writer. Network-metered and runner-voltage flags are written at startup and on changes, eliminating four redundant writes per second during steady operation (each persistence operation includes file and directory fsync). Shutdown and ignition-cycle acknowledgements retain their synchronous writes. No controller command, preview duration, safety gate, thermal limit, or communication threshold changes.
|
||||
|
||||
Regression tests run the production hardware loop with delayed persistence. Before the change, a 5.5-second injected delay produces 6–11.5-second publication gaps. The fixed loop retains its 0.5-second cadence. A separate native integration test holds the actual Params file lock: the old loop cannot finish while the lock is held; the fixed loop publishes all 125 test messages and persists the final values in order after unlock. Flag transitions and overtemperature inhibition/fan behavior remain covered.
|
||||
|
||||
The thermal-read stall is not explained by this reproduction. Individual sensor reads taking over one second now report their sensor name, zone, and elapsed time in quick logs, preserving the original values and failure behavior. No stale-temperature cache or relaxed watchdog is introduced. Truck validation is still needed; this patch fixes demonstrated blocking-persistence behavior, not a proven diagnosis of every recorded stall.
|
||||
|
||||
Validation: 200 hardware, Params, thermal, and Ford tests passed; Ruff and diff checks passed. The broader suite initially hung after its success summary in an existing shared StatLog ZeroMQ-context destructor. A local-only pytest cleanup plugin closed both statistics sockets before their shared context; the same 200 tests then exited successfully. Production statistics code is unchanged. Reproduction, profiling, and validation artifacts are under `.cache/ford_health_fix`.
|
||||
@@ -0,0 +1,39 @@
|
||||
# Ford coordinated allocation trial
|
||||
|
||||
The existing `FordModelActionController` + `FordPscmJointControl` gate now uses
|
||||
the recovered normal C0/C1 slew rates to choose its steady endpoint split:
|
||||
|
||||
```
|
||||
t = desired_internal_curvature / (g0 * c0_rate + g1 * c1_rate)
|
||||
c0 = t * c0_rate
|
||||
c1 = t * c1_rate
|
||||
```
|
||||
|
||||
When a field clips, allocate its missing contribution to the other field's
|
||||
remaining capacity. The existing dynamic selector still handles held state,
|
||||
heading-dependent filtering, target preview and release. No new gain, control
|
||||
mode, search horizon or native-library ABI is added. DBC bounds and C2=C3=0 remain.
|
||||
The normal upstream controller is unchanged with the existing master toggle off.
|
||||
|
||||
This is the exact endpoint policy tested against ML3V-14D003-BD /
|
||||
ML34-14D007-EDL instructions. At 18 mph, the approximately 204-degree internal
|
||||
target settles within 2.5 degrees in 2.864 rather than 3.384 seconds. The first
|
||||
90% of its rise is unchanged. The approximately 25-degree release takes .624
|
||||
rather than .456 seconds to settle: this is a known trial tradeoff, not a proven
|
||||
improvement in all driving. These are internal target results, not wheel-motion
|
||||
or closed-loop smoothness predictions. Actual steering feedback must assess both
|
||||
large turns and ordinary correction/release.
|
||||
|
||||
For stopping, controlsd and card share the same normalization: a filtered speed
|
||||
between -0.3 and 0 m/s counts as zero only when raw speed is in [0, 0.3) m/s and
|
||||
reverse is not selected. This prevents small speed-filter undershoot from
|
||||
invalidating and clearing the last active packet. Larger/inconsistent negative
|
||||
speeds, reverse, stale inputs, faults, override and disengagement still release.
|
||||
No global CarState speed or PSCM speed message is altered. Retaining the request
|
||||
does not bypass the firmware's separate near-zero-speed output gate.
|
||||
|
||||
Validation: 926 Ford tests and two subtests passed; lint and diff checks passed.
|
||||
The shipped encoder reproduces all 9,000 saved candidate command samples from
|
||||
the native-verified step/release fixture (maximum difference 8.89e-16). Native
|
||||
instruction validation and the underlying fixtures are recorded in the local
|
||||
PSCM authority report. Diagnostics include `allocation: equal-arrival`.
|
||||
@@ -0,0 +1,38 @@
|
||||
# Ford joint endpoint: hold C0 at the supervisor saturation
|
||||
|
||||
The equal-arrival endpoint (`C0 = 1.5 t`, `C1 = 0.1 t`) holds 2-4 m of C0 in large
|
||||
turns. In the reference ML3V-14D003-BD / ML34-14D007-EDL calibration the path
|
||||
supervisor centre is
|
||||
|
||||
```
|
||||
S(v) * (clip(0.5 * held_C0, +-0.5) + clip(10 * held_C1, +-0.349609))
|
||||
```
|
||||
|
||||
with width `0.399994 * S + 5` below 40 km/h. Its C0 term saturates at 1.0 m, so
|
||||
held C0 above that adds primary curvature but no turn-direction allowance. On
|
||||
release, zero demand stays excluded until held C0 slews (1.5 m/s) below about
|
||||
0.41 m while C1 is saturated, so extra C0 adds (C0 - 0.41) / 1.5 s of forced
|
||||
old-direction output.
|
||||
|
||||
`static_pair` therefore caps its equal-arrival C0 at 1.0 m and gives the rest to
|
||||
C1; the remainder returns to C0 only when C1 reaches its bound. Endpoints with
|
||||
equal-arrival C0 of at most 1.0 m are unchanged. The paired selector, dynamics,
|
||||
quantization, preview, gates, stop hold and DBC bounds are unchanged; it still
|
||||
builds with both fields, then trades C0 for C1 at constant curvature.
|
||||
|
||||
Offline evidence (reference firmware, not the live RL38 calibration; no wheel,
|
||||
assist or vehicle model):
|
||||
|
||||
- Native zero-demand supervisor probe, controlled 15/20/25 km/h large steps:
|
||||
zero excluded after release 1.48/1.88/2.17 s -> 0.39/0.39/0.48 s. The first
|
||||
50%/90% of the rise is unchanged; the last 1% can settle up to 0.5 s later.
|
||||
- Native probe on replayed routes 194/195/196 packets: forced old-direction
|
||||
output in the 3 s after large releases 24.9 -> 6.4, 19.6 -> 8.4,
|
||||
15.0 -> 3.6, 16.6 -> 8.3, 11.0 -> 4.4 unit*s (one driver-involved release
|
||||
9.6 -> 9.2).
|
||||
- 81,340 replay updates per policy: with no |target| >= 30 deg within 10 s,
|
||||
packets are identical on 194/196 and 98.9% identical on 195.
|
||||
|
||||
These are firmware-stage predictions. A physical A/B must show whether the wheel
|
||||
releases large turns earlier; diagnostics report `allocation:
|
||||
equal-arrival-supervisor-c0`.
|
||||
@@ -0,0 +1,155 @@
|
||||
# Offline Ford selected-action candidate
|
||||
|
||||
This document and `ford_model_action_validation.json` record the offline
|
||||
stage committed as `7ca3c6e3b`. The candidate is now available behind a
|
||||
separate default-off Sunnylink toggle; see
|
||||
[drive-test setup and validation](ford_model_action_drive_test.md).
|
||||
The counts, source hashes and selector status below describe that earlier
|
||||
stage. The current experiment adds [measured-curvature C1 feedback](ford_c1_feedback.md)
|
||||
to this original mapping; the historical no-feedback description below is
|
||||
not the current controller specification.
|
||||
|
||||
The decision is `C0 = current model y(7 m)`,
|
||||
`C1 = max(7 m, speed × 1 s) × selected upstream-limited desiredCurvature`,
|
||||
with C2=C3=0. The 7 m station and one-second scale are engineering choices,
|
||||
not identified PSCM gains. `calibration_approved=false`.
|
||||
|
||||
`openpilot/selfdrive/controls/lib/ford_model_action.py` contains the core
|
||||
and a separate adapter compatible with the existing controlsd call.
|
||||
At that stage, the production selector, v8 implementation, settings, opendbc
|
||||
submodule and Panda safety remained unchanged. Tests injected the adapter
|
||||
offline; there was no production setting. No hardware or CAN transmission
|
||||
occurs in the lab tools.
|
||||
|
||||
## Construction and integration
|
||||
|
||||
Only the unquantized C0 and C1 slew positions persist in the core.
|
||||
Each field is clipped independently (±5.11 m / ±0.5 rad), slewed independently
|
||||
(4 m/s / 0.5 rad/s), then packed using the existing Float32/sign-negation
|
||||
rounding contract (0.01 m / 0.0005 rad). Heading overflow is not transferred
|
||||
to C0. No yaw integral, blend, additional curvature contribution, reference
|
||||
filter, turn modes, or 10 m C1 cap is introduced.
|
||||
|
||||
The selected standalone implementation from worktree 3548 is the provenance
|
||||
for this law. Its two-state packer has been moved into the library core so
|
||||
the controller does not depend on experimental lab code. Invalid numeric
|
||||
types, overflowing arc geometry and malformed paths reset the core instead
|
||||
of throwing or retaining a command.
|
||||
|
||||
Arc stations use cumulative model x/y distance, not forward x. As in the
|
||||
reviewed standalone core, a path ending before 7 m holds its available
|
||||
endpoint instead of extrapolating. This matters: route95 contains 44 active
|
||||
cycles with 5.45–6.94 m of path at 2.78–3.46 m/s. A tested strict 7 m
|
||||
coverage gate would have introduced disengagements and was removed. There
|
||||
is no speed-dependent C0 horizon beyond this existing endpoint behavior.
|
||||
|
||||
The adapter retains the existing input age allowance (−5 to +150 ms),
|
||||
speed domain (0.3–55 m/s), yaw sanity bound (±3 rad/s), selected curvature
|
||||
sanity bound (±1/m), and control interval (2–100 ms). It rejects backward
|
||||
model/measurement timestamps and invalid services. Repeated timestamps may
|
||||
continue slew, but geometry is validated again on each tick. Disengagement,
|
||||
invalid inputs and timing faults clear all command and adapter timing state.
|
||||
The first valid tick after reset uses 10 ms, as v8 does.
|
||||
|
||||
controlsd still owns reference selection, upstream curvature limiting,
|
||||
service health and engagement. Tests execute its actual source-selection
|
||||
and limiter code, its Ford call, Float32 publication in ControlsExt, conversion
|
||||
to CarControlSP, and the pinned Ford CarController's in-memory CAN builder.
|
||||
Both model-action and maneuver-planner selection are covered, including
|
||||
disabling latActive after invalid output. Only the test chooses the adapter.
|
||||
|
||||
Yaw is not an input to the control law. The adapter checks it solely for the
|
||||
inherited invalid-input policy. Driver override and optional PSCM status
|
||||
do not modify the candidate base; existing engagement and downstream driver
|
||||
arbitration remain responsible for authorization, as with v8's base request.
|
||||
|
||||
## Offline evidence
|
||||
|
||||
The checked-in `ford_model_action_validation.json` records the completed
|
||||
checks and source hashes. Full arrays and detailed reports are generated
|
||||
locally under `.cache/ford_model_action/`; original route files are read-only.
|
||||
|
||||
Completed validation: **264 Ford tests and 150 subtests pass**, including
|
||||
120 new core/adapter/replay-validator cases. The candidate module has 100%
|
||||
statement and branch coverage (78 statements, 24 branches). Ruff and Ty pass.
|
||||
The 200,000-cycle numerical stress test also checks 200,000 mirrored core
|
||||
updates and 18,138 field-boundary cases. Across route and stress runs,
|
||||
485,238 Float32/CAN round trips pass. Eight deliberately injected faults
|
||||
(heading gain/cap, erased C0, wrong C0 slew, retained invalid state, stale
|
||||
model acceptance, model clock rollback and reversed C0 sign) are all caught
|
||||
by the tests. Mutation runs replace code only inside isolated Python
|
||||
processes; production source files are never modified by those probes.
|
||||
|
||||
Independent Standards and Spec reviews reported zero findings. The full
|
||||
suite's Params setting test uses an existing local native library from
|
||||
worktree 3548 after checking relevant source files are byte-identical;
|
||||
its hash and provenance are in the manifest. That library is an ignored
|
||||
test dependency, not part of this change. This is the full relevant Ford
|
||||
suite, not the hardware-dependent test suite for every openpilot subsystem.
|
||||
|
||||
The replay has two separate passes:
|
||||
|
||||
* Core compatibility uses the archived eligibility mask and requires exact
|
||||
equality with the independently implemented `action_heading` commands.
|
||||
* Adapter reconstruction derives eligibility from recorded service streams
|
||||
independently of the archived output mask. It retains original timestamps,
|
||||
gaps and consumed model frames. Controls publication time proxies the
|
||||
unlogged computation clock, and complete SubMaster health is unavailable.
|
||||
|
||||
All 54,738 route95 and 78,812 route90 core cycles match exactly, including
|
||||
37,614 and 73,055 active cycles. The adapter preserves those active counts.
|
||||
Its 59 / 19 changed commands arise solely from the fresh 10 ms engagement
|
||||
tick instead of the archived harness's preceding publication interval;
|
||||
the replay checks that attribution on every cycle. Maximum differences are
|
||||
0.01 m / 0.001 rad (95) and 0.02 m / 0.002 rad (90).
|
||||
|
||||
Every core and adapter replay output is round-tripped through Float32 and
|
||||
the real CAN packer/parser, including zero C2/C3, signs, mode and counter.
|
||||
Continuous field slew and quantization allowance are checked separately
|
||||
from immediate invalid-command resets. The original driver-clean cohorts,
|
||||
speed strata and command RMS are reproduced without redoing the encoder search.
|
||||
|
||||
The numerical stress harness uses analytic rotated paths, scalar slew
|
||||
arithmetic, mirrored requests, irregular intervals and invalid-input resets.
|
||||
It also sweeps every representable host field value and the Float32 values
|
||||
immediately below, at and above every half-quantum boundary. Direct CAN
|
||||
packing of the continuous state must agree with the host's quantized output.
|
||||
The unit tests cover releases, reversals, clipping, service freshness,
|
||||
clock resets, malformed inputs, endpoint fallback and actual integration.
|
||||
|
||||
## Limits of the result
|
||||
|
||||
On turns at ≥15 m/s, candidate C0 RMS is 79%/81% below v8 on routes95/90,
|
||||
while C1 is 33%/41% higher. Those are command changes, not evidence of
|
||||
equivalent steering authority. The PSCM's independent C0/C1 response remains
|
||||
unknown. Replay cannot establish physical model following, strong turns,
|
||||
centering, overshoot, oscillation or closed-loop stability.
|
||||
|
||||
The release probe is intentionally explicit: a model bend can increase
|
||||
while selected curvature decreases. At 20 m/s, one synthetic probe changes
|
||||
C0/C1 from 0.24 m / 0.10 rad to 0.49 m / 0.08 rad. Zero selected curvature
|
||||
sets the C1 target to zero but does not erase a nonzero current model C0.
|
||||
Removing a yaw-integral tail does not prove that physical overshoot is solved.
|
||||
No additional release policy or unsupported plant model is added to hide
|
||||
that uncertainty.
|
||||
|
||||
## Reproduce
|
||||
|
||||
From this worktree, use the logged construction dependency explicitly:
|
||||
|
||||
```sh
|
||||
export PYTHONDONTWRITEBYTECODE=1
|
||||
export PYTHONPATH=.:/Users/ibpersonal/.codex/worktrees/b926/sunnypilot/opendbc_repo
|
||||
PY=/Users/ibpersonal/dev/sunnypilot/.venv/bin/python
|
||||
EVIDENCE=/Users/ibpersonal/.codex/worktrees/3548/sunnypilot/analysis/controller_search_20260904
|
||||
$PY -m pytest -q -p no:cacheprovider openpilot/selfdrive/controls/tests/test_ford_*.py tools/ford_pscm_lab openpilot/selfdrive/car/tests/test_ford_pscm_status.py
|
||||
$PY -m tools.ford_pscm_lab.model_action_replay "$EVIDENCE/route95" --output .cache/ford_model_action/route95
|
||||
$PY -m tools.ford_pscm_lab.model_action_replay "$EVIDENCE/route90" --output .cache/ford_model_action/route90
|
||||
$PY -m tools.ford_pscm_lab.stress_model_action --cycles 200000 --seed 20260907 --output .cache/ford_model_action/stress.json
|
||||
```
|
||||
|
||||
The route replay refuses an opendbc revision other than
|
||||
`72a775d35e54c21ff5c5798acef22016eedcc0a7`. Stress defaults to this pin and
|
||||
also accepts an explicitly required commit with `--opendbc-revision` for
|
||||
deployment checks. A mismatch still fails. This historical pin reproduces
|
||||
logged construction; it does not change the merge's submodule pointer.
|
||||
@@ -0,0 +1,125 @@
|
||||
# Ford selected-action drive-test branch
|
||||
|
||||
This v14 trial increases C1's integral gain from 0.25 to **1.0**, retaining
|
||||
P=0.75, [curvature-derived C0](ford_curvature_c0_v8.md), direct C0/C1 requests,
|
||||
and [continuous C1 PI feedback](ford_c1_minimal_pi.md).
|
||||
Only integrated tracking error accumulates correction; C0/C1 reflect the current bounded request. C0 defaults to a 7 m circular arc from selected desired curvature. An on-device toggle can instead use max(7 m, speed × 1 second).
|
||||
[Base C1 overflow allocation to C0](ford_c1_overflow.md) remains.
|
||||
It is selectable on **any Ford CAN FD vehicle**
|
||||
through the existing persistent, default-off Sunnylink
|
||||
toggle. Offline checks establish software behavior; physical tracking,
|
||||
turn-exit behavior and closed-loop stability remain unvalidated.
|
||||
|
||||
Both base commands use selected, upstream-limited desired curvature. The +0.40 s
|
||||
low-speed model preview from `b720e9f1b` remains: full offset at 15 mph and below,
|
||||
tapering to zero at 30 mph. The trial multiplies each fresh integral error increment
|
||||
by four, for both accumulation and retirement. P, PSCM `LimitReached` handling,
|
||||
integral arithmetic, field bounds, and selection are retained.
|
||||
See [I=1.0 replay results](ford_c1_i1_trial.md) for scope, tradeoffs, and reproduction.
|
||||
|
||||
## Select and restore
|
||||
|
||||
1. Install branch `hiimisaac-dev` from `sunnypilot/sunnypilot` using the device's
|
||||
normal branch-switch process and allow its build to finish.
|
||||
2. While offroad, open Sunnylink device settings → Vehicle → Ford and enable
|
||||
**Selected-Action Path Tracking (Experimental)** (`FordModelActionController`).
|
||||
3. Complete a real offroad-to-onroad cycle. Selection occurs when `controlsd`
|
||||
starts; a stored toggle change or disengagement alone cannot swap an active
|
||||
controller. Initial physical evaluation remains controlled testing.
|
||||
|
||||
The startup event `Ford path controller selected` should report
|
||||
`FordModelActionController`. Periodic `Ford C2-free path tracking` events
|
||||
identify **`hypothesis=model-action-curvature-c0-distance-pi-v14`**. They report desired and measured
|
||||
curvature, base heading, proportional and accumulated correction, applied heading,
|
||||
feedback timing and driver/PSCM gating. `proportional_gain=0.75` and
|
||||
`integral_gain=1.0` identify the trial. `offset_overflow` reports the extra C0
|
||||
target in meters before C0 amplitude limits. `calibration_approved=false`
|
||||
remains. The retired request/unwind/reversal diagnostic fields are removed.
|
||||
|
||||
Turning the toggle off and completing another offroad-to-onroad cycle restores
|
||||
**upstream Ford curvature control**: 20 Hz steering messages, limited mode on
|
||||
CAN FD, zero C0/C1/C3, and upstream curvature limiting and platform-specific
|
||||
overshoot handling. Stored observer or retired controller settings cannot select
|
||||
a custom controller. The observer toggle is no longer exposed. The experiment
|
||||
only runs on Ford CAN FD vehicles; legacy Ford uses upstream control as well.
|
||||
See [toggle-off validation](ford_upstream_fallback.md).
|
||||
|
||||
## C0 distance toggle on comma four
|
||||
|
||||
With the experimental Ford controller enabled, open **Settings → toggles → C0: 1 second**.
|
||||
The toggle is visible for Ford CAN FD vehicles and can be changed while disengaged.
|
||||
|
||||
- **Off (default):** C0 uses a fixed 7 m arc.
|
||||
- **On:** C0 uses a distance of max(7 m, speed × 1 second), matching the base C1 distance.
|
||||
|
||||
Disengage assistance, change the toggle, and remain disengaged for at least three seconds
|
||||
before reengaging. This setting uses the existing three-second runtime parameter refresh;
|
||||
**no ignition cycle or controlsd restart is required**. Engaged or paused MADS and stale
|
||||
engagement messages prevent applying a change. A mode change resets the PI correction and
|
||||
adapter timestamps. Reapplying the same value does not reset anything.
|
||||
|
||||
The persistent parameter is `FordC0TimeBased`. It cannot enable the experimental controller
|
||||
by itself. The existing Sunnylink controller-selection toggle still requires an onroad cycle.
|
||||
C1, the gains, the 7 m heading-overflow allocation, the upstream reference limits and the CAN
|
||||
field bounds are unchanged. Below 7 m/s (about 15.7 mph), both distance modes are identical.
|
||||
At 20/30/60 mph the enabled distance is approximately 8.9/13.4/26.8 m, respectively; C0 can
|
||||
therefore be substantially larger, especially at higher speeds. Its release still follows the
|
||||
current selected curvature immediately, with no additional slew.
|
||||
|
||||
The `Ford C0 distance changed` event records an applied switch. Periodic tracking events
|
||||
include `c0_time_based` and the actual `offset_distance` in meters, including the default mode.
|
||||
Offline checks verify selection, runtime switching, resets, unchanged C1 and CAN encoding;
|
||||
they do not establish which distance the PSCM follows better.
|
||||
|
||||
Validation on 2026-09-14: 410 tests and 25 subtests passed, plus Ruff and the local comma four
|
||||
UI construction/write/refresh/visibility/render check. The 54,146-cycle maneuver-route replay
|
||||
(`84865544361f55cb/0000011c--99f4537696`) matched `775012167` exactly with the new toggle off.
|
||||
With it on, C0 changed in 35,668 cycles (maximum difference 0.74 m), while C1 and accumulated
|
||||
correction remained identical on the same recorded motion. The two comparisons completed
|
||||
216,584 controller updates and CAN round trips. No vehicle build, installation or road test
|
||||
was performed for this change.
|
||||
|
||||
## Wiring and validation
|
||||
|
||||
`controlsd` supplies the selected, upstream-limited desired curvature and the
|
||||
measured steering-derived curvature already used in its tracking diagnostics.
|
||||
Fresh steering publications advance C1 integration. P responds to the current
|
||||
error without accumulating. Repeated publications use current feedforward and P
|
||||
but cannot integrate the same elapsed interval twice.
|
||||
Driver override clears P and I. A fresh PSCM reached-limit flag stops
|
||||
extra outward accumulation while preserving unwind and base model changes.
|
||||
With fresh feedback, the part of the error increment that cancels existing I
|
||||
is applied before the ordinary accumulation clamp. Any remainder must fit the
|
||||
combined feedforward/P/I amplitude envelope. There is no C0 confirmation
|
||||
threshold or remembered turn direction. Zero error removes P and holds I; it
|
||||
does not trigger a release. Final command limits still apply.
|
||||
|
||||
C0 starts with the selected-distance circular arc of selected desired curvature. It does not
|
||||
add independent live model-path position or heading. Valid model geometry is
|
||||
still required as a health gate. When the raw base heading
|
||||
exceeds ±0.5 rad, C0 additionally receives 7 m times the clipped-away heading.
|
||||
Accumulated C1 feedback does not spill into C0. The extra target returns to zero
|
||||
as the base heading falls below the cap. Applied C0 changes in that same update.
|
||||
C2 and C3 remain zero. The
|
||||
existing field bounds, 100 Hz custom sender and Float32 publication remain in
|
||||
place. An explicit selection flag distinguishes upstream mode from an invalid
|
||||
experimental command; invalid experimental input cannot switch to upstream.
|
||||
The opendbc sender restores upstream behavior when that flag is false.
|
||||
|
||||
[Direct-command validation](ford_direct_path_v9.md) records the same command law introduced in v9.
|
||||
[Curvature-C0](ford_curvature_c0_v8.md) and its validation JSON record v8.
|
||||
[Continuous PI](ford_c1_minimal_pi.md) and its validation JSON record v7.
|
||||
[Proportional feedback](ford_c1_pi.md) and its validation JSON record v6.
|
||||
[Completed-unwind release](ford_unwind_catchup.md) and
|
||||
`ford_unwind_catchup_validation.json` record v5. [Changed-request release](ford_c1_request_release.md) and its
|
||||
validation JSON record v4. The [overflow specification](ford_c1_overflow.md) and
|
||||
`ford_c1_overflow_validation.json` record v3. The carryover specification and `ford_c1_carryover_validation.json`
|
||||
record the previous experiment. `ford_c1_feedback_validation.json` records the initial feedback
|
||||
version at `5fbb583e5`. `ford_model_action_validation.json` and
|
||||
`ford_model_action_drive_test_validation.json` are historical records for the
|
||||
original offline candidate and its first wiring, respectively; their counts
|
||||
and coverage are not claims about the current version.
|
||||
|
||||
The full hardware build and device boot are not performed by these offline
|
||||
checks. Pushing the branch does not install it on the device or change its
|
||||
stored toggle.
|
||||
@@ -0,0 +1,145 @@
|
||||
{
|
||||
"date": "2026-09-07",
|
||||
"baseline_commit": "7ca3c6e3b3e659c6f446039501c5826bbd14092e",
|
||||
"branch": "codex/ford-model-action-drive-test",
|
||||
"scope": "Default-off Sunnylink selection and v8 retirement; offline validation only. No device installation or physical performance validation.",
|
||||
"calibration_approved": false,
|
||||
"production_selector_changed": true,
|
||||
"toggle": "FordModelActionController",
|
||||
"default_enabled": false,
|
||||
"v8_removed": true,
|
||||
"panda_safety_changed": false,
|
||||
"opendbc_submodule_changed": false,
|
||||
"deployment_opendbc_head": "c21a9013700734dd20b09e05aa68329ad8cc20f9",
|
||||
"controller_size": {
|
||||
"total_lines": 145,
|
||||
"code_lines_excluding_blanks_comments_docstrings": 95,
|
||||
"core_persistent_values": 2,
|
||||
"adapter_timestamps": 3,
|
||||
"removed_v8_module_lines": 469
|
||||
},
|
||||
"tests": {
|
||||
"combined_ford_params_sunnylink_suite": "284 passed, 26 subtests passed in 2.63s",
|
||||
"suite_log_sha256": "2e223a507f0630481cf6f83b9f8893d226f3f4273a79a09fc35905aa875b1d2c",
|
||||
"coverage": {
|
||||
"covered_lines": 87,
|
||||
"num_statements": 87,
|
||||
"percent_covered": 100.0,
|
||||
"percent_covered_display": "100",
|
||||
"missing_lines": 0,
|
||||
"excluded_lines": 0,
|
||||
"percent_statements_covered": 100.0,
|
||||
"percent_statements_covered_display": "100",
|
||||
"num_branches": 26,
|
||||
"num_partial_branches": 0,
|
||||
"covered_branches": 26,
|
||||
"missing_branches": 0,
|
||||
"percent_branches_covered": 100.0,
|
||||
"percent_branches_covered_display": "100"
|
||||
},
|
||||
"ruff": "pass",
|
||||
"ty_controller_and_lab": "pass",
|
||||
"settings_compiler_check": "pass",
|
||||
"standards_review_remaining_findings": 0,
|
||||
"spec_review_remaining_findings": 0,
|
||||
"resolved_review_finding": "Updated YAML authoring source and regenerated settings JSON before final compiler/schema suite."
|
||||
},
|
||||
"routes": {
|
||||
"route95": {
|
||||
"cycles": 54738,
|
||||
"core_active_cycles": 37614,
|
||||
"core_exact_archived_match": true,
|
||||
"cohorts_reproduced": true,
|
||||
"adapter_active_cycles": 37614,
|
||||
"adapter_exact_match_with_fresh_engagement_dt": true,
|
||||
"adapter_validity_differs_from_archive_cycles": 0,
|
||||
"adapter_command_differs_from_archive_cycles": 59,
|
||||
"adapter_max_absolute_command_difference_c0_c1": [
|
||||
0.010000000000000675,
|
||||
0.0010000000000000009
|
||||
],
|
||||
"field_slew_zero_c2_c3_pass": true,
|
||||
"float32_can_round_trips": 109476,
|
||||
"opendbc_import_head": "72a775d35e54c21ff5c5798acef22016eedcc0a7",
|
||||
"report_sha256": "72fab710dc81c8c7d9b75d371b97fec32402d3b01fa05517dfa814d8daff3134"
|
||||
},
|
||||
"route90": {
|
||||
"cycles": 78812,
|
||||
"core_active_cycles": 73055,
|
||||
"core_exact_archived_match": true,
|
||||
"cohorts_reproduced": true,
|
||||
"adapter_active_cycles": 73055,
|
||||
"adapter_exact_match_with_fresh_engagement_dt": true,
|
||||
"adapter_validity_differs_from_archive_cycles": 0,
|
||||
"adapter_command_differs_from_archive_cycles": 19,
|
||||
"adapter_max_absolute_command_difference_c0_c1": [
|
||||
0.020000000000000462,
|
||||
0.0020000000000000018
|
||||
],
|
||||
"field_slew_zero_c2_c3_pass": true,
|
||||
"float32_can_round_trips": 157624,
|
||||
"opendbc_import_head": "72a775d35e54c21ff5c5798acef22016eedcc0a7",
|
||||
"report_sha256": "cc2224bae597a341697a7681560a077cb209d77e4d060c0850997691c57d32fb"
|
||||
}
|
||||
},
|
||||
"stress": {
|
||||
"seed": 20260907,
|
||||
"random_cycles": 200000,
|
||||
"mirrored_core_updates": 200000,
|
||||
"invalid_or_inactive_resets": 3537,
|
||||
"field_boundary_cases": 18138,
|
||||
"float32_can_round_trips": 218138,
|
||||
"analytic_targets_scalar_slew_and_mirror_checks_pass": true,
|
||||
"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": "c21a9013700734dd20b09e05aa68329ad8cc20f9"
|
||||
},
|
||||
"total_float32_can_round_trips": 485238,
|
||||
"native_params": {
|
||||
"source": "Rebuilt locally from this branch with clang++ and generated Capnp headers; ignored test dependency, not committed binary.",
|
||||
"library_sha256": "270bf43241cf7c02cc432cf78ec9411a62d7653ca445695efe785ae82241aa09",
|
||||
"sources_sha256": {
|
||||
"openpilot/common/params_c.cc": "57e3bcc7eba939bc91aadafb4ed1248b8123a8fe5c48fd8530298d966ea4db63",
|
||||
"openpilot/common/params.cc": "a5adacb1d47cb3bf6e0d87d44ce158b41982d7eaf2e8114e32c48d3a6631304c",
|
||||
"openpilot/common/params.h": "ed03d137e126ecd6f1608016020af18c0339fb987e27d0a2aa6830bba396970c",
|
||||
"openpilot/common/params_keys.h": "39d36465f66405843b926ba18473fb6aee81c0f1c7bea87246aa08ffe3f67c58",
|
||||
"openpilot/common/util.cc": "4479ecf72465e8f453d8af78447f7715f02d9397c58a49048f2bbc87a96d6b8a",
|
||||
"openpilot/common/swaglog.cc": "9c2f88a2f1c3c4253b73defb264cc367a13ade23e02928e1d469b5c5833df176"
|
||||
}
|
||||
},
|
||||
"test_dependency_notes": {
|
||||
"python": "/Users/ibpersonal/dev/sunnypilot/.venv/bin/python",
|
||||
"pyyaml": "6.0.3 from local uv cache",
|
||||
"jsonschema": "Local cached package appended after venv to run schema validator without skips",
|
||||
"hardware_build_and_device_boot": "not performed"
|
||||
},
|
||||
"source_sha256": {
|
||||
"docs/ford_model_action_candidate.md": "c968132348d20891a9396f6e69db1315d570505796e19e09ffbb2da748e6e687",
|
||||
"docs/ford_virtual_angle_experiment.md": "da6322f3c3d2d81463e44c50cc6cad1a962f97008ff9425c314da333ebe47a87",
|
||||
"openpilot/common/params_c.cc": "57e3bcc7eba939bc91aadafb4ed1248b8123a8fe5c48fd8530298d966ea4db63",
|
||||
"openpilot/common/params_keys.h": "39d36465f66405843b926ba18473fb6aee81c0f1c7bea87246aa08ffe3f67c58",
|
||||
"openpilot/common/tests/test_params.py": "557a1f616af5fd9f5e623fbee0fd6f44cb059a30c290c68ce2b57e9bcceed081",
|
||||
"openpilot/selfdrive/controls/controlsd.py": "102b383e5beff43b8dd7c219178bef62e8a4b54443ebe682694606862bcd4e7f",
|
||||
"openpilot/selfdrive/controls/lib/ford_model_action.py": "8f3bc5d68e0051776f614a2ccffae84a88f7898dc95bdc12c23dcfe10dfe676a",
|
||||
"openpilot/selfdrive/controls/tests/test_ford_controlsd_logging.py": "1c9448d88d8021e5d34a5dccd14a17c6c1bc64b5531342bfc6d15574d9d3e710",
|
||||
"openpilot/selfdrive/controls/tests/test_ford_model_action_adapter.py": "2c5b14f814e84e59d749f61a43ef1dcfe06253f6e185443fa123c37525ac8466",
|
||||
"openpilot/sunnypilot/sunnylink/settings_ui.json": "9974df3ac4cc58ae78d47848cd18ef4aca1bcbb00edb257f28b4220d92890528",
|
||||
"openpilot/sunnypilot/sunnylink/settings_ui_src/pages/vehicle.yaml": "7a3fa18e562d5a03a5b85c72ee3f3ebeb836c497285dcd9aaad24f8fb4dd6942",
|
||||
"openpilot/sunnypilot/sunnylink/tests/test_settings_schema.py": "3db566612381fd87d3655a3ccff470be7da998c4ea7f6365f53e58cdb9c0ffb7",
|
||||
"tools/ford_pscm_lab/model_action_replay.py": "827a6dc488d554bdf6e87438c6a2a985b3195bf6d01ab09002bfd6049d22a868",
|
||||
"tools/ford_pscm_lab/stress_model_action.py": "2d5c72cc4b8ae214f2f5a19a050fe138f5c2ab0294d92d24e2481af5f8185613",
|
||||
"tools/ford_pscm_lab/test_model_action_replay.py": "ebf6bcd9260745100311521f8e11e85b7aebdd5561ab0876bfc2e802429d6896",
|
||||
"openpilot/selfdrive/controls/tests/test_ford_model_action_selection.py": "f826c6328f0abac2a61f1a0a6f8d119fdbab858e363cb466d84ba9a7783059cd",
|
||||
"docs/ford_model_action_drive_test.md": "d825b177cd099efd797fe89b7041695d6d41e4b9e8bba6bcaeb32aece164262b"
|
||||
},
|
||||
"deployment_target": {
|
||||
"repository": "sunnypilot/sunnypilot",
|
||||
"branch": "hiimisaac-dev",
|
||||
"validated_code_commit": "ea1ed70c718d32539ef6b9a89b89c0e297c92e06"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,220 @@
|
||||
{
|
||||
"date": "2026-09-07",
|
||||
"baseline_commit": "c4b3c55c826fca1ce09618e418e95f0a24478d96",
|
||||
"calibration_approved": false,
|
||||
"production_selector_changed": false,
|
||||
"vehicle_settings_changed": false,
|
||||
"panda_safety_changed": false,
|
||||
"scope": "Offline command construction, adapter integration, numerical fault probes and Ford regression tests. Physical response remains unvalidated.",
|
||||
"controller_size": {
|
||||
"total_lines": 131,
|
||||
"code_lines_excluding_blanks_comments_docstrings": 86,
|
||||
"core_persistent_values": 2,
|
||||
"adapter_timestamps": 3
|
||||
},
|
||||
"opendbc_import_head": "72a775d35e54c21ff5c5798acef22016eedcc0a7",
|
||||
"tests": {
|
||||
"ford_suite": "264 passed, 150 subtests passed in 14.29s",
|
||||
"new_core_adapter_tests": 107,
|
||||
"new_replay_validator_tests": 13,
|
||||
"controller_coverage": {
|
||||
"statements": 78,
|
||||
"missing_statements": 0,
|
||||
"branches": 24,
|
||||
"partial_branches": 0,
|
||||
"percent": 100
|
||||
},
|
||||
"ruff": "pass",
|
||||
"ty_controller_and_lab": "pass"
|
||||
},
|
||||
"routes": {
|
||||
"route95": {
|
||||
"cycles": 54738,
|
||||
"core_active_cycles": 37614,
|
||||
"core_exact_archived_match": true,
|
||||
"cohorts_reproduced": true,
|
||||
"adapter_active_cycles": 37614,
|
||||
"adapter_status_counts": {
|
||||
"inactive": 17124,
|
||||
"active": 37614
|
||||
},
|
||||
"adapter_exact_match_with_fresh_engagement_dt": true,
|
||||
"core_active_path_shorter_than_7m_cycles": 44,
|
||||
"adapter_validity_differs_from_archive_cycles": 0,
|
||||
"adapter_command_differs_from_archive_cycles": 59,
|
||||
"adapter_max_absolute_command_difference_c0_c1": [
|
||||
0.010000000000000675,
|
||||
0.0010000000000000009
|
||||
],
|
||||
"field_slew_zero_c2_c3_pass": true,
|
||||
"float32_can_round_trips": 109476,
|
||||
"turn_speed_15_55": {
|
||||
"seconds": 33.332073582999925,
|
||||
"core_c0_c1_rms": [
|
||||
0.07257996778378971,
|
||||
0.03628926246224237
|
||||
],
|
||||
"recorded_v8_c0_c1_rms": [
|
||||
0.3416081588451539,
|
||||
0.027322786376822172
|
||||
],
|
||||
"adapter_eligible_seconds": 33.332073582999925,
|
||||
"adapter_c0_c1_rms": [
|
||||
0.07257996778378971,
|
||||
0.03628926246224237
|
||||
]
|
||||
},
|
||||
"input_sha256": {
|
||||
"route.npz": "6e5438867ea618e53ca395b60ff4b9b146256f1bf6f07dc8e92d663286074154",
|
||||
"encoder_comparison.npz": "23bd05c4b6400299844acaba1d97051c96c23c682bf17b46e89e7f2cca5fce38",
|
||||
"pose_replay.npz": "9cfbb3c6f0b1fdb7e3d38e6b64b8c94cffbcc9fabe41f6344d84a9b657b6af9b"
|
||||
},
|
||||
"report_sha256": "cf4e3804f2ebdb0e50f3c49636bf60a30e3c1180411b582826a115970ab972fc"
|
||||
},
|
||||
"route90": {
|
||||
"cycles": 78812,
|
||||
"core_active_cycles": 73055,
|
||||
"core_exact_archived_match": true,
|
||||
"cohorts_reproduced": true,
|
||||
"adapter_active_cycles": 73055,
|
||||
"adapter_status_counts": {
|
||||
"inactive": 5757,
|
||||
"active": 73055
|
||||
},
|
||||
"adapter_exact_match_with_fresh_engagement_dt": true,
|
||||
"core_active_path_shorter_than_7m_cycles": 0,
|
||||
"adapter_validity_differs_from_archive_cycles": 0,
|
||||
"adapter_command_differs_from_archive_cycles": 19,
|
||||
"adapter_max_absolute_command_difference_c0_c1": [
|
||||
0.020000000000000462,
|
||||
0.0020000000000000018
|
||||
],
|
||||
"field_slew_zero_c2_c3_pass": true,
|
||||
"float32_can_round_trips": 157624,
|
||||
"turn_speed_15_55": {
|
||||
"seconds": 47.53485040600012,
|
||||
"core_c0_c1_rms": [
|
||||
0.08686835454709245,
|
||||
0.043216347944464766
|
||||
],
|
||||
"recorded_v8_c0_c1_rms": [
|
||||
0.4471065308273131,
|
||||
0.030663943784303503
|
||||
],
|
||||
"adapter_eligible_seconds": 47.53485040600012,
|
||||
"adapter_c0_c1_rms": [
|
||||
0.08686835454709245,
|
||||
0.043216347944464766
|
||||
]
|
||||
},
|
||||
"input_sha256": {
|
||||
"route.npz": "51e8c26eedde253e171af47d704c1967ba45ae6825d883393bec1fb9e00251c1",
|
||||
"encoder_comparison.npz": "7a625d3ed5cbd8013d1028aa3bc421740551dcae5c3d60981208bd047af9794c",
|
||||
"pose_replay.npz": "4457ccc0868354749da5b72c1dea0faf750f783dfdc87038101288fdcba1e707"
|
||||
},
|
||||
"report_sha256": "b95247bdf6bbec16e5dc4781eaf7a678aca787418251cd161f6438e3173ad490"
|
||||
}
|
||||
},
|
||||
"stress": {
|
||||
"seed": 20260907,
|
||||
"random_cycles": 200000,
|
||||
"mirrored_core_updates": 200000,
|
||||
"invalid_or_inactive_resets": 3537,
|
||||
"field_boundary_cases": 18138,
|
||||
"float32_can_round_trips": 218138,
|
||||
"analytic_targets_scalar_slew_and_mirror_checks_pass": true,
|
||||
"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": "72a775d35e54c21ff5c5798acef22016eedcc0a7"
|
||||
},
|
||||
"mutation_checks": {
|
||||
"mutations": [
|
||||
{
|
||||
"mutation": "halve_heading",
|
||||
"detected_by_tests": true,
|
||||
"failed_tests": 9
|
||||
},
|
||||
{
|
||||
"mutation": "cap_heading_preview",
|
||||
"detected_by_tests": true,
|
||||
"failed_tests": 9
|
||||
},
|
||||
{
|
||||
"mutation": "erase_centering",
|
||||
"detected_by_tests": true,
|
||||
"failed_tests": 15
|
||||
},
|
||||
{
|
||||
"mutation": "slow_c0_to_c1_rate",
|
||||
"detected_by_tests": true,
|
||||
"failed_tests": 11
|
||||
},
|
||||
{
|
||||
"mutation": "retain_invalid_state",
|
||||
"detected_by_tests": true,
|
||||
"failed_tests": 18
|
||||
},
|
||||
{
|
||||
"mutation": "ignore_model_freshness",
|
||||
"detected_by_tests": true,
|
||||
"failed_tests": 2
|
||||
},
|
||||
{
|
||||
"mutation": "ignore_model_clock_rollback",
|
||||
"detected_by_tests": true,
|
||||
"failed_tests": 1
|
||||
},
|
||||
{
|
||||
"mutation": "reverse_c0_wire_sign",
|
||||
"detected_by_tests": true,
|
||||
"failed_tests": 11
|
||||
}
|
||||
],
|
||||
"all_detected": true
|
||||
},
|
||||
"native_test_dependency": {
|
||||
"scope": "Native dependency for inherited Params selection test only; copied existing local build, not rebuilt.",
|
||||
"source_library": "/Users/ibpersonal/.codex/worktrees/3548/sunnypilot/openpilot/common/libparams_c.dylib",
|
||||
"sha256": "ddde738108eab18b75f085c865c43aa197fd79a0384b390b1515ff90116e20e0",
|
||||
"byte_identical_source_files": [
|
||||
"openpilot/common/params.cc",
|
||||
"openpilot/common/params.h",
|
||||
"openpilot/common/params_c.cc",
|
||||
"openpilot/common/params.py",
|
||||
"openpilot/common/params_keys.h",
|
||||
"openpilot/common/queue.h",
|
||||
"openpilot/common/util.cc",
|
||||
"openpilot/common/util.h",
|
||||
"openpilot/common/hardware/hw.h"
|
||||
]
|
||||
},
|
||||
"review": {
|
||||
"standards_findings": 0,
|
||||
"spec_findings": 0,
|
||||
"method": "Independent parallel read-only reviews; 120 focused tests independently passed."
|
||||
},
|
||||
"source_sha256": {
|
||||
"openpilot/selfdrive/controls/lib/ford_model_action.py": "cb6353f00f2f5c84df4e606c6b7e20650f8c1e908b9fd71890f72aa4a5e42592",
|
||||
"openpilot/selfdrive/controls/tests/test_ford_model_action.py": "c3b971622cc4041575aeec1826d45b76cebab9a2f77b2b9525c85d4184961295",
|
||||
"openpilot/selfdrive/controls/tests/test_ford_model_action_adapter.py": "5aaa29c1f11080b7df0165fd0202a29b065053809e5f9c4b9ed8abfcce47e41b",
|
||||
"tools/ford_pscm_lab/__init__.py": "db4b8b7d2e317ed34ca0ec220bf9d53e7b80a23e766f4e1cb2c8224dada45f2e",
|
||||
"tools/ford_pscm_lab/model_action_replay.py": "c114c479bd22e4fc61a3e8d3ee7fae5d71d1d80ec4b952faad8f8f3692fb1508",
|
||||
"tools/ford_pscm_lab/stress_model_action.py": "a78a50eed1f801f3b096d694ab8c2fd70804b6c250465b4152f38d83770a982b",
|
||||
"tools/ford_pscm_lab/test_model_action_replay.py": "09d024c59d44b83ec081d6416d0f946a7719a73f22213af1f4ddc03dc4e6f4ac"
|
||||
},
|
||||
"artifacts": {
|
||||
"directory": ".cache/ford_model_action",
|
||||
"route_reports": [
|
||||
"route95/report.json",
|
||||
"route90/report.json"
|
||||
],
|
||||
"stress_report": "stress.json",
|
||||
"mutation_report": "mutations/report.json",
|
||||
"test_log": "ford_suite.txt"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
# Signaled-turn preview trial
|
||||
|
||||
The installed option now also includes the independently tested geometry entry
|
||||
candidate. See [stop continuity and geometry entry](ford_stop_geometry_entry.md)
|
||||
for current behavior. The original action-only trial and evidence follow below.
|
||||
|
||||
The coordinated Ford controller can start a large intersection turn late even
|
||||
when the model geometry already shows the turn. This opt-in trial uses geometry
|
||||
as a timing cue for the existing action request. It does not replace the action
|
||||
with path heading or lateral position.
|
||||
|
||||
## Selection
|
||||
|
||||
In sunnylink Ford settings, enable **Signaled-Turn Preview (Experimental)** with
|
||||
**Selected-Action Path Tracking** and **Coordinated C0/C1 Steering** on, and
|
||||
**Model Geometry Reference** off. Keep **Large-Turn Entry Assist** on to match the
|
||||
offline candidate. Apply settings while offroad, then start a new onroad session.
|
||||
The new parameter, `FordPscmTurnPreview`, defaults off. The existing turn-entry
|
||||
assist remains a separate option. Turning the master selected-action option off
|
||||
still selects upstream Ford control.
|
||||
|
||||
## Command change
|
||||
|
||||
`controlsd` samples heading change at 7 and 14 metres of cumulative model-path
|
||||
distance, once per model message. The cue fades in above 5 degrees at 7 metres
|
||||
and 20 degrees at 14 metres; both headings must have the same sign. Full weight
|
||||
requires at least 10 and 30 degrees respectively. This uses path distance even
|
||||
when forward X folds back during a turn.
|
||||
|
||||
The cue and original model timestamp travel in `carControlSP.fordTurnPreview`.
|
||||
`card` adds at most 0.20 seconds of the existing filtered action-angle rate,
|
||||
capped at 30 wheel degrees, to the trimmed inverse target. It requires exactly
|
||||
one matching driver turn signal and both raw model action and filtered angle
|
||||
request growing toward the turn. The original action, rate estimate, and trim
|
||||
error remain unchanged. The existing inverse acceleration allowance, encoder,
|
||||
CAN bounds, transmit observer, and optional post-encoder assist still apply.
|
||||
C2 and C3 remain zero.
|
||||
|
||||
Missing, invalid, future or older-than-150-ms geometry removes this addition.
|
||||
Lane changes, maneuver injection, driver steering input, or a fresh PSCM limit
|
||||
of 2 or greater suppress it too. These cue failures do not invalidate the base
|
||||
controller. New model array work stays out of the CAN loop. There are no new
|
||||
subscriptions, logging frequencies, shared process changes, or correction
|
||||
integrators.
|
||||
|
||||
## Offline evidence and limits
|
||||
|
||||
The frozen candidate's latest-route left entry first changed commands about
|
||||
0.60 seconds earlier, but the reconstructed reference reached 90 degrees only
|
||||
21 ms earlier. Another good left entry reached that reference 239 ms earlier.
|
||||
The latest-route right entry retained identical packets. Those are estimates
|
||||
from the older-firmware reconstruction, not measured wheel improvements.
|
||||
|
||||
Nine of ten bookmarked wobble windows retained identical packets. The remaining
|
||||
window differed by at most 0.02 m C0 and 0.001 rad C1. Two camera-verified ordinary
|
||||
lane-following sections retained identical packets with the matching-signal
|
||||
gate. This does not establish unchanged behavior on all ordinary roads.
|
||||
|
||||
Retained command history can affect output after the signal/cue clears. In one
|
||||
good unwind the reconstructed 30-degree crossing was 0.103 seconds later, with
|
||||
the 5-degree crossing unchanged. Offline replay holds the measured wheel fixed;
|
||||
it cannot establish stability, physical smoothness, or successful turn completion.
|
||||
|
||||
The installation checks exercise actual model messages, additive Cap'n Proto
|
||||
transport, both process startup selectors, card's reader contract, and the real
|
||||
Ford CAN packer. They compare full command histories with the retained candidate
|
||||
when enabled and the prior controller when disabled. Route gaps reset only the
|
||||
offline predictor; production timing-gap behavior is unchanged.
|
||||
|
||||
All 11 retained routes (18f, 190, 183, 185, 17c, 166, 16a, 172, 177, 175, 17a)
|
||||
matched exactly in both configurations: 1,208,782 controller updates and 116,886
|
||||
model messages. The installed helper's heading samples and action-trend signs
|
||||
also matched the original offline extractor. The relevant controller,
|
||||
integration, startup, and sunnylink suites passed 410 tests; lint and generated
|
||||
settings consistency checks passed.
|
||||
|
||||
On the development Mac, the new cue helper's median/p99 update cost was
|
||||
20.5/37.5 microseconds per new model message; cached calls were 0.125/0.25
|
||||
microseconds. A 10,000-message Python allocation check retained 32 additional
|
||||
bytes after warmup. These are local checks, not device realtime or whole-process
|
||||
memory validation.
|
||||
@@ -0,0 +1,64 @@
|
||||
# Stop request continuity and geometry entry
|
||||
|
||||
The joint controller previously marked the path invalid below 0.3 m/s in both
|
||||
controlsd's validity carrier and card. The sender then transmitted inactive mode
|
||||
0 with zero C0/C1. The new joint path stays active through a stop while engagement,
|
||||
freshness, CAN, motion, and steering-fault checks permit it.
|
||||
|
||||
Below 0.3 m/s, card repeats the last transmitted active C0/C1 packet. The observer
|
||||
continues advancing at actual speed; no fictitious speed is supplied to the
|
||||
inverse, which divides by speed squared. Trim is retained, the latest action
|
||||
target is remembered, and the trend rate is zero while holding. At 0.3 m/s and
|
||||
above, normal allocation resumes using the current action. Disengagement, stale
|
||||
inputs, steering faults, and explicit PSCM override/denial still release the
|
||||
request. Starting or reengaging at rest after an inactive packet begins with a
|
||||
neutral active packet; it does not restore an old turn command.
|
||||
|
||||
This preserves the transmitted request, not a proven physical wheel angle. A
|
||||
different model action on restart can still request straighter steering. The
|
||||
legacy non-joint adapter's speed gate and master-off upstream path are unchanged.
|
||||
|
||||
## Geometry candidate
|
||||
|
||||
The existing **Signaled-Turn Preview (Experimental)** toggle now also enables the
|
||||
offline `boot08` candidate. Keep Selected-Action Path Tracking, Coordinated C0/C1
|
||||
Steering, and Large-Turn Entry Assist enabled, with Model Geometry Reference off.
|
||||
Apply settings offroad and start a new onroad session.
|
||||
|
||||
On each new model message, controlsd measures local heading change over a complete
|
||||
2 m path interval centered at speed times 0.8 seconds (start clamped to zero).
|
||||
It converts this curvature through the existing VehicleModel, roll and angle
|
||||
offset. Geometry and vehicle parameters must be valid; insufficient path disables
|
||||
only the independent addition. The angle travels as additive carControlSP metadata.
|
||||
|
||||
The 7/14 m headings and exactly one matching signal must agree on a sharp turn.
|
||||
The independent addition is capped at 90 wheel degrees, fades during action unwind,
|
||||
and fades from full strength at 30 action degrees to zero at 90. Strong opposing
|
||||
action suppresses it. Card selects the larger of this addition and the existing
|
||||
bounded action-rate preview, rather than adding both. The action base, trim,
|
||||
encoder, acceleration allowance, C0/C1 field bounds and zero C2/C3 remain.
|
||||
|
||||
The geometry calculation uses speed and vehicle parameters available when the
|
||||
new model message is processed. The archived offline experiment sampled their
|
||||
latest recorded values at the model timestamp. Transport timing can therefore
|
||||
cause small input differences; installation parity uses identical sampled inputs.
|
||||
|
||||
## Validation
|
||||
|
||||
Nine new stop-continuity regression cases failed before the fix and pass with
|
||||
active packets through the stop, retained commands/trim, and fault/disengagement
|
||||
release without reviving old commands. The real Ford CAN packer is exercised.
|
||||
|
||||
The prior frozen experiment covered 12 routes (828,178 controller samples per
|
||||
candidate). Installation checks match its geometry calculation on 4,854 sampled
|
||||
paths across all 12 routes, its policy on 20,000 random cases, and its transmitted
|
||||
commands and observer state on 3,000 continuous moving updates. Separate tests
|
||||
cover flat-action entry through real model messages and Cap'n Proto transport,
|
||||
takeover fade, health and signal gates, and toggle-off behavior.
|
||||
|
||||
The earlier experiment advanced several nominal entry crossings by roughly
|
||||
0.07–0.39 seconds; some turns were unchanged. Nine of ten wobble windows retained
|
||||
identical packets, and the known unwind's 90/60/30-degree reference crossings were
|
||||
unchanged. A near-stop entry reference was busier. These are frozen command and
|
||||
older-firmware reference comparisons, not a new physical wheel response, proof
|
||||
of stability, or truck validation of the combined changes.
|
||||
@@ -0,0 +1,106 @@
|
||||
# Ford large-turn entry assist
|
||||
|
||||
The coordinated encoder can select the same C0/C1 packet even when its angle
|
||||
target increases: larger fields sometimes have the same predicted next state,
|
||||
and its tie break favors the smaller fields. This does not prove that the live
|
||||
PSCM responds identically. This opt-in trial adds a bounded command after that
|
||||
selection, making the turn-entry experiment observable at the CAN output.
|
||||
|
||||
## Enable and revert
|
||||
|
||||
In sunnylink Ford settings, keep **Selected-Action Path Tracking** and
|
||||
**Coordinated C0/C1 Steering** enabled, and **Model Geometry Reference** disabled.
|
||||
Enable **Large-Turn Entry Assist (Experimental)**, then make an offroad-to-onroad
|
||||
transition. The setting is default off and read once at startup. Disabling only
|
||||
the new setting and cycling offroad/onroad restores the previous coordinated
|
||||
controller. Disabling Selected-Action Path Tracking restores upstream Ford
|
||||
control. No hot switching or added steering-loop parameter I/O.
|
||||
|
||||
## Exactly what changes
|
||||
|
||||
The original desired steering angle, inverse, target-trend preview and centering
|
||||
trim remain. An additional command is weighted by three continuous ramps:
|
||||
|
||||
- Requested angle magnitude: zero through 30 degrees, full at 60 degrees.
|
||||
- Wheel lag into that turn: zero through 25 degrees, full at 50 degrees.
|
||||
- Growing filtered target rate: zero at steady/relaxing target, full at
|
||||
30 degrees/second.
|
||||
|
||||
Multiply the three weights. At full weight, add **0.60 m C0 and 0.04 rad C1**
|
||||
in the requested turn direction, then quantize and clip to the existing DBC
|
||||
bounds. These are trial tuning values, not recovered Ford constants or a
|
||||
guaranteed conversion to wheel angle. The 4× label refers to the earlier offline
|
||||
starting budget of 0.15 m / 0.01 rad, not a multiplication of all steering.
|
||||
|
||||
Only the extra term is suppressed on steeringPressed, fresh PSCM limit >=2,
|
||||
or a target already limited by the existing inverse acceleration allowance.
|
||||
Existing disengagement, fault and explicit override gates still govern the
|
||||
whole command. C2/C3 stay zero. No new integrator, retained maneuver state or
|
||||
encoder search is added. The observer continues consuming actual packed commands.
|
||||
|
||||
The extra goes to zero when the filtered target stops growing. **This does not
|
||||
clear the consequences of earlier commands:** the observer and encoder retain
|
||||
their state, so later requests can change. Normal path following and physical
|
||||
wheel release are not guaranteed unchanged. Delayed unwind is an accepted trial
|
||||
tradeoff, not claimed fixed.
|
||||
|
||||
## Why this strength
|
||||
|
||||
Nine recorded routes were compared: 183, 185, 17c, 166, 16a, 172, 177, 175 and
|
||||
17a. Each run used 556,776 input rows, actual adapter/packer calls, frozen model
|
||||
and vehicle measurements, and full available histories. Compare against the
|
||||
unchanged coordinated controller at `d203a109a`.
|
||||
|
||||
| Extra budget | Weak right 2: max total C0/C1 difference | Changed ordinary samples |
|
||||
|---|---|---|
|
||||
| 0.15 m / 0.01 rad | 0.26 m / 0.0185 rad | 2,923 / 236,385 (1.24%) |
|
||||
| 0.30 m / 0.02 rad | 0.41 m / 0.0285 rad | 2,071 / 236,385 (0.88%) |
|
||||
| **0.60 m / 0.04 rad** | **0.71 m / 0.0485 rad** | **1,493 / 236,385 (0.63%)** |
|
||||
|
||||
Total differences can exceed the immediate additive budget because earlier
|
||||
commands alter subsequent encoder decisions. The ordinary screen requires
|
||||
requests within ±30 degrees, engaged control, no touch/limitReached, and two
|
||||
seconds of continuously qualifying data on each side. It measures command
|
||||
differences, not lane error. Lower counts are not proof of better centering.
|
||||
|
||||
The 4× trial changes 76/334 and 104/326 samples in the two weak-right windows.
|
||||
Their peak absolute fields stay below 3.10 m C0 and 0.225 rad C1. Roundabout
|
||||
windows reach maximum differences of 0.63–0.66 m / 0.0425–0.0445 rad.
|
||||
|
||||
Nine of ten marked wobble windows match exactly. In route172's bookmark, 98/396
|
||||
samples change, up to 0.21 m C0 / 0.007 rad C1. Command total variation is slightly
|
||||
lower (C0 5.95→5.85 m; C1 0.3605→0.3545 rad); maximum individual steps are unchanged.
|
||||
This does not prove that the real wobble improves or remains equal.
|
||||
|
||||
Across all nine routes the trial touches a DBC field bound on 29 samples versus
|
||||
12 for baseline, including a short interval on175. It never exceeds those
|
||||
bounds. It changes the marked good unwind's channel split substantially:
|
||||
maximum C0/C1 differences of 2.16 m / 0.058 rad. That is not a measured wheel
|
||||
release delay. Larger requests reaching CAN do not establish more wheel motion.
|
||||
|
||||
## Verification and interpretation
|
||||
|
||||
Tests exercise the real Ford adapter and packer: startup/fallback, default-off
|
||||
parameter, both turn signs, no extra for small requests/errors, DBC clipping,
|
||||
target limits, overrides, fault gates, and clearing the added term while retaining
|
||||
the observer's real history. Full-route production verification compares both
|
||||
settings directly against the frozen 4× research output, including state and
|
||||
transmitted commands. UI source and generated schema are checked together.
|
||||
|
||||
The deployment gate additionally suppresses the term when the inverse target
|
||||
is acceleration-limited; no active extra in these nine research tapes coincided
|
||||
with that condition. This does not introduce a new acceleration allowance.
|
||||
|
||||
`Ford joint path tracking` diagnostics identify the enabled experiment as
|
||||
`ford-joint-turn-entry-v1`, with `turn_entry_enabled`, `turn_entry_weight`,
|
||||
`turn_entry_c0`, `turn_entry_c1`, `base_wire_command` and actual `wire_sent`.
|
||||
The existing `predicted_curvature` field describes the encoder prediction before
|
||||
the added term; `filtered_curvature` is the observer advanced from actual sends.
|
||||
Logging cadence is unchanged.
|
||||
|
||||
These are offline numerical checks, not a closed-loop steering validation.
|
||||
166/16a retain raw-yaw sensitivity; 183 has incomplete earlier history; 185 has
|
||||
log gaps. The recovered older firmware model is not proven identical to the
|
||||
live truck. The physical question for this trial is whether the stronger entry
|
||||
requests move the wheel farther/faster without unacceptable normal-driving
|
||||
changes. Neither turn completion nor preserved driving feel is established.
|
||||
@@ -0,0 +1,122 @@
|
||||
# Ford completed-unwind correction release
|
||||
|
||||
Version `model-action-c1-feedback-v5` releases dominant C1 correction after a
|
||||
confirmed unwind reaches the selected curvature. This fixes stored correction
|
||||
continuing to request a new turn after its original unwind is complete.
|
||||
|
||||
Route 115 (`codex-last2`) ran v4, `6df5eabb7`. Near 2:13.2, desired and actual
|
||||
steering were both near zero after a right turn, but C1 still requested about
|
||||
0.1165 rad left. About 0.114 rad was accumulated unwind correction. V4's
|
||||
changed-request release did not apply: the small new left request was increasing
|
||||
while the measured error called for less left steering. The earlier reversal
|
||||
release also did not apply because measured and requested curvature were
|
||||
already on the same side.
|
||||
|
||||
## Rule
|
||||
|
||||
On a fresh steering measurement, remember an unwind direction when the selected
|
||||
curvature relaxes toward zero (or crosses it), measured curvature remains on
|
||||
the previous side, and measured error calls for leaving that old turn.
|
||||
|
||||
When measured error reaches or passes zero in that unwind direction, release
|
||||
the stored correction only if all of these agree:
|
||||
|
||||
- The selected curvature has reached zero or crossed into the unwind direction.
|
||||
- Correction points in that direction and exceeds the magnitude of base C1.
|
||||
- Original model C0 and applied C0 both confirm that direction by at least the
|
||||
existing 0.01 m DBC step.
|
||||
|
||||
Consume the unwind marker at this first catch-up, even if the other conditions
|
||||
prevent release. A steady old-side bend, neutral/conflicting C0, or a correction
|
||||
smaller than base C1 retains its correction. Steady requests cannot arm the
|
||||
marker. Duplicate steering publications cannot arm or consume it. Driver/PSCM
|
||||
feedback inhibition and controller resets clear it; correction reversing
|
||||
direction also clears it.
|
||||
|
||||
After retirement, the v4 command law still runs: bounded changed-request
|
||||
release, reversal release, measured-error integration, PSCM arbitration, and
|
||||
final C1 slew. Removing stored correction therefore does not jump the output.
|
||||
There is one additional control state, `unwind_direction`; `unwind_release`
|
||||
only reports the signed correction retired on the current cycle. Periodic
|
||||
diagnostics may miss individual release cycles.
|
||||
|
||||
This is a conditional correction-reset policy, not a PSCM plant model. It adds
|
||||
no strength multiplier. The existing 1:1 feedback choice, C0 mapping/overflow,
|
||||
C2/C3 zeroing, amplitude/slew limits, sender cadence and input gates remain.
|
||||
Toggle off still selects upstream Ford control; toggle on selects the experiment
|
||||
on Ford CAN FD platforms. See [selection and restore](ford_model_action_drive_test.md).
|
||||
|
||||
## Exact exit replay
|
||||
|
||||
Frozen route 115 measurements trigger one release at **2:13.203501**. The
|
||||
selected steering angle is 0.469 degrees left and measured angle is 0.500 degrees
|
||||
left. The controller retires 0.114026 rad of left unwind correction. Its first
|
||||
C1 output moves from 0.1165 to 0.1110 rad left, respecting the original slew.
|
||||
|
||||
At **2:13.594615**, old C1 is **0.1240 rad left**, versus **0.0105 rad left** in
|
||||
v5. C0 is identical. The earlier unwind (2:09 through 2:13.2), comparison turn
|
||||
(3:20 through 3:34), and comparison bend (5:33 through 5:45) have identical
|
||||
commands throughout their windows.
|
||||
|
||||
The recorded wheel motion stays fixed in this replay. It does not predict a
|
||||
new steering angle, prove stability, or establish that the full overshoot is
|
||||
fixed. This maneuver also includes driver input and a changing C0 request;
|
||||
neither its whole swing nor every hanging exit can be attributed to stored I.
|
||||
|
||||
## Validation
|
||||
|
||||
Four targeted regressions failed on v4 because correction persisted after
|
||||
catch-up; all now pass. Expanded tests cover both directions, fresh/duplicate
|
||||
feedback, catch-up confirmation, steady tracking/noise, old-side bends, C0
|
||||
agreement, dominant correction, reset/override, limit-reached behavior and slew.
|
||||
Integration exercises actual controlsd request selection/limiting for model
|
||||
and maneuver sources, Float32 publication, and Ford CAN packing/checksums.
|
||||
|
||||
The combined suite passes **753 tests and 9,145 subtests**, with **178 inherited
|
||||
or unsupported safety-test skips**. Random feedback stress covers 200,000 cycles
|
||||
and their mirrors. Each cycle exactly matches v4 after only the declared new
|
||||
retirement; 59 cycles retire correction. Independent zero-error checks cover
|
||||
200,000 cycles and 18,138 field-boundary cases. Ruff, controller Ty and settings
|
||||
compilation checks pass.
|
||||
|
||||
| Frozen route | Cycles | New releases | Changed C1 cycles | Largest C1 difference |
|
||||
| --- | ---: | ---: | ---: | ---: |
|
||||
| 114 | 61,027 | 4 | 2,849 | 0.0150 rad |
|
||||
| 115 | 40,037 | 1 | 384 | 0.1140 rad |
|
||||
| 112 | 108,971 | 14 | 30,036 | 0.0720 rad |
|
||||
| 113 | 49,614 | 1 | 951 | 0.0050 rad |
|
||||
| Historical b9 | 90,774 | 16 | 7,013 | 0.1435 rad |
|
||||
|
||||
All routes compare v5 against v4 with the same recorded inputs. Activation and
|
||||
C0 match exactly on all 350,423 cycles. Releases also occur at smaller exits;
|
||||
changed history can affect subsequent ordinary bends. These results do not
|
||||
establish unchanged physical centering. Route 114's large segment-2 overshoot
|
||||
does not trigger this new release, so it remains a separate unresolved case.
|
||||
|
||||
The five replays and two stress runs verify **768,561 Float32/CAN round trips**,
|
||||
separately from the integration suite. Exact source hashes and numerical
|
||||
results are in `ford_unwind_catchup_validation.json`.
|
||||
|
||||
## Reproduction
|
||||
|
||||
Use the native project Python dependencies and unchanged opendbc revision
|
||||
`64aa61b9b3fd26e70a7caa915acab207ff3cd64a`. Route replay requires the full-rlog
|
||||
extracts identified by validation hashes; the original logs are not modified.
|
||||
|
||||
```sh
|
||||
export PYTHONDONTWRITEBYTECODE=1
|
||||
export PYTHONPATH=.:opendbc_repo
|
||||
export PARAMS_ROOT=/tmp/ford-v5-test-params
|
||||
export LOG_ROOT=/tmp/ford-v5-test-logs
|
||||
python -m pytest -q -p no:cacheprovider openpilot/selfdrive/controls/tests/test_ford_*.py tools/ford_pscm_lab openpilot/selfdrive/car/tests/test_ford_pscm_status.py openpilot/sunnypilot/sunnylink/tests openpilot/common/tests/test_params.py opendbc_repo/opendbc/car/ford/tests/test_ford.py opendbc_repo/opendbc/safety/tests/test_ford.py
|
||||
python -m tools.ford_pscm_lab.feedback_replay stress --cycles 200000 --output .cache/ford_unwind_catchup/stress.json
|
||||
python -m tools.ford_pscm_lab.stress_model_action --cycles 200000 --seed 20260913 --opendbc-revision 64aa61b9b3fd26e70a7caa915acab207ff3cd64a --output .cache/ford_unwind_catchup/zero_error.json
|
||||
for route in 114 115 112 113 b9; do
|
||||
python -m tools.ford_pscm_lab.feedback_replay route .cache/ford_route${route} --baseline 6df5eabb7e7f6bc4e206644d5ca9069df820124e --output .cache/ford_unwind_catchup/route${route}
|
||||
done
|
||||
```
|
||||
|
||||
Publication time approximates the computation clock; full SubMaster health is
|
||||
not reconstructable. These route replays do not reconstruct selected maneuver
|
||||
messages; integration tests cover that source. No device build, boot,
|
||||
installation or physical steering test is performed offline.
|
||||
@@ -0,0 +1,346 @@
|
||||
{
|
||||
"hypothesis": "model-action-c1-feedback-v5",
|
||||
"baseline_revision": "6df5eabb7e7f6bc4e206644d5ca9069df820124e",
|
||||
"opendbc_revision": "64aa61b9b3fd26e70a7caa915acab207ff3cd64a",
|
||||
"scope": "Software command release and numerical invariants only; no counterfactual steering motion, physical stability or improved tracking claim.",
|
||||
"calibration_approved": false,
|
||||
"tests": {
|
||||
"passed": 753,
|
||||
"subtests_passed": 9145,
|
||||
"skipped": 178,
|
||||
"log_sha256": "64c683622cc91125e32cb0d78f4a5340b8d58fa149d90b06361629394489731d",
|
||||
"initial_regression": "4 failed on v4: stored unwind correction remains after catch-up; all pass on v5",
|
||||
"regression_log_sha256": "0ced2a0686cccba3c321c58749a679843a3179fa57078a6b030d20f0f9ae33e2"
|
||||
},
|
||||
"feedback_stress": {
|
||||
"cycles": 200000,
|
||||
"mirrored_updates": 200000,
|
||||
"can_round_trips": 200000,
|
||||
"carryover_release_count": 181,
|
||||
"baseline_revision": "6df5eabb7e7f6bc4e206644d5ca9069df820124e",
|
||||
"baseline_source_sha256": "1a4ce5f5f63b4d2f1f6e0537c9b2ca7c463ca44b427349d71d28fb8a1138b00f",
|
||||
"seed": 20260913,
|
||||
"request_release_cycles": 20214,
|
||||
"unwind_release_cycles": 59,
|
||||
"exact_unchanged_state_and_commands_without_unwind_release": 199941,
|
||||
"exact_v4_match_after_only_declared_retirement": 200000,
|
||||
"checks": "Symmetry, resets, amplitude/slew, bounded retirement, carryover confirmation, integration, PSCM limits, CAN.",
|
||||
"scope": "Numerical software invariants only; no model of vehicle motion.",
|
||||
"calibration_approved": false,
|
||||
"controller_sha256": "2ceb4cd8717bb3325f9b22189c78605ad5d42a1061dec9ca2e4dbb90fd256d1e"
|
||||
},
|
||||
"zero_error_stress": {
|
||||
"seed": 20260913,
|
||||
"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,
|
||||
"direct_raw_float32_packing_matches_host_output": true,
|
||||
"max_continuous_step_c0_c1": [
|
||||
0.4000000000000019,
|
||||
0.05000000000000002
|
||||
],
|
||||
"calibration_approved": false,
|
||||
"scope": "Zero-error numerical construction: measured equals requested curvature. No PSCM response claims.",
|
||||
"opendbc_import_head": "64aa61b9b3fd26e70a7caa915acab207ff3cd64a",
|
||||
"source_sha256": {
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/stress_model_action.py": "cec2619285dd41274562ac035ee8ea0a389269a0c4ef1b62efa6252ad1a714aa",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/openpilot/selfdrive/controls/lib/ford_model_action.py": "2ceb4cd8717bb3325f9b22189c78605ad5d42a1061dec9ca2e4dbb90fd256d1e",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/model_action_replay.py": "af97c665f342c66b1be2502e188c63e6f3ee106d0a0d5e80997bc3040373ff9f"
|
||||
}
|
||||
},
|
||||
"replays": {
|
||||
"114": {
|
||||
"baseline_revision": "6df5eabb7e7f6bc4e206644d5ca9069df820124e",
|
||||
"baseline_source_sha256": "1a4ce5f5f63b4d2f1f6e0537c9b2ca7c463ca44b427349d71d28fb8a1138b00f",
|
||||
"calibration_approved": false,
|
||||
"cycles": 61027,
|
||||
"active_cycles": 48419,
|
||||
"validity_matches_baseline_exactly": true,
|
||||
"status_counts": {
|
||||
"inactive": 12608,
|
||||
"active": 48419
|
||||
},
|
||||
"c0_matches_baseline_exactly": true,
|
||||
"c0_changed_cycles": 0,
|
||||
"max_abs_c0_change_m": 0.0,
|
||||
"offset_overflow_seconds": 6.548916987999917,
|
||||
"max_abs_offset_overflow_target_m": 1.233181856572628,
|
||||
"request_release_cycles": 288,
|
||||
"request_release_seconds": 2.98796386799998,
|
||||
"max_abs_request_release_rad": 0.006280367394214892,
|
||||
"unwind_release_cycles": 4,
|
||||
"max_abs_unwind_release_rad": 0.01457856219656457,
|
||||
"feedback_enabled_seconds": 438.3550780740002,
|
||||
"pscm_limit_2_seconds": 8.604224759000118,
|
||||
"c1_changed_cycles": 2849,
|
||||
"max_abs_c1_change_rad": 0.015000000000000013,
|
||||
"max_abs_correction_rad": 0.2921633626620207,
|
||||
"can_round_trips": 61027,
|
||||
"source_sha256": {
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route114/route.npz": "83524f07d61104b84b004ddc46bb751ca7f61da67798aa47db56d307765f5b3e",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route114/model_paths.npz": "14d14362d1a3e46398edd8e22b7cc4e36277a7596a73f546192d0e14c6642b07",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route114/metadata.json": "ec677b9275c1707e477ebd0ffa235d49b5ec63a1ee717b16040c3acdbcd3bdc0",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/feedback_replay.py": "e869520839aef948ccbf20b44e3efb6ec854a539ec0d66c07779825bcd8037a7",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/openpilot/selfdrive/controls/lib/ford_model_action.py": "2ceb4cd8717bb3325f9b22189c78605ad5d42a1061dec9ca2e4dbb90fd256d1e"
|
||||
},
|
||||
"timing_limit": "Controls publication time proxies the computation clock; full SubMaster checks are unavailable.",
|
||||
"reference_limit": "Uses exact consumed model publication as reference; selected maneuver-plan messages are not reconstructed.",
|
||||
"report_sha256": "9a9d7a6c92ec2cc1e19dc6b6e628e402cbe3f6344e09ccbee1c5c76deedebc7e",
|
||||
"commands_sha256": "fc1a5249cc9a15e3369f177a88781377c327e27b681e1028ad26063cb70167a5"
|
||||
},
|
||||
"115": {
|
||||
"baseline_revision": "6df5eabb7e7f6bc4e206644d5ca9069df820124e",
|
||||
"baseline_source_sha256": "1a4ce5f5f63b4d2f1f6e0537c9b2ca7c463ca44b427349d71d28fb8a1138b00f",
|
||||
"calibration_approved": false,
|
||||
"cycles": 40037,
|
||||
"active_cycles": 33976,
|
||||
"validity_matches_baseline_exactly": true,
|
||||
"status_counts": {
|
||||
"inactive": 6061,
|
||||
"active": 33976
|
||||
},
|
||||
"c0_matches_baseline_exactly": true,
|
||||
"c0_changed_cycles": 0,
|
||||
"max_abs_c0_change_m": 0.0,
|
||||
"offset_overflow_seconds": 2.095635206999873,
|
||||
"max_abs_offset_overflow_target_m": 0.6360401958227158,
|
||||
"request_release_cycles": 181,
|
||||
"request_release_seconds": 1.7931359259980582,
|
||||
"max_abs_request_release_rad": 0.005393094571379859,
|
||||
"unwind_release_cycles": 1,
|
||||
"max_abs_unwind_release_rad": 0.11402585053291069,
|
||||
"unwind_releases": [
|
||||
{
|
||||
"time_s": 133.203500567,
|
||||
"retired_rad": -0.11402585053291069,
|
||||
"correction_after_rad": 0.0,
|
||||
"base_c1_rad": -0.0025691102200653404,
|
||||
"desired_angle_deg": 0.46853354573249817,
|
||||
"actual_angle_deg": 0.5,
|
||||
"baseline_c0_c1": [
|
||||
-0.04999999999999982,
|
||||
-0.11650000000000005
|
||||
],
|
||||
"candidate_c0_c1": [
|
||||
-0.04999999999999982,
|
||||
-0.11099999999999999
|
||||
]
|
||||
}
|
||||
],
|
||||
"feedback_enabled_seconds": 306.2414766659987,
|
||||
"pscm_limit_2_seconds": 6.4724571650002645,
|
||||
"c1_changed_cycles": 384,
|
||||
"max_abs_c1_change_rad": 0.1140000000000001,
|
||||
"max_abs_correction_rad": 0.13572457044904282,
|
||||
"can_round_trips": 40037,
|
||||
"source_sha256": {
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route115/route.npz": "e0df32d9e80f1c6b7d56327b37cf07af60070ffc212b8d111e343306148bff23",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route115/model_paths.npz": "52f3ed9e188e4947618a57a3ed872fd1966a887fe1014999df741553b6c13bc0",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route115/metadata.json": "d3c73035bad8eb5059e07962fd274c19cb70c8952b6f1514835d312d08b87a16",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/feedback_replay.py": "e869520839aef948ccbf20b44e3efb6ec854a539ec0d66c07779825bcd8037a7",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/openpilot/selfdrive/controls/lib/ford_model_action.py": "2ceb4cd8717bb3325f9b22189c78605ad5d42a1061dec9ca2e4dbb90fd256d1e"
|
||||
},
|
||||
"timing_limit": "Controls publication time proxies the computation clock; full SubMaster checks are unavailable.",
|
||||
"reference_limit": "Uses exact consumed model publication as reference; selected maneuver-plan messages are not reconstructed.",
|
||||
"report_sha256": "e3e4183e14ce20b2d3b0938028a5a4ab3200f72dd46e898174751aa94912ad84",
|
||||
"commands_sha256": "ba9b6411a432430bb1380f63fe011ba91235b393f3785c17d2904657c95d27d9"
|
||||
},
|
||||
"112": {
|
||||
"baseline_revision": "6df5eabb7e7f6bc4e206644d5ca9069df820124e",
|
||||
"baseline_source_sha256": "1a4ce5f5f63b4d2f1f6e0537c9b2ca7c463ca44b427349d71d28fb8a1138b00f",
|
||||
"calibration_approved": false,
|
||||
"cycles": 108971,
|
||||
"active_cycles": 91414,
|
||||
"validity_matches_baseline_exactly": true,
|
||||
"status_counts": {
|
||||
"inactive": 17557,
|
||||
"active": 91414
|
||||
},
|
||||
"c0_matches_baseline_exactly": true,
|
||||
"c0_changed_cycles": 0,
|
||||
"max_abs_c0_change_m": 0.0,
|
||||
"offset_overflow_seconds": 1.43945091800002,
|
||||
"max_abs_offset_overflow_target_m": 0.5727187991142273,
|
||||
"request_release_cycles": 381,
|
||||
"request_release_seconds": 3.8611647240012985,
|
||||
"max_abs_request_release_rad": 0.008723706007003784,
|
||||
"unwind_release_cycles": 14,
|
||||
"max_abs_unwind_release_rad": 0.07267236868778636,
|
||||
"feedback_enabled_seconds": 840.7664582650004,
|
||||
"pscm_limit_2_seconds": 14.441855805999936,
|
||||
"c1_changed_cycles": 30036,
|
||||
"max_abs_c1_change_rad": 0.07200000000000006,
|
||||
"max_abs_correction_rad": 0.205313389369823,
|
||||
"can_round_trips": 108971,
|
||||
"source_sha256": {
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route112/route.npz": "2b08a2fb636f7d14556d7df4035eafc1d1b97932237955528562a16db2b31d3e",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route112/model_paths.npz": "9837afe78aab4cad288cad98a595a5777fa8a66bb235986b1272a7f7c54e559a",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route112/metadata.json": "726d78a7e7aa45307dcfe27cb00775c20ecc9d54538eb7f26a0166fc216226ce",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/feedback_replay.py": "e869520839aef948ccbf20b44e3efb6ec854a539ec0d66c07779825bcd8037a7",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/openpilot/selfdrive/controls/lib/ford_model_action.py": "2ceb4cd8717bb3325f9b22189c78605ad5d42a1061dec9ca2e4dbb90fd256d1e"
|
||||
},
|
||||
"timing_limit": "Controls publication time proxies the computation clock; full SubMaster checks are unavailable.",
|
||||
"reference_limit": "Uses exact consumed model publication as reference; selected maneuver-plan messages are not reconstructed.",
|
||||
"report_sha256": "aaa1d819cf344de668ea92eb58714a6ae537054a0feadc05a0fa15097a57c553",
|
||||
"commands_sha256": "9f191699d19af8f123e9245a77827a2eca27d4b3191f9acddf9389edbda0ef24"
|
||||
},
|
||||
"113": {
|
||||
"baseline_revision": "6df5eabb7e7f6bc4e206644d5ca9069df820124e",
|
||||
"baseline_source_sha256": "1a4ce5f5f63b4d2f1f6e0537c9b2ca7c463ca44b427349d71d28fb8a1138b00f",
|
||||
"calibration_approved": false,
|
||||
"cycles": 49614,
|
||||
"active_cycles": 27207,
|
||||
"validity_matches_baseline_exactly": true,
|
||||
"status_counts": {
|
||||
"inactive": 22407,
|
||||
"active": 27207
|
||||
},
|
||||
"c0_matches_baseline_exactly": true,
|
||||
"c0_changed_cycles": 0,
|
||||
"max_abs_c0_change_m": 0.0,
|
||||
"offset_overflow_seconds": 2.7607263189997866,
|
||||
"max_abs_offset_overflow_target_m": 0.6065444126725197,
|
||||
"request_release_cycles": 119,
|
||||
"request_release_seconds": 1.2281319819985583,
|
||||
"max_abs_request_release_rad": 0.009946223348379135,
|
||||
"unwind_release_cycles": 1,
|
||||
"max_abs_unwind_release_rad": 0.005007614799767142,
|
||||
"feedback_enabled_seconds": 243.3033761190004,
|
||||
"pscm_limit_2_seconds": 14.003248144999816,
|
||||
"c1_changed_cycles": 951,
|
||||
"max_abs_c1_change_rad": 0.00500000000000006,
|
||||
"max_abs_correction_rad": 0.16966817302181283,
|
||||
"can_round_trips": 49614,
|
||||
"source_sha256": {
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route113/route.npz": "774ca4a21b7113c2706d6130bc180c3216ea4833155300ab01e75b3486e36327",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route113/model_paths.npz": "93c41761eb85263f534f5371b905482cf7c948582eb1e9149966594be1d3768f",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_route113/metadata.json": "1c0ca74dd48b90ab9d5444c5ca7f8aa9361700bbdf98bd5e853be50ad2895d7f",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/feedback_replay.py": "e869520839aef948ccbf20b44e3efb6ec854a539ec0d66c07779825bcd8037a7",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/openpilot/selfdrive/controls/lib/ford_model_action.py": "2ceb4cd8717bb3325f9b22189c78605ad5d42a1061dec9ca2e4dbb90fd256d1e"
|
||||
},
|
||||
"timing_limit": "Controls publication time proxies the computation clock; full SubMaster checks are unavailable.",
|
||||
"reference_limit": "Uses exact consumed model publication as reference; selected maneuver-plan messages are not reconstructed.",
|
||||
"report_sha256": "032aaf4b2dd3a54264330c9c6367ad4a63f49d779ce9acd4c3c1d355d731d545",
|
||||
"commands_sha256": "c1d4e59718400d21d724d502e2befa315db56802a7622d3d68e8b3af7daea319"
|
||||
},
|
||||
"b9": {
|
||||
"baseline_revision": "6df5eabb7e7f6bc4e206644d5ca9069df820124e",
|
||||
"baseline_source_sha256": "1a4ce5f5f63b4d2f1f6e0537c9b2ca7c463ca44b427349d71d28fb8a1138b00f",
|
||||
"calibration_approved": false,
|
||||
"cycles": 90774,
|
||||
"active_cycles": 86474,
|
||||
"validity_matches_baseline_exactly": true,
|
||||
"status_counts": {
|
||||
"inactive": 4300,
|
||||
"active": 86474
|
||||
},
|
||||
"c0_matches_baseline_exactly": true,
|
||||
"c0_changed_cycles": 0,
|
||||
"max_abs_c0_change_m": 0.0,
|
||||
"offset_overflow_seconds": 5.703841178999909,
|
||||
"max_abs_offset_overflow_target_m": 4.280821338295937,
|
||||
"request_release_cycles": 416,
|
||||
"request_release_seconds": 4.241454379000558,
|
||||
"max_abs_request_release_rad": 0.017186015844345093,
|
||||
"unwind_release_cycles": 16,
|
||||
"max_abs_unwind_release_rad": 0.15648483206475247,
|
||||
"feedback_enabled_seconds": 816.0284774219999,
|
||||
"pscm_limit_2_seconds": 9.308613716000167,
|
||||
"c1_changed_cycles": 7013,
|
||||
"max_abs_c1_change_rad": 0.14349999999999996,
|
||||
"max_abs_correction_rad": 0.18457476562660308,
|
||||
"can_round_trips": 90774,
|
||||
"source_sha256": {
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_routeb9/route.npz": "b07c789d8155335f5d120d0262fced6e4d5803fe767b0ff49b6413dce4140b5c",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_routeb9/model_paths.npz": "6b1f87897c050273fdc05af051307a049b6fc3a93072e7cda1721195ce7c3861",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/.cache/ford_routeb9/metadata.json": "9ce452220cab61b81883f32fc2fcaf5db6c78a674cb255a49cc77d5029580fee",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/tools/ford_pscm_lab/feedback_replay.py": "e869520839aef948ccbf20b44e3efb6ec854a539ec0d66c07779825bcd8037a7",
|
||||
"/Users/ibpersonal/.codex/worktrees/1a1c/sunnypilot/openpilot/selfdrive/controls/lib/ford_model_action.py": "2ceb4cd8717bb3325f9b22189c78605ad5d42a1061dec9ca2e4dbb90fd256d1e"
|
||||
},
|
||||
"timing_limit": "Controls publication time proxies the computation clock; full SubMaster checks are unavailable.",
|
||||
"reference_limit": "Uses exact consumed model publication as reference; selected maneuver-plan messages are not reconstructed.",
|
||||
"report_sha256": "d66308cefae9956d2b67b8d9ceb83804054e838c70e84999ba758ab1cbf21d5d",
|
||||
"commands_sha256": "406275e9c230a2975fd8c9d0824d3a902283d3a5cb597328026578db66c2cb6b"
|
||||
}
|
||||
},
|
||||
"checks": {
|
||||
"ruff": true,
|
||||
"controller_ty": true,
|
||||
"settings_compilation": true
|
||||
},
|
||||
"limitations": [
|
||||
"Recorded driver input and PSCM response remain fixed during replay.",
|
||||
"No device build, boot or physical test performed.",
|
||||
"Releases occur at smaller exits too; unchanged real-world centering is not established.",
|
||||
"Route 114 segment-2 overshoot does not trigger this release."
|
||||
],
|
||||
"float32_can_round_trips_excluding_integration_tests": 768561,
|
||||
"exit_points_left_positive": [
|
||||
{
|
||||
"time_s": 131.19208205699988,
|
||||
"baseline_c1_rad": -0.10949999999999999,
|
||||
"candidate_c1_rad": -0.10949999999999999,
|
||||
"baseline_correction_rad": 0.04102452737994574,
|
||||
"candidate_correction_rad": 0.04102452737994574
|
||||
},
|
||||
{
|
||||
"time_s": 133.19194825599993,
|
||||
"baseline_c1_rad": 0.11650000000000005,
|
||||
"candidate_c1_rad": 0.11650000000000005,
|
||||
"baseline_correction_rad": 0.11402585053291069,
|
||||
"candidate_correction_rad": 0.11402585053291069
|
||||
},
|
||||
{
|
||||
"time_s": 133.203500567,
|
||||
"baseline_c1_rad": 0.11650000000000005,
|
||||
"candidate_c1_rad": 0.11099999999999999,
|
||||
"baseline_correction_rad": 0.1140256728569634,
|
||||
"candidate_correction_rad": -0.0
|
||||
},
|
||||
{
|
||||
"time_s": 133.40571050699987,
|
||||
"baseline_c1_rad": 0.118,
|
||||
"candidate_c1_rad": 0.009500000000000064,
|
||||
"baseline_correction_rad": 0.11367557589137142,
|
||||
"candidate_correction_rad": -0.0
|
||||
},
|
||||
{
|
||||
"time_s": 133.59461515599992,
|
||||
"baseline_c1_rad": 0.124,
|
||||
"candidate_c1_rad": 0.010500000000000065,
|
||||
"baseline_correction_rad": 0.1120761218192909,
|
||||
"candidate_correction_rad": -0.0015653052344988395
|
||||
},
|
||||
{
|
||||
"time_s": 207.90007524899988,
|
||||
"baseline_c1_rad": 0.16449999999999998,
|
||||
"candidate_c1_rad": 0.16449999999999998,
|
||||
"baseline_correction_rad": 0.016763380618580685,
|
||||
"candidate_correction_rad": 0.016763380618580685
|
||||
},
|
||||
{
|
||||
"time_s": 338.168560822,
|
||||
"baseline_c1_rad": -0.173,
|
||||
"candidate_c1_rad": -0.173,
|
||||
"baseline_correction_rad": -0.024831306563109386,
|
||||
"candidate_correction_rad": -0.024831306563109386
|
||||
}
|
||||
],
|
||||
"identical_route115_command_windows_s": [
|
||||
[
|
||||
129.0,
|
||||
133.2
|
||||
],
|
||||
[
|
||||
200.0,
|
||||
214.0
|
||||
],
|
||||
[
|
||||
333.0,
|
||||
345.0
|
||||
]
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
# Ford toggle-off upstream restoration
|
||||
|
||||
`FordModelActionController` is the only setting that can select custom Ford
|
||||
steering. It defaults false. With it false or absent, no custom path controller
|
||||
is created and normal lateral-control curvature passes unchanged to the Ford
|
||||
sender. A stored `FordPscmObserver` or retired virtual-angle setting cannot
|
||||
override that choice. The observer toggle is removed from Sunnylink; its stored
|
||||
parameter remains readable for compatibility but has no selection effect.
|
||||
|
||||
Selection remains fixed for the lifetime of controlsd. Sunnylink changes require
|
||||
a real offroad-to-onroad cycle, as before. The startup diagnostic reports
|
||||
`controller=upstream` when the experiment is not selected.
|
||||
|
||||
## Sender behavior
|
||||
|
||||
The new `fordLateralPath.enabled` field conveys startup selection independently
|
||||
of `valid`. Its default is false. The sender uses custom mode only when this
|
||||
field is true on a Ford CAN FD vehicle. This prevents invalid model geometry
|
||||
or disengagement in the selected experiment from choosing a different controller.
|
||||
|
||||
Toggle-off restores the upstream Ford sender:
|
||||
|
||||
- 20 Hz steering messages on both CAN FD and legacy Ford.
|
||||
- CAN FD limited mode 1 while active, mode 0 while inactive, with upstream ramp
|
||||
type 0, counters and checksums.
|
||||
- C0, C1 and C3 zero; C2 follows upstream actuator curvature.
|
||||
- Upstream curvature amplitude/rate limits and the measured-curvature error
|
||||
clamp above 9 m/s.
|
||||
- Upstream anti-overshoot handling for Bronco Sport and F-150 MK14.
|
||||
|
||||
The reference is the upstream implementation already merged into this branch,
|
||||
opendbc `f95f996f5917dcbbf2e32fe51b606a24cf836af6`. Its Ford sender differs from
|
||||
the locally available comma opendbc `3e92d112129507debe45364891954db70238997a`
|
||||
only in sunnypilot's additional `CP_SP`/`CC_SP` interface arguments. This change
|
||||
restores that implementation; it does not upgrade unrelated upstream code.
|
||||
|
||||
Toggle-on retains the previous custom 100 Hz sender, mode 2, ramp type 3 and
|
||||
existing path limits. The model-action controller's command law and diagnostic
|
||||
identity `model-action-c1-feedback-v3` are unchanged. Legacy Ford always uses
|
||||
upstream control. The opendbc dependency is now
|
||||
`64aa61b9b3fd26e70a7caa915acab207ff3cd64a`. No Panda safety code is changed;
|
||||
its existing limited-mode checks already use 20 Hz curvature limits.
|
||||
|
||||
## Validation
|
||||
|
||||
- Combined Ford, Sunnylink, parameter, logging, replay-tool and Ford safety
|
||||
suite: **683 passed, 178 existing skips, 9,145 subtests passed**.
|
||||
- Real startup → controlsd → Float32 publication → conversion → Ford sender:
|
||||
14 new toggle-off cases, covering all six CAN FD platforms plus legacy
|
||||
Escape, with both stored observer settings. They preserve the upstream
|
||||
actuator output, including when custom model geometry is missing, and verify
|
||||
20 Hz cadence, engage/disengage/reengage, zero path terms, mode, ramp,
|
||||
counters and checksums across 4,200 control cycles / 840 steering messages.
|
||||
- The existing toggle-on, stale-input, invalid-input and 100 Hz integration
|
||||
regressions continue to pass.
|
||||
- Additional Ford interface fuzz checks: **11 passed**, 60 generated examples
|
||||
each, with real Cap'n Proto conversion and car-interface application. The
|
||||
initially missing neural-network-data dependency was initialized at the
|
||||
repository's existing pin `03cac2d30e111e0689c0429cb8c1fe6cb5a905af`.
|
||||
- Packet equivalence: **55,000 toggle-off cycles across all 11 Ford platforms**
|
||||
match the pinned upstream sender exactly. **30,000 toggle-on cycles across
|
||||
six CAN FD platforms** match the previous custom sender exactly. All 90,305
|
||||
outgoing packets and returned actuator values match, including invalid paths,
|
||||
inactive periods, both turn directions and speed boundaries. Disabled
|
||||
selection also ignores deliberately nonzero, valid custom path fields.
|
||||
- Ruff, controller type check, generated Sunnylink schema check and diff
|
||||
whitespace checks pass.
|
||||
|
||||
The packet comparison loads the exact old controller **and its old CAN builder**
|
||||
from trusted local Git sources. It does not compare two aliases of the modified
|
||||
code. Results and source hashes are in `ford_upstream_fallback_validation.json`.
|
||||
No device build, boot or physical steering validation is claimed.
|
||||
|
||||
## Reproduction
|
||||
|
||||
Use the pinned opendbc dependency and native project dependencies:
|
||||
|
||||
```sh
|
||||
export PYTHONDONTWRITEBYTECODE=1
|
||||
export PYTHONPATH=.:opendbc_repo
|
||||
python -m pytest -q -p no:cacheprovider openpilot/selfdrive/controls/tests/test_ford_*.py tools/ford_pscm_lab openpilot/selfdrive/car/tests/test_ford_pscm_status.py openpilot/sunnypilot/sunnylink/tests openpilot/common/tests/test_params.py opendbc_repo/opendbc/car/ford/tests/test_ford.py opendbc_repo/opendbc/safety/tests/test_ford.py
|
||||
FUZZ_SEED=20260911 python -m pytest -q -p no:cacheprovider openpilot/selfdrive/car/tests/test_car_interfaces.py -k FORD
|
||||
python -m tools.ford_pscm_lab.upstream_fallback_check --cycles 5000 --output .cache/ford_upstream_fallback/equivalence.json
|
||||
python openpilot/sunnypilot/sunnylink/tools/compile_settings_ui.py --check
|
||||
```
|
||||
|
||||
The comparison requires both pinned baseline commits in the local opendbc Git
|
||||
object store. The previous overflow/replay records describe their historical
|
||||
source hashes; the comparison here establishes unchanged toggle-on sender output.
|
||||
@@ -0,0 +1,190 @@
|
||||
{
|
||||
"created_at_utc": "2026-09-11T16:11:23.341760+00:00",
|
||||
"scope": "Default-off upstream Ford control, including CAN sender; exact software comparison only.",
|
||||
"parent_commit": "b81c00f5b9c3658d72675ec3ee0ac07e0ef14807",
|
||||
"opendbc_commit": "64aa61b9b3fd26e70a7caa915acab207ff3cd64a",
|
||||
"target": {
|
||||
"repository": "sunnypilot/sunnypilot",
|
||||
"branch": "hiimisaac-dev"
|
||||
},
|
||||
"selection": {
|
||||
"param": "FordModelActionController",
|
||||
"default": false,
|
||||
"off": "upstream",
|
||||
"on": "Ford CAN FD model-action v3",
|
||||
"activation": "Existing controlsd startup; real offroad-to-onroad cycle",
|
||||
"observer_setting": "Ignored for control selection; no longer exposed in Sunnylink",
|
||||
"sender_field": "fordLateralPath.enabled defaults false, independent of valid"
|
||||
},
|
||||
"custom_command_law_ast_matches_parent": [
|
||||
"_packed",
|
||||
"_finite",
|
||||
"encode_model_action",
|
||||
"ModelActionController",
|
||||
"FordModelActionController"
|
||||
],
|
||||
"tests": {
|
||||
"combined": "683 passed, 178 skipped, 9145 subtests passed in 9.61s",
|
||||
"additional_ford_interface_fuzz": "11 passed, 258 non-Ford deselected in 8.15s; 60 examples per Ford platform",
|
||||
"fuzz_seed": 20260911,
|
||||
"dependency_setup": "Initialized existing neural-network-data pin 03cac2d30e111e0689c0429cb8c1fe6cb5a905af after missing-model-data failure.",
|
||||
"new_toggle_off_integration_cases": 14,
|
||||
"new_integration_cycles": 4200,
|
||||
"new_steering_frames": 840,
|
||||
"ruff_changed_python": "pass",
|
||||
"ty_controller": "pass",
|
||||
"settings_compiler_check": "pass",
|
||||
"diff_check": "pass"
|
||||
},
|
||||
"packet_equivalence": {
|
||||
"scope": "Compare all outgoing Ford packets with pinned upstream and custom senders.\n\nUses trusted local Git sources, identical synthetic inputs, and the actual CAN\npackers. Establishes software equivalence, not physical steering performance.\n",
|
||||
"seed": 20260911,
|
||||
"upstream_revision": "f95f996f5917dcbbf2e32fe51b606a24cf836af6",
|
||||
"previous_custom_revision": "c21a9013700734dd20b09e05aa68329ad8cc20f9",
|
||||
"upstream_source_sha256": {
|
||||
"opendbc/car/ford/fordcan.py": "8b3c74bff68146cf9f97d17203b7deebb9254561d9591a4a92d978bf01808a75",
|
||||
"opendbc/car/ford/carcontroller.py": "c7e590c13cfe2434d77d6224359d659bb5092a3fdd65b12c7d8d9eddfb3deada"
|
||||
},
|
||||
"previous_custom_source_sha256": {
|
||||
"opendbc/car/ford/fordcan.py": "5b73c568149bde299f71f92f034f3032a94ecae4ee4bae8af938f34ef9590062",
|
||||
"opendbc/car/ford/carcontroller.py": "b2d327a1833fb1f0d09ee17f54c9c8d45517fa29beb04a4543cfbf1b43f1a65e"
|
||||
},
|
||||
"results": [
|
||||
{
|
||||
"fingerprint": "FORD_BRONCO_SPORT_MK1",
|
||||
"custom_enabled": false,
|
||||
"cycles": 5000,
|
||||
"identical_packets": 5665
|
||||
},
|
||||
{
|
||||
"fingerprint": "FORD_ESCAPE_MK4",
|
||||
"custom_enabled": false,
|
||||
"cycles": 5000,
|
||||
"identical_packets": 5665
|
||||
},
|
||||
{
|
||||
"fingerprint": "FORD_ESCAPE_MK4_5",
|
||||
"custom_enabled": false,
|
||||
"cycles": 5000,
|
||||
"identical_packets": 3165
|
||||
},
|
||||
{
|
||||
"fingerprint": "FORD_ESCAPE_MK4_5",
|
||||
"custom_enabled": true,
|
||||
"cycles": 5000,
|
||||
"identical_packets": 7165
|
||||
},
|
||||
{
|
||||
"fingerprint": "FORD_EXPLORER_MK6",
|
||||
"custom_enabled": false,
|
||||
"cycles": 5000,
|
||||
"identical_packets": 5665
|
||||
},
|
||||
{
|
||||
"fingerprint": "FORD_EXPEDITION_MK4",
|
||||
"custom_enabled": false,
|
||||
"cycles": 5000,
|
||||
"identical_packets": 3165
|
||||
},
|
||||
{
|
||||
"fingerprint": "FORD_EXPEDITION_MK4",
|
||||
"custom_enabled": true,
|
||||
"cycles": 5000,
|
||||
"identical_packets": 7165
|
||||
},
|
||||
{
|
||||
"fingerprint": "FORD_F_150_MK14",
|
||||
"custom_enabled": false,
|
||||
"cycles": 5000,
|
||||
"identical_packets": 3165
|
||||
},
|
||||
{
|
||||
"fingerprint": "FORD_F_150_MK14",
|
||||
"custom_enabled": true,
|
||||
"cycles": 5000,
|
||||
"identical_packets": 7165
|
||||
},
|
||||
{
|
||||
"fingerprint": "FORD_F_150_LIGHTNING_MK1",
|
||||
"custom_enabled": false,
|
||||
"cycles": 5000,
|
||||
"identical_packets": 3165
|
||||
},
|
||||
{
|
||||
"fingerprint": "FORD_F_150_LIGHTNING_MK1",
|
||||
"custom_enabled": true,
|
||||
"cycles": 5000,
|
||||
"identical_packets": 7165
|
||||
},
|
||||
{
|
||||
"fingerprint": "FORD_FOCUS_MK4",
|
||||
"custom_enabled": false,
|
||||
"cycles": 5000,
|
||||
"identical_packets": 5665
|
||||
},
|
||||
{
|
||||
"fingerprint": "FORD_MAVERICK_MK1",
|
||||
"custom_enabled": false,
|
||||
"cycles": 5000,
|
||||
"identical_packets": 5665
|
||||
},
|
||||
{
|
||||
"fingerprint": "FORD_MUSTANG_MACH_E_MK1",
|
||||
"custom_enabled": false,
|
||||
"cycles": 5000,
|
||||
"identical_packets": 3165
|
||||
},
|
||||
{
|
||||
"fingerprint": "FORD_MUSTANG_MACH_E_MK1",
|
||||
"custom_enabled": true,
|
||||
"cycles": 5000,
|
||||
"identical_packets": 7165
|
||||
},
|
||||
{
|
||||
"fingerprint": "FORD_RANGER_MK2",
|
||||
"custom_enabled": false,
|
||||
"cycles": 5000,
|
||||
"identical_packets": 3165
|
||||
},
|
||||
{
|
||||
"fingerprint": "FORD_RANGER_MK2",
|
||||
"custom_enabled": true,
|
||||
"cycles": 5000,
|
||||
"identical_packets": 7165
|
||||
}
|
||||
],
|
||||
"total_cycles": 85000,
|
||||
"total_identical_packets": 90305,
|
||||
"candidate_source_sha256": {
|
||||
"opendbc/car/ford/carcontroller.py": "6d33f288de87e3baa69e9161b1b85367dc1d8542c06c3d3a9ce2d1f2347dbd30",
|
||||
"opendbc/car/ford/fordcan.py": "0e241f19f152df897b294d4562bfd729dbfcbc56bcc9770379f76922f2864cb8",
|
||||
"opendbc/car/structs.py": "82ecc4de1e5fda486d68fcf67903098dd083a56b78e65866744f42d5fb97b385"
|
||||
},
|
||||
"checker_sha256": "1d33a07cc5e6188c6d1b5de2a2a603efaee691d25d91b7bcd843ab4909753ae1"
|
||||
},
|
||||
"source_sha256": {
|
||||
"openpilot/selfdrive/controls/lib/ford_model_action.py": "167ae5a01fdd7ea014e6ad3fe9d0b6e31c67de8ba057ec5ecf18ab38fc16353f",
|
||||
"openpilot/selfdrive/controls/controlsd.py": "c9b68431d212178ae2177b16ddba4cf023ece84c7c0ea8a1db02a2527dc27aba",
|
||||
"openpilot/cereal/custom.capnp": "c877eac4a77ea4cb42447edcf38da4baf20708e8993124852234008e0a084664",
|
||||
"openpilot/sunnypilot/selfdrive/controls/controlsd_ext.py": "45bfaafa9a96d3ccbb56f34ec0b9a71abb7cd7f01e7e80620796d13c222e4720",
|
||||
"openpilot/selfdrive/controls/tests/test_ford_model_action_selection.py": "3dc4f7236937358aad09b544577a796e47c15dbc96607739ff0874b900d55089",
|
||||
"openpilot/selfdrive/controls/tests/test_ford_model_action_adapter.py": "073b16ffa611d654b954711f9e4f24df95477dfc0c09e75eff89d02eb29d7f2a",
|
||||
"openpilot/selfdrive/controls/tests/test_ford_controlsd_logging.py": "5b082f3c1f6dc596a40a2011c71928debeb04fa70af36841f6f9a237a9ca439e",
|
||||
"openpilot/sunnypilot/sunnylink/settings_ui_src/pages/vehicle.yaml": "e37a662618b6ccd2620ac355b4cc40a2253707bb4ba1d5d4631fdc89fd01a800",
|
||||
"openpilot/sunnypilot/sunnylink/settings_ui.json": "36ac7f6177de2679d35c5f7f77234336e17a8632d31b195f40aec6014efb8577",
|
||||
"openpilot/sunnypilot/sunnylink/tests/test_settings_schema.py": "53a3f1f807c638661c8ef60b5dc5c28ecf5604a9d35b610b8e4a5f5d1d99eedb",
|
||||
"tools/ford_pscm_lab/feedback_replay.py": "860aff9fd00d26b2bd7c2b627286918d3b52c25cc31b0b0768a51fc55b0df37e"
|
||||
},
|
||||
"validation_environment": {
|
||||
"python": "/Users/ibpersonal/dev/sunnypilot/.venv/bin/python",
|
||||
"PYTHONPATH": ".:opendbc_repo:.cache/ford_v6/test_deps",
|
||||
"PYTHONDONTWRITEBYTECODE": "1",
|
||||
"LOG_ROOT": "/private/tmp/ford-upstream-logs",
|
||||
"PARAMS_ROOT": "/private/tmp/ford-upstream-params"
|
||||
},
|
||||
"panda_safety_changed": false,
|
||||
"limitations": [
|
||||
"No full device build, boot, installation or physical steering validation.",
|
||||
"Upstream means the pinned upstream implementation merged into this branch, not an upgrade to unrelated latest source."
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,327 @@
|
||||
# Ford C2-free model-pose tracking with measured feedback
|
||||
|
||||
This experiment is retired. Its implementation, setting and dedicated tests
|
||||
were removed from the selected-action drive-test branch. For current setup,
|
||||
see [Ford selected-action drive testing](ford_model_action_drive_test.md).
|
||||
The material below is historical; it does not describe an available toggle.
|
||||
|
||||
Hypothesis `model-pose-c0-c1-feedback-v8` retains the model-pose C0/C1 base
|
||||
and adds two guarded release policies. When measured turning exceeds both
|
||||
current and delayed requests, a separate output guard prevents same-direction
|
||||
C0/C1 growth, including while feedback history rebuilds after driver input.
|
||||
When turning instead falls below both requests and is no longer increasing,
|
||||
bounded C1 tracking can use remaining release-entry command headroom.
|
||||
Existing opposing-bias recovery still stops at zero bias. Geometry, blending,
|
||||
feedback gain, slew rates and field limits are unchanged; C2/C3 remain zero.
|
||||
|
||||
This is an experimental outer controller around the multivariable PSCM.
|
||||
Its geometry does not define a calibrated C0/C1-to-wheel mapping or an angle
|
||||
servo. V8 has offline validation only. Command replay cannot establish the
|
||||
truck's response, closed-loop stability, or an overshoot improvement.
|
||||
|
||||
## Evidence and scope
|
||||
|
||||
Route80 ran v3 and contains both sustained under-response and over-response.
|
||||
Representative eligible windows had median CAN response/request ratios of
|
||||
0.78, 1.77 and 0.69 with a declared 0.2-second comparison interval. These
|
||||
are descriptive tracking ratios, not identified controller gains.
|
||||
|
||||
V4 replaced separate model-heading C1 with selected-curvature C1 and reduced
|
||||
heading demand in several large maneuvers. The user subsequently reported
|
||||
weak turning and steering repeatedly stopping near 85 degrees. Older logs
|
||||
contain larger wheel angles; the inspected host code has no fixed 85-degree
|
||||
wheel stop, although upstream curvature limits depend on speed.
|
||||
|
||||
Route83 had the Sunnylink toggle on, but omitted EPS firmware responses.
|
||||
The former firmware gate selected the default `FordPathController`; replay
|
||||
reproduced its recorded C0/C1/C2 requests. Its favorable turns are evidence
|
||||
for the existing model-pose construction, not validation of v5 or v6.
|
||||
V6 reuses that construction while replacing its remaining C2 request with
|
||||
C0/C1 geometry. Removing C2 changes the request received by the PSCM, so
|
||||
matching large C0/C1 commands does not guarantee matching vehicle motion.
|
||||
|
||||
Route8a ran v6 and was reported as the best drive. Route8e ran v7 throughout
|
||||
with the experiment enabled; it includes entry lag and excessive turning
|
||||
while requests release. Fixed-input v6/v7 replay produced identical commands
|
||||
in the main reversal and over-response examples, so the v7 recovery change
|
||||
does not directly explain their command behavior. In the over-response
|
||||
example, model C0/C1 grew while selected curvature fell and driver resets
|
||||
repeatedly removed feedback history. Another exit remained deficient after
|
||||
opposing bias reached zero. These observations motivate the v8 guards; they
|
||||
do not isolate an EPS transfer function or demonstrate the proposed response.
|
||||
|
||||
## Base request
|
||||
|
||||
controlsd selects valid `lateralManeuverPlan.desiredCurvature`, otherwise
|
||||
`modelV2.action.desiredCurvature`, after the existing curvature limiter.
|
||||
This action already includes upstream delay handling; it receives no extra
|
||||
response advance here.
|
||||
|
||||
The model contribution uses the existing allocator's raw forward pose and
|
||||
bounded short-pose correction. `_model_pose` advances 0.1 seconds, retains
|
||||
the model's remaining forward geometry, and separately corrects the short
|
||||
pose using measured curvature and its recent change. Its offset preview is
|
||||
up to 7 m and its heading preview is up to max(7 m, speed × 1 s), bounded by
|
||||
available path length. This raw pose is not passed through a second model
|
||||
filter. The filtered, ego-aligned reference remains available for comparison
|
||||
and the existing geometry-validity checks.
|
||||
|
||||
```text
|
||||
share(k) = clip((k - 0.006/m) / (0.012/m - 0.006/m), 0, 1)
|
||||
aligned = desired_curvature × model_forward_heading > 0
|
||||
model_share = min(share(abs(desired_curvature)), share(model_curvature_demand))
|
||||
if aligned, otherwise 0
|
||||
model_pair = existing_pose_encoder(model_pose, model_share, C2=0)
|
||||
|
||||
remaining_curvature = desired_curvature × (1 - model_share)
|
||||
L0 = max(8 m, speed × 1 s)
|
||||
L1 = max(7 m, speed × 1 s)
|
||||
curvature_C0 = 0.5 × remaining_curvature × L0²
|
||||
curvature_C1 = remaining_curvature × L1
|
||||
C0_base = clip(model_pair.C0 + curvature_C0, ±5.11 m)
|
||||
C1_base = clip(model_pair.C1 + curvature_C1, ±0.5 rad)
|
||||
```
|
||||
|
||||
`model_curvature_demand` is the larger absolute curvature implied by the
|
||||
forward offset and heading previews. The share uses the existing allocator's
|
||||
0.006–0.012/m thresholds. Both model and action must request a substantial
|
||||
turn in the same direction before model pose supplies the full base.
|
||||
Small, flat, opposed or zero requests use the curvature contribution; zero
|
||||
action produces a zero base. Partial shares combine both contributions.
|
||||
The existing pose encoder retains its quantization and field-allocation rules.
|
||||
The residual-curvature lift is geometric, not a claim of EPS equivalence to C2.
|
||||
|
||||
The inherited pose encoder allocates heading overflow using its asymmetric
|
||||
limits (+0.5235/−0.5 rad), before the symmetric final ±0.5 rad
|
||||
heading bound. On clipped tails, this can leave mirrored C0 requests differing
|
||||
by up to 0.0235 rad × 7 m = 0.1645 m. The favorable comparison anchors lie
|
||||
below that heading cap; full model-base odd symmetry is not claimed.
|
||||
|
||||
## Measured feedback and limits
|
||||
|
||||
```text
|
||||
past_request = selected curvature held at or before (measurement_time - delay)
|
||||
yaw_error = measured_speed × past_request - measured_yaw_rate
|
||||
bias_trial = released_bias + feedback_gain × yaw_error × measurement_dt
|
||||
C1_unconstrained = clip(C1_base + accepted_bias, ±0.5 rad)
|
||||
C1_target = temporary_backoff_ceiling(C1_unconstrained) if backoff_active
|
||||
otherwise C1_unconstrained
|
||||
```
|
||||
|
||||
Measured yaw is negated Ford CAN yaw, matching the control sign convention.
|
||||
The historical request uses zero-order hold; it never interpolates toward a
|
||||
future publication. Nominal comparison delay is `CP.steerActuatorDelay`
|
||||
(0.2 seconds on the source vehicle). Feedback compares against selected
|
||||
curvature, not curvature inferred from the model-pose coefficients.
|
||||
|
||||
| Quantity | Value |
|
||||
|---|---:|
|
||||
| C0 / C1 final bounds | ±5.11 m / ±0.5 rad |
|
||||
| Independent C0 / C1 slew | 4 m/s / 0.5 rad/s |
|
||||
| Feedback integration scale | 1.0 |
|
||||
| Feedback minimum speed | 2 m/s |
|
||||
| Maximum PSCM/core input age | 150 ms |
|
||||
| Allowed timestamp lead | 5 ms |
|
||||
| Release comparison tolerance | one C1 wire quantum, 0.0005 rad |
|
||||
|
||||
The integration scale, preview distances and blend thresholds are effective
|
||||
gains; none establishes stability. No wheel-response gain is fitted.
|
||||
Zero yaw error retains acquired bias while an eligible turn continues.
|
||||
Host anti-windup admits reachable correction within the combined C1 field
|
||||
and slew limits. Feedback overflow is not transferred into C0.
|
||||
|
||||
The release logic scales bias as the bounded base decreases and resets on
|
||||
zero/reversal. When delayed curvature still represents a stronger or opposing
|
||||
request, or PSCM reports LimitReached, new integration is normally frozen.
|
||||
One exception permits measured-error backoff: measured turning must exceed
|
||||
both the delayed and current selected yaw requests in the base's direction,
|
||||
and total heading must still have the base's sign. Exceeding only an older,
|
||||
smaller request during turn-in does not qualify. The accepted increment may
|
||||
only reduce that existing total toward zero; it cannot grow the request or
|
||||
carry it through zero. Existing host field and slew limits still apply.
|
||||
|
||||
The existing release-recovery exception requires fresh valid PSCM status with
|
||||
limit below 2, retained bias opposing the base, and both current and delayed
|
||||
requests aligned with that base. Measured turning must be below both requests
|
||||
in their direction. It then uses the current yaw deficit × the existing
|
||||
feedback gain × measurement interval to unwind only the opposing bias toward
|
||||
zero. The increment is clipped so recovery cannot cross zero bias or create
|
||||
demand beyond the existing base. Common host anti-windup still limits what
|
||||
can be accepted. A separate release-tracking exception is described below;
|
||||
other constrained cases remain frozen. PSCM limit 2 never permits either
|
||||
request-increasing exception.
|
||||
The no-new-bias restriction applies to `release_recovery`. It does not apply
|
||||
to the separate bounded `release_tracking` branch. Once release ends,
|
||||
ordinary eligible integration can add correction beyond the base as before;
|
||||
its existing limits and guards are unchanged.
|
||||
|
||||
`release_recovery` and `feedback_recovery_active=true` indicate that the
|
||||
recovery branch actually changed bias on that update. If host anti-windup
|
||||
blocks the entire increment, the status remains `host_limit` and the flag is
|
||||
false. Recovery is evaluated only on fresh measurements; the flag is false
|
||||
on repeated-measurement updates and after reset.
|
||||
|
||||
Diagnostics distinguish `release_backoff` and `pscm_backoff`; a release takes
|
||||
precedence when both conditions apply. While `feedback_backoff_active` is
|
||||
true, total C1 is also capped at the preceding continuous heading request in
|
||||
the current request direction and at zero in the opposite direction. This
|
||||
ceiling affects the output only: it is not stored or projected into bias.
|
||||
The measured-error increment can still update bias under the normal limits,
|
||||
but a changing model base does not create persistent integral suppression.
|
||||
The ceiling persists between repeated measurements; C1 cannot grow or reverse
|
||||
while it applies. The next fresh measurement clears it unless backoff is
|
||||
again warranted. It does not cap C0, and normal feedback has its own rules
|
||||
outside backoff. Independent slew remains 0.5 rad/s for C1 and 4 m/s for C0.
|
||||
Backoff still compares against the delayed reference, so response lag remains.
|
||||
Reducing a request does not demonstrate that physical overshoot is resolved.
|
||||
|
||||
## V8 release guard and tracking
|
||||
|
||||
`ReleaseGuard` retains selected-request history independently of feedback
|
||||
bias history. Driver-related feedback resets do not erase that reference,
|
||||
but the guard still requires current fresh valid PSCM status, no current
|
||||
driver override, and the existing input and speed eligibility. Invalid core
|
||||
input or disengagement resets its history with the controller.
|
||||
|
||||
During release, measured yaw must exceed both the current and delay-matched
|
||||
requests in the requested turn direction. Only then does the guard cap
|
||||
same-direction C0/C1 growth at each preceding continuous request. Terms
|
||||
already reducing the turn, including an opposing C0 centering offset, remain
|
||||
available. The guard follows base allocation and C1 feedback, so changing
|
||||
model geometry cannot bypass it. Its ceilings affect outputs, never stored
|
||||
bias. No scalar-curvature cap replaces strong model geometry during turn-in
|
||||
or undertracking. Existing independent slew and field limits still apply.
|
||||
|
||||
`release_tracking` addresses an eligible release deficit once bias is zero
|
||||
or already in the base's direction. Both current and delayed requests must
|
||||
align with that base, measured turning must be below both, and measured
|
||||
curvature must not be rising in the turn direction across the response
|
||||
interval by more than one C1 wire quantum after scaling by heading preview.
|
||||
Fresh valid PSCM status with limit below 2 is required. The current yaw deficit
|
||||
uses the existing integration gain and measurement interval;
|
||||
new C1 tracking increments are limited by command headroom captured at
|
||||
release entry, tapered with remaining desired curvature. The allowance is
|
||||
`max(0, entry_command_magnitude - abs(base)) × min(1, abs(desired) / entry_reference)`
|
||||
above the current base; any existing same-direction bias consumes it first.
|
||||
This limits new tracking integration, not the existing model base or bias.
|
||||
Only that additional allowance is tapered; strong model geometry remains
|
||||
available. A brief pause does not reacquire a higher entry
|
||||
ceiling; a full response interval without release ends the retained episode.
|
||||
Common host anti-windup, field and slew bounds still apply. Opposing bias
|
||||
continues through `release_recovery`, which stops at zero, before any separate
|
||||
tracking exception can be considered.
|
||||
|
||||
Neither exception relaxes the PSCM LimitReached growth restriction. The
|
||||
reference delay and finite response time remain; these output policies are
|
||||
command-construction changes, not evidence of improved physical tracking.
|
||||
|
||||
## PSCM status and driver handling
|
||||
|
||||
card publishes `Lane_Assist_Data3_FD1` in `carStateSP.fordPscmStatus`, retaining
|
||||
the original CAN receipt timestamp. Republishing carStateSP or receiving
|
||||
unrelated frames cannot refresh it. The opendbc submodule is unchanged.
|
||||
|
||||
Feedback requires valid fresh status, InProgress lateral state (2), capability
|
||||
LimitedModeAvailable or ExtendedModeAvailable (1 or 2), and no denial.
|
||||
Missing, malformed, stale, backward-timestamped, denied or unavailable status
|
||||
clears feedback bias/history and disables the separate release guard,
|
||||
leaving the base subject to its core validity gates.
|
||||
LimitReached (2) permits only the bounded request-reducing backoff described
|
||||
above and otherwise freezes integration. LimitWithDriverActive (3) clears
|
||||
feedback. Backoff still requires fresh, valid, InProgress status with an
|
||||
available capability and no denial. These generic PSCM reports do not identify
|
||||
a specific torque or rate limit.
|
||||
|
||||
`steeringPressed`, raw torque above the existing Ford driver allowance, or
|
||||
nonfinite torque clear feedback. Below 2 m/s feedback also clears. A fresh
|
||||
feedback reference interval is required after override; the independent
|
||||
release guard can use retained valid request history once its current gates
|
||||
are satisfied. Base requests retain normal
|
||||
PSCM driver arbitration while lateral control remains authorized; an unset
|
||||
override flag cannot rule out subthreshold driver influence.
|
||||
|
||||
## Gates and Sunnylink selection
|
||||
|
||||
Core model/action/car-state freshness, finite-value, clock and speed checks
|
||||
remain in place. Invalid core inputs reset both commands and clear latActive.
|
||||
Raw model geometry is validated on every update, including repeated model
|
||||
timestamps; an invalid raw path cannot reuse the cached valid reference.
|
||||
Missing PSCM status disables feedback, not an otherwise valid base request.
|
||||
|
||||
Vehicle → Ford → **C2-Free Path Tracking (Experimental)** retains the
|
||||
`FordVirtualAngleController` key, default-off setting and offroad/onroad cycle
|
||||
requirement. Enabled selects v8 on Ford CAN FD `FORD_F_150_LIGHTNING_MK1`
|
||||
regardless of missing or different EPS firmware-query results. Other platforms
|
||||
retain their existing controller. V8 takes priority over PSCM Coefficient
|
||||
Observer while selected; disabling and cycling offroad/onroad restores the
|
||||
previous selection. Controller selection does not force lateral engagement.
|
||||
|
||||
The analyzed firmware is `RL38-14D003-AA`; removing the eligibility check
|
||||
is not validation of other firmware. No live device setting is changed.
|
||||
|
||||
## Diagnostics and verification
|
||||
|
||||
The 5 Hz `Ford C2-free path tracking` event keeps its name and identifies v8.
|
||||
`model_offset_base` / `model_heading_base` report the already weighted and
|
||||
encoded model contribution; `curvature_offset_base` / `curvature_heading_base`
|
||||
report the residual-curvature contribution. `model_share` and `base_guard`
|
||||
identify model-pose, blended, curvature-only, opposed-model and zero-request
|
||||
cases. `heading_base` is the bounded pre-feedback C1. `offset_target` and
|
||||
`heading_target` are the final targets after the independent release guard;
|
||||
`offset_target_unguarded` and `heading_target_unguarded` retain the inputs to
|
||||
that guard. The latter C1 already includes its normal feedback/backoff policy.
|
||||
|
||||
The event retains source timestamps, measured curvature/yaw, final commands,
|
||||
slew scales, feedback bias/status/history, raw torque and PSCM status/age.
|
||||
`feedback_backoff_active` records the persistent heading ceiling, including
|
||||
cycles whose feedback status is `no_new_measurement`.
|
||||
`release_guard_active` and `release_guard_reference_curvature` expose the
|
||||
independent C0/C1 guard and its retained delayed reference.
|
||||
`feedback_release_tracking_active`, `feedback_release_ceiling` and
|
||||
`feedback_curvature_delta` identify accepted release
|
||||
tracking, the total-heading threshold used to admit new bias, and the
|
||||
measured-curvature change across the response interval (1/m). The tracking
|
||||
flag is true only when the branch accepts a bias change on a new measurement;
|
||||
it is false on repeated measurements. The ceiling/trend fields can describe
|
||||
an evaluated condition even when no increment is accepted.
|
||||
`feedback_recovery_active` records an accepted recovery increment on this
|
||||
update only; it does not persist between measurements.
|
||||
`feedback_yaw_error` retains its delayed-reference meaning. Recovery instead
|
||||
uses current error, reconstructed from logged `desired_curvature`,
|
||||
synchronized car-state speed and `yaw_rate`; those two errors can differ.
|
||||
During backoff or the independent release guard, `heading_target` can be lower in the request direction than
|
||||
the bounded sum of `heading_base` and `heading_bias`, because the temporary
|
||||
ceiling is not part of the stored bias.
|
||||
`model_heading_target` remains a filtered comparison reference; it is not the
|
||||
weighted model contribution. `angleState.saturated` is not an EPS-limit signal.
|
||||
|
||||
Validation must cover large recorded maneuvers, flat-model centering, both
|
||||
turn directions, model/action disagreement, share transitions, release and
|
||||
reversal, release/limit backoff without growth or zero crossing, status/driver
|
||||
resets, reference causality, bounds, slew and CAN packing with C2/C3 zero.
|
||||
Recovery checks cover both directions, stopping at zero bias, repeated
|
||||
measurements, current-and-delayed agreement, and rejection at PSCM limit 2.
|
||||
Old v3/v4 command-equality expectations do not define
|
||||
v8 success. Guard checks also cover driver reset/history rebuilding,
|
||||
same-direction growth, opposing coefficients, repeated measurements,
|
||||
undertracking and invalid-status inhibition. Tracking checks cover delayed
|
||||
curvature trends and tapered release-entry headroom. Historical v5–v7 replay
|
||||
results remain historical observations.
|
||||
|
||||
The v8 recorded-input fixture contains 15,273 cycles with 4,879 selected
|
||||
evidence samples. Base allocation and output eligibility match v7. In the
|
||||
clean deficient exit, median absolute C1 changes from 0.0665 to 0.0845 rad
|
||||
while C0 stays unchanged. The growth guard also acts while feedback history
|
||||
rebuilds; the largest over-growth witness includes nearby driver input and
|
||||
is excluded from the strict autonomous tracking score. Both good comparison
|
||||
curves in that fixture retain their median requests, and the older large-turn
|
||||
fixtures retain their required command scale.
|
||||
|
||||
On the earlier good drive, one comparison curve retains extra C1 after
|
||||
eligible release tracking: median magnitude changes from 0.121 to 0.128 rad.
|
||||
In its 103–110 s interval, tracking increments occur only while measured
|
||||
turning falls short, with a median current response/request ratio of 0.895.
|
||||
Acquired bias can persist after matching, as with ordinary integral feedback.
|
||||
This collateral command change remains a reason to compare new vehicle logs.
|
||||
Replay fixes recorded motion and planner outputs, so enabled vehicle logs
|
||||
are still required to assess tracking error, oscillation and interventions.
|
||||
+1
-1
@@ -16,7 +16,7 @@ export VECLIB_MAXIMUM_THREADS=1
|
||||
export QCOM_PRIORITY=12
|
||||
|
||||
if [ -z "$AGNOS_VERSION" ]; then
|
||||
export AGNOS_VERSION="19.6"
|
||||
export AGNOS_VERSION="19.7"
|
||||
fi
|
||||
|
||||
export STAGING_ROOT="/data/safe_staging"
|
||||
|
||||
+1
-1
Submodule opendbc_repo updated: 06743dfb39...64aa61b9b3
@@ -383,6 +383,8 @@ struct CarControlSP @0xa5cd762cd951a455 {
|
||||
leadOne @2 :LeadData;
|
||||
leadTwo @3 :LeadData;
|
||||
intelligentCruiseButtonManagement @4 :IntelligentCruiseButtonManagement;
|
||||
fordLateralPath @5 :FordLateralPath;
|
||||
fordTurnPreview @6 :FordTurnPreview;
|
||||
|
||||
struct Param {
|
||||
key @0 :Text;
|
||||
@@ -403,6 +405,25 @@ struct CarControlSP @0xa5cd762cd951a455 {
|
||||
}
|
||||
}
|
||||
|
||||
struct FordTurnPreview {
|
||||
valid @0 :Bool;
|
||||
modelMonoTime @1 :UInt64;
|
||||
heading7 @2 :Float64; # Heading change at 7 m of path distance, native pinion degrees.
|
||||
heading14 @3 :Float64; # Heading change at 14 m of path distance, native pinion degrees.
|
||||
actionRate @4 :Float64; # Negative raw model curvature derivative; only its sign is used.
|
||||
geometryAngle @5 :Float64; # Local path curvature converted to native wheel degrees at v*0.8 s.
|
||||
geometryValid @6 :Bool;
|
||||
}
|
||||
|
||||
struct FordLateralPath {
|
||||
pathOffset @0 :Float32; # c0 [m]
|
||||
pathAngle @1 :Float32; # c1 [rad]
|
||||
curvature @2 :Float32; # c2 [1/m]
|
||||
curvatureRate @3 :Float32; # c3 [1/m^2]
|
||||
valid @4 :Bool;
|
||||
enabled @5 :Bool; # Startup-selected custom controller; independent of command validity.
|
||||
}
|
||||
|
||||
struct BackupManagerSP @0xf98d843bfd7004a3 {
|
||||
backupStatus @0 :Status;
|
||||
restoreStatus @1 :Status;
|
||||
@@ -447,6 +468,16 @@ struct BackupManagerSP @0xf98d843bfd7004a3 {
|
||||
|
||||
struct CarStateSP @0xb86e6369214c01c8 {
|
||||
speedLimit @0 :Float32;
|
||||
fordPscmStatus @1 :FordPscmStatus;
|
||||
|
||||
struct FordPscmStatus {
|
||||
valid @0 :Bool;
|
||||
canMonoTime @1 :UInt64; # Last accepted Lane_Assist_Data3_FD1 CAN receipt, not carStateSP publication time.
|
||||
lateralState @2 :UInt8; # LatCtlSte_D_Stat
|
||||
limit @3 :UInt8; # LatCtlLim_D_Stat: generic lateral limit, not a torque/rate diagnosis.
|
||||
capability @4 :UInt8; # LatCtlCpblty_D_Stat
|
||||
denied @5 :Bool; # LaActDeny_B_Actl
|
||||
}
|
||||
}
|
||||
|
||||
struct LiveMapDataSP @0xf416ec09499d9d19 {
|
||||
@@ -462,6 +493,18 @@ struct ModelDataV2SP @0xa1680744031fdb2d {
|
||||
laneTurnDirection @0 :TurnDirection;
|
||||
leftLaneChangeEdgeBlock @1 :Bool;
|
||||
rightLaneChangeEdgeBlock @2 :Bool;
|
||||
fordGeometryReference @3 :FordGeometryReference;
|
||||
|
||||
struct FordGeometryReference {
|
||||
enabled @0 :Bool;
|
||||
valid @1 :Bool; # False while enabled means invalid geometry; original action is retained.
|
||||
modelMonoTime @2 :UInt64;
|
||||
actionDesiredCurvature @3 :Float32; # Original model action, with its own unchanged smoothing history.
|
||||
rawCurvature @4 :Float32;
|
||||
selectedCurvature @5 :Float32; # Published modelV2.action, before controlsd's normal limits/maneuver override.
|
||||
previewSeconds @6 :Float32;
|
||||
smoothSeconds @7 :Float32;
|
||||
}
|
||||
|
||||
enum TurnDirection {
|
||||
none @0;
|
||||
@@ -470,7 +513,30 @@ struct ModelDataV2SP @0xa1680744031fdb2d {
|
||||
}
|
||||
}
|
||||
|
||||
struct CustomReserved10 @0xcb9fd56c7057593a {
|
||||
struct AssistedDrivingMilestoneState @0xcb9fd56c7057593a {
|
||||
enabled @0 :Bool;
|
||||
madsDistanceMeters @1 :Float64;
|
||||
fullAssistDistanceMeters @2 :Float64;
|
||||
event @3 :Event;
|
||||
|
||||
struct Event {
|
||||
id @0 :UInt64;
|
||||
category @1 :Category;
|
||||
distanceMeters @2 :Float64;
|
||||
previousDistanceMeters @3 :Float64;
|
||||
unit @4 :Unit;
|
||||
}
|
||||
|
||||
enum Category {
|
||||
none @0;
|
||||
mads @1;
|
||||
fullAssist @2;
|
||||
}
|
||||
|
||||
enum Unit {
|
||||
imperial @0;
|
||||
metric @1;
|
||||
}
|
||||
}
|
||||
|
||||
struct CustomReserved11 @0xc2243c65e0340384 {
|
||||
|
||||
@@ -725,6 +725,7 @@ struct ChestnutState {
|
||||
pcieLtssm @7 :UInt8;
|
||||
supplyVoltage @8 :UInt16; # mV
|
||||
supplyCurrent @9 :Int16; # mA
|
||||
supplyFault @10 :Bool;
|
||||
}
|
||||
|
||||
struct RadarState @0x9a185389d6fdd05f {
|
||||
@@ -1004,6 +1005,7 @@ struct DrivingModelData {
|
||||
frameIdExtra @1 :UInt32;
|
||||
frameDropPerc @6 :Float32;
|
||||
modelExecutionTime @7 :Float32;
|
||||
big @8 :Bool;
|
||||
|
||||
action @2 :ModelDataV2.Action;
|
||||
|
||||
@@ -2117,6 +2119,9 @@ struct Joystick {
|
||||
# convenient for debug and live tuning
|
||||
axes @0: List(Float32);
|
||||
buttons @1: List(Bool);
|
||||
fordChannel @2 :FordChannel;
|
||||
|
||||
enum FordChannel { standard @0; c0 @1; c1 @2; }
|
||||
}
|
||||
|
||||
struct DriverStateV2 {
|
||||
@@ -2640,7 +2645,7 @@ struct Event {
|
||||
carStateSP @114 :Custom.CarStateSP;
|
||||
liveMapDataSP @115 :Custom.LiveMapDataSP;
|
||||
modelDataV2SP @116 :Custom.ModelDataV2SP;
|
||||
customReserved10 @136 :Custom.CustomReserved10;
|
||||
assistedDrivingMilestoneState @136 :Custom.AssistedDrivingMilestoneState;
|
||||
customReserved11 @137 :Custom.CustomReserved11;
|
||||
customReserved12 @138 :Custom.CustomReserved12;
|
||||
customReserved13 @139 :Custom.CustomReserved13;
|
||||
|
||||
@@ -90,6 +90,7 @@ _services: dict[str, tuple] = {
|
||||
"carParamsSP": (True, 0.02, 1),
|
||||
"carControlSP": (True, 100., 10),
|
||||
"carStateSP": (True, 100., 10),
|
||||
"assistedDrivingMilestoneState": (True, 10., 1),
|
||||
"liveMapDataSP": (True, 1., 1),
|
||||
"modelDataV2SP": (True, 20., None, QueueSize.BIG),
|
||||
"liveLocationKalman": (True, 20.),
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import os
|
||||
import time
|
||||
from abc import abstractmethod, ABC
|
||||
from dataclasses import dataclass, fields
|
||||
|
||||
@@ -16,6 +17,17 @@ class ThermalZone:
|
||||
zone_number = -1
|
||||
|
||||
def read(self) -> float:
|
||||
start = time.monotonic()
|
||||
try:
|
||||
return self._read()
|
||||
finally:
|
||||
elapsed = time.monotonic() - start
|
||||
if elapsed > 1.0:
|
||||
# Import lazily: swaglog's hardware paths depend on this module.
|
||||
from openpilot.common.swaglog import cloudlog
|
||||
cloudlog.event("Thermal sensor read slow", sensor=self.name, zone=self.zone_number, elapsed=elapsed, error=True)
|
||||
|
||||
def _read(self) -> float:
|
||||
if self.zone_number < 0:
|
||||
for n in os.listdir("/sys/devices/virtual/thermal"):
|
||||
if not n.startswith("thermal_zone"):
|
||||
|
||||
@@ -56,29 +56,29 @@
|
||||
},
|
||||
{
|
||||
"name": "boot",
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/boot-b30f5eef65ec3878f3aa3dcaf2cc95c09e2c1e661cd3a38e94da37dee76f68bd.img.xz",
|
||||
"hash": "b30f5eef65ec3878f3aa3dcaf2cc95c09e2c1e661cd3a38e94da37dee76f68bd",
|
||||
"hash_raw": "b30f5eef65ec3878f3aa3dcaf2cc95c09e2c1e661cd3a38e94da37dee76f68bd",
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/boot-6ecf6f987cd11968104abcccabbe268485d329cdb73012dfd3c381a6b8deb27d.img.xz",
|
||||
"hash": "6ecf6f987cd11968104abcccabbe268485d329cdb73012dfd3c381a6b8deb27d",
|
||||
"hash_raw": "6ecf6f987cd11968104abcccabbe268485d329cdb73012dfd3c381a6b8deb27d",
|
||||
"size": 46897152,
|
||||
"sparse": false,
|
||||
"full_check": true,
|
||||
"has_ab": true,
|
||||
"ondevice_hash": "6650e4c46df99ae6dfd6ee895a34b8a2a3cc490a8ce18e16cc3c451c3f822b6e"
|
||||
"ondevice_hash": "d12e1e5b9455b62a1464558716493b33e470d7a7e88da1c4105a3b21d0961808"
|
||||
},
|
||||
{
|
||||
"name": "system",
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/system-5b6ce7965904a157fd3a134ccfcb854f9ca5c1cc2a26b7cb80a4fa4e1cc4aaa3.img.xz",
|
||||
"hash": "b134fd04e9da27fa1d359ea0f2742c216fa21a08b5c47e9be22ab3b0563d9b9b",
|
||||
"hash_raw": "5b6ce7965904a157fd3a134ccfcb854f9ca5c1cc2a26b7cb80a4fa4e1cc4aaa3",
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/system-3c271e2b3d20d2f0a8bf6555a1319f3efb12845490967d6151195174a01e912f.img.xz",
|
||||
"hash": "74ffc9c551e1f29cda897ace8a69080fe644f8039977c6885f2b48362e39b744",
|
||||
"hash_raw": "3c271e2b3d20d2f0a8bf6555a1319f3efb12845490967d6151195174a01e912f",
|
||||
"size": 4718592000,
|
||||
"sparse": true,
|
||||
"full_check": false,
|
||||
"has_ab": true,
|
||||
"ondevice_hash": "91242772af771ae96fe2eebc105f2b80a7e1dbaaf6003c2574b62d51b806f468",
|
||||
"ondevice_hash": "6a992680183685eea9db99d915219a37935f45989330d9b619e880450257f448",
|
||||
"alt": {
|
||||
"hash": "5b6ce7965904a157fd3a134ccfcb854f9ca5c1cc2a26b7cb80a4fa4e1cc4aaa3",
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/system-5b6ce7965904a157fd3a134ccfcb854f9ca5c1cc2a26b7cb80a4fa4e1cc4aaa3.img",
|
||||
"hash": "3c271e2b3d20d2f0a8bf6555a1319f3efb12845490967d6151195174a01e912f",
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/system-3c271e2b3d20d2f0a8bf6555a1319f3efb12845490967d6151195174a01e912f.img",
|
||||
"size": 4718592000
|
||||
}
|
||||
}
|
||||
]
|
||||
]
|
||||
@@ -5,6 +5,7 @@ import logging
|
||||
import os
|
||||
import select
|
||||
import signal
|
||||
import string
|
||||
import struct
|
||||
import subprocess
|
||||
import tempfile
|
||||
@@ -354,7 +355,7 @@ class Modem:
|
||||
imei = ""
|
||||
|
||||
iccid = (self._atv("AT+QCCID", "+QCCID:") or "").rstrip("F")
|
||||
if not iccid.isdigit():
|
||||
if not all(c in string.hexdigits for c in iccid):
|
||||
iccid = ""
|
||||
|
||||
imsi = first_line("AT+CIMI")
|
||||
|
||||
@@ -4,11 +4,17 @@ from pathlib import Path
|
||||
CHESTNUT_FW_VERSION = "ed4e39b7"
|
||||
CHESTNUT_USB_IDS = ((0xADD1, 0x0001), (0x3801, 0x0001))
|
||||
CHESTNUT_ROM_USB_IDS = ((0x174C, 0x2464), (0x174C, 0x2463))
|
||||
CHESTNUT_USB_PRODUCT = f"custom {CHESTNUT_FW_VERSION}-CLEAN"
|
||||
USB_DEVICES_PATH = Path("/sys/bus/usb/devices")
|
||||
TYPEC_CC_ORIENTATION_PATH = Path("/sys/class/power_supply/usb/typec_cc_orientation")
|
||||
PRIMARY_USB_CONTROLLER = "a600000.ssusb"
|
||||
|
||||
|
||||
def is_chestnut_usb_id(vendor_id: int, product_id: int, include_bootloader: bool = False) -> bool:
|
||||
ids = CHESTNUT_USB_IDS + CHESTNUT_ROM_USB_IDS if include_bootloader else CHESTNUT_USB_IDS
|
||||
return (vendor_id, product_id) in ids
|
||||
|
||||
|
||||
def get_usb_topology() -> set[str]:
|
||||
try:
|
||||
return set(os.listdir(USB_DEVICES_PATH))
|
||||
@@ -81,7 +87,7 @@ def set_usb_state(device_state, devices: list[dict]) -> None:
|
||||
entry.linkErrorCount = device["linkErrorCount"]
|
||||
entry.usb3Lane = device.get("usb3Lane", "unknown")
|
||||
|
||||
if (entry.vendorId, entry.productId) in CHESTNUT_USB_IDS:
|
||||
if is_chestnut_usb_id(entry.vendorId, entry.productId):
|
||||
chestnut_present = True
|
||||
|
||||
device_state.chestnutPresent = chestnut_present
|
||||
|
||||
@@ -97,6 +97,10 @@ Params::Params(const std::string &path) {
|
||||
}
|
||||
|
||||
Params::~Params() {
|
||||
flushNonBlockingWrites();
|
||||
}
|
||||
|
||||
void Params::flushNonBlockingWrites() {
|
||||
if (future.valid()) {
|
||||
future.wait();
|
||||
}
|
||||
|
||||
@@ -75,6 +75,7 @@ public:
|
||||
return put(key.c_str(), val ? "1" : "0", 1);
|
||||
}
|
||||
void putNonBlocking(const std::string &key, const std::string &val);
|
||||
void flushNonBlockingWrites();
|
||||
inline void putBoolNonBlocking(const std::string &key, bool val) {
|
||||
putNonBlocking(key, val ? "1" : "0");
|
||||
}
|
||||
|
||||
@@ -73,6 +73,7 @@ params_get = _bind("params_get", [ParamsHandle, ctypes.c_char_p, ctypes.c_bool],
|
||||
params_get_bool = _bind("params_get_bool", [ParamsHandle, ctypes.c_char_p, ctypes.c_bool], ctypes.c_bool)
|
||||
params_put = _bind("params_put", [ParamsHandle, ctypes.c_char_p, ctypes.c_char_p, ctypes.c_size_t, ctypes.c_bool], ctypes.c_int)
|
||||
params_put_bool = _bind("params_put_bool", [ParamsHandle, ctypes.c_char_p, ctypes.c_bool, ctypes.c_bool], ctypes.c_int)
|
||||
params_flush = _bind("params_flush", [ParamsHandle])
|
||||
params_remove = _bind("params_remove", [ParamsHandle, ctypes.c_char_p], ctypes.c_int)
|
||||
params_get_path = _bind("params_get_path", [ParamsHandle, ctypes.c_char_p, ctypes.c_size_t], ParamsBuffer)
|
||||
params_keys_size = _bind("params_keys_size", [ParamsHandle], ctypes.c_size_t)
|
||||
@@ -178,6 +179,10 @@ class Params:
|
||||
def put_bool(self, key, val, block=False):
|
||||
params_put_bool(self.p, self.check_key(key), val, block)
|
||||
|
||||
def flush(self):
|
||||
"""Wait for all prior nonblocking writes from this Params instance."""
|
||||
params_flush(self.p)
|
||||
|
||||
def remove(self, key):
|
||||
params_remove(self.p, self.check_key(key))
|
||||
|
||||
|
||||
@@ -133,6 +133,12 @@ int params_put_bool(ParamsHandle *handle, const char *key, bool value, bool bloc
|
||||
});
|
||||
}
|
||||
|
||||
void params_flush(ParamsHandle *handle) noexcept {
|
||||
translate_exceptions([&]() {
|
||||
handle->params.flushNonBlockingWrites();
|
||||
});
|
||||
}
|
||||
|
||||
int params_remove(ParamsHandle *handle, const char *key) noexcept {
|
||||
return translate_exceptions(-1, [&]() {
|
||||
return handle->params.remove(key);
|
||||
@@ -162,12 +168,15 @@ ParamsBuffer params_key_at(ParamsHandle *handle, size_t index) noexcept {
|
||||
|
||||
size_t params_keys_by_flag(ParamsHandle *handle, uint32_t flag, ParamsBuffer *out, size_t out_size) noexcept {
|
||||
return translate_exceptions(size_t{0}, [&]() {
|
||||
auto filtered = handle->params.allKeys(static_cast<ParamKeyFlag>(flag));
|
||||
size_t count = std::min(filtered.size(), out_size);
|
||||
for (size_t i = 0; i < count; i++) {
|
||||
out[i] = return_string(filtered[i]);
|
||||
size_t count = 0;
|
||||
for (const auto &key : handle->keys) {
|
||||
if (flag == ALL || (handle->params.getKeyFlag(key) & flag)) {
|
||||
// Each buffer borrows a different string, stable for the handle's lifetime.
|
||||
if (count < out_size) out[count] = {key.data(), key.size()};
|
||||
++count;
|
||||
}
|
||||
}
|
||||
return filtered.size();
|
||||
return count;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -92,6 +92,12 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
|
||||
{"ObdMultiplexingEnabled", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, BOOL}},
|
||||
{"Offroad_CarUnrecognized", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, JSON}},
|
||||
{"Offroad_ChestnutBranch", {CLEAR_ON_MANAGER_START, JSON}},
|
||||
{"Offroad_ChestnutNotDetected", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, JSON}},
|
||||
{"Offroad_ChestnutOverheated", {CLEAR_ON_MANAGER_START, JSON}},
|
||||
{"Offroad_ChestnutPcieUnavailable", {CLEAR_ON_MANAGER_START, JSON}},
|
||||
{"Offroad_ChestnutUncompiled", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, JSON}},
|
||||
{"Offroad_ChestnutUpdateFailed", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, JSON}},
|
||||
{"Offroad_ChestnutUsbSlow", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, JSON}},
|
||||
{"Offroad_ConnectivityNeeded", {CLEAR_ON_MANAGER_START, JSON}},
|
||||
{"Offroad_ConnectivityNeededPrompt", {CLEAR_ON_MANAGER_START, JSON}},
|
||||
{"Offroad_ExcessiveActuation", {PERSISTENT, JSON}},
|
||||
@@ -132,10 +138,13 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
|
||||
{"UptimeOnroad", {PERSISTENT, FLOAT, "0.0"}},
|
||||
{"ChestnutActive", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION | CLEAR_ON_IGNITION_ON, BOOL}},
|
||||
{"ChestnutLoading", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION | CLEAR_ON_IGNITION_ON, BOOL}},
|
||||
{"ChestnutModelError", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION | CLEAR_ON_IGNITION_ON, BOOL}},
|
||||
{"Version", {PERSISTENT, STRING}},
|
||||
|
||||
// --- sunnypilot params --- //
|
||||
{"ApiCache_DriveStats", {PERSISTENT, JSON}},
|
||||
{"AssistedDrivingMilestonesEnabled", {PERSISTENT | BACKUP, BOOL, "1"}},
|
||||
{"AssistedDrivingMilestoneState", {PERSISTENT, JSON, "{}"}},
|
||||
{"AutoLaneChangeBsmDelay", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
{"AutoLaneChangeTimer", {PERSISTENT | BACKUP, INT, "0"}},
|
||||
{"BlinkerLateralReengageDelay", {PERSISTENT | BACKUP, INT, "0"}}, // seconds
|
||||
@@ -156,6 +165,7 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
|
||||
{"DevUIInfo", {PERSISTENT | BACKUP, INT, "0"}},
|
||||
{"EnableCopyparty", {PERSISTENT | BACKUP, BOOL}},
|
||||
{"EnableGithubRunner", {PERSISTENT | BACKUP, BOOL}},
|
||||
{"FullAssistDrivenDistanceMeters", {PERSISTENT, FLOAT, "0.0"}},
|
||||
{"GreenLightAlert", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
{"GithubRunnerSufficientVoltage", {CLEAR_ON_MANAGER_START , BOOL}},
|
||||
{"HasAcceptedTermsSP", {PERSISTENT, STRING, "0"}},
|
||||
@@ -165,7 +175,9 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
|
||||
{"IsDevelopmentBranch", {CLEAR_ON_MANAGER_START, BOOL}},
|
||||
{"IsReleaseSpBranch", {CLEAR_ON_MANAGER_START, BOOL}},
|
||||
{"LastGPSPositionLLK", {PERSISTENT, STRING}},
|
||||
{"LastDriveAssistedDrivingSummary", {PERSISTENT, JSON, "{}"}},
|
||||
{"LeadDepartAlert", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
{"MadsDrivenDistanceMeters", {PERSISTENT, FLOAT, "0.0"}},
|
||||
{"MaxTimeOffroad", {PERSISTENT | BACKUP, INT, "1800"}},
|
||||
{"ModelRunnerTypeCache", {CLEAR_ON_ONROAD_TRANSITION, INT}},
|
||||
{"OffroadMode", {CLEAR_ON_MANAGER_START, BOOL}},
|
||||
@@ -225,6 +237,13 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
|
||||
{"BackupManager_RestoreVersion", {PERSISTENT, STRING}},
|
||||
|
||||
// sunnypilot car specific params
|
||||
{"FordPscmObserver", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
{"FordModelActionController", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
{"FordC0TimeBased", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
{"FordGeometryReference", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
{"FordPscmJointControl", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
{"FordPscmTurnEntryAssist", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
{"FordPscmTurnPreview", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
{"HyundaiLongitudinalTuning", {PERSISTENT | BACKUP, INT, "0"}},
|
||||
{"SubaruStopAndGo", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
{"SubaruStopAndGoManualParkingBrake", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
|
||||
@@ -27,14 +27,14 @@ public:
|
||||
auto param_path = Params().getParamPath();
|
||||
if (util::file_exists(param_path)) {
|
||||
std::string real_path = util::readlink(param_path);
|
||||
util::check_system(util::string_format("rm %s -rf", real_path.c_str()));
|
||||
util::check_system(util::string_format("rm -rf %s", real_path.c_str()));
|
||||
unlink(param_path.c_str());
|
||||
}
|
||||
if (getenv("COMMA_CACHE") == nullptr) {
|
||||
util::check_system(util::string_format("rm %s -rf", Path::download_cache_root().c_str()));
|
||||
util::check_system(util::string_format("rm -rf %s", Path::download_cache_root().c_str()));
|
||||
}
|
||||
util::check_system(util::string_format("rm %s -rf", Path::comma_home().c_str()));
|
||||
util::check_system(util::string_format("rm %s -rf", msgq_path.c_str()));
|
||||
util::check_system(util::string_format("rm -rf %s", Path::comma_home().c_str()));
|
||||
util::check_system(util::string_format("rm -rf %s", msgq_path.c_str()));
|
||||
unsetenv("OPENPILOT_PREFIX");
|
||||
}
|
||||
|
||||
|
||||
@@ -106,6 +106,13 @@ class TestParams(OpenpilotTestCase):
|
||||
assert q.get("CarParams") is None
|
||||
assert q.get("CarParams", True) == b"1"
|
||||
|
||||
def test_flush_non_blocking_writes(self):
|
||||
self.params.put("DongleId", "first")
|
||||
self.params.put("DongleId", "last")
|
||||
self.params.flush()
|
||||
|
||||
assert self.params.get("DongleId") == "last"
|
||||
|
||||
def test_params_all_keys(self):
|
||||
keys = Params().all_keys()
|
||||
|
||||
@@ -126,6 +133,16 @@ class TestParams(OpenpilotTestCase):
|
||||
assert self.params.get("LiveParametersV2") is None
|
||||
assert self.params.get("LiveParametersV2", return_default=True) is None
|
||||
|
||||
def test_filtered_keys_are_distinct_registered_strings(self):
|
||||
registered = set(self.params.all_keys())
|
||||
for flag in (ParamKeyFlag.PERSISTENT, ParamKeyFlag.BACKUP, ParamKeyFlag.CLEAR_ON_MANAGER_START):
|
||||
filtered = self.params.all_keys(flag)
|
||||
assert len(filtered) > 1
|
||||
assert len(filtered) == len(set(filtered))
|
||||
assert set(filtered) <= registered
|
||||
assert all(key.decode('utf-8') for key in filtered)
|
||||
assert self.params.all_keys(flag) == filtered
|
||||
|
||||
def test_params_get_type(self):
|
||||
# json
|
||||
self.params.put("ApiCache_FirehoseStats", {"a": 0}, block=True)
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
from unittest.mock import Mock, mock_open
|
||||
|
||||
import pytest
|
||||
|
||||
from openpilot.common.hardware import base
|
||||
from openpilot.common.swaglog import cloudlog
|
||||
|
||||
|
||||
@pytest.mark.parametrize('elapsed', [.05, 1.5])
|
||||
def test_thermal_read_preserves_value_and_identifies_slow_sensor(monkeypatch, elapsed):
|
||||
sensor = base.ThermalZone('cpu0-gold-usr')
|
||||
sensor.zone_number = 7
|
||||
read = mock_open(read_data='62000')
|
||||
report = Mock()
|
||||
monkeypatch.setattr(base, 'open', read, raising=False)
|
||||
monkeypatch.setattr(base.time, 'monotonic', Mock(side_effect=[0., elapsed]))
|
||||
monkeypatch.setattr(cloudlog, 'event', report)
|
||||
assert sensor.read() == 62.
|
||||
read.assert_called_once_with('/sys/devices/virtual/thermal/thermal_zone7/temp')
|
||||
if elapsed > 1.:
|
||||
report.assert_called_once_with('Thermal sensor read slow', sensor='cpu0-gold-usr', zone=7, elapsed=elapsed, error=True)
|
||||
else:
|
||||
report.assert_not_called()
|
||||
|
||||
|
||||
def test_slow_thermal_failure_is_reported_without_hiding_error(monkeypatch):
|
||||
sensor = base.ThermalZone('memory')
|
||||
sensor.zone_number = 8
|
||||
monkeypatch.setattr(base, 'open', Mock(side_effect=OSError('sensor read failed')), raising=False)
|
||||
monkeypatch.setattr(base.time, 'monotonic', Mock(side_effect=[0., 2.]))
|
||||
report = Mock()
|
||||
monkeypatch.setattr(cloudlog, 'event', report)
|
||||
with pytest.raises(OSError, match='sensor read failed'):
|
||||
sensor.read()
|
||||
report.assert_called_once_with('Thermal sensor read slow', sensor='memory', zone=8, elapsed=2., error=True)
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:07bda2fe5d6be0b2854044053c384fe002e96406da119863a443b9344258b500
|
||||
size 1544
|
||||
Binary file not shown.
@@ -21,6 +21,8 @@ from opendbc.car.interfaces import CarInterfaceBase, RadarInterfaceBase
|
||||
from openpilot.selfdrive.pandad import can_capnp_to_list, can_list_to_can_capnp
|
||||
from openpilot.selfdrive.car.cruise import VCruiseHelper
|
||||
from openpilot.selfdrive.car.helpers import convert_carControlSP, convert_to_capnp
|
||||
from openpilot.selfdrive.car.ford_pscm_status import populate_ford_pscm_status
|
||||
from openpilot.selfdrive.car.ford_joint_control import select_joint_control
|
||||
|
||||
from openpilot.sunnypilot.mads.helpers import set_alternative_experience, set_car_specific_params
|
||||
from openpilot.sunnypilot.selfdrive.car import interfaces as sunnypilot_interfaces
|
||||
@@ -123,6 +125,7 @@ class Car:
|
||||
self.RI = RI
|
||||
|
||||
self.CP.alternativeExperience = 0
|
||||
self.ford_joint_control = select_joint_control(self.CP, self.params)
|
||||
# mads
|
||||
set_alternative_experience(self.CP, self.CP_SP, self.params)
|
||||
set_car_specific_params(self.CP, self.CP_SP, self.params)
|
||||
@@ -198,6 +201,11 @@ class Car:
|
||||
# Update carState from CAN
|
||||
CS, CS_SP = self.CI.update(can_list)
|
||||
CS_SP = convert_to_capnp(CS_SP)
|
||||
populate_ford_pscm_status(self.CP, self.CI.can_parsers, CS_SP, CS.canValid)
|
||||
if self.ford_joint_control is not None and self.ford_joint_control.fault:
|
||||
# Surface a latched loss of command history through the normal steering
|
||||
# fault alert; never silently leave the UI engaged with this trial disabled.
|
||||
CS.steerFaultTemporary = True
|
||||
|
||||
# Update radar tracks from CAN
|
||||
RD: structs.RadarDataT | None = self.RI.update(can_list)
|
||||
@@ -267,7 +275,7 @@ class Car:
|
||||
cs_sp_send.carStateSP = CS_SP
|
||||
self.pm.send('carStateSP', cs_sp_send)
|
||||
|
||||
def controls_update(self, CS: car.CarState, CC: car.CarControl, CC_SP: custom.CarControlSP):
|
||||
def controls_update(self, CS: car.CarState, CC: car.CarControl, CC_SP: custom.CarControlSP, pscm_status=None):
|
||||
"""control update loop, driven by carControl"""
|
||||
|
||||
if not self.initialized_prev:
|
||||
@@ -280,8 +288,17 @@ class Car:
|
||||
if self.sm.all_alive(['carControl']):
|
||||
# send car controls over can
|
||||
now_nanos = self.can_log_mono_time if REPLAY else int(time.monotonic() * 1e9)
|
||||
self.last_actuators_output, can_sends = self.CI.apply(CC, convert_carControlSP(CC_SP), now_nanos)
|
||||
control_sp = convert_carControlSP(CC_SP)
|
||||
if self.ford_joint_control is not None:
|
||||
CC = self.ford_joint_control.prepare(CC, control_sp, CS, now_nanos * 1e-9,
|
||||
fresh=self.sm.all_checks(['carControl', 'carControlSP']), pscm_status=pscm_status,
|
||||
turn_preview=CC_SP.fordTurnPreview)
|
||||
self.last_actuators_output, can_sends = self.CI.apply(CC, control_sp, now_nanos)
|
||||
self.pm.send('sendcan', can_list_to_can_capnp(can_sends, msgtype='sendcan', valid=CS.canValid))
|
||||
if self.ford_joint_control is not None:
|
||||
self.ford_joint_control.record_sent(can_sends, now_nanos)
|
||||
if self.sm.frame % 20 == 0:
|
||||
cloudlog.event('Ford joint path tracking', **self.ford_joint_control.diagnostics)
|
||||
|
||||
self.CC_prev = CC
|
||||
|
||||
@@ -293,7 +310,7 @@ class Car:
|
||||
initialized = (not any(e.name == EventName.selfdriveInitializing for e in self.sm['onroadEvents']) and
|
||||
self.sm.seen['onroadEvents'])
|
||||
if not self.CP.passive and initialized:
|
||||
self.controls_update(CS, self.sm['carControl'], self.sm['carControlSP'])
|
||||
self.controls_update(CS, self.sm['carControl'], self.sm['carControlSP'], CS_SP.fordPscmStatus)
|
||||
|
||||
self.initialized_prev = initialized
|
||||
self.CS_prev = CS
|
||||
|
||||
@@ -0,0 +1,280 @@
|
||||
"""Default-off joint C0/C1 trial at the existing Ford transmit boundary.
|
||||
|
||||
The observer consumes decoded packets queued to panda, not tentative requests.
|
||||
It estimates older-firmware state; it cannot observe ECU RAM or prove acceptance.
|
||||
"""
|
||||
|
||||
import math
|
||||
|
||||
from opendbc.car.ford.values import FordFlags
|
||||
from openpilot.selfdrive.controls.lib.ford_path import joint_control_speed
|
||||
|
||||
# Small steering-wheel trim around the nominal inverse, in degrees. These are
|
||||
# trial tuning values, not recovered PSCM constants. Entry assist is separately gated.
|
||||
ANGLE_TRIM_MAX = 2.0
|
||||
ANGLE_TRIM_KI = 0.2 # 1/s; error clipping limits adaptation to 0.4 deg/s.
|
||||
ANGLE_TRIM_ERROR_MAX = 5.0
|
||||
ANGLE_TRIM_RATE_MAX = 5.0 # deg/s: do not learn a bias from fast wheel motion.
|
||||
TARGET_PREVIEW = 0.1 # seconds; causal action-trend forecast, then hold.
|
||||
TURN_ENTRY_C0_MAX = 0.60 # metres; experimental post-encoder correction budget.
|
||||
TURN_ENTRY_C1_MAX = 0.04 # radians; not a recovered Ford calibration value.
|
||||
|
||||
|
||||
def turn_entry_weight(target, angle, requested_rate):
|
||||
"""Fade in only for a large, growing turn request with substantial wheel lag."""
|
||||
direction = math.copysign(1.0, target)
|
||||
size = max(0.0, min(1.0, (abs(target) - 30.0) / 30.0))
|
||||
lag = max(0.0, min(1.0, (direction * (target - angle) - 25.0) / 25.0))
|
||||
growth = max(0.0, min(1.0, direction * requested_rate / 30.0))
|
||||
return size * lag * growth
|
||||
|
||||
|
||||
def joint_control_enabled(CP, params):
|
||||
return bool(
|
||||
CP.brand == 'ford'
|
||||
and CP.flags & FordFlags.CANFD
|
||||
and params.get_bool('FordModelActionController')
|
||||
and params.get_bool('FordPscmJointControl')
|
||||
and not params.get_bool('FordGeometryReference')
|
||||
and not params.get_bool('JoystickDebugMode')
|
||||
)
|
||||
|
||||
|
||||
def select_joint_control(CP, params):
|
||||
# No calibration or native-library load on the normal/default path.
|
||||
if not joint_control_enabled(CP, params):
|
||||
return None
|
||||
return FordJointControl(CP, turn_entry_assist=params.get_bool('FordPscmTurnEntryAssist'),
|
||||
turn_preview=params.get_bool('FordPscmTurnPreview'))
|
||||
|
||||
|
||||
class FordJointControl:
|
||||
def __init__(self, CP, *, turn_entry_assist=False, turn_preview=False):
|
||||
from opendbc.can import CANParser
|
||||
from opendbc.car.ford.fordcan import CanBus
|
||||
from openpilot.selfdrive.controls.lib.ford_joint.model import MainRequest
|
||||
from openpilot.selfdrive.controls.lib.ford_joint.angle import AngleModel
|
||||
from openpilot.selfdrive.controls.lib.ford_joint.encoder import PairedRelease
|
||||
|
||||
self.turn_entry_assist = turn_entry_assist
|
||||
self.turn_preview = turn_preview
|
||||
self.wheelbase, self.ratio = CP.wheelbase, CP.steerRatio
|
||||
if not all(math.isfinite(v) and v > 0 for v in (self.wheelbase, self.ratio)):
|
||||
raise ValueError('Joint control requires finite positive vehicle geometry')
|
||||
self.request = MainRequest(native_lookup=True)
|
||||
self.angle = AngleModel(self.request.cal)
|
||||
self.encoder = PairedRelease(self.request, preserve_now=True)
|
||||
self.bus = CanBus(CP).main
|
||||
self.parser = CANParser('ford_lincoln_base_pt', [('LateralMotionControl2', 100)], self.bus)
|
||||
self.last_time = None
|
||||
self.last_sent_time = None
|
||||
self.phase = 0.0
|
||||
self.sent = (0.0, 0.0, False)
|
||||
self.measurement = (0.0, 0.0, 0.0)
|
||||
self.fault = ''
|
||||
self.angle_trim = 0.0
|
||||
self.wheel_rate = 0.0
|
||||
self.last_target = None
|
||||
self.requested_rate = 0.0
|
||||
self.diagnostics = {'hypothesis': 'ford-joint-v24', 'status': 'inactive'}
|
||||
|
||||
def advance(self, now):
|
||||
"""Retain channel/filter state through inactive commands and driver override."""
|
||||
if self.last_time is not None:
|
||||
elapsed = now - self.last_time
|
||||
if not 0.0 <= elapsed <= 0.1:
|
||||
# Lost command history cannot be repaired by pretending the held state
|
||||
# reset. Disable this trial until the next onroad process start.
|
||||
self.fault = 'command_timing_gap'
|
||||
else:
|
||||
self.phase += elapsed
|
||||
ticks = int((self.phase + 1e-12) / 0.008)
|
||||
self.phase -= ticks * 0.008
|
||||
speed, angle, yaw = self.measurement
|
||||
for _ in range(ticks):
|
||||
r = self.request.step(speed, self.sent[0], self.sent[1], active=self.sent[2], freeze_i=True)
|
||||
self.angle.step(speed, r['filtered_curvature'], angle, yaw, yaw * speed / 3.6, self.wheelbase, self.ratio)
|
||||
self.last_time = now
|
||||
|
||||
def prepare(self, CC, CC_SP, CS, now, *, fresh=True, pscm_status=None, turn_preview=None):
|
||||
from openpilot.selfdrive.controls.lib.ford_joint.inverse import C0_BOUND, C1_BOUND, invert_angle, quantize
|
||||
|
||||
dt = now - self.last_time if self.last_time is not None else 0.0
|
||||
self.advance(now)
|
||||
path = CC_SP.fordLateralPath
|
||||
target = float(CC.actuators.steeringAngleDeg)
|
||||
speed_ms = joint_control_speed(CS)
|
||||
# controlsd publishes calibrated car-frame motion and gates the path on its
|
||||
# health/freshness. Raw Ford CAN yaw has a zero offset on the audited truck.
|
||||
# Calibrated Z is opposite the CAN/pinion sign used by the recovered model.
|
||||
yaw = -float(CC.angularVelocity[2]) if len(CC.angularVelocity) == 3 else math.nan
|
||||
finite = all(math.isfinite(v) for v in (speed_ms, CS.steeringAngleDeg, CS.yawRate, yaw, target))
|
||||
if finite:
|
||||
# Ford does not populate CarState.steeringRateDeg. Derive motion from the
|
||||
# measured angle; 0.1 s filtering suppresses its 0.1-degree quantization.
|
||||
if 0.0 < dt <= 0.1:
|
||||
rate = (CS.steeringAngleDeg - self.measurement[1]) / dt
|
||||
self.wheel_rate += dt / (0.1 + dt) * (rate - self.wheel_rate)
|
||||
self.measurement = (max(0.0, speed_ms * 3.6), CS.steeringAngleDeg, yaw)
|
||||
else:
|
||||
self.wheel_rate = 0.0
|
||||
status_fresh = pscm_status is not None and pscm_status.valid and pscm_status.canMonoTime > 0 and -0.005 <= now - pscm_status.canMonoTime * 1e-9 <= 0.15
|
||||
# Like upstream Ford, steeringPressed alone does not zero the path. Retain
|
||||
# disengagement/fault gates and the PSCM's explicit override/denial status.
|
||||
override = bool(status_fresh and (pscm_status.limit == 3 or pscm_status.denied))
|
||||
valid = bool(
|
||||
fresh
|
||||
and CS.canValid
|
||||
and finite
|
||||
and 0.0 <= speed_ms <= 55
|
||||
and abs(CS.yawRate) <= 3
|
||||
and abs(yaw) <= 3
|
||||
and path.enabled
|
||||
and path.valid
|
||||
and not CS.steerFaultTemporary
|
||||
and not CS.steerFaultPermanent
|
||||
)
|
||||
# A missing previous transmit receipt means the prediction is not synchronized.
|
||||
if self.last_sent_time is not None and now - self.last_sent_time > 0.1:
|
||||
self.fault = 'missing_transmit_history'
|
||||
active = bool(CC.latActive and valid and not override and not self.fault)
|
||||
if not active:
|
||||
self.angle_trim = 0.0
|
||||
self.last_target = None
|
||||
self.requested_rate = 0.0
|
||||
command = (0.0, 0.0)
|
||||
details = {}
|
||||
stop_hold = active and speed_ms < 0.3
|
||||
if stop_hold:
|
||||
# The moving inverse divides by v**2. At a stop retain the last actual
|
||||
# active packet, not an inactive/zero request or a fictitious speed.
|
||||
# A disengagement/fault packet clears this naturally via record_sent.
|
||||
command = self.sent[:2] if self.sent[2] else command
|
||||
self.last_target = target
|
||||
self.requested_rate = 0.0
|
||||
elif active:
|
||||
try:
|
||||
speed, angle, yaw = self.measurement
|
||||
if self.last_target is not None and 0.0 < dt <= 0.1:
|
||||
rate = (target - self.last_target) / dt
|
||||
self.requested_rate += dt / (0.1 + dt) * (rate - self.requested_rate)
|
||||
self.last_target = target
|
||||
error = target - angle
|
||||
settling = abs(error) <= ANGLE_TRIM_ERROR_MAX and abs(self.wheel_rate) <= ANGLE_TRIM_RATE_MAX
|
||||
# Brief wheel motion freezes learning but does not invalidate a learned
|
||||
# bias. Clear opposing trim for a large error or a fast-moving request.
|
||||
release = abs(error) > ANGLE_TRIM_ERROR_MAX or abs(self.requested_rate) > ANGLE_TRIM_RATE_MAX
|
||||
if not CS.steeringPressed and release and error * self.angle_trim < 0.0:
|
||||
self.angle_trim = 0.0
|
||||
trimmed_target = target + self.angle_trim
|
||||
entry_lead, cue = 0.0, 0.0
|
||||
if self.turn_preview and not CS.steeringPressed and not (status_fresh and pscm_status.limit >= 2):
|
||||
from openpilot.selfdrive.controls.lib.ford_turn_preview import geometry_entry_lead, turn_preview_lead
|
||||
entry_lead, cue = turn_preview_lead(self.requested_rate, turn_preview, now)
|
||||
independent = geometry_entry_lead(trimmed_target, self.requested_rate, turn_preview, now)
|
||||
if abs(independent) > abs(entry_lead):
|
||||
entry_lead = independent
|
||||
signal = int(CS.leftBlinker) - int(CS.rightBlinker)
|
||||
if signal * entry_lead <= 0.0:
|
||||
entry_lead = 0.0
|
||||
inverse_target = trimmed_target + entry_lead
|
||||
inverse = invert_angle(self.angle, speed, inverse_target, angle, yaw, yaw * speed / 3.6, self.wheelbase, self.ratio)
|
||||
# Firmware phase is estimated from elapsed time. Cover the 1/2 firmware
|
||||
# ticks before the next nominal 100 Hz transmit; all phases are tested.
|
||||
ticks = max(1, min(2, int((self.phase + 0.01 + 1e-12) / 0.008)))
|
||||
command, info = self.encoder.choose(
|
||||
speed, inverse['curvature'], phase=0 if ticks == 2 else 2,
|
||||
curvature_rate=self.requested_rate / inverse['slope_per_curvature'], preview=TARGET_PREVIEW,
|
||||
curvature_bound=inverse['allowance'] / (speed / 3.6)**2,
|
||||
)
|
||||
base_command = command
|
||||
entry_weight = 0.0
|
||||
if self.turn_entry_assist and not CS.steeringPressed and not inverse['accel_limited'] and not (status_fresh and pscm_status.limit >= 2):
|
||||
entry_weight = turn_entry_weight(target, angle, self.requested_rate)
|
||||
if entry_weight:
|
||||
extra = math.copysign(entry_weight, target)
|
||||
command = quantize((command[0] + extra * TURN_ENTRY_C0_MAX, command[1] + extra * TURN_ENTRY_C1_MAX))
|
||||
# Apply after selection so nominal slew equivalence cannot discard the
|
||||
# added request. record_sent/advance still observe the actual packets;
|
||||
# their retained state can affect later commands after this term clears.
|
||||
details = {
|
||||
'geometry_lead': entry_lead,
|
||||
'geometry_cue': cue,
|
||||
'inverse_target': inverse_target,
|
||||
'base_wire_command': tuple(map(float, base_command)),
|
||||
'turn_entry_weight': entry_weight,
|
||||
'turn_entry_c0': float(command[0] - base_command[0]),
|
||||
'turn_entry_c1': float(command[1] - base_command[1]),
|
||||
'requested_angle': target,
|
||||
'trimmed_angle': trimmed_target,
|
||||
'angle_trim': self.angle_trim,
|
||||
'wheel_rate': self.wheel_rate,
|
||||
'requested_rate': self.requested_rate,
|
||||
'target_preview_seconds': TARGET_PREVIEW,
|
||||
'target_preview_delta': self.requested_rate * TARGET_PREVIEW,
|
||||
'target_preview_limited': info['preview_limited'],
|
||||
'reachable_angle': inverse['reachable_target'],
|
||||
'target_curvature': inverse['curvature'],
|
||||
'predicted_curvature': float(info['first_state'][3]), # Encoder prediction before optional entry assist.
|
||||
'accel_limited': inverse['accel_limited'],
|
||||
}
|
||||
# Learn only small residual errors while the wheel is settling. Large
|
||||
# entry lag and saturated actuation must not wind up a centering trim.
|
||||
# Opposing error can still bleed existing trim toward zero. A light
|
||||
# touch freezes learning; it does not cut the base command or held trim.
|
||||
limited = inverse['accel_limited'] or (status_fresh and pscm_status.limit == 2) or abs(command[0]) >= C0_BOUND or abs(command[1]) >= C1_BOUND
|
||||
learning = CS.vEgo >= 2.0 and settling and not limited
|
||||
if not CS.steeringPressed:
|
||||
increment = ANGLE_TRIM_KI * max(-ANGLE_TRIM_MAX, min(ANGLE_TRIM_MAX, error)) * dt
|
||||
if not learning:
|
||||
increment = max(min(-self.angle_trim, 0.0), min(max(-self.angle_trim, 0.0), increment))
|
||||
self.angle_trim = max(-ANGLE_TRIM_MAX, min(ANGLE_TRIM_MAX, self.angle_trim + increment))
|
||||
details['trim_learning'] = bool(learning and not CS.steeringPressed)
|
||||
except (ValueError, OverflowError, ArithmeticError):
|
||||
self.fault = 'invalid_prediction'
|
||||
self.angle_trim = 0.0
|
||||
active = False
|
||||
command = (0.0, 0.0)
|
||||
# The inverse operates in CAN/pinion coordinates. CarController negates
|
||||
# FordLateralPath on TX, so negate here exactly once to retain the tested sign.
|
||||
path.pathOffset, path.pathAngle = -float(command[0]), -float(command[1])
|
||||
path.curvature = path.curvatureRate = 0.0
|
||||
path.valid = active
|
||||
cc = CC.as_reader().as_builder() if hasattr(CC, 'as_reader') else CC.as_builder()
|
||||
cc.latActive = active
|
||||
self.diagnostics = {
|
||||
'hypothesis': 'ford-joint-geometry-entry-v2' if self.turn_preview else ('ford-joint-turn-entry-v1' if self.turn_entry_assist else 'ford-joint-v24'),
|
||||
'turn_preview_enabled': self.turn_preview,
|
||||
'turn_entry_enabled': self.turn_entry_assist,
|
||||
'turn_entry_weight': 0.0,
|
||||
'turn_entry_c0': 0.0,
|
||||
'turn_entry_c1': 0.0,
|
||||
'status': self.fault or ('active' if active else 'inactive'),
|
||||
'driver_override': override,
|
||||
'driver_pressed': bool(CS.steeringPressed),
|
||||
'stop_hold': bool(stop_hold),
|
||||
'allocation': 'equal-arrival-supervisor-c0',
|
||||
'yaw_source': 'calibrated_pose',
|
||||
'yaw_rate': yaw if math.isfinite(yaw) else None,
|
||||
'can_yaw_rate': float(CS.yawRate) if math.isfinite(CS.yawRate) else None,
|
||||
'held_c0': self.request.c0,
|
||||
'held_c1': self.request.c1,
|
||||
'filtered_curvature': self.request.filtered,
|
||||
'wire_command': tuple(map(float, command)),
|
||||
**details,
|
||||
}
|
||||
# Preserve card's reader contract: CarController copies actuators with
|
||||
# as_builder(), which is only available on a Cap'n Proto reader.
|
||||
return cc.as_reader()
|
||||
|
||||
def record_sent(self, can_sends, now_nanos):
|
||||
packets = [msg for msg in can_sends if msg[0] == 0x3D6 and msg[2] == self.bus]
|
||||
if not packets:
|
||||
return
|
||||
self.parser.update([now_nanos, packets])
|
||||
values = self.parser.vl['LateralMotionControl2']
|
||||
if values['LatCtlCurv_No_Actl'] != 0.0 or values['LatCtlCrv_NoRate2_Actl'] != 0.0:
|
||||
self.fault = 'unexpected_curvature_channel'
|
||||
self.sent = (values['LatCtlPathOffst_L_Actl'], values['LatCtlPath_An_Actl'], values['LatCtl_D2_Rq'] == 2)
|
||||
self.last_sent_time = now_nanos * 1e-9
|
||||
self.diagnostics['wire_sent'] = self.sent
|
||||
@@ -0,0 +1,36 @@
|
||||
"""Publish the Ford PSCM's actual CAN status without changing opendbc structs."""
|
||||
import math
|
||||
|
||||
from opendbc.car import Bus
|
||||
from opendbc.car.ford.values import FordFlags
|
||||
|
||||
|
||||
MESSAGE = 'Lane_Assist_Data3_FD1'
|
||||
SIGNALS = ('LatCtlSte_D_Stat', 'LatCtlLim_D_Stat', 'LatCtlCpblty_D_Stat', 'LaActDeny_B_Actl')
|
||||
|
||||
|
||||
def populate_ford_pscm_status(CP, can_parsers, CS_SP, can_valid):
|
||||
if CP.brand != 'ford' or not CP.flags & FordFlags.CANFD:
|
||||
return
|
||||
status = CS_SP.init('fordPscmStatus')
|
||||
parser = can_parsers.get(Bus.pt)
|
||||
if parser is None:
|
||||
return
|
||||
values = parser.vl.get(MESSAGE, {})
|
||||
timestamps = parser.ts_nanos.get(MESSAGE, {})
|
||||
if any(signal not in values or signal not in timestamps for signal in SIGNALS):
|
||||
return
|
||||
received = timestamps[SIGNALS[0]]
|
||||
if received <= 0 or any(timestamps[signal] != received for signal in SIGNALS):
|
||||
return
|
||||
decoded = [values[signal] for signal in SIGNALS]
|
||||
if any(not math.isfinite(value) or int(value) != value or not 0 <= value <= maximum
|
||||
for value, maximum in zip(decoded, (7, 3, 3, 1), strict=True)):
|
||||
return
|
||||
status.canMonoTime = received
|
||||
status.lateralState, status.limit, status.capability = map(int, decoded[:3])
|
||||
status.denied = bool(decoded[3])
|
||||
# CI.update already checked all parser validity. Reading can_valid again here
|
||||
# would advance the parser's invalid-message counter a second time per tick.
|
||||
# Age is evaluated by the feedback consumer using this original CAN timestamp.
|
||||
status.valid = bool(can_valid)
|
||||
@@ -54,6 +54,8 @@ def convert_carControlSP(struct: capnp.lib.capnp._DynamicStructReader) -> struct
|
||||
return {k: v for k, v in s.items() if not k.endswith('DEPRECATED')}
|
||||
|
||||
struct_dict = struct.to_dict()
|
||||
# Consumed by card's Ford adapter, not by opendbc or the CAN packer.
|
||||
struct_dict.pop('fordTurnPreview', None)
|
||||
struct_dataclass = structs.CarControlSP(**remove_deprecated({k: v for k, v in struct_dict.items() if not isinstance(k, dict)}))
|
||||
|
||||
struct_dataclass.mads = structs.ModularAssistiveDrivingSystem(**remove_deprecated(struct_dict.get('mads', {})))
|
||||
@@ -63,5 +65,6 @@ def convert_carControlSP(struct: capnp.lib.capnp._DynamicStructReader) -> struct
|
||||
struct_dataclass.intelligentCruiseButtonManagement = structs.IntelligentCruiseButtonManagement(
|
||||
**remove_deprecated(struct_dict.get('intelligentCruiseButtonManagement', {}))
|
||||
)
|
||||
struct_dataclass.fordLateralPath = structs.FordLateralPath(**remove_deprecated(struct_dict.get('fordLateralPath', {})))
|
||||
|
||||
return struct_dataclass
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
"""Native encoder calls must release their arguments with card's GC disabled."""
|
||||
import ctypes
|
||||
import gc
|
||||
import weakref
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
from openpilot.selfdrive.controls.lib.ford_joint.encoder import DoubleArray, PairedRelease
|
||||
from openpilot.selfdrive.controls.lib.ford_joint.model import MainRequest
|
||||
|
||||
|
||||
@pytest.mark.parametrize('preview', [0., .1])
|
||||
def test_native_arguments_do_not_accumulate_without_gc(preview):
|
||||
encoder = PairedRelease(MainRequest(native_lookup=True))
|
||||
kwargs = {'curvature_rate': .01, 'preview': preview, 'curvature_bound': .04}
|
||||
encoder.choose(20., .01, **kwargs)
|
||||
enabled = gc.isenabled()
|
||||
gc.collect()
|
||||
gc.disable()
|
||||
try:
|
||||
before = sum(isinstance(obj, ctypes.c_void_p) for obj in gc.get_objects())
|
||||
for _ in range(500):
|
||||
encoder.choose(20., .01, **kwargs)
|
||||
after = sum(isinstance(obj, ctypes.c_void_p) for obj in gc.get_objects())
|
||||
assert after <= before, f'{after - before} native pointer objects retained in 500 control updates'
|
||||
finally:
|
||||
gc.collect()
|
||||
if enabled:
|
||||
gc.enable()
|
||||
|
||||
|
||||
@pytest.mark.parametrize('value', [[1., 2.], np.ones(3, dtype=np.float32), np.ones(6)[::2]])
|
||||
def test_native_array_validation_is_retained(value):
|
||||
with pytest.raises(TypeError):
|
||||
DoubleArray.from_param(value)
|
||||
|
||||
|
||||
def test_native_argument_owns_array_only_until_call_finishes():
|
||||
value = np.array([1., 2.])
|
||||
ref = weakref.ref(value)
|
||||
pointer = DoubleArray.from_param(value)
|
||||
assert pointer.value == value.ctypes.data
|
||||
del value
|
||||
assert ref() is not None
|
||||
del pointer
|
||||
assert ref() is None
|
||||
@@ -0,0 +1,103 @@
|
||||
"""A field reaching its bound must not discard a jointly reachable target."""
|
||||
import pytest
|
||||
|
||||
from openpilot.selfdrive.controls.lib.ford_joint.angle import AngleModel
|
||||
from openpilot.selfdrive.controls.lib.ford_joint.encoder import PairedRelease
|
||||
from openpilot.selfdrive.controls.lib.ford_joint.inverse import C0_BOUND, C0_SUPERVISOR_SATURATION, C1_BOUND, invert_angle, static_pair
|
||||
from openpilot.selfdrive.controls.lib.ford_joint.model import MainRequest
|
||||
|
||||
|
||||
@pytest.mark.parametrize('sign', [-1., 1.])
|
||||
def test_joint_encoder_preserves_large_reachable_target(sign):
|
||||
request = MainRequest(native_lookup=True)
|
||||
speed = 15 * 1.609344
|
||||
inverse = invert_angle(AngleModel(request.cal), speed, sign * 280., 0., 0., 0., 3.7, 16.9)
|
||||
assert not inverse['accel_limited']
|
||||
probe = MainRequest(native_lookup=True).step(speed, 0., 0., freeze_i=True)
|
||||
c0, c1 = static_pair(request, inverse['curvature'], speed)
|
||||
assert abs(c1) <= C1_BOUND
|
||||
assert abs(c0) <= C0_BOUND
|
||||
assert probe['g0'] * c0 + probe['g1'] * c1 == pytest.approx(inverse['curvature'], abs=1e-12)
|
||||
|
||||
_, info = PairedRelease(request).choose(speed, inverse['curvature'])
|
||||
# The planned target is the CAN-quantized steady pair, not next-tick wheel motion.
|
||||
quantization_error = probe['g0'] * .005 + probe['g1'] * .00025
|
||||
assert abs(info['planned_target'] - inverse['curvature']) <= quantization_error + 1e-12
|
||||
|
||||
|
||||
@pytest.mark.parametrize('speed', [8., 15., 19.312128, 24.14016, 28.968192, 50., 100., 180.])
|
||||
@pytest.mark.parametrize('fraction', [-2., -1., -.8, -.3, 0., .3, .8, 1., 2.])
|
||||
def test_static_allocation_uses_available_combined_field_range(speed, fraction):
|
||||
request = MainRequest(native_lookup=True)
|
||||
probe = request.step(speed, 0., 0., freeze_i=True)
|
||||
g0, g1 = probe['g0'], probe['g1']
|
||||
capacity = g0 * C0_BOUND + g1 * C1_BOUND
|
||||
c0, c1 = static_pair(request, fraction * capacity, speed)
|
||||
assert abs(c0) <= C0_BOUND and abs(c1) <= C1_BOUND
|
||||
expected = max(-capacity, min(capacity, fraction * capacity))
|
||||
assert g0 * c0 + g1 * c1 == pytest.approx(expected, abs=1e-12)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('sign', [-1., 1.])
|
||||
def test_unsaturated_pair_reaches_both_endpoints_together(sign):
|
||||
# Below the supervisor's C0 saturation the endpoint is still equal arrival.
|
||||
request = MainRequest(native_lookup=True)
|
||||
speed = 15 * 1.609344
|
||||
inverse = invert_angle(AngleModel(request.cal), speed, sign * 40., 0., 0., 0., 3.7, 16.9)
|
||||
c0, c1 = static_pair(request, inverse['curvature'], speed)
|
||||
assert abs(c0) < C0_SUPERVISOR_SATURATION
|
||||
assert c0 / request.cal.f(0xFEF259F8) == pytest.approx(c1 / request.cal.f(0xFEF25A08))
|
||||
probe = request.step(speed, 0., 0., freeze_i=True)
|
||||
assert probe['g0'] * c0 + probe['g1'] * c1 == pytest.approx(inverse['curvature'])
|
||||
|
||||
|
||||
@pytest.mark.parametrize('speed', [8., 15., 19.312128, 24.14016, 28.968192, 50., 100., 180.])
|
||||
@pytest.mark.parametrize('fraction', [.05, .2, .4, .6, .8, .95, 1.])
|
||||
@pytest.mark.parametrize('sign', [-1., 1.])
|
||||
def test_endpoint_holds_c0_at_supervisor_saturation(speed, fraction, sign):
|
||||
request = MainRequest(native_lookup=True)
|
||||
probe = request.step(speed, 0., 0., freeze_i=True)
|
||||
g0, g1 = probe['g0'], probe['g1']
|
||||
curvature = sign * fraction * (g0 * C0_BOUND + g1 * C1_BOUND)
|
||||
c0, c1 = static_pair(request, curvature, speed)
|
||||
assert g0 * c0 + g1 * c1 == pytest.approx(curvature, abs=1e-12)
|
||||
# Held C0 exceeds the supervisor saturation only when C1 alone cannot carry the rest.
|
||||
assert abs(c0) <= C0_SUPERVISOR_SATURATION + 1e-12 or abs(c1) == C1_BOUND
|
||||
assert static_pair(request, -curvature, speed) == pytest.approx((-c0, -c1), abs=1e-15)
|
||||
|
||||
|
||||
def _equal_arrival(request, curvature, speed_kmh, *, gains):
|
||||
r0, r1 = request.cal.f(0xFEF259F8), request.cal.f(0xFEF25A08)
|
||||
duration = curvature / (gains[0] * r0 + gains[1] * r1)
|
||||
return duration * r0, duration * r1
|
||||
|
||||
|
||||
@pytest.mark.parametrize('speed,curvature', [(15., .06), (20., .05), (25., .04)])
|
||||
def test_capped_endpoint_keeps_entry_and_parks_c0_before_release(monkeypatch, speed, curvature):
|
||||
"""Production selector, quantized packets: same rise, less held C0 at the end of the hold."""
|
||||
from openpilot.selfdrive.controls.lib.ford_joint import encoder
|
||||
from openpilot.selfdrive.controls.lib.ford_joint.inverse import quantize
|
||||
|
||||
def run():
|
||||
m = MainRequest(native_lookup=True)
|
||||
selector = PairedRelease(m)
|
||||
sent, phase, rise = (0., 0.), 0., None
|
||||
for i in range(500): # 5 s step-and-hold at 100 Hz
|
||||
phase += .01
|
||||
ticks = int((phase + 1e-12) / .008)
|
||||
phase -= ticks * .008
|
||||
for _ in range(ticks):
|
||||
m.step(speed, *sent, freeze_i=True)
|
||||
if rise is None and m.filtered >= .9 * curvature:
|
||||
rise = i * .01
|
||||
count = max(1, min(2, int((phase + .01 + 1e-12) / .008)))
|
||||
command, _ = selector.choose(speed, curvature, phase=0 if count == 2 else 2)
|
||||
sent = quantize(command)
|
||||
return rise, m.c0, m.filtered
|
||||
|
||||
capped = run()
|
||||
monkeypatch.setattr(encoder, 'static_pair', _equal_arrival)
|
||||
equal = run()
|
||||
assert capped[0] == pytest.approx(equal[0], abs=.02)
|
||||
assert capped[2] == pytest.approx(curvature, rel=.01)
|
||||
assert abs(capped[1]) <= C0_SUPERVISOR_SATURATION + .15 < abs(equal[1])
|
||||
@@ -0,0 +1,553 @@
|
||||
"""Exercise the opt-in trial through the production Ford CAN packer."""
|
||||
import copy
|
||||
import itertools
|
||||
import json
|
||||
import math
|
||||
from collections import defaultdict
|
||||
from types import SimpleNamespace
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
from opendbc.car import Bus, structs
|
||||
from opendbc.car.ford.carcontroller import CarController
|
||||
from opendbc.car.ford.values import FordFlags
|
||||
from opendbc.car.interfaces import CarInterfaceBase
|
||||
from openpilot.common.params import Params, ParamKeyFlag
|
||||
from openpilot.selfdrive.car.ford_joint_control import ANGLE_TRIM_MAX, FordJointControl, joint_control_enabled, select_joint_control
|
||||
from openpilot.selfdrive.controls.lib.ford_joint.angle import AngleModel
|
||||
from openpilot.selfdrive.controls.lib.ford_joint.encoder import PairedRelease, state
|
||||
from openpilot.selfdrive.controls.lib.ford_joint.inverse import invert_angle
|
||||
from openpilot.selfdrive.controls.lib.ford_joint.model import MainRequest
|
||||
from openpilot.selfdrive.controls.tests.test_ford_model_action_adapter import update
|
||||
from openpilot.selfdrive.controls.tests.test_ford_model_action_selection import car_params, startup
|
||||
|
||||
|
||||
def cp():
|
||||
return structs.CarParams(brand='ford', flags=int(FordFlags.CANFD), carFingerprint='FORD_F_150_LIGHTNING_MK1', wheelbase=3.7, steerRatio=16.9)
|
||||
|
||||
|
||||
def controls(target=30., active=True):
|
||||
cc = structs.CarControl(latActive=active, longActive=True)
|
||||
cc.angularVelocity = [0., 0., 0.]
|
||||
cc.actuators.steeringAngleDeg = target
|
||||
cc.actuators.accel = .3
|
||||
sp = structs.CarControlSP()
|
||||
sp.fordLateralPath.enabled = sp.fordLateralPath.valid = True
|
||||
return cc, sp
|
||||
|
||||
|
||||
class Pipeline:
|
||||
def __init__(self):
|
||||
self.joint = FordJointControl(cp())
|
||||
self.sender = CarController({Bus.pt: 'ford_lincoln_base_pt'}, cp(), structs.CarParamsSP())
|
||||
self.cs = structs.CarState(vEgo=5.36, vEgoRaw=5.36, canValid=True)
|
||||
self.vehicle = SimpleNamespace(out=self.cs, acc_tja_status_stock_values=defaultdict(int),
|
||||
lkas_status_stock_values=defaultdict(int), buttons_stock_values=defaultdict(int))
|
||||
self.interface = SimpleNamespace(CC=self.sender, CS=self.vehicle)
|
||||
|
||||
def tick(self, now, target=30., active=True, *, angular_velocity=None, **kwargs):
|
||||
cc, sp = controls(target, active)
|
||||
cc.angularVelocity = [0., 0., -self.cs.yawRate] if angular_velocity is None else angular_velocity
|
||||
result = self.joint.prepare(cc.as_reader(), sp, self.cs, now, **kwargs)
|
||||
_, packets = CarInterfaceBase.apply(self.interface, result, sp, round(now * 1e9))
|
||||
assert sum(p[0] == 0x3d6 for p in packets) == 1
|
||||
self.joint.record_sent(packets, round(now * 1e9))
|
||||
assert result.longActive and result.actuators.accel == cc.actuators.accel
|
||||
wire = self.joint.parser.vl['LateralMotionControl2']
|
||||
assert wire['LatCtlCurv_No_Actl'] == wire['LatCtlCrv_NoRate2_Actl'] == 0.
|
||||
assert abs(wire['LatCtlPathOffst_L_Actl']) <= 5.11 + 1e-10
|
||||
assert abs(wire['LatCtlPath_An_Actl']) <= .5 + 1e-10
|
||||
assert wire['LatCtl_D2_Rq'] == (2 if result.latActive else 0)
|
||||
assert self.joint.sent == pytest.approx((*self.joint.diagnostics['wire_command'], result.latActive))
|
||||
return result
|
||||
|
||||
|
||||
@pytest.mark.parametrize('master,trial,geometry,joystick', itertools.product((False, True), repeat=4))
|
||||
@pytest.mark.parametrize('compatible', [False, True])
|
||||
def test_startup_gate_matches_both_processes(master, trial, geometry, joystick, compatible):
|
||||
params = SimpleNamespace(get_bool=lambda k: {'FordModelActionController': master, 'FordPscmJointControl': trial,
|
||||
'FordGeometryReference': geometry, 'JoystickDebugMode': joystick}.get(k, False))
|
||||
params_cp = car_params(flags=FordFlags.CANFD if compatible else 0)
|
||||
expected = compatible and master and trial and not geometry and not joystick
|
||||
assert joint_control_enabled(params_cp, params) == expected
|
||||
selected = startup(params_cp, params).ford_path_controller
|
||||
assert bool(selected and selected.joint_control) == expected
|
||||
if not expected:
|
||||
assert select_joint_control(params_cp, params) is None
|
||||
if not master:
|
||||
assert selected is None
|
||||
|
||||
|
||||
def test_parameter_is_persistent_default_off(tmp_path):
|
||||
params = Params(str(tmp_path))
|
||||
assert params.get_default_value('FordPscmJointControl') is False
|
||||
assert not params.get_bool('FordPscmJointControl')
|
||||
for flag in (ParamKeyFlag.PERSISTENT, ParamKeyFlag.BACKUP):
|
||||
assert b'FordPscmJointControl' in params.all_keys(flag)
|
||||
|
||||
|
||||
def test_reference_carrier_adds_no_second_feedback_loop():
|
||||
from openpilot.selfdrive.controls.lib.ford_model_action import FordModelActionController
|
||||
c = FordModelActionController(joint_control=True, c0_time_based=True)
|
||||
for i in range(100):
|
||||
p = update(c, now=1+i*.01, current_curvature=-.005)
|
||||
assert p.valid and p.path_offset == p.path_angle == p.curvature == p.curvature_rate == 0.
|
||||
assert c.core.correction == c.core.proportional == c.core.offset_proportional == 0.
|
||||
assert not c.set_c0_time_based(True, lateral_engaged=False)
|
||||
|
||||
|
||||
def test_joint_reference_carrier_stays_valid_through_stop():
|
||||
from openpilot.selfdrive.controls.lib.ford_model_action import FordModelActionController
|
||||
c = FordModelActionController(joint_control=True)
|
||||
for i, speed in enumerate([5., .3, .29, .01, 0., 0., .01, .29, .3, 5.]):
|
||||
path = update(c, now=1+i*.01, speed=speed)
|
||||
assert path.valid, (speed, c.diagnostics)
|
||||
assert path.path_offset == path.path_angle == 0.
|
||||
assert not update(c, now=1.1, speed=0., valid=False).valid
|
||||
|
||||
|
||||
@pytest.mark.parametrize('sign', [-1., 1.])
|
||||
def test_stop_repeats_transmitted_request_and_resume_keeps_target(sign):
|
||||
p = Pipeline()
|
||||
p.cs.steeringAngleDeg = sign*120.
|
||||
p.cs.yawRate = p.cs.vEgo*math.radians(sign*120.)/(3.7*16.9)
|
||||
for i in range(200):
|
||||
p.tick(1+i*.01, sign*120.)
|
||||
sent = p.joint.sent
|
||||
assert sent[2] and any(abs(v) > .01 for v in sent[:2])
|
||||
p.joint.angle_trim = sign*.5
|
||||
for i, speed in enumerate([.29, .01]+[0.]*200+[.01, .29]):
|
||||
p.cs.vEgo = speed
|
||||
p.cs.yawRate = speed*math.radians(sign*120.)/(3.7*16.9)
|
||||
assert p.tick(3+i*.01, sign*120.).latActive
|
||||
assert p.joint.sent == sent
|
||||
assert p.joint.angle_trim == sign*.5
|
||||
assert not p.joint.fault
|
||||
json.dumps(p.joint.diagnostics, allow_nan=False)
|
||||
p.cs.vEgo = .3
|
||||
p.cs.yawRate = .3*math.radians(sign*120.)/(3.7*16.9)
|
||||
assert p.tick(5.04, sign*120.).latActive
|
||||
assert p.joint.diagnostics['requested_angle'] == sign*120.
|
||||
assert p.joint.diagnostics['requested_rate'] == 0.
|
||||
assert not p.joint.fault
|
||||
|
||||
|
||||
@pytest.mark.parametrize('sign', [-1., 1.])
|
||||
def test_stop_speed_filter_undershoot_keeps_actual_transmitted_hold(sign):
|
||||
p = Pipeline()
|
||||
p.cs.gearShifter = structs.CarState.GearShifter.drive
|
||||
for i in range(30):
|
||||
p.tick(1+i*.01, sign*120.)
|
||||
sent = p.joint.sent
|
||||
assert sent[2] and any(abs(v) > .01 for v in sent[:2])
|
||||
p.cs.vEgoRaw, p.cs.standstill = 0., True
|
||||
for i, speed in enumerate([.01, 0., -.01, -.04, -.01, 0., .01, .29]):
|
||||
p.cs.vEgo = speed
|
||||
assert p.tick(1.3+i*.01, sign*120.).latActive
|
||||
assert p.joint.sent == sent
|
||||
assert p.joint.diagnostics['stop_hold']
|
||||
p.cs.vEgo = p.cs.vEgoRaw = .3
|
||||
assert p.tick(1.38, sign*120.).latActive
|
||||
assert p.joint.sent[0]*sign > 0 and p.joint.sent[1]*sign > 0
|
||||
p.cs.gearShifter = structs.CarState.GearShifter.reverse
|
||||
assert not p.tick(1.39, sign*120.).latActive
|
||||
assert p.joint.sent == (0., 0., False)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('failure', ['disengage', 'stale', 'can', 'steering_fault', 'override', 'denied'])
|
||||
def test_stopped_hold_still_releases_and_does_not_resurrect_old_command(failure):
|
||||
p = Pipeline()
|
||||
for i in range(20):
|
||||
p.tick(1+i*.01, 120.)
|
||||
previous = p.joint.sent
|
||||
p.cs.vEgo = 0.
|
||||
assert p.tick(1.2, 120.).latActive
|
||||
assert p.joint.sent == previous
|
||||
p.cs.canValid = failure != 'can'
|
||||
p.cs.steerFaultTemporary = failure == 'steering_fault'
|
||||
status = SimpleNamespace(valid=True, canMonoTime=1_210_000_000, limit=3 if failure == 'override' else 0, denied=failure == 'denied')
|
||||
assert not p.tick(1.21, 120., active=failure != 'disengage', fresh=failure != 'stale', pscm_status=status).latActive
|
||||
assert p.joint.sent == (0., 0., False)
|
||||
p.cs.canValid, p.cs.steerFaultTemporary = True, False
|
||||
assert p.tick(1.22, 120.).latActive
|
||||
assert p.joint.sent == (0., 0., True)
|
||||
|
||||
|
||||
def test_can_yaw_zero_offset_does_not_bias_the_command():
|
||||
reference, biased = Pipeline(), Pipeline()
|
||||
biased.cs.yawRate = -.008 # Route 175: raw CAN disagrees with calibrated yaw.
|
||||
for i in range(200):
|
||||
now = 1. + i*.01
|
||||
reference.tick(now, 0., angular_velocity=[0., 0., 0.])
|
||||
biased.tick(now, 0., angular_velocity=[0., 0., 0.])
|
||||
assert biased.joint.sent == reference.joint.sent
|
||||
assert biased.joint.measurement[2] == 0.
|
||||
|
||||
|
||||
def test_calibrated_yaw_sign_matches_pinion_coordinates():
|
||||
p = Pipeline()
|
||||
p.cs.yawRate = .012
|
||||
p.tick(1., angular_velocity=[.01, .02, -.02])
|
||||
assert p.joint.measurement[2] == pytest.approx(.02)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('sign', [-1., 1.])
|
||||
def test_persistent_small_wheel_error_builds_bounded_correction(sign):
|
||||
# Route 17a: a steady request ~1.5 degrees from the wheel, no driver or
|
||||
# PSCM limit. The nominal encoder alone has no way to remove that residual.
|
||||
p = Pipeline()
|
||||
p.cs.vEgo = 20.
|
||||
before = None
|
||||
for i in range(601):
|
||||
p.tick(1. + i*.01, sign*1.5)
|
||||
if i == 100:
|
||||
before = p.joint.diagnostics['target_curvature']
|
||||
assert sign*(p.joint.diagnostics['target_curvature'] - before) > 0.00005
|
||||
assert 1.7 < sign*p.joint.angle_trim < ANGLE_TRIM_MAX
|
||||
|
||||
|
||||
@pytest.mark.parametrize('bias', [-1.5, 1.5])
|
||||
def test_trim_removes_static_mismatch_in_synthetic_angle_plant(bias):
|
||||
# A regression for residual feedback, NOT a validated PSCM/wheel simulation.
|
||||
# Use the actual packed-command observer and angle stage, then a deliberately
|
||||
# simple 0.4 s wheel lag with a known additive mismatch.
|
||||
p = Pipeline()
|
||||
p.cs.vEgo = 20.
|
||||
angle = rate = 0.
|
||||
errors = []
|
||||
for i in range(3000):
|
||||
p.cs.steeringAngleDeg, p.cs.steeringRateDeg = angle, rate
|
||||
p.cs.yawRate = math.radians(angle)/16.9*20./(3.7+(33/16384)*20**2)
|
||||
p.tick(1.+i*.01, 0.)
|
||||
nominal = copy.copy(p.joint.angle).step(72., p.joint.diagnostics['predicted_curvature'], angle,
|
||||
p.cs.yawRate, p.cs.yawRate*20., 3.7, 16.9)
|
||||
rate = (nominal+bias-angle)/.4
|
||||
angle += rate*.01
|
||||
errors.append(angle)
|
||||
assert np.mean(np.abs(errors[-500:])) < .1
|
||||
|
||||
|
||||
@pytest.mark.parametrize('target,rate,speed', [(60., 0., 5.), (1.5, 20., 5.), (1.5, 0., 1.)])
|
||||
def test_trim_does_not_learn_large_lag_fast_motion_or_crawl(target, rate, speed):
|
||||
p = Pipeline()
|
||||
p.cs.vEgo = speed
|
||||
for i in range(200):
|
||||
# Real Ford CarState leaves steeringRateDeg at zero, even during motion.
|
||||
p.cs.steeringAngleDeg = rate*i*.01
|
||||
p.tick(1.+i*.01, target+p.cs.steeringAngleDeg)
|
||||
assert abs(p.joint.angle_trim) < .015 # Only the derivative's initial settling.
|
||||
if rate:
|
||||
assert p.joint.wheel_rate == pytest.approx(rate, abs=.01)
|
||||
assert not p.joint.diagnostics['trim_learning']
|
||||
|
||||
|
||||
def test_trim_bounded_retained_at_zero_error_and_frozen_on_light_touch():
|
||||
p = Pipeline()
|
||||
for i in range(1201):
|
||||
p.tick(1.+i*.01, 2.)
|
||||
assert abs(p.joint.angle_trim) <= ANGLE_TRIM_MAX
|
||||
assert p.joint.angle_trim == ANGLE_TRIM_MAX
|
||||
p.cs.steeringPressed = True
|
||||
assert p.tick(13.01, -2.).latActive
|
||||
assert p.joint.angle_trim == ANGLE_TRIM_MAX
|
||||
p.cs.steeringPressed = False
|
||||
p.tick(13.02, 0.)
|
||||
assert p.joint.angle_trim == ANGLE_TRIM_MAX
|
||||
assert not p.tick(13.03, active=False).latActive
|
||||
assert p.joint.angle_trim == 0.
|
||||
|
||||
|
||||
@pytest.mark.parametrize('limit,denied', [(3, False), (0, True)])
|
||||
def test_explicit_override_resets_trim(limit, denied):
|
||||
p = Pipeline()
|
||||
p.joint.angle_trim = 1.
|
||||
status = SimpleNamespace(valid=True, canMonoTime=1_000_000_000, limit=limit, denied=denied)
|
||||
assert not p.tick(1., 1.5, pscm_status=status).latActive
|
||||
assert p.joint.angle_trim == 0.
|
||||
|
||||
|
||||
def test_limit_reached_does_not_grow_trim_but_allows_relief():
|
||||
p = Pipeline()
|
||||
p.joint.angle_trim = 1.
|
||||
for i in range(101):
|
||||
now = 1.+i*.01
|
||||
status = SimpleNamespace(valid=True, canMonoTime=round(now*1e9), limit=2, denied=False)
|
||||
assert p.tick(now, 1.5, pscm_status=status).latActive
|
||||
assert p.joint.angle_trim == 1.
|
||||
p.cs.steeringAngleDeg = 3.
|
||||
p.tick(2.01, 1.5, pscm_status=status)
|
||||
assert 0.99 < p.joint.angle_trim < 1.
|
||||
|
||||
|
||||
def test_acceleration_clipping_does_not_grow_trim():
|
||||
p = Pipeline()
|
||||
p.cs.vEgo = 50.
|
||||
p.cs.steeringAngleDeg = 200.
|
||||
for i in range(100):
|
||||
p.tick(1.+i*.01, 201.5)
|
||||
assert p.joint.diagnostics['accel_limited']
|
||||
assert p.joint.angle_trim == 0.
|
||||
|
||||
|
||||
@pytest.mark.parametrize('sign', [-1., 1.])
|
||||
def test_small_wheel_motion_does_not_discard_steady_request_trim(sign):
|
||||
# Route 17c, 1461 s: a ~1-degree trim disappears when a brief wheel
|
||||
# movement crosses the target, although the requested angle barely changes.
|
||||
p = Pipeline()
|
||||
p.cs.vEgo = 24.
|
||||
p.cs.steeringAngleDeg = sign * -2.1
|
||||
for i in range(60):
|
||||
p.tick(1. + i * .01, sign * -1.3)
|
||||
p.joint.angle_trim = sign * .985
|
||||
p.cs.steeringAngleDeg = sign * -.8
|
||||
assert p.tick(1.6, sign * -1.3).latActive
|
||||
assert abs(p.joint.wheel_rate) > 5.
|
||||
assert not p.joint.diagnostics['trim_learning']
|
||||
assert p.joint.diagnostics['angle_trim'] == pytest.approx(sign * .985)
|
||||
assert .98 < sign * p.joint.angle_trim < .985 # Opposing error can bleed it.
|
||||
|
||||
|
||||
@pytest.mark.parametrize('sign', [-1., 1.])
|
||||
def test_fast_request_releases_opposing_trim_before_wheel_moves(sign):
|
||||
p = Pipeline()
|
||||
for i in range(60):
|
||||
p.tick(1. + i * .01, 0.)
|
||||
p.joint.angle_trim = sign
|
||||
request = sign * -2.
|
||||
p.tick(1.6, request)
|
||||
assert p.joint.wheel_rate == 0.
|
||||
assert p.joint.diagnostics['angle_trim'] == 0.
|
||||
assert p.joint.diagnostics['trimmed_angle'] == request
|
||||
|
||||
|
||||
def test_inactive_request_history_cannot_clear_trim_after_reengagement():
|
||||
p = Pipeline()
|
||||
p.tick(1., 0.)
|
||||
p.tick(1.01, 60.)
|
||||
assert abs(p.joint.requested_rate) > 5.
|
||||
p.tick(1.02, 0., active=False)
|
||||
assert p.joint.requested_rate == 0. and p.joint.last_target is None
|
||||
p.tick(1.03, 1.)
|
||||
p.joint.angle_trim = -.5
|
||||
p.tick(1.04, 1.)
|
||||
assert p.joint.diagnostics['angle_trim'] == -.5
|
||||
|
||||
|
||||
@pytest.mark.parametrize('target,rate', [(-60., 0.), (-2., -30.)])
|
||||
def test_reversal_or_fast_unwind_clears_old_trim_before_allocation(target, rate):
|
||||
p = Pipeline()
|
||||
for i in range(10):
|
||||
p.cs.steeringAngleDeg = rate*i*.01
|
||||
p.tick(1.+i*.01, target+p.cs.steeringAngleDeg)
|
||||
p.joint.angle_trim = ANGLE_TRIM_MAX
|
||||
for i in range(10, 101):
|
||||
p.cs.steeringAngleDeg = rate*i*.01
|
||||
request = target+p.cs.steeringAngleDeg
|
||||
p.tick(1.+i*.01, request)
|
||||
assert p.joint.angle_trim == 0.
|
||||
assert p.joint.diagnostics['trimmed_angle'] == pytest.approx(request, abs=1e-5)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('angular_velocity', [[], [0., 0.], [0., 0., math.nan], [0., 0., math.inf], [0., 0., 3.1]])
|
||||
def test_missing_or_invalid_calibrated_yaw_does_not_fall_back_to_raw_can(angular_velocity):
|
||||
p = Pipeline()
|
||||
p.tick(1.)
|
||||
assert not p.tick(1.01, angular_velocity=angular_velocity).latActive
|
||||
assert p.joint.sent == (0., 0., False)
|
||||
assert p.tick(1.02).latActive
|
||||
|
||||
|
||||
def test_touch_does_not_zero_the_command_or_reset_held_state():
|
||||
reference, touched = Pipeline(), Pipeline()
|
||||
for i in range(160):
|
||||
touched.cs.steeringPressed = 40 <= i < 50 or 100 <= i < 115
|
||||
target = 60. if i < 100 else -30.
|
||||
reference.tick(1. + i*.01, target)
|
||||
assert touched.tick(1. + i*.01, target).latActive
|
||||
assert touched.joint.sent == reference.joint.sent
|
||||
np.testing.assert_array_equal(state(touched.joint.request), state(reference.joint.request))
|
||||
|
||||
|
||||
@pytest.mark.parametrize('sign', [-1., 1.])
|
||||
def test_can_coordinates_entry_reversal_release_and_100hz(sign):
|
||||
p = Pipeline()
|
||||
for i in range(250):
|
||||
target = sign * (30. if i < 80 else -30. if i < 160 else 0.)
|
||||
assert p.tick(1+i*.01, target).latActive
|
||||
if i == 0:
|
||||
# The existing sender negates OP path fields; the adapter compensates once.
|
||||
assert sign * p.joint.sent[0] > 0 and sign * p.joint.sent[1] > 0
|
||||
json.dumps(p.joint.diagnostics, allow_nan=False)
|
||||
assert not p.joint.fault
|
||||
|
||||
|
||||
def test_observer_follows_packed_command_only_and_inactive_slew():
|
||||
p = Pipeline()
|
||||
cc, sp = controls()
|
||||
before = state(p.joint.request).copy()
|
||||
p.joint.prepare(cc, sp, p.cs, 1.) # An untransmitted candidate is not held input.
|
||||
p.joint.advance(1.01)
|
||||
np.testing.assert_array_equal(state(p.joint.request), before)
|
||||
for i in range(100):
|
||||
p.tick(1.02+i*.01, 60.)
|
||||
held = copy.copy(p.joint.request)
|
||||
result = p.tick(2.02, 60., active=False)
|
||||
assert not result.latActive and p.joint.sent == (0., 0., False)
|
||||
assert abs(p.joint.request.c0) > 0 or abs(p.joint.request.c1) > 0
|
||||
held = copy.copy(p.joint.request)
|
||||
phase = p.joint.phase
|
||||
p.tick(2.03, 60., active=False)
|
||||
ticks = int((phase+.01+1e-12)/.008)
|
||||
for _ in range(ticks):
|
||||
held.step(5.36*3.6, 0., 0., active=False, freeze_i=True)
|
||||
np.testing.assert_allclose(state(p.joint.request), state(held), atol=1e-12)
|
||||
assert p.tick(2.04, -30.).latActive
|
||||
assert p.joint.diagnostics['requested_angle'] == -30.
|
||||
|
||||
|
||||
@pytest.mark.parametrize('limit,denied,active', [(0, False, True), (2, False, True), (3, False, False), (0, True, False)])
|
||||
def test_limit_reached_does_not_freeze_but_override_inhibits(limit, denied, active):
|
||||
p = Pipeline()
|
||||
status = SimpleNamespace(valid=True, canMonoTime=1_000_000_000, limit=limit, denied=denied)
|
||||
assert p.tick(1., pscm_status=status).latActive == active
|
||||
|
||||
|
||||
@pytest.mark.parametrize('field,value', [('vEgo', -.01), ('vEgo', 56.), ('yawRate', 3.1),
|
||||
('steeringAngleDeg', math.nan), ('vEgo', math.inf), ('canValid', False),
|
||||
('steerFaultTemporary', True), ('steerFaultPermanent', True)])
|
||||
def test_bad_measurements_never_send_previous_request(field, value):
|
||||
p = Pipeline()
|
||||
p.tick(1.)
|
||||
setattr(p.cs, field, value)
|
||||
assert not p.tick(1.01).latActive
|
||||
assert p.joint.sent == (0., 0., False)
|
||||
json.dumps(p.joint.diagnostics, allow_nan=False)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('target', [math.nan, math.inf, -math.inf])
|
||||
def test_nonfinite_target_and_stale_control_are_inactive(target):
|
||||
p = Pipeline()
|
||||
assert not p.tick(1., target).latActive
|
||||
assert not p.tick(1.01, fresh=False).latActive
|
||||
assert p.tick(1.02).latActive
|
||||
|
||||
|
||||
@pytest.mark.parametrize('next_time', [.9, 1.101])
|
||||
def test_unknown_clock_history_latches_inactive(next_time):
|
||||
p = Pipeline()
|
||||
p.tick(1.)
|
||||
assert not p.tick(next_time).latActive
|
||||
assert not p.tick(next_time+.01).latActive
|
||||
assert p.joint.fault
|
||||
|
||||
|
||||
def test_angle_inverse_matches_forward_stage_across_speeds():
|
||||
rng = np.random.default_rng(418)
|
||||
for _ in range(80):
|
||||
m = AngleModel(MainRequest().cal)
|
||||
m.yaw_acc, m.bank_residual, m.angle_bias = rng.uniform(-1., 1., 3)
|
||||
speed = float(rng.uniform(1.1, 180.))
|
||||
target, angle = map(float, rng.uniform(-300., 300., 2))
|
||||
yaw = float(rng.uniform(-.5, .5))
|
||||
result = invert_angle(m, speed, target, angle, yaw, yaw*speed/3.6, 3.7, 16.9)
|
||||
actual = m.step(speed, result['curvature'], angle, yaw, yaw*speed/3.6, 3.7, 16.9)
|
||||
assert actual == pytest.approx(result['reachable_target'], abs=1e-9)
|
||||
assert abs(result['bounded_accel']) <= result['allowance']
|
||||
|
||||
|
||||
def test_candidate_does_not_mutate_state_and_native_step_matches_python():
|
||||
rng = np.random.default_rng(919)
|
||||
for j in range(40):
|
||||
m = MainRequest(native_lookup=True)
|
||||
m.c0, m.c1, m.filtered = map(float, rng.uniform([-5., -.49, -.08], [5., .49, .08]))
|
||||
m.fast = bool(j % 2)
|
||||
speed, curvature = float(rng.uniform(1.1, 180.)), float(rng.uniform(-.1, .1))
|
||||
old = state(m).copy()
|
||||
pair, info = PairedRelease(m).choose(speed, curvature, phase=j % 10)
|
||||
np.testing.assert_array_equal(state(m), old)
|
||||
for _ in range(info['count']):
|
||||
m.step(speed, *pair, freeze_i=True)
|
||||
np.testing.assert_allclose(state(m), info['first_state'], atol=1e-12, rtol=0)
|
||||
assert abs(info['first_state'][3]-info['planned_target']) <= info['immediate_error_bound']+1e-12
|
||||
|
||||
|
||||
@pytest.mark.parametrize('speed,c0,c1,filtered,fast,target,phase,pair,cost', [
|
||||
(20., 0., 0., 0., False, .03, 0, (.03, .002), 5.206693787207076),
|
||||
(20., 3., .4, .03, False, -.03, 2, (2.98, .399), 149.0975016923494),
|
||||
(40., 3., .4, .03, True, 0., 0, (2.95, .395), 12.167867914611671),
|
||||
(6., 5.11, .5, .1, False, -.1, 0, (5.08, .498), 1423.8735141422555),
|
||||
(80., -5.11, -.5, -.1, True, .03, 2, (-5.08, -.4975), 39.42744549117561),
|
||||
(40., 3., -.4, 0., False, 0., 2, (3.02, -.399), 6.184073685045232),
|
||||
])
|
||||
def test_optimized_selection_matches_frozen_cases(speed, c0, c1, filtered, fast, target, phase, pair, cost):
|
||||
# Frozen outputs from 528ed3615 before pruning/caching the full-return search.
|
||||
# Cover entry, reversal, release, saturation, cancellation and both tick counts.
|
||||
# Costs refreshed for the equal-buildup endpoint and residual allocation at
|
||||
# field bounds, then for the supervisor-saturation C0 endpoint (its return
|
||||
# path differs). All six immediate selected packets remain unchanged.
|
||||
m = MainRequest(native_lookup=True)
|
||||
m.c0, m.c1, m.filtered, m.fast = c0, c1, filtered, fast
|
||||
command, info = PairedRelease(m).choose(speed, target, phase)
|
||||
assert command == pytest.approx(pair, abs=1e-15, rel=0)
|
||||
assert info['cost'] == pytest.approx(cost, abs=1e-12, rel=1e-12)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('active,override', list(itertools.product((False, True), repeat=2)))
|
||||
def test_card_transmit_hook_and_fault_alert_use_actual_source(active, override):
|
||||
import ast
|
||||
from pathlib import Path
|
||||
from openpilot.cereal import custom
|
||||
from openpilot.selfdrive.car.helpers import convert_carControlSP
|
||||
|
||||
source_path = Path(__file__).resolve().parents[1] / 'card.py'
|
||||
tree = ast.parse(source_path.read_text())
|
||||
car = next(n for n in tree.body if isinstance(n, ast.ClassDef) and n.name == 'Car')
|
||||
method = next(n for n in car.body if isinstance(n, ast.FunctionDef) and n.name == 'controls_update')
|
||||
branch = next(n for n in method.body if isinstance(n, ast.If) and ast.unparse(n.test) == "self.sm.all_alive(['carControl'])")
|
||||
code = compile(ast.Module(body=[branch], type_ignores=[]), str(source_path), 'exec')
|
||||
p = Pipeline()
|
||||
p.cs.steeringPressed = override
|
||||
sent = []
|
||||
owner = SimpleNamespace(ford_joint_control=p.joint, sm=SimpleNamespace(all_alive=lambda _: True, all_checks=lambda _: True, frame=1),
|
||||
CI=SimpleNamespace(apply=lambda cc, sp, now: CarInterfaceBase.apply(p.interface, cc, sp, now)),
|
||||
pm=SimpleNamespace(send=lambda service, data: sent.append((service, data))))
|
||||
cc, _ = controls(active=active)
|
||||
original = cc.to_dict()
|
||||
sp = custom.CarControlSP.new_message()
|
||||
sp.fordLateralPath.enabled = sp.fordLateralPath.valid = True
|
||||
exec(code, {'self': owner, 'CC': cc.as_reader(), 'CC_SP': sp, 'CS': p.cs, 'pscm_status': None, 'REPLAY': False,
|
||||
'time': SimpleNamespace(monotonic=lambda: 1.), 'convert_carControlSP': convert_carControlSP,
|
||||
'can_list_to_can_capnp': lambda data, **kwargs: data})
|
||||
assert sent[0][0] == 'sendcan'
|
||||
assert p.joint.sent[2] == active
|
||||
assert p.joint.last_sent_time == 1.
|
||||
assert owner.CC_prev.to_dict() == {**original, 'latActive': active}
|
||||
assert cc.to_dict() == original
|
||||
|
||||
update_method = next(n for n in car.body if isinstance(n, ast.FunctionDef) and n.name == 'state_update')
|
||||
alert = next(n for n in update_method.body if isinstance(n, ast.If) and 'self.ford_joint_control.fault' in ast.unparse(n.test))
|
||||
alert_code = compile(ast.Module(body=[alert], type_ignores=[]), str(source_path), 'exec')
|
||||
p.joint.fault = 'command_timing_gap'
|
||||
exec(alert_code, {'self': owner, 'CS': p.cs})
|
||||
assert p.cs.steerFaultTemporary
|
||||
|
||||
|
||||
def test_nonfinite_native_result_latches_inactive(monkeypatch):
|
||||
from openpilot.selfdrive.controls.lib.ford_joint.encoder import LIB
|
||||
p = Pipeline()
|
||||
monkeypatch.setattr(LIB, 'paired_select', lambda *args: None)
|
||||
assert not p.tick(1.).latActive
|
||||
assert p.joint.fault == 'invalid_prediction'
|
||||
|
||||
|
||||
def test_duplicate_clock_and_missing_receipts_cannot_advance_unknown_history():
|
||||
p = Pipeline()
|
||||
p.tick(1.)
|
||||
assert p.tick(1.).latActive
|
||||
assert state(p.joint.request).tolist() == [0., 0., 0., 0., 0.]
|
||||
# Prepare can run without sending, but stale TX history must end the trial.
|
||||
for now in (1.05, 1.10, 1.15):
|
||||
cc, sp = controls()
|
||||
result = p.joint.prepare(cc, sp, p.cs, now)
|
||||
assert not result.latActive and p.joint.fault == 'missing_transmit_history'
|
||||
@@ -0,0 +1,89 @@
|
||||
"""Causal target preview: numerical scoring and the real Ford transmit boundary."""
|
||||
import copy
|
||||
import math
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
from openpilot.selfdrive.car.tests.test_ford_joint_control import Pipeline
|
||||
from openpilot.selfdrive.controls.lib.ford_joint.encoder import PairedRelease, parameters, state
|
||||
from openpilot.selfdrive.controls.lib.ford_joint.inverse import static_pair, quantize
|
||||
from openpilot.selfdrive.controls.lib.ford_joint.model import MainRequest
|
||||
|
||||
|
||||
@pytest.mark.parametrize('speed', [18., 36., 90.])
|
||||
@pytest.mark.parametrize('phase', range(10))
|
||||
def test_preview_score_matches_explicit_forward_recurrence(speed, phase):
|
||||
m = MainRequest(native_lookup=True)
|
||||
m.c0, m.c1, m.filtered = .7, -.03, -.001
|
||||
curvature, rate, preview = .002, .015, .1
|
||||
bound = 3 / (speed / 3.6)**2
|
||||
encoder = PairedRelease(m)
|
||||
_, baseline = encoder.choose(speed, curvature, phase)
|
||||
pair, info = encoder.choose(speed, curvature, phase, curvature_rate=rate, preview=preview, curvature_bound=bound)
|
||||
p = parameters(m, speed)
|
||||
targets, prefs = [], []
|
||||
n = math.ceil(preview / .008)
|
||||
for j in range(n + 1):
|
||||
k = min(bound, max(-bound, curvature + rate * min(j * .008, preview)))
|
||||
pref = quantize(static_pair(m, k, speed, gains=(p[0], p[1])))
|
||||
prefs.append(pref)
|
||||
targets.append(p[0] * pref[0] + p[1] * pref[1])
|
||||
model, cost = copy.copy(m), 0.
|
||||
for j in range(1600):
|
||||
command = pair if j < info['count'] else prefs[min(j + 1, n)]
|
||||
model.step(speed, *command, freeze_i=True)
|
||||
cost += .008 * ((model.filtered - targets[min(j + 1, n)]) / .01)**2
|
||||
if j + 1 == info['count']:
|
||||
np.testing.assert_allclose(state(model), info['first_state'], atol=1e-14, rtol=0)
|
||||
assert info['cost'] == pytest.approx(cost, abs=2e-10, rel=2e-12)
|
||||
assert abs(info['first_state'][3] - baseline['planned_target']) <= baseline['immediate_error_bound'] + 1.1e-12
|
||||
|
||||
|
||||
@pytest.mark.parametrize('curvature_rate,preview', [(0., .1), (.02, 0.)])
|
||||
def test_constant_or_disabled_preview_preserves_ordinary_encoder(curvature_rate, preview):
|
||||
m = MainRequest(native_lookup=True)
|
||||
m.c0, m.c1, m.filtered = -1., .1, .004
|
||||
encoder = PairedRelease(m)
|
||||
expected, before = encoder.choose(36., .01)
|
||||
actual, after = encoder.choose(36., .01, curvature_rate=curvature_rate, preview=preview, curvature_bound=.03)
|
||||
assert actual == expected and after['cost'] == before['cost']
|
||||
np.testing.assert_array_equal(after['first_state'], before['first_state'])
|
||||
|
||||
|
||||
@pytest.mark.parametrize('kwargs', [
|
||||
{'curvature_rate': math.nan}, {'preview': -.1}, {'preview': .16},
|
||||
{'preview': .1, 'curvature_bound': 0.}, {'preview': .1, 'curvature_bound': math.inf},
|
||||
])
|
||||
def test_invalid_forecast_is_rejected(kwargs):
|
||||
with pytest.raises(ValueError):
|
||||
PairedRelease(MainRequest(native_lookup=True)).choose(36., .002, **kwargs)
|
||||
|
||||
|
||||
def test_target_trend_enters_real_adapter_without_changing_requested_angle():
|
||||
p = Pipeline()
|
||||
for i in range(81):
|
||||
p.tick(1. + i * .01, i * .5)
|
||||
d = p.joint.diagnostics
|
||||
assert d['requested_angle'] == 40.
|
||||
assert d['trimmed_angle'] == 40. + d['angle_trim']
|
||||
assert d['target_preview_seconds'] == .1
|
||||
assert 4.9 < d['target_preview_delta'] < 5.1
|
||||
assert d['wheel_rate'] == 0.
|
||||
p.tick(1.81, 40., active=False)
|
||||
p.tick(1.82, -40.)
|
||||
assert p.joint.diagnostics['target_preview_delta'] == 0.
|
||||
|
||||
|
||||
def test_preview_does_not_run_an_unused_full_constant_target_search(monkeypatch):
|
||||
from openpilot.selfdrive.controls.lib.ford_joint.encoder import LIB
|
||||
|
||||
def unused_search(*args):
|
||||
pytest.fail('Preview needs only the current-target accuracy bound, not a second full search')
|
||||
|
||||
monkeypatch.setattr(LIB, 'paired_select', unused_search)
|
||||
command, info = PairedRelease(MainRequest(native_lookup=True)).choose(
|
||||
36., .002, curvature_rate=.015, preview=.1, curvature_bound=.03,
|
||||
)
|
||||
assert np.isfinite(command).all()
|
||||
assert abs(info['first_state'][3] - info['planned_target']) <= info['immediate_error_bound'] + 1.1e-12
|
||||
@@ -0,0 +1,109 @@
|
||||
import ast
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
import unittest
|
||||
|
||||
from openpilot.cereal import custom
|
||||
from openpilot.selfdrive.car.ford_pscm_status import MESSAGE, SIGNALS, populate_ford_pscm_status
|
||||
from openpilot.selfdrive.car.helpers import convert_to_capnp
|
||||
from opendbc.can import CANPacker, CANParser
|
||||
from opendbc.car import Bus, structs
|
||||
from opendbc.car.ford.values import FordFlags
|
||||
|
||||
|
||||
class TestFordPscmStatus(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.cp = SimpleNamespace(brand='ford', flags=FordFlags.CANFD)
|
||||
self.packer = CANPacker('ford_lincoln_base_pt')
|
||||
self.parser = CANParser('ford_lincoln_base_pt', [(MESSAGE, 33), ('Yaw_Data_FD1', 100)], 0)
|
||||
|
||||
def update_status(self, timestamp, *, lateral_state=2, limit=0, capability=2, denied=False):
|
||||
status = self.packer.make_can_msg(MESSAGE, 0, dict(zip(SIGNALS, (lateral_state, limit, capability, denied), strict=True)))
|
||||
yaw = self.packer.make_can_msg('Yaw_Data_FD1', 0, {'VehYaw_W_Actl': 0.1})
|
||||
self.parser.update([(timestamp, [status, yaw])])
|
||||
|
||||
def publish(self, *, can_valid=True):
|
||||
state_sp = convert_to_capnp(structs.CarStateSP(speedLimit=13.5))
|
||||
populate_ford_pscm_status(self.cp, {Bus.pt: self.parser}, state_sp, can_valid)
|
||||
return state_sp
|
||||
|
||||
def test_decodes_status_and_preserves_receipt_time_across_other_can_messages(self):
|
||||
self.update_status(1_000_000_000, limit=2, capability=1, denied=True)
|
||||
original = self.publish()
|
||||
self.assertEqual(original.speedLimit, 13.5)
|
||||
status = original.fordPscmStatus
|
||||
self.assertTrue(status.valid)
|
||||
self.assertEqual(status.canMonoTime, 1_000_000_000)
|
||||
self.assertEqual((status.lateralState, status.limit, status.capability, status.denied), (2, 2, 1, True))
|
||||
|
||||
# carStateSP may publish at 100 Hz while this 33 Hz message is absent. New
|
||||
# unrelated CAN must not freshen the timestamp of an old PSCM status.
|
||||
yaw = self.packer.make_can_msg('Yaw_Data_FD1', 0, {'VehYaw_W_Actl': .2})
|
||||
self.parser.update([(1_080_000_000, [yaw])])
|
||||
copied = self.publish().fordPscmStatus
|
||||
self.assertEqual(copied.canMonoTime, 1_000_000_000)
|
||||
self.assertEqual((copied.limit, copied.capability, copied.denied), (2, 1, True))
|
||||
|
||||
self.update_status(1_090_000_000, lateral_state=3, limit=3, capability=2)
|
||||
next_state = self.publish()
|
||||
with custom.CarStateSP.from_bytes(next_state.to_bytes()) as decoded:
|
||||
latest = decoded.fordPscmStatus
|
||||
self.assertTrue(latest.valid)
|
||||
self.assertEqual(latest.canMonoTime, 1_090_000_000)
|
||||
self.assertEqual((latest.lateralState, latest.limit, latest.capability, latest.denied), (3, 3, 2, False))
|
||||
|
||||
def test_absent_parser_unseen_message_and_invalid_can_do_not_claim_valid_status(self):
|
||||
state = custom.CarStateSP.new_message()
|
||||
populate_ford_pscm_status(self.cp, {}, state, True)
|
||||
self.assertFalse(state.fordPscmStatus.valid)
|
||||
self.assertEqual(state.fordPscmStatus.canMonoTime, 0)
|
||||
self.assertFalse(self.publish().fordPscmStatus.valid)
|
||||
self.update_status(1_000_000_000)
|
||||
invalid = self.publish(can_valid=False).fordPscmStatus
|
||||
self.assertFalse(invalid.valid)
|
||||
self.assertEqual(invalid.canMonoTime, 1_000_000_000)
|
||||
|
||||
def test_mixed_timestamps_or_malformed_status_cannot_enable_feedback(self):
|
||||
self.update_status(1_000_000_000)
|
||||
self.parser.ts_nanos[MESSAGE][SIGNALS[-1]] = 990_000_000
|
||||
self.assertFalse(self.publish().fordPscmStatus.valid)
|
||||
self.parser.ts_nanos[MESSAGE][SIGNALS[-1]] = 1_000_000_000
|
||||
for value in (float('nan'), -1, 1.5, 4):
|
||||
self.parser.vl[MESSAGE]['LatCtlLim_D_Stat'] = value
|
||||
self.assertFalse(self.publish().fordPscmStatus.valid)
|
||||
|
||||
def test_other_vehicles_and_legacy_messages_default_to_unavailable(self):
|
||||
for cp in (SimpleNamespace(brand='toyota'), SimpleNamespace(brand='ford', flags=0)):
|
||||
state = custom.CarStateSP.new_message(speedLimit=10.)
|
||||
populate_ford_pscm_status(cp, {}, state, True)
|
||||
self.assertFalse(state.fordPscmStatus.valid)
|
||||
self.assertEqual(state.fordPscmStatus.canMonoTime, 0)
|
||||
self.assertEqual(state.speedLimit, 10.)
|
||||
# Old recordings/readers have no appended status pointer; defaults must
|
||||
# remain unavailable rather than interpreting zeroed enums as fresh data.
|
||||
self.assertFalse(custom.CarStateSP.new_message().fordPscmStatus.valid)
|
||||
|
||||
def test_actual_card_update_populates_status_after_dataclass_conversion(self):
|
||||
self.update_status(1_000_000_000, limit=1)
|
||||
source_path = Path(__file__).resolve().parents[1] / 'card.py'
|
||||
source = ast.parse(source_path.read_text())
|
||||
car_class = next(n for n in source.body if isinstance(n, ast.ClassDef) and n.name == 'Car')
|
||||
method = next(n for n in car_class.body if isinstance(n, ast.FunctionDef) and n.name == 'state_update')
|
||||
statements = method.body
|
||||
first = next(i for i, n in enumerate(statements) if isinstance(n, ast.Assign) and ast.unparse(n.value) == 'self.CI.update(can_list)')
|
||||
last = next(i for i, n in enumerate(statements) if isinstance(n, ast.Expr) and isinstance(n.value, ast.Call)
|
||||
and isinstance(n.value.func, ast.Name) and n.value.func.id == 'populate_ford_pscm_status')
|
||||
self.assertGreater(last, first)
|
||||
code = compile(ast.Module(body=statements[first:last + 1], type_ignores=[]), str(source_path), 'exec')
|
||||
ci = SimpleNamespace(update=lambda _: (SimpleNamespace(canValid=True), structs.CarStateSP(speedLimit=11.)),
|
||||
can_parsers={Bus.pt: self.parser})
|
||||
environment = {'self': SimpleNamespace(CP=self.cp, CI=ci), 'can_list': [], 'convert_to_capnp': convert_to_capnp,
|
||||
'populate_ford_pscm_status': populate_ford_pscm_status}
|
||||
exec(code, environment)
|
||||
self.assertTrue(environment['CS_SP'].fordPscmStatus.valid)
|
||||
self.assertEqual(environment['CS_SP'].fordPscmStatus.canMonoTime, 1_000_000_000)
|
||||
self.assertEqual(environment['CS_SP'].fordPscmStatus.limit, 1)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
@@ -0,0 +1,110 @@
|
||||
"""Opt-in entry assist at the real Ford adapter and CAN packing boundary."""
|
||||
from types import SimpleNamespace
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
from openpilot.common.params import Params, ParamKeyFlag
|
||||
from openpilot.selfdrive.car.ford_joint_control import FordJointControl, select_joint_control
|
||||
from openpilot.selfdrive.car.tests.test_ford_joint_control import Pipeline, cp
|
||||
from openpilot.selfdrive.controls.lib.ford_joint.encoder import state
|
||||
|
||||
|
||||
def assisted():
|
||||
p = Pipeline()
|
||||
p.joint = FordJointControl(cp(), turn_entry_assist=True)
|
||||
return p
|
||||
|
||||
|
||||
def test_turn_entry_is_default_off_and_requires_the_existing_joint_controller(tmp_path):
|
||||
params = Params(str(tmp_path))
|
||||
assert params.get_default_value('FordPscmTurnEntryAssist') is False
|
||||
assert not params.get_bool('FordPscmTurnEntryAssist')
|
||||
assert b'FordPscmTurnEntryAssist' in params.all_keys(ParamKeyFlag.PERSISTENT | ParamKeyFlag.BACKUP)
|
||||
params.put_bool('FordPscmTurnEntryAssist', True, block=True)
|
||||
assert select_joint_control(cp(), params) is None
|
||||
params.put_bool('FordModelActionController', True, block=True)
|
||||
assert select_joint_control(cp(), params) is None
|
||||
params.put_bool('FordPscmJointControl', True, block=True)
|
||||
assert select_joint_control(cp(), params).turn_entry_assist
|
||||
params.put_bool('FordPscmTurnEntryAssist', False, block=True)
|
||||
assert not select_joint_control(cp(), params).turn_entry_assist
|
||||
params.put_bool('FordPscmTurnEntryAssist', True, block=True)
|
||||
for key in ('FordGeometryReference', 'JoystickDebugMode'):
|
||||
params.put_bool(key, True, block=True)
|
||||
assert select_joint_control(cp(), params) is None
|
||||
params.put_bool(key, False, block=True)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('sign', [-1., 1.])
|
||||
def test_large_growing_undertracked_request_adds_to_real_wire_commands(sign):
|
||||
base, trial = Pipeline(), assisted()
|
||||
for p in (base, trial):
|
||||
p.tick(1., 0.)
|
||||
p.tick(1.01, sign * 120.)
|
||||
np.testing.assert_allclose(np.array(trial.joint.sent[:2]) - base.joint.sent[:2], sign * np.array([.60, .04]), atol=1e-12)
|
||||
assert trial.joint.diagnostics['requested_angle'] == sign * 120.
|
||||
assert trial.joint.diagnostics['trimmed_angle'] == base.joint.diagnostics['trimmed_angle']
|
||||
assert trial.joint.diagnostics['requested_rate'] == base.joint.diagnostics['requested_rate']
|
||||
assert trial.joint.diagnostics['turn_entry_weight'] == 1.
|
||||
assert trial.joint.sent[2] and base.joint.sent[2]
|
||||
|
||||
|
||||
@pytest.mark.parametrize('sign', [-1., 1.])
|
||||
def test_extra_clips_at_dbc_bounds_through_real_packer(sign, monkeypatch):
|
||||
p = assisted()
|
||||
p.tick(1., 0.)
|
||||
choose = p.joint.encoder.choose
|
||||
|
||||
def near_bounds(*args, **kwargs):
|
||||
_, info = choose(*args, **kwargs)
|
||||
return (sign * 5.10, sign * .49), info
|
||||
|
||||
monkeypatch.setattr(p.joint.encoder, 'choose', near_bounds)
|
||||
p.tick(1.01, sign * 120.)
|
||||
assert p.joint.sent[:2] == pytest.approx((sign * 5.11, sign * .5))
|
||||
|
||||
|
||||
def test_small_requests_and_small_errors_keep_full_history_identical():
|
||||
base, trial = Pipeline(), assisted()
|
||||
for i in range(1200):
|
||||
target = 25 * np.sin(i * .02) if i < 600 else 100.
|
||||
wheel = 0. if i < 600 else 90.
|
||||
for p in (base, trial):
|
||||
p.cs.steeringAngleDeg = wheel
|
||||
p.tick(1. + i * .01, float(target))
|
||||
assert trial.joint.sent == base.joint.sent
|
||||
np.testing.assert_array_equal(state(trial.joint.request), state(base.joint.request))
|
||||
assert trial.joint.angle_trim == base.joint.angle_trim
|
||||
|
||||
|
||||
@pytest.mark.parametrize('sign', [-1., 1.])
|
||||
def test_new_term_clears_on_relaxation_without_resetting_observed_state(sign):
|
||||
p = assisted()
|
||||
for i in range(100):
|
||||
p.cs.steeringAngleDeg = sign * 20.
|
||||
p.tick(1. + i * .01, sign * (100. + i))
|
||||
assert p.joint.diagnostics['turn_entry_c0'] * sign > 0
|
||||
p.tick(2., sign * 50.)
|
||||
assert p.joint.diagnostics['turn_entry_c0'] == p.joint.diagnostics['turn_entry_c1'] == 0.
|
||||
assert abs(p.joint.request.c0) + abs(p.joint.request.c1) > 0
|
||||
assert p.joint.sent[2]
|
||||
|
||||
|
||||
@pytest.mark.parametrize('gate', ['touch', 'limit', 'denied', 'fault', 'stale', 'inactive', 'accel'])
|
||||
def test_extra_respects_intervention_health_and_target_limit_gates(gate):
|
||||
p = assisted()
|
||||
p.cs.vEgo = 30. if gate == 'accel' else 5.36
|
||||
p.tick(1., 0.)
|
||||
p.cs.steeringPressed = gate == 'touch'
|
||||
p.cs.steerFaultTemporary = gate == 'fault'
|
||||
status = SimpleNamespace(valid=True, canMonoTime=1_010_000_000, limit=2 if gate == 'limit' else 0, denied=gate == 'denied')
|
||||
p.tick(1.01, 120., active=gate != 'inactive', fresh=gate != 'stale', pscm_status=status)
|
||||
d = p.joint.diagnostics
|
||||
assert d['turn_entry_c0'] == d['turn_entry_c1'] == 0.
|
||||
if gate in ('touch', 'limit', 'accel'):
|
||||
assert p.joint.sent[2] # Only the added term is suppressed.
|
||||
else:
|
||||
assert p.joint.sent == (0., 0., False)
|
||||
if gate == 'accel':
|
||||
assert d['accel_limited']
|
||||
@@ -0,0 +1,297 @@
|
||||
"""Signaled preview through real model messages, SP transport and Ford packing."""
|
||||
import ast
|
||||
import itertools
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
from openpilot.cereal import custom, log
|
||||
from openpilot.common.params import Params, ParamKeyFlag
|
||||
from opendbc.car.vehicle_model import VehicleModel
|
||||
from openpilot.selfdrive.car.ford_joint_control import FordJointControl, select_joint_control
|
||||
from openpilot.selfdrive.car.helpers import convert_carControlSP
|
||||
from openpilot.selfdrive.car.tests.test_ford_joint_control import Pipeline, cp
|
||||
from openpilot.selfdrive.controls.lib.ford_joint.encoder import state
|
||||
from openpilot.selfdrive.controls.lib.ford_turn_preview import FordTurnPreview, geometry_entry_lead, turn_preview_lead
|
||||
from openpilot.selfdrive.controls.tests.test_ford_model_action_adapter import _method
|
||||
from openpilot.selfdrive.controls.tests.test_ford_model_action_selection import car_params, startup
|
||||
|
||||
|
||||
def model(sign=1., action=-.01):
|
||||
m = log.ModelDataV2.new_message()
|
||||
station = np.linspace(0, 32, 33)
|
||||
m.position.x = station.tolist()
|
||||
m.position.y = np.zeros(33).tolist()
|
||||
m.orientation.z = (-sign * np.radians(station * 2.2)).tolist()
|
||||
m.action.desiredCurvature = action
|
||||
return m
|
||||
|
||||
|
||||
def sample(sign=1., stamp=1_010_000_000):
|
||||
cue = FordTurnPreview()
|
||||
cue.update(model(sign, 0.), stamp - 50_000_000, True)
|
||||
data = cue.update(model(sign, -sign * .01), stamp, True)
|
||||
# Serialize exactly the additive production carrier; use its reader at card.
|
||||
sp = custom.CarControlSP.new_message(fordTurnPreview=data)
|
||||
with custom.CarControlSP.from_bytes(sp.to_bytes()) as parsed:
|
||||
convert_carControlSP(parsed) # The cue is card metadata, not an opendbc field.
|
||||
return parsed.as_builder().as_reader().fordTurnPreview
|
||||
|
||||
|
||||
def pipeline(enabled=True):
|
||||
p = Pipeline()
|
||||
p.joint = FordJointControl(cp(), turn_entry_assist=True, turn_preview=enabled)
|
||||
return p
|
||||
|
||||
|
||||
def test_default_off_startup_and_master_fallback(tmp_path):
|
||||
params = Params(str(tmp_path))
|
||||
assert params.get_default_value('FordPscmTurnPreview') is False
|
||||
assert b'FordPscmTurnPreview' in params.all_keys(ParamKeyFlag.PERSISTENT | ParamKeyFlag.BACKUP)
|
||||
params.put_bool('FordPscmTurnPreview', True, block=True)
|
||||
assert select_joint_control(cp(), params) is None
|
||||
params.put_bool('FordModelActionController', True, block=True)
|
||||
assert select_joint_control(cp(), params) is None
|
||||
params.put_bool('FordPscmJointControl', True, block=True)
|
||||
assert select_joint_control(cp(), params).turn_preview
|
||||
for key in ('FordGeometryReference', 'JoystickDebugMode'):
|
||||
params.put_bool(key, True, block=True)
|
||||
assert select_joint_control(cp(), params) is None
|
||||
params.put_bool(key, False, block=True)
|
||||
params.put_bool('FordPscmTurnPreview', False, block=True)
|
||||
assert not select_joint_control(cp(), params).turn_preview
|
||||
|
||||
|
||||
@pytest.mark.parametrize('master,joint,geometry,joystick,preview', list(itertools.product((False, True), repeat=5)))
|
||||
def test_both_processes_select_preview_with_the_same_gates(master, joint, geometry, joystick, preview):
|
||||
flags = {'FordModelActionController': master, 'FordPscmJointControl': joint, 'FordGeometryReference': geometry,
|
||||
'JoystickDebugMode': joystick, 'FordPscmTurnPreview': preview}
|
||||
params = SimpleNamespace(get_bool=lambda key: flags.get(key, False))
|
||||
selected = select_joint_control(cp(), params)
|
||||
expected = master and joint and preview and not geometry and not joystick
|
||||
assert bool(selected and selected.turn_preview) == expected
|
||||
assert (startup(params=params).ford_turn_preview is not None) == expected
|
||||
|
||||
|
||||
def publish_preview(tracker, message, stamp, *, healthy=True, maneuver=False):
|
||||
root = Path(__file__).resolve().parents[3]
|
||||
filename = root / 'sunnypilot/selfdrive/controls/controlsd_ext.py'
|
||||
body = _method(filename, 'ControlsExt', 'state_control_ext').body
|
||||
nodes = [n for n in body if (isinstance(n, ast.Assign) and ast.unparse(n.targets[0]) == 'turn_preview') or
|
||||
(isinstance(n, ast.If) and ast.unparse(n.test) == 'turn_preview is not None')]
|
||||
assert len(nodes) == 2
|
||||
|
||||
class Subscriptions(dict):
|
||||
valid = {'modelV2': True, 'lateralManeuverPlan': maneuver}
|
||||
logMonoTime = {'modelV2': stamp}
|
||||
|
||||
def all_checks(self, services):
|
||||
return healthy
|
||||
|
||||
sp = custom.CarControlSP.new_message()
|
||||
sp.fordLateralPath.enabled = sp.fordLateralPath.valid = True
|
||||
exec(compile(ast.Module(body=nodes, type_ignores=[]), str(filename), 'exec'),
|
||||
{'self': SimpleNamespace(ford_turn_preview=tracker, VM=VehicleModel(car_params())),
|
||||
'sm': Subscriptions(modelV2=message, carState=SimpleNamespace(vEgo=5.),
|
||||
vehicleParameters=SimpleNamespace(roll=0., angleOffsetDeg=0.)), 'CC_SP': sp})
|
||||
return sp
|
||||
|
||||
|
||||
@pytest.mark.parametrize('healthy,maneuver', [(True, False), (False, False), (True, True)])
|
||||
def test_actual_publication_gates_and_original_model_timestamp(healthy, maneuver):
|
||||
tracker = FordTurnPreview()
|
||||
publish_preview(tracker, model(action=0.), 1_000_000_000)
|
||||
sp = publish_preview(tracker, model(), 1_050_000_000, healthy=healthy, maneuver=maneuver)
|
||||
assert sp.fordTurnPreview.valid == (healthy and not maneuver)
|
||||
assert sp.fordTurnPreview.modelMonoTime == 1_050_000_000
|
||||
assert sp.fordTurnPreview.geometryValid == healthy
|
||||
assert sp.fordLateralPath.valid
|
||||
convert_carControlSP(sp.as_reader())
|
||||
|
||||
|
||||
def test_card_consumes_published_preview_and_preserves_reader_contract():
|
||||
from opendbc.car import structs
|
||||
from opendbc.car.interfaces import CarInterfaceBase
|
||||
from openpilot.selfdrive.car.tests.test_ford_joint_control import controls
|
||||
|
||||
root = Path(__file__).resolve().parents[3]
|
||||
filename = root / 'selfdrive/car/card.py'
|
||||
method = _method(filename, 'Car', 'controls_update')
|
||||
env = {'car': structs.car, 'custom': custom, 'REPLAY': True, 'convert_carControlSP': convert_carControlSP,
|
||||
'can_list_to_can_capnp': lambda *args, **kwargs: None}
|
||||
exec(compile(ast.Module(body=[method], type_ignores=[]), str(filename), 'exec'), env)
|
||||
p = pipeline()
|
||||
p.cs.leftBlinker = True
|
||||
card = SimpleNamespace(initialized_prev=True, ford_joint_control=p.joint,
|
||||
sm=SimpleNamespace(all_alive=lambda services: True, all_checks=lambda services: True, frame=1),
|
||||
pm=SimpleNamespace(send=lambda *args: None),
|
||||
CI=SimpleNamespace(apply=lambda *args: CarInterfaceBase.apply(p.interface, *args)))
|
||||
tracker = FordTurnPreview()
|
||||
for stamp, target, raw in ((1_000_000_000, 0., 0.), (1_050_000_000, 100., -.01)):
|
||||
sp = publish_preview(tracker, model(action=raw), stamp)
|
||||
with custom.CarControlSP.from_bytes(sp.to_bytes()) as reader:
|
||||
card.can_log_mono_time = stamp
|
||||
control, _ = controls(target)
|
||||
env['controls_update'](card, p.cs, control.as_reader(), reader)
|
||||
assert p.joint.sent[2]
|
||||
assert p.joint.diagnostics['geometry_lead'] == 30.
|
||||
assert card.CC_prev.actuators.as_builder().steeringAngleDeg == 100.
|
||||
|
||||
|
||||
@pytest.mark.parametrize('sign', [-1., 1.])
|
||||
def test_heading_distance_and_action_sign(sign):
|
||||
p = sample(sign)
|
||||
assert p.valid
|
||||
assert p.heading7 == pytest.approx(sign * 15.4, abs=1e-5)
|
||||
assert p.heading14 == pytest.approx(sign * 30.8, abs=1e-5)
|
||||
assert turn_preview_lead(sign * 50., p, 1.01) == (sign * 10., 1.)
|
||||
assert turn_preview_lead(sign * 1000., p, 1.01) == (sign * 30., 1.)
|
||||
assert turn_preview_lead(-sign * 50., p, 1.01)[0] == 0.
|
||||
|
||||
|
||||
def test_folded_path_uses_arc_distance_and_unwrapped_heading():
|
||||
m = model()
|
||||
angle = np.linspace(0., 3.5, 33)
|
||||
m.position.x = (6 * np.sin(angle)).tolist()
|
||||
m.position.y = (6 * (1 - np.cos(angle))).tolist()
|
||||
m.orientation.z = ((angle + np.pi) % (2 * np.pi) - np.pi).tolist()
|
||||
p = FordTurnPreview().update(m, 1_000_000_000, True)
|
||||
assert p['valid']
|
||||
assert p['heading14'] < p['heading7'] < -60. # x folds back, distance keeps increasing.
|
||||
|
||||
|
||||
@pytest.mark.parametrize('bad', ['invalid', 'short', 'mismatch', 'nan', 'action_nan', 'lane_change'])
|
||||
def test_invalid_geometry_disables_only_preview(bad):
|
||||
m = model()
|
||||
if bad == 'short':
|
||||
m.position.x = np.linspace(0., 13., 33).tolist()
|
||||
elif bad == 'mismatch':
|
||||
m.orientation.z = [0.]
|
||||
elif bad == 'nan':
|
||||
m.position.y = [float('nan')] * 33
|
||||
elif bad == 'action_nan':
|
||||
m.action.desiredCurvature = float('nan')
|
||||
elif bad == 'lane_change':
|
||||
m.meta.laneChangeState = 'laneChangeStarting'
|
||||
p = FordTurnPreview().update(m, 1_000_000_000, bad != 'invalid')
|
||||
assert not p['valid']
|
||||
|
||||
|
||||
def test_duplicate_stale_and_invalid_action_history():
|
||||
c = FordTurnPreview()
|
||||
c.update(model(action=0.), 1_000_000_000, True)
|
||||
good = c.update(model(), 1_050_000_000, True).copy()
|
||||
assert good['actionRate'] > 0
|
||||
assert c.update(model(action=.1), 1_050_000_000, True) == good
|
||||
p = sample(stamp=1_050_000_000)
|
||||
for now in (1.049, 1.201):
|
||||
assert turn_preview_lead(100., p, now) == (0., 0.)
|
||||
c.update(model(), 1_100_000_000, False)
|
||||
assert c.update(model(action=-.02), 1_150_000_000, True)['actionRate'] == 0.
|
||||
assert c.update(model(action=-.03), 1_400_000_000, True)['actionRate'] == 0.
|
||||
|
||||
|
||||
@pytest.mark.parametrize('signal', ['none', 'opposite', 'hazards'])
|
||||
def test_no_matching_signal_retains_identical_packet_history(signal):
|
||||
base, trial = pipeline(False), pipeline()
|
||||
for i in range(500):
|
||||
now = 1. + i * .01
|
||||
target = 70. + 50. * np.sin(i * .02)
|
||||
for p in (base, trial):
|
||||
p.cs.leftBlinker = signal == 'hazards'
|
||||
p.cs.rightBlinker = signal != 'none'
|
||||
p.tick(now, float(target), turn_preview=sample(stamp=round(now * 1e9)))
|
||||
assert trial.joint.sent == base.joint.sent
|
||||
np.testing.assert_array_equal(state(trial.joint.request), state(base.joint.request))
|
||||
assert trial.joint.angle_trim == base.joint.angle_trim
|
||||
|
||||
|
||||
@pytest.mark.parametrize('sign', [-1., 1.])
|
||||
def test_matching_signal_changes_inverse_only_and_retains_bounds(sign):
|
||||
p = pipeline()
|
||||
p.cs.leftBlinker, p.cs.rightBlinker = sign > 0, sign < 0
|
||||
p.tick(1., 0.)
|
||||
p.tick(1.01, sign * 100., turn_preview=sample(sign))
|
||||
d = p.joint.diagnostics
|
||||
assert d['geometry_lead'] == sign * 30.
|
||||
assert d['inverse_target'] == d['trimmed_angle'] + sign * 30.
|
||||
assert d['requested_angle'] == sign * 100.
|
||||
assert d['requested_rate'] * sign > 0.
|
||||
# Raw action relaxing suppresses the extra even while filtered request grows.
|
||||
p.tick(1.02, sign * 105., turn_preview=sample(-sign, 1_020_000_000))
|
||||
assert p.joint.diagnostics['geometry_lead'] == 0.
|
||||
assert p.joint.sent[2]
|
||||
|
||||
|
||||
@pytest.mark.parametrize('gate', ['touch', 'limit', 'denied', 'fault', 'stale', 'inactive', 'missing'])
|
||||
def test_preview_respects_existing_health_and_override_gates(gate):
|
||||
p = pipeline()
|
||||
p.cs.leftBlinker = True
|
||||
p.tick(1., 0.)
|
||||
p.cs.steeringPressed = gate == 'touch'
|
||||
p.cs.steerFaultTemporary = gate == 'fault'
|
||||
status = SimpleNamespace(valid=True, canMonoTime=1_010_000_000, limit=2 if gate == 'limit' else 0, denied=gate == 'denied')
|
||||
p.tick(1.01, 120., active=gate != 'inactive', fresh=gate != 'stale', pscm_status=status,
|
||||
turn_preview=None if gate == 'missing' else sample())
|
||||
assert p.joint.diagnostics.get('geometry_lead', 0.) == 0.
|
||||
if gate in ('touch', 'limit', 'missing'):
|
||||
assert p.joint.sent[2]
|
||||
else:
|
||||
assert p.joint.sent == (0., 0., False)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('sign', [-1., 1.])
|
||||
def test_flat_action_geometry_reaches_wire_and_fades_when_action_takes_over(sign):
|
||||
p, base = pipeline(), pipeline(False)
|
||||
p.cs.leftBlinker, p.cs.rightBlinker = sign > 0, sign < 0
|
||||
tracker = FordTurnPreview()
|
||||
for i in range(30):
|
||||
now = 1. + i*.01
|
||||
sp = publish_preview(tracker, model(sign, action=0.), round((now-.001)*1e9))
|
||||
with custom.CarControlSP.from_bytes(sp.to_bytes()) as parsed:
|
||||
cue = parsed.fordTurnPreview
|
||||
assert cue.valid and cue.geometryValid and cue.actionRate == 0.
|
||||
p.tick(now, 0., turn_preview=cue)
|
||||
base.tick(now, 0., turn_preview=cue)
|
||||
d = p.joint.diagnostics
|
||||
assert d['geometry_lead'] == sign*90.
|
||||
assert d['inverse_target'] == sign*90. and d['requested_angle'] == 0.
|
||||
assert geometry_entry_lead(sign*60., 0., cue, now) == pytest.approx(.5*(cue.geometryAngle-sign*60.))
|
||||
assert geometry_entry_lead(sign*90., 0., cue, now) == 0.
|
||||
assert geometry_entry_lead(0., -sign*30., cue, now) == 0.
|
||||
assert p.joint.sent != base.joint.sent
|
||||
|
||||
|
||||
@pytest.mark.parametrize('speed', [0., .3, 5., 20., 40.])
|
||||
def test_geometry_angle_uses_local_heading_interval_and_vehicle_model(speed):
|
||||
m = model()
|
||||
station = np.asarray(m.position.x)
|
||||
m.orientation.z = (-.001*station**2).tolist()
|
||||
vm = VehicleModel(car_params())
|
||||
data = FordTurnPreview().update(m, 1_000_000_000, True, speed=speed, VM=vm, roll=.01, angle_offset=1.2, geometry_valid=True)
|
||||
lo, hi = max(0., speed*.8-1.), max(0., speed*.8-1.)+2.
|
||||
if hi > station[-1]:
|
||||
assert not data.get('geometryValid', False)
|
||||
else:
|
||||
curvature = -(np.interp(hi, station, m.orientation.z)-np.interp(lo, station, m.orientation.z))/2.
|
||||
expected = np.degrees(vm.get_steer_from_curvature(curvature, speed, .01))+1.2
|
||||
assert data['geometryValid'] and data['geometryAngle'] == pytest.approx(expected)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('gate', ['touch', 'limit', 'stale', 'missing', 'opposite', 'hazards', 'lane_change', 'unhealthy'])
|
||||
def test_independent_geometry_obeys_gates_with_flat_action(gate):
|
||||
base, p = pipeline(False), pipeline()
|
||||
p.cs.leftBlinker, p.cs.rightBlinker = gate != 'opposite', gate in ('opposite', 'hazards')
|
||||
p.cs.steeringPressed = gate == 'touch'
|
||||
m = model(action=0.)
|
||||
if gate == 'lane_change':
|
||||
m.meta.laneChangeState = 'laneChangeStarting'
|
||||
sp = publish_preview(FordTurnPreview(), m, 1_000_000_000, healthy=gate != 'unhealthy')
|
||||
status = SimpleNamespace(valid=True, canMonoTime=1_000_000_000, limit=2 if gate == 'limit' else 0, denied=False)
|
||||
cue = sp.as_reader().fordTurnPreview
|
||||
for pipe in (base, p):
|
||||
pipe.tick(1.2 if gate == 'stale' else 1., 0., turn_preview=None if gate == 'missing' else cue, pscm_status=status)
|
||||
assert p.joint.diagnostics['geometry_lead'] == 0.
|
||||
assert p.joint.sent == base.joint.sent
|
||||
@@ -1,5 +1,6 @@
|
||||
#!/usr/bin/env python3
|
||||
import math
|
||||
import time
|
||||
from numbers import Number
|
||||
|
||||
from openpilot.cereal import log
|
||||
@@ -13,6 +14,8 @@ from openpilot.common.swaglog import cloudlog
|
||||
from opendbc.car.car_helpers import interfaces
|
||||
from opendbc.car.vehicle_model import VehicleModel
|
||||
from openpilot.selfdrive.controls.lib.drive_helpers import clip_curvature
|
||||
from openpilot.selfdrive.controls.lib.ford_model_action import FordModelActionController, select_model_action_controller
|
||||
from openpilot.selfdrive.controls.lib.ford_path import FordPath
|
||||
from openpilot.selfdrive.controls.lib.latcontrol import LatControl
|
||||
from openpilot.selfdrive.controls.lib.latcontrol_pid import LatControlPID
|
||||
from openpilot.selfdrive.controls.lib.latcontrol_angle import LatControlAngle, STEER_ANGLE_SATURATION_THRESHOLD
|
||||
@@ -44,7 +47,7 @@ class Controls(ControlsExt):
|
||||
self.CI = interfaces[self.CP.carFingerprint](self.CP, self.CP_SP)
|
||||
|
||||
self.sm = messaging.SubMaster(['lateralDelay', 'vehicleParameters', 'lateralTorqueParameters', 'modelV2', 'selfdriveState',
|
||||
'extrinsicsCalibration', 'deviceMotion', 'longitudinalPlan', 'lateralManeuverPlan', 'carState', 'carOutput',
|
||||
'extrinsicsCalibration', 'deviceMotion', 'longitudinalPlan', 'lateralManeuverPlan', 'carState', 'carStateSP', 'carOutput',
|
||||
'driverMonitoringState', 'onroadEvents', 'driverAssistance'] + self.sm_services_ext,
|
||||
poll='selfdriveState')
|
||||
self.pm = messaging.PubMaster(['carControl', 'controlsState'] + self.pm_services_ext)
|
||||
@@ -52,6 +55,20 @@ class Controls(ControlsExt):
|
||||
self.steer_limited_by_safety = False
|
||||
self.curvature = 0.0
|
||||
self.desired_curvature = 0.0
|
||||
self.ford_path_controller = select_model_action_controller(self.CP, self.params.get_bool("FordModelActionController"),
|
||||
c0_time_based=self.params.get_bool("FordC0TimeBased"),
|
||||
direct_path=self.params.get_bool("FordGeometryReference"),
|
||||
joint_control=self.params.get_bool("FordPscmJointControl") and
|
||||
not self.params.get_bool("JoystickDebugMode"))
|
||||
self.ford_model_action = isinstance(self.ford_path_controller, FordModelActionController)
|
||||
if self.CP.brand == "ford":
|
||||
cloudlog.event("Ford path controller selected",
|
||||
controller=type(self.ford_path_controller).__name__ if self.ford_model_action else "upstream")
|
||||
self.ford_path = FordPath()
|
||||
self.ford_turn_preview = None
|
||||
if self.ford_model_action and self.ford_path_controller.joint_control and self.params.get_bool('FordPscmTurnPreview'):
|
||||
from openpilot.selfdrive.controls.lib.ford_turn_preview import FordTurnPreview
|
||||
self.ford_turn_preview = FordTurnPreview()
|
||||
|
||||
self.pose_calibrator = PoseCalibrator()
|
||||
self.calibrated_pose: Pose | None = None
|
||||
@@ -141,6 +158,8 @@ class Controls(ControlsExt):
|
||||
# Reset desired curvature to current to avoid violating the limits on engage
|
||||
if self.sm.valid['lateralManeuverPlan']:
|
||||
new_desired_curvature = self.sm['lateralManeuverPlan'].desiredCurvature if CC.latActive else self.curvature
|
||||
elif self.ford_model_action and self.ford_path_controller.direct_path:
|
||||
new_desired_curvature = self.ford_path_controller.path_curvature(model_v2, CS.vEgo) if CC.latActive else self.curvature
|
||||
else:
|
||||
new_desired_curvature = model_v2.action.desiredCurvature if CC.latActive else self.curvature
|
||||
self.desired_curvature, curvature_limited = clip_curvature(CS.vEgo, self.desired_curvature, new_desired_curvature, lp.roll)
|
||||
@@ -155,6 +174,46 @@ class Controls(ControlsExt):
|
||||
actuators.curvature = float(lateral_output)
|
||||
else:
|
||||
actuators.steeringAngleDeg = float(lateral_output)
|
||||
if self.CP.brand == "ford":
|
||||
ford_model = model_v2 if self.sm.valid['modelV2'] else None
|
||||
if self.ford_model_action:
|
||||
reference_service = 'lateralManeuverPlan' if self.sm.valid['lateralManeuverPlan'] else 'modelV2'
|
||||
now = time.monotonic()
|
||||
yaw_rate, motion_valid = -CS.yawRate, True
|
||||
ford_speed = CS.vEgo
|
||||
if self.ford_path_controller.joint_control:
|
||||
from openpilot.selfdrive.controls.lib.ford_path import joint_control_speed
|
||||
ford_speed = joint_control_speed(CS)
|
||||
# card's inverse uses the calibrated yaw published in carControl.
|
||||
# Never keep steering from a stale pose or fall back to raw CAN yaw.
|
||||
motion = self.sm['deviceMotion']
|
||||
motion_valid = (self.calibrated_pose is not None and self.pose_calibrator.calib_valid
|
||||
and self.sm.all_checks(['deviceMotion', 'extrinsicsCalibration'])
|
||||
and motion.inputsOK and motion.sensorsOK and motion.angularVelocityDevice.valid
|
||||
and -0.005 <= now - self.sm.logMonoTime['deviceMotion'] * 1e-9 <= 0.15)
|
||||
yaw_rate = float(self.calibrated_pose.angular_velocity.xyz[2]) if motion_valid else math.nan
|
||||
self.ford_path = self.ford_path_controller.update(
|
||||
ford_model, self.desired_curvature, current_curvature=self.curvature, yaw_rate=yaw_rate, speed=ford_speed, now=now,
|
||||
# Roll/angle offset cancel in the error; retain the normal steering-angle conversion's speed and stiffness effects.
|
||||
curvature_scale=self.VM.get_steer_from_curvature(1., CS.vEgo, 0.) / (self.CP.steerRatio*self.CP.wheelbase),
|
||||
measurement_time=self.sm.logMonoTime['carState'] * 1e-9,
|
||||
model_time=self.sm.logMonoTime['modelV2'] * 1e-9,
|
||||
reference_time=self.sm.logMonoTime[reference_service] * 1e-9,
|
||||
active=CC.latActive, valid=motion_valid and CS.canValid and self.sm.all_checks(['carState', 'vehicleParameters', 'modelV2', reference_service]),
|
||||
lat_delay=lat_delay,
|
||||
driver_pressed=CS.steeringPressed, driver_torque=CS.steeringTorque,
|
||||
reference_source=reference_service, roll=lp.roll,
|
||||
pscm_status=self.sm['carStateSP'].fordPscmStatus if self.sm.valid['carStateSP'] else None,
|
||||
)
|
||||
if not self.ford_path.valid:
|
||||
CC.latActive = False
|
||||
if self.sm.frame % 20 == 0:
|
||||
cloudlog.event("Ford C2-free path tracking", model_mono_time=self.sm.logMonoTime['modelV2'],
|
||||
measurement_mono_time=self.sm.logMonoTime['carState'],
|
||||
reference_service=reference_service, reference_mono_time=self.sm.logMonoTime[reference_service],
|
||||
measured_curvature=self.curvature,
|
||||
**self.ford_path_controller.diagnostics)
|
||||
actuators.curvature = float(self.ford_path.curvature)
|
||||
# Ensure no NaNs/Infs
|
||||
for p in ACTUATOR_FIELDS:
|
||||
attr = getattr(actuators, p)
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
Import('env')
|
||||
|
||||
env.SharedLibrary('encoder', 'encoder.cc', LIBS=[])
|
||||
@@ -0,0 +1,31 @@
|
||||
"""Measurement states of the recovered curvature-to-angle stage, before torque."""
|
||||
|
||||
import math
|
||||
|
||||
from openpilot.selfdrive.controls.lib.ford_joint.model import bracket, interp_int, clip
|
||||
|
||||
|
||||
class AngleModel:
|
||||
def __init__(self, cal):
|
||||
self.cal = cal
|
||||
self.speed_bp = cal.read(0xFEF25E40, '8H')
|
||||
self.tables = {address: cal.read(address, '8H') for address in (0xFEF25CA0, 0xFEF25CB0)}
|
||||
self.yaw_acc = self.bank_residual = self.angle_bias = 0.0
|
||||
|
||||
def step(self, speed_kmh, curvature, angle, yaw, accel, wheelbase, ratio):
|
||||
c = self.cal
|
||||
si, sf = bracket(int(speed_kmh * 256) & 0xFFFF, self.speed_bp)
|
||||
v = speed_kmh / 3.6
|
||||
self.yaw_acc += c.h(0xFEF25A90) / 65536 * (v * yaw - self.yaw_acc)
|
||||
self.bank_residual += c.h(0xFEF25A88) / 65536 * (accel - v * yaw - self.bank_residual)
|
||||
opposite = int(self.yaw_acc > 0) - int(self.yaw_acc < 0) != int(self.bank_residual > 0) - int(self.bank_residual < 0)
|
||||
allowance = 3.0 + (abs(self.bank_residual) if opposite else 0.0)
|
||||
requested_accel = clip(curvature * v * v, -allowance, allowance)
|
||||
control_accel = requested_accel * c.b(0xFEF25AB1) / 128 + interp_int(self.tables[0xFEF25CA0], si, sf) / 2048 * (requested_accel - self.yaw_acc)
|
||||
understeer = c.read(0xFEF25A9A, 'h')[0] / 16384
|
||||
floor_v = max(v, c.b(0xFEF25AB7) / 32, 1e-6)
|
||||
conversion = 180 / math.pi * ratio
|
||||
target = ((self.bank_residual * c.read(0xFEF25AB0, 'b')[0] / 64 + control_accel) * understeer + control_accel / floor_v**2 * wheelbase) * conversion
|
||||
error = (floor_v * understeer * yaw + yaw / floor_v * wheelbase) * conversion - angle - self.angle_bias
|
||||
self.angle_bias += interp_int(self.tables[0xFEF25CB0], si, sf) / 65536 * error
|
||||
return target + self.angle_bias * c.read(0xFEF25AB2, 'b')[0] / 64
|
||||
@@ -0,0 +1,475 @@
|
||||
{
|
||||
"firmware": "ML3V-14D003-BD",
|
||||
"calibration": "ML34-14D007-EDL",
|
||||
"firmware_sha256": "8de3eb1f8191b13b57de07430c33f7b723f11b6ed2144991f91dc56476bbbf62",
|
||||
"note": "Recovered numerical calibration only; RL38 equivalence unverified. No executable firmware.",
|
||||
"entries": {
|
||||
"0xfef25e40": {
|
||||
"format": "8H",
|
||||
"values": [
|
||||
0,
|
||||
3840,
|
||||
7680,
|
||||
15360,
|
||||
25600,
|
||||
33280,
|
||||
40960,
|
||||
56320
|
||||
]
|
||||
},
|
||||
"0xfef25ca0": {
|
||||
"format": "8H",
|
||||
"values": [
|
||||
0,
|
||||
410,
|
||||
410,
|
||||
410,
|
||||
614,
|
||||
1024,
|
||||
410,
|
||||
410
|
||||
]
|
||||
},
|
||||
"0xfef25cb0": {
|
||||
"format": "8H",
|
||||
"values": [
|
||||
1311,
|
||||
1311,
|
||||
1147,
|
||||
983,
|
||||
819,
|
||||
590,
|
||||
393,
|
||||
229
|
||||
]
|
||||
},
|
||||
"0xfef25d98": {
|
||||
"format": "10f",
|
||||
"values": [
|
||||
10.0,
|
||||
20.0,
|
||||
30.0,
|
||||
40.0,
|
||||
50.0,
|
||||
60.0,
|
||||
70.0,
|
||||
80.0,
|
||||
100.0,
|
||||
150.0
|
||||
]
|
||||
},
|
||||
"0xfef258fc": {
|
||||
"format": "8B",
|
||||
"values": [
|
||||
3,
|
||||
3,
|
||||
4,
|
||||
5,
|
||||
7,
|
||||
10,
|
||||
11,
|
||||
11
|
||||
]
|
||||
},
|
||||
"0xfef25b78": {
|
||||
"format": "8f",
|
||||
"values": [
|
||||
0.0,
|
||||
0.019999999552965164,
|
||||
0.05000000074505806,
|
||||
0.10000000149011612,
|
||||
0.11749999970197678,
|
||||
0.125,
|
||||
0.125,
|
||||
0.125
|
||||
]
|
||||
},
|
||||
"0xfef25b98": {
|
||||
"format": "8f",
|
||||
"values": [
|
||||
15.0,
|
||||
15.0,
|
||||
25.0,
|
||||
41.666664123535156,
|
||||
55.55555725097656,
|
||||
72.22222137451172,
|
||||
88.88888549804688,
|
||||
122.22222137451172
|
||||
]
|
||||
},
|
||||
"0xfef25bb8": {
|
||||
"format": "8f",
|
||||
"values": [
|
||||
15.0,
|
||||
15.0,
|
||||
25.0,
|
||||
41.666664123535156,
|
||||
50.0,
|
||||
46.94444274902344,
|
||||
57.77777862548828,
|
||||
79.44444274902344
|
||||
]
|
||||
},
|
||||
"0xfef25be8": {
|
||||
"format": "8f",
|
||||
"values": [
|
||||
0.0,
|
||||
0.0,
|
||||
5.0,
|
||||
5.0,
|
||||
18.518518447875977,
|
||||
24.074073791503906,
|
||||
29.629629135131836,
|
||||
29.0
|
||||
]
|
||||
},
|
||||
"0xfef25c60": {
|
||||
"format": "8H",
|
||||
"values": [
|
||||
512,
|
||||
512,
|
||||
512,
|
||||
1024,
|
||||
1536,
|
||||
1536,
|
||||
1536,
|
||||
1536
|
||||
]
|
||||
},
|
||||
"0xfef25d70": {
|
||||
"format": "10f",
|
||||
"values": [
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0
|
||||
]
|
||||
},
|
||||
"0xfef25dc0": {
|
||||
"format": "10f",
|
||||
"values": [
|
||||
0.19599999487400055,
|
||||
0.09799999743700027,
|
||||
0.06530000269412994,
|
||||
0.04899999871850014,
|
||||
0.03920000046491623,
|
||||
0.03269999846816063,
|
||||
0.02800000086426735,
|
||||
0.02449999935925007,
|
||||
0.019600000232458115,
|
||||
0.013100000098347664
|
||||
]
|
||||
},
|
||||
"0xfef25de8": {
|
||||
"format": "10f",
|
||||
"values": [
|
||||
0.019999999552965164,
|
||||
0.004999999888241291,
|
||||
0.002222222276031971,
|
||||
0.0012499999720603228,
|
||||
0.0007999999797903001,
|
||||
0.0005555555690079927,
|
||||
0.0004081632650922984,
|
||||
0.0003124999930150807,
|
||||
0.00019999999494757503,
|
||||
8.888888987712562e-05
|
||||
]
|
||||
},
|
||||
"0xfef25e20": {
|
||||
"format": "8H",
|
||||
"values": [
|
||||
24576,
|
||||
24576,
|
||||
24576,
|
||||
24576,
|
||||
24576,
|
||||
24576,
|
||||
16384,
|
||||
819
|
||||
]
|
||||
},
|
||||
"0xfef25e30": {
|
||||
"format": "8H",
|
||||
"values": [
|
||||
24576,
|
||||
24576,
|
||||
24576,
|
||||
24576,
|
||||
20480,
|
||||
18022,
|
||||
16384,
|
||||
819
|
||||
]
|
||||
},
|
||||
"0xfef25a58": {
|
||||
"format": "f",
|
||||
"values": [
|
||||
2.0
|
||||
]
|
||||
},
|
||||
"0xfef259ec": {
|
||||
"format": "f",
|
||||
"values": [
|
||||
1.0
|
||||
]
|
||||
},
|
||||
"0xfef259f0": {
|
||||
"format": "f",
|
||||
"values": [
|
||||
2.0
|
||||
]
|
||||
},
|
||||
"0xfef259f4": {
|
||||
"format": "f",
|
||||
"values": [
|
||||
0.6000000238418579
|
||||
]
|
||||
},
|
||||
"0xfef25a14": {
|
||||
"format": "f",
|
||||
"values": [
|
||||
30.0
|
||||
]
|
||||
},
|
||||
"0xfef25a04": {
|
||||
"format": "f",
|
||||
"values": [
|
||||
300.0
|
||||
]
|
||||
},
|
||||
"0xfef2596c": {
|
||||
"format": "f",
|
||||
"values": [
|
||||
1200.0
|
||||
]
|
||||
},
|
||||
"0xfef259a0": {
|
||||
"format": "f",
|
||||
"values": [
|
||||
0.20000000298023224
|
||||
]
|
||||
},
|
||||
"0xfef2599c": {
|
||||
"format": "f",
|
||||
"values": [
|
||||
1.350000023841858
|
||||
]
|
||||
},
|
||||
"0xfef259a4": {
|
||||
"format": "f",
|
||||
"values": [
|
||||
-10.0
|
||||
]
|
||||
},
|
||||
"0xfef259a8": {
|
||||
"format": "f",
|
||||
"values": [
|
||||
0.15000000596046448
|
||||
]
|
||||
},
|
||||
"0xfef25a70": {
|
||||
"format": "H",
|
||||
"values": [
|
||||
1229
|
||||
]
|
||||
},
|
||||
"0xfef25a72": {
|
||||
"format": "H",
|
||||
"values": [
|
||||
0
|
||||
]
|
||||
},
|
||||
"0xfef25a60": {
|
||||
"format": "H",
|
||||
"values": [
|
||||
256
|
||||
]
|
||||
},
|
||||
"0xfef25aa5": {
|
||||
"format": "B",
|
||||
"values": [
|
||||
0
|
||||
]
|
||||
},
|
||||
"0xfef25aa6": {
|
||||
"format": "B",
|
||||
"values": [
|
||||
38
|
||||
]
|
||||
},
|
||||
"0xfef25aa1": {
|
||||
"format": "B",
|
||||
"values": [
|
||||
0
|
||||
]
|
||||
},
|
||||
"0xfef25aa2": {
|
||||
"format": "B",
|
||||
"values": [
|
||||
160
|
||||
]
|
||||
},
|
||||
"0xfef25a9e": {
|
||||
"format": "B",
|
||||
"values": [
|
||||
0
|
||||
]
|
||||
},
|
||||
"0xfef25a9f": {
|
||||
"format": "B",
|
||||
"values": [
|
||||
32
|
||||
]
|
||||
},
|
||||
"0xfef25aa4": {
|
||||
"format": "B",
|
||||
"values": [
|
||||
255
|
||||
]
|
||||
},
|
||||
"0xfef25aa7": {
|
||||
"format": "B",
|
||||
"values": [
|
||||
26
|
||||
]
|
||||
},
|
||||
"0xfef25aa3": {
|
||||
"format": "B",
|
||||
"values": [
|
||||
9
|
||||
]
|
||||
},
|
||||
"0xfef25aa0": {
|
||||
"format": "B",
|
||||
"values": [
|
||||
38
|
||||
]
|
||||
},
|
||||
"0xfef25a74": {
|
||||
"format": "H",
|
||||
"values": [
|
||||
51
|
||||
]
|
||||
},
|
||||
"0xfef25a00": {
|
||||
"format": "f",
|
||||
"values": [
|
||||
0.30000001192092896
|
||||
]
|
||||
},
|
||||
"0xfef25a10": {
|
||||
"format": "f",
|
||||
"values": [
|
||||
0.014999999664723873
|
||||
]
|
||||
},
|
||||
"0xfef259e8": {
|
||||
"format": "f",
|
||||
"values": [
|
||||
0.0003000000142492354
|
||||
]
|
||||
},
|
||||
"0xfef259e4": {
|
||||
"format": "f",
|
||||
"values": [
|
||||
0.003000000026077032
|
||||
]
|
||||
},
|
||||
"0xfef25a0c": {
|
||||
"format": "f",
|
||||
"values": [
|
||||
0.30000001192092896
|
||||
]
|
||||
},
|
||||
"0xfef259fc": {
|
||||
"format": "f",
|
||||
"values": [
|
||||
2.799999952316284
|
||||
]
|
||||
},
|
||||
"0xfef25968": {
|
||||
"format": "f",
|
||||
"values": [
|
||||
-0.30000001192092896
|
||||
]
|
||||
},
|
||||
"0xfef25960": {
|
||||
"format": "f",
|
||||
"values": [
|
||||
0.30000001192092896
|
||||
]
|
||||
},
|
||||
"0xfef25964": {
|
||||
"format": "f",
|
||||
"values": [
|
||||
-0.4000000059604645
|
||||
]
|
||||
},
|
||||
"0xfef2595c": {
|
||||
"format": "f",
|
||||
"values": [
|
||||
0.4000000059604645
|
||||
]
|
||||
},
|
||||
"0xfef25a08": {
|
||||
"format": "f",
|
||||
"values": [
|
||||
0.10000000149011612
|
||||
]
|
||||
},
|
||||
"0xfef259f8": {
|
||||
"format": "f",
|
||||
"values": [
|
||||
1.5
|
||||
]
|
||||
},
|
||||
"0xfef25a90": {
|
||||
"format": "H",
|
||||
"values": [
|
||||
6554
|
||||
]
|
||||
},
|
||||
"0xfef25a88": {
|
||||
"format": "H",
|
||||
"values": [
|
||||
1627
|
||||
]
|
||||
},
|
||||
"0xfef25ab7": {
|
||||
"format": "B",
|
||||
"values": [
|
||||
32
|
||||
]
|
||||
},
|
||||
"0xfef25a9a": {
|
||||
"format": "h",
|
||||
"values": [
|
||||
33
|
||||
]
|
||||
},
|
||||
"0xfef25ab1": {
|
||||
"format": "B",
|
||||
"values": [
|
||||
128
|
||||
]
|
||||
},
|
||||
"0xfef25ab0": {
|
||||
"format": "b",
|
||||
"values": [
|
||||
0
|
||||
]
|
||||
},
|
||||
"0xfef25ab2": {
|
||||
"format": "b",
|
||||
"values": [
|
||||
-64
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,206 @@
|
||||
// Opt-in Ford joint encoder. Numerical request state is estimated, not ECU RAM.
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
#include <map>
|
||||
|
||||
static double clip(double x, double lo, double hi) {
|
||||
return std::min(std::max(x, lo), hi);
|
||||
}
|
||||
|
||||
// State: held C0, held C1, internal I, filtered curvature, fast-slew latch.
|
||||
// Parameters: g0/g1, base alpha, heading low/high/gain, alpha max, speed,
|
||||
// interaction threshold/value, C0 slow/fast, C1 slow/fast, latch clear C0/C1,
|
||||
// opposite C0/I thresholds, I gain, delta low/high, I low/high, release rate.
|
||||
static void step(double *s, const double *p, double c0, double c1) {
|
||||
if (p[9] < p[8]) {
|
||||
s[4] = 1;
|
||||
} else if (std::abs(c0) <= p[14] && std::abs(c1) <= p[15]) {
|
||||
s[4] = 0;
|
||||
}
|
||||
double r0 = p[s[4] ? 11 : 10], r1 = p[s[4] ? 13 : 12];
|
||||
s[0] += clip(c0 - s[0], -r0 * .008, r0 * .008);
|
||||
s[1] += clip(c1 - s[1], -r1 * .008, r1 * .008);
|
||||
if (std::abs(s[0]) > p[16] && std::abs(s[2]) > p[17] && s[0] * s[2] < 0) {
|
||||
s[2] += clip(-s[2], -p[23], p[23]);
|
||||
} else {
|
||||
s[2] = clip(s[2] + clip(s[0], p[19], p[20]) * p[18], p[21], p[22]);
|
||||
}
|
||||
double raw = p[0] * (s[0] + s[2]) + p[1] * s[1];
|
||||
double w = clip(std::abs(s[1] * p[7]) - p[3], 0, std::max(1e-6, p[4] - p[3])) / std::max(1e-6, p[4] - p[3]);
|
||||
double alpha = std::min(p[6], p[2] + p[5] * w);
|
||||
s[3] += alpha * (raw - s[3]);
|
||||
}
|
||||
|
||||
static double command_cost(const double *initial, const double *p, double target,
|
||||
const double *pref, int count, double c0, double c1, double *s) {
|
||||
std::memcpy(s, initial, 5 * sizeof(double));
|
||||
double steady_raw = p[0] * pref[0] + p[1] * pref[1];
|
||||
double steady_error = (steady_raw - target) / .01, cost = 0;
|
||||
for (int j = 0; j < count; j++) {
|
||||
step(s, p, c0, c1);
|
||||
double error = (s[3] - target) / .01;
|
||||
cost += .008 * (error * error - steady_error * steady_error);
|
||||
}
|
||||
return cost;
|
||||
}
|
||||
|
||||
static double return_cost(const double *first, const double *p, double target, const double *pref, double cost) {
|
||||
double s[5];
|
||||
std::memcpy(s, first, sizeof(s));
|
||||
double steady_raw = p[0] * pref[0] + p[1] * pref[1];
|
||||
double steady_error = (steady_raw - target) / .01;
|
||||
auto tick = [&](double a, double b) {
|
||||
step(s, p, a, b);
|
||||
double error = (s[3] - target) / .01;
|
||||
cost += .008 * (error * error - steady_error * steady_error);
|
||||
};
|
||||
// Include both channels' entire return, followed by the exact filter tail.
|
||||
// There is no adjustable planning horizon or retained future command plan.
|
||||
int n = 2 + std::ceil(std::max(std::abs(s[0] - pref[0]) / std::min(p[10], p[11]),
|
||||
std::abs(s[1] - pref[1]) / std::min(p[12], p[13])) / .008);
|
||||
for (int j = 0; j < n; j++) tick(pref[0], pref[1]);
|
||||
double w = clip(std::abs(s[1] * p[7]) - p[3], 0, p[4] - p[3]) / (p[4] - p[3]);
|
||||
double alpha = std::min(p[6], p[2] + p[5] * w), a = 1 - alpha;
|
||||
double d = (s[3] - steady_raw) / .01;
|
||||
return cost + .008 * (2 * steady_error * d * a / alpha + d * d * a * a / (1 - a * a));
|
||||
}
|
||||
|
||||
extern "C" double paired_cost(const double *initial, const double *p, double target,
|
||||
const double *pref, int count, double c0, double c1, double *first) {
|
||||
double s[5];
|
||||
double cost = command_cost(initial, p, target, pref, count, c0, c1, s);
|
||||
if (first) std::memcpy(first, s, sizeof(s));
|
||||
return return_cost(s, p, target, pref, cost);
|
||||
}
|
||||
|
||||
// Preview uses the ordinary C1-anchored accuracy bound, but does not need the
|
||||
// ordinary full C0/C1 search. Keep its scoring and tie breaks exactly the same.
|
||||
extern "C" double paired_immediate_bound(const double *initial, const double *p, double target,
|
||||
const double *pref, int count, const double *c0s, int n0) {
|
||||
double max_error = 1e300, best = 1e300, best_move = 1e300, best_remaining = 1e300;
|
||||
std::map<std::array<double, 6>, double> costs;
|
||||
for (int i = 0; i < n0; i++) {
|
||||
double first[5];
|
||||
double cost = command_cost(initial, p, target, pref, count, c0s[i], pref[1], first);
|
||||
std::array<double, 6> key = {first[0], first[1], first[2], first[3], first[4], cost};
|
||||
auto entry = costs.emplace(key, 0.0);
|
||||
if (entry.second) entry.first->second = return_cost(first, p, target, pref, cost);
|
||||
double score = std::nearbyint(entry.first->second * 1e12) / 1e12;
|
||||
double move = std::abs(c0s[i] - initial[0]), remaining = std::abs(c0s[i] - pref[0]);
|
||||
if (score < best || (score == best && (move < best_move || (move == best_move && remaining < best_remaining)))) {
|
||||
best = score;
|
||||
best_move = move;
|
||||
best_remaining = remaining;
|
||||
max_error = std::abs(first[3] - target);
|
||||
}
|
||||
}
|
||||
return max_error;
|
||||
}
|
||||
|
||||
extern "C" void paired_select(const double *initial, const double *p, double target,
|
||||
const double *pref, int count, const double *c0s, int n0,
|
||||
const double *c1s, int n1, int preserve_now, double *result) {
|
||||
double best = 1e300, best_move = 1e300, best_remaining = 1e300;
|
||||
double first[5];
|
||||
// Slew clipping makes different command fields reach identical states. Reuse
|
||||
// their exact return cost within this selection only. Include the prefix cost
|
||||
// so the original floating-point accumulation and tie breaks are preserved.
|
||||
std::map<std::array<double, 6>, double> costs;
|
||||
auto finish = [&](double cost) {
|
||||
if (!std::isfinite(cost)) return return_cost(first, p, target, pref, cost);
|
||||
std::array<double, 6> key = {first[0], first[1], first[2], first[3], first[4], cost};
|
||||
auto entry = costs.emplace(key, 0.0);
|
||||
if (entry.second) entry.first->second = return_cost(first, p, target, pref, cost);
|
||||
return entry.first->second;
|
||||
};
|
||||
double max_error = 1e300, anchor_score = 1e300, anchor_move = 1e300, anchor_remaining = 1e300;
|
||||
if (preserve_now) {
|
||||
// Preserve the C1-anchored policy's immediate target accuracy.
|
||||
// An inequality against a feasible reference, not a new gain/deadband.
|
||||
for (int i = 0; i < n0; i++) {
|
||||
double cost = command_cost(initial, p, target, pref, count, c0s[i], pref[1], first);
|
||||
cost = finish(cost);
|
||||
double score = std::nearbyint(cost * 1e12) / 1e12;
|
||||
double move = std::abs(c0s[i] - initial[0]), remaining = std::abs(c0s[i] - pref[0]);
|
||||
if (score < anchor_score || (score == anchor_score && (move < anchor_move || (move == anchor_move && remaining < anchor_remaining)))) {
|
||||
anchor_score = score;
|
||||
anchor_move = move;
|
||||
anchor_remaining = remaining;
|
||||
max_error = std::abs(first[3] - target);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (int i = 0; i < n0; i++) {
|
||||
for (int j = 0; j < n1; j++) {
|
||||
double cost = command_cost(initial, p, target, pref, count, c0s[i], c1s[j], first);
|
||||
if (std::abs(first[3] - target) > max_error + 1e-12) continue;
|
||||
// Reject infeasible candidates before simulating their full return.
|
||||
cost = finish(cost);
|
||||
// Numerical equality only. Tie breaks cannot trade worse tracking for
|
||||
// less channel motion; normalize them using the existing field spans.
|
||||
double score = std::nearbyint(cost * 1e12) / 1e12;
|
||||
double move = std::abs(c0s[i] - initial[0]) / 5.11 + std::abs(c1s[j] - initial[1]) / .5;
|
||||
double remaining = std::abs(c0s[i] - pref[0]) / 5.11 + std::abs(c1s[j] - pref[1]) / .5;
|
||||
if (score < best || (score == best && (move < best_move || (move == best_move && remaining < best_remaining)))) {
|
||||
best = score;
|
||||
best_move = move;
|
||||
best_remaining = remaining;
|
||||
result[0] = c0s[i];
|
||||
result[1] = c1s[j];
|
||||
result[2] = cost;
|
||||
std::memcpy(result + 3, first, 5 * sizeof(double));
|
||||
}
|
||||
}
|
||||
}
|
||||
result[8] = max_error;
|
||||
}
|
||||
|
||||
// Forecast the recent target trend briefly, then hold. The current-target
|
||||
// accuracy bound remains the ordinary encoder's feasible reference.
|
||||
extern "C" void paired_preview_select(const double *initial, const double *p,
|
||||
const double *targets, const double *prefs, int n,
|
||||
int count, double max_error, const double *c0s, int n0,
|
||||
const double *c1s, int n1, double *result) {
|
||||
double best = 1e300, best_move = 1e300, best_remaining = 1e300;
|
||||
std::map<std::array<double, 6>, double> cache;
|
||||
for (int i = 0; i < n0; i++) for (int j = 0; j < n1; j++) {
|
||||
double first[5];
|
||||
std::memcpy(first, initial, sizeof(first));
|
||||
double cost = 0;
|
||||
for (int k = 0; k < count; k++) {
|
||||
step(first, p, c0s[i], c1s[j]);
|
||||
double e = (first[3] - targets[std::min(k + 1, n)]) / .01;
|
||||
cost += .008 * e * e;
|
||||
}
|
||||
// Preserve the ordinary C1-anchored bound against the current target.
|
||||
if (std::abs(first[3] - targets[0]) > max_error + 1e-12) continue;
|
||||
std::array<double, 6> key = {first[0], first[1], first[2], first[3], first[4], cost};
|
||||
auto entry = cache.emplace(key, 0.);
|
||||
if (entry.second) {
|
||||
double s[5];
|
||||
std::memcpy(s, first, sizeof(s));
|
||||
for (int k = count; k < n; k++) {
|
||||
step(s, p, prefs[2 * (k + 1)], prefs[2 * (k + 1) + 1]);
|
||||
double e = (s[3] - targets[k + 1]) / .01;
|
||||
cost += .008 * e * e;
|
||||
}
|
||||
entry.first->second = return_cost(s, p, targets[n], prefs + 2 * n, cost);
|
||||
}
|
||||
cost = entry.first->second;
|
||||
double score = std::nearbyint(cost * 1e12) / 1e12;
|
||||
double move = std::abs(c0s[i] - initial[0]) / 5.11 + std::abs(c1s[j] - initial[1]) / .5;
|
||||
double remaining = std::abs(c0s[i] - prefs[2 * n]) / 5.11 + std::abs(c1s[j] - prefs[2 * n + 1]) / .5;
|
||||
if (score < best || (score == best && (move < best_move || (move == best_move && remaining < best_remaining)))) {
|
||||
best = score;
|
||||
best_move = move;
|
||||
best_remaining = remaining;
|
||||
result[0] = c0s[i];
|
||||
result[1] = c1s[j];
|
||||
result[2] = cost;
|
||||
std::memcpy(result + 3, first, sizeof(first));
|
||||
}
|
||||
}
|
||||
result[8] = max_error;
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user