lateral: stop intentional carcontroller shaping from starving the integrator; make the Clarity L/R splits tunable - #8
Open
anasufovic wants to merge 4 commits into
Conversation
The modified-EPS lateral output scale is asymmetric by design, but its constants were fitted on the Clarity. On a Civic Bosch with the TGG-4250 linear-max rack the left-turn branch floors at 0.6863 while the right sits around 1.11-1.15, so the same corner taken left gets ~1.6x less authority than taken right. On the road that reads as lazy left turns: measured left turn-in error -16.33 deg vs -1.50 right, and a 12.44 deg left/right gap. Two file-backed knobs blend the Clarity constants toward the opposite side: /data/HondaTurnInAsymmetry blends ONLY the two turn-in constants /data/HondaUnwindAsymmetry blends ONLY the two unwind constants Both default to the existing Clarity values, so an absent or unparseable file is bit-identical to before on every car. Turn-in and unwind are deliberately separate. Blending all four constants together buys turn-in strength by weakening the corner exit -- that was tried first and produced a left-exit over-rotation (+5.08/+7.34 deg exit error vs -2.65/-5.36 right) that put the car over a centre divider. Also clears PID state when lateral goes inactive, so a stored integrator cannot carry across a disengagement. Measured on a 2021 Civic Hatchback EX (Bosch), EPS 39990-TGG,A120 with the TGG-4250 mod, comma 3, both knobs at 50: left/right turn-in gap 12.44 deg -> 0.56-0.75 deg left exit error +5.08/+7.34 deg -> +0.70/+0.03 deg Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DMd7c2D7J2y5srtSMasSvy
…teral integrator controlsd sets steer_limited_by_safety whenever the applied torque differs from the requested one by more than a flat 1e-2. On a modified-EPS Honda the carcontroller deliberately reshapes the command every frame -- override fade, the torque LPF (tau 0.08-0.10), low-speed zeroing below NrdrMinSteerSpeed -- so that flag is true on 99.4-99.9% of frames. The lateral integrator is frozen on 81-87% of a drive and the loop runs close to P-only. Intentional shaping is not a safety limit, but the binary threshold cannot tell the two apart. Replaces the freeze decision (only the freeze -- the flag itself is untouched and still reported) with a relative rule, shared by one implementation in latcontrol.py so the online path and any offline reconstruction cannot drift: blocked = |requested - applied| > max(1e-2, rel * |requested|) or sign flip with rel from /data/HondaIntegratorFix as a percent; absent or 0 keeps the legacy binary behaviour exactly. Threshold settled at 0.30 by replaying the rule over 67k logged frames: steady turns 18% frozen vs 80% under the old rule, early override fade still ~100% at every threshold, and the two regimes separate cleanly (p50 0.12 vs 0.92). Anti-windup now also clips against the command that is actually sent. This controller reshapes p/i/f with per-term scales, a scheduled output multiplier and an additive learned trim, so clipping against a bare p+i+d+f protected the integrator against a number that was never on the wire. PIDController.update takes an optional compose callback; callers that pass nothing keep the original arithmetic byte-for-byte. Below 2 mph the integrator now leaks (tau 0.5 s) instead of hard-freezing, so it cannot hold a stale value across a low-speed cutoff. Tests: the freeze rule is pinned against a 4200-frame logged fixture, and the controlsd dispatch is pinned by source order after an earlier version of this change used self.LaC before it was assigned. Measured on a 2021 Civic Hatchback EX (Bosch), EPS 39990-TGG,A120 with the TGG-4250 mod, comma 3: integrator frozen 81-87% -> 29% driver torque >1500 on 8.1% of frames, lowest recorded 4.9 min above 50 mph, zero controller-initiated corrections (9 of 13 wheel touches were signalled lane changes; the other 4 were one driver-initiated manoeuvre where |cmd| peaked at 0.60, not at the rail) Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DMd7c2D7J2y5srtSMasSvy
… duplicated block Behaviour unchanged -- the code is byte-identical to the driven build once comments and blank lines are stripped. Removes a comment describing a hard-clear that the code no longer does (it leaks), a duplicated raw-press explanation, and a params comment mangled by an earlier squash.
LatControl.reset() cleared only the saturation timer, so pid.i survived a disengagement and was re-injected whole on the first frame after the next engagement. Measured on the road: an integral of +0.19115 carried across a 0.20 s gap, and -0.13616 across a 0.35 s gap where it landed against a POSITIVE proportional term. It also survives long gaps -- one case carried across 57.8 s of inactive time, arriving ~90x larger than a fresh integrator would have been. This was latent while the shaping mismatch kept the integrator frozen 81-87% of the time: there was rarely anything meaningful stored to re-inject. The previous commit removes that starvation, which arms it. Fixed by overriding reset() rather than patching update()'s inactive branch. controlsd calls LaC.reset() directly on inactive frames without going through update(), so putting the state in reset() and having the inactive branch call it makes the two paths the same code by construction. LatControlCurvature.reset() already does this; this removes an inconsistency between the two controllers. The full PIDController.reset() is used rather than clearing i alone: p, d and f are recomputed unconditionally at the top of PIDController.update() before anything reads them, so zeroing them cannot change the first active frame, and using the controller's own API keeps this from depending on which fields happen to be stale today. Offline replay over the logged frames, real controller and VehicleModel with controlsd's reset() call reproduced: 14,734 continuously-active frames are bit-identical, and no frame differs before a re-engagement. No driving improvement is claimed. Adversarial review of the two recorded occurrences found the effect invisible in one (peak delta 0.0022 of steer_max) and confined to a low-speed driver-initiated re-engagement in the other. This is a correctness fix for stale state. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DMd7c2D7J2y5srtSMasSvy
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Two lateral fixes found on a 2021 Civic Hatchback EX (Bosch), EPS
39990-TGG,A120with the TGG-4250 linear-max mod (not the 2.5x), comma 3, Pop V2, LatControlPID. Both are default-off and bit-identical to current behaviour when their gate file is absent, on every car.Base is
vfn(merge-basee61af6208). The four control files on this branch are hash-identical to the build that produced every number below — this is what was driven, not a reconstruction.1. Intentional carcontroller shaping starves the lateral integrator
steer_limited_by_safetyis set on any applied-vs-requested mismatch over a flat1e-2. But the modified-EPS Honda carcontroller reshapes the command every frame on purpose — override fade, the torque LPF (tau 0.08-0.10), low-speed zeroing underNrdrMinSteerSpeed. So the flag is true on 99.4-99.9% of frames, the integrator is frozen 81-87% of a drive, and the loop runs close to P-only. Intentional shaping isn't a safety limit; the binary threshold can't tell them apart.Replaces the freeze decision only — the flag itself is untouched and still reported — with a relative rule, one shared implementation in
latcontrol.pyso the online path and any offline reconstruction can't drift:relcomes from/data/HondaIntegratorFixas a percent; absent or0keeps the legacy binary behaviour exactly. 0.30 was chosen by replaying the rule over 67k logged frames: steady turns 18% frozen vs 80% under the old rule, early override fade still ~100% at every threshold, and the two regimes separate cleanly (p50 0.12 vs 0.92).Anti-windup now also clips against the command that's actually sent. This controller reshapes p/i/f with per-term scales, a scheduled output multiplier and an additive learned trim, so clipping against a bare
p+i+d+fwas protecting the integrator against a number that was never on the wire.PIDController.updatetakes an optionalcomposecallback; callers that pass nothing keep the original arithmetic byte-for-byte.Below 2 mph the integrator leaks (tau 0.5 s) instead of hard-freezing, so it can't hold a stale value across a low-speed cutoff.
2. The Clarity left/right splits aren't right for this rack
The output scale is asymmetric by design, but its constants were fitted on the Clarity. On the TGG-4250 Civic the left branch floors at
0.6863while right sits ~1.11-1.15— same corner, ~1.6x less authority going left. It reads as lazy left turns.Two file-backed knobs blend the constants toward the opposite side, turn-in and unwind separately:
/data/HondaTurnInAsymmetry— blends only the two turn-in constants/data/HondaUnwindAsymmetry— blends only the two unwind constantsScale is 0-100: 100 = the current Clarity split exactly (so absent/unset is a no-op), 0 = fully symmetric. I drove both at 50, i.e. halfway — that's where all the numbers below come from. I didn't tune past 50 because it was already at the noise floor; there may be a better value.
Why not let
TuneLearnerhandle it? It was on for every drive here, it keeps separate left/right maps, and its docstring says it exists to cancel direction-dependent bias — so this is the first thing it should have caught. It can't, for two structural reasons: the trim is additive (output_torque += learner_trim) while the defect is a multiplicative output scale, so no single per-cell offset cancels it (whatever fixes a gentle corner is wrong for a hard one); and it's clamped to 10% of the rail against a 1.62x authority difference. So the mechanism designed for this class of problem was running and structurally couldn't fix it. That's the argument for the fix living at the scale, not in the trim.On the knobs — I wouldn't ship it in this shape. They exist so I could measure this on the road across a range of values, not because a runtime knob is the right end state. The correct value looks like a property of the rack rather than a user preference, so a per-fingerprint constant is probably right, and the whole thing wants to be neater and more compact than it is. Leaving it as-is for now rather than guessing at the shape you'd want — happy to fold it into a constant for the modified-EPS Civic Bosch, make it fingerprint-keyed, or restructure it however you'd rather review. The measurements are the part I'd stand behind; the plumbing is scaffolding.
The separation matters. Blending all four together buys turn-in strength by weakening the corner exit — that was tried first and produced a left-exit over-rotation (+5.08/+7.34° vs -2.65/-5.36° right) that put the car over a centre divider. Learned that one the expensive way.
3. Stale PID integral across a disengagement
LatControl.reset()cleared only the saturation timer, sopid.isurvived a disengagement and was re-injected whole on the first frame after re-engaging. Measured on the road:+0.19115carried across a 0.20 s gap, and-0.13616across a 0.35 s gap where it landed against a positive proportional term. One case carried across 57.8 s of inactive time.Latent while the shaping mismatch kept the integrator frozen 81-87% of the time — there was rarely anything meaningful stored. Fix 1 above arms it, which is why it's in this PR.
Fixed by overriding
reset()rather than patching the inactive branch:controlsdcallsLaC.reset()directly on inactive frames without going throughupdate(), so putting the state inreset()and having the inactive branch call it makes the two paths the same code by construction.LatControlCurvature.reset()already does this.Offline replay over the logged frames with
controlsd's reset call reproduced: 14,734 continuously-active frames bit-identical, no frame differs before a re-engagement. 9 new tests, 7 of which fail against the unfixed code.No driving improvement is claimed for this one. Adversarial review of both recorded occurrences found the effect invisible in one (peak Δ 0.0022 of
steer_max) and confined to a low-speed driver-initiated re-engagement in the other. It's a correctness fix for stale state, and it has not been road-tested yet.(An earlier version of this description claimed the asymmetry commit already did this. It didn't — that claim was wrong, and the commit message on
922f9b298carries the same error. Now actually fixed, inb440094c9.)Numbers
Full lateral config the numbers were taken at, since some of it is non-default and would change what you'd reproduce:
HondaIntegratorFix=30,HondaTurnInAsymmetry=50,HondaUnwindAsymmetry=50LatPScalelow/std/hwyLatIScale,LatFScaleLatSmoothSecondsLaneCenteringE2EAuthorityNrdrTuneLearner/ rateNrdrLatStictionNrdrLearnSteerRatio,NrdrLearnStiffnessNrdrMinSteerSpeedNrdrDriverOverrideThresholdHondaCenterScale/ boost thr / min speedHondaUnwindFreeze/ ff mult / boost sHondaLateralPidKpScale,KiScalepop223)LatPScaleat 130 is the one most likely to matter — at stock 100 the left/right numbers below will probably look different.NrdrTuneLearnerandNrdrLatStictionare both on and supply a large share of the torque at small commands, so results with either off aren't comparable.On that last row, so it isn't oversold: there were 13 wheel touches above 50 mph, 9 of them signalled lane changes. The other 4 are one 3-second manoeuvre where the car's own error stayed under 2° and
|cmd|peaked at 0.60, not at the rail.Caveats
/data/, not registered params — deliberate, to avoid aparams_pyx.sorebuild during testing. Happy to convert for merge if you'd rather.selfdrive/controls/tests/test_controlsd.pycan't be collected on a mac host (native msgq), so controlsd's runtime path is covered by a source-order test only.Not in this PR
A variable-ratio fix (solve the desired angle on the rack rather than selecting the ratio at the measured angle) is on a separate branch and deliberately not proposed here — it's worth about 0.2° and it's still in review.
Also unknown: whether the integrator-starvation bug exists on the
nrdr-*dialect. It isn't dialect-specific in principle — it's carcontroller shaping vssteer_limited_by_safety— but that file differs by ~586 lines and I haven't checked. Happy to look if it's useful.Have rlogs for all of the above if you want them.