Skip to content

fix(mpc_lateral_controller): keep the steering state within the steering limit - #3584

Draft
danielsanchezaran wants to merge 3 commits into
feat/v0.64/e2efrom
fix/e2e-mpc-steer-limit-latch
Draft

danielsanchezaran wants to merge 3 commits into
feat/v0.64/e2efrom
fix/e2e-mpc-steer-limit-latch

Conversation

@danielsanchezaran

@danielsanchezaran danielsanchezaran commented Oct 8, 2026 •

Copy link
Copy Markdown

Description

Problem. If the driver steers beyond the MPC steering limit (36.67 deg = 0.640 rad) while in manual, the MPC locks onto a command beyond the limit and its QP fails every cycle until the vehicle stops. When Autoware control is engaged during that time, the wheel is steered toward the stale command, even if the driver had already straightened the wheel.

Seen on the vehicle (J6 gen2, 2026-10-06 14:24:27-14:25:56 JST):

  • In manual, the driver steered to 40.1 deg. The MPC published 39.58 deg and then failed with optimization failure (qp solver error) for 90 s.
  • Four manual-to-autonomous engages at ~0 deg wheel and 3.5-6.7 m/s: each swung the wheel toward 39.58 deg until the driver took over again.
  • The latch released only when the vehicle stopped.

latch overview

Root cause.

  1. Not under control, m_ctrl_cmd_prev = measured steering (40.12 deg). applyConfidenceSteerSlewLimit moves one step from it and publishes 39.58 deg. Nothing clamps it to the limit.
  2. syncMpcSteerStateToCommand writes 39.58 deg into m_raw_steer_cmd_prev. The next QP's first-step rate constraint (prev ± rate * ctrl_period) cannot be met by any angle within ±m_steer_lim → primal infeasible.
  3. The failure path publishes getStopControlCommand() (= the last command, 39.58 deg) and syncs it again → every following QP is infeasible too.

latch onset

Fix.

  • MPC::resetSteeringCmdFilter: clamp to ±m_steer_lim (as resetPrevResult already does). This breaks the latch.
  • getInitialControlCommand: clamp the measured steering, so the published command starts within the limit.
  • QP failure while Autoware is not in control: publish the measured steering instead of the last command.

Before / after. The vehicle bag (14:24:18-14:26:02) replayed into the MPC twice, without and with this PR, plotted together:

  • orange = MPC output without the fix: stuck at 39.58 deg, failing every cycle (red) until the car stops;
  • cyan = MPC output with the fix: never fails, follows the wheel (black, as recorded on the car);
  • green = the 4 engages. Without the fix the gate steers toward 39.58 deg there; with the fix the MPC output is at the wheel angle.
mpc_before_after.mp4

More detail: (vehicle bag replayed; right: tire angle and speed. Black = measured, orange = MPC on the car, blue = gate output sent to the car, cyan = MPC with this PR replayed on the same inputs, red = car MPC failing, green = Autoware in control):

Onset + engages 1-2 (32 s):

mpc_latch_onset_engages_1_2.mp4

Engages 3-4 + release at stop (66 s):

mpc_latch_engages_3_4_release.mp4

Related links

Parent Issue:

  • None

How was this PR tested?

1. Unit test MPCTest.OsqpCalculateAfterSyncBeyondSteerLimit (new): sync a command 0.15 rad beyond the limit, then solve.

  • Before: OSQP primal infeasible, qp solver error.
  • This PR: solved, command within the limit. All 33 tests of test_lateral_controller pass.

Controller test FakeNodeFixture.manual_steering_beyond_limit_does_not_latch (new, autoware_trajectory_follower_node): in manual, the driver steers 0.1 rad beyond the limit with a low-confidence reference, straightens the wheel, then engages; the command must follow the wheel.

  • Before: the command stays at 0.767 rad after engaging.
  • This PR: passes. All 14 tests of test_trajectory_follower_node and 13 of the PID controller pass.
colcon test --packages-select autoware_mpc_lateral_controller --event-handlers console_direct+

2. Vehicle bag replay into the trajectory follower alone (open loop, 14:23:16-14:26:14).

failed cycles steering command 14:24:28-14:25:56 max steering command
vehicle 2993 (90 s) 39.58 deg constant 39.58 deg
replay, before 2993 39.58 deg constant (same as vehicle) 39.58 deg
replay, this PR 0 follows the measured steering 36.71 deg

Outside the latched span, the replayed command matches the vehicle's within 0.03 deg (before) / 0.1 deg (this PR).

How to reproduce the replay

Workspace: pilot-auto.x2 on feat/v0.64/e2e (needs autoware_launch from autoware_launch.x2 and j6_gen2_description). Build once without and once with this PR.

Bag: vehicle drive 2026-10-06, 14:23:16-14:26:16 JST (shared internally). Needed topics: /planning/trajectory, /localization/kinematic_state, /localization/acceleration, /vehicle/status/steering_status, /system/operation_mode/state.

a. Launch the trajectory follower alone (same remaps and params as tier4_control_launch), controller_replay.launch.xml:

<launch>
  <node pkg="autoware_trajectory_follower_node" exec="controller_node_exe" name="controller_node_exe" namespace="control/trajectory_follower" output="screen">
    <remap from="~/input/reference_trajectory" to="/planning/trajectory"/>
    <remap from="~/input/current_odometry" to="/localization/kinematic_state"/>
    <remap from="~/input/current_steering" to="/vehicle/status/steering_status"/>
    <remap from="~/input/current_accel" to="/localization/acceleration"/>
    <remap from="~/input/current_operation_mode" to="/system/operation_mode/state"/>
    <remap from="~/input/steering_offset_update" to="/vehicle/calibration/steer_offset_estimator/steering_offset_update"/>
    <remap from="~/output/predicted_trajectory" to="lateral/predicted_trajectory"/>
    <remap from="~/output/lateral_diagnostic" to="lateral/diagnostic"/>
    <remap from="~/output/slope_angle" to="longitudinal/slope_angle"/>
    <remap from="~/output/longitudinal_diagnostic" to="longitudinal/diagnostic"/>
    <remap from="~/output/stop_reason" to="longitudinal/stop_reason"/>
    <remap from="~/output/control_cmd" to="control_cmd"/>
    <param name="use_sim_time" value="true"/>
    <param name="lateral_controller_mode" value="mpc"/>
    <param name="longitudinal_controller_mode" value="pid"/>
    <param from="$(find-pkg-share autoware_launch)/config/control/common/nearest_search.param.yaml"/>
    <param from="$(find-pkg-share autoware_launch)/config/control/trajectory_follower/trajectory_follower_node.param.yaml"/>
    <param from="$(find-pkg-share autoware_launch)/config/control/trajectory_follower/longitudinal/pid.param.yaml"/>
    <param from="$(find-pkg-share autoware_launch)/config/control/trajectory_follower/lateral/mpc.param.yaml"/>
    <param from="$(find-pkg-share j6_gen2_description)/config/vehicle_info.param.yaml"/>
  </node>
</launch>

b. Play the inputs only. /system/operation_mode/state needs a transient_local QoS override (qos.yaml):

/system/operation_mode/state:
  history: keep_last
  depth: 1
  reliability: reliable
  durability: transient_local
ros2 launch ./controller_replay.launch.xml &
ros2 bag record -s mcap -o replay_out /control/trajectory_follower/control_cmd /control/trajectory_follower/lateral/diagnostic &
ros2 bag play <bag> --clock 100 --qos-profile-overrides-path qos.yaml \
  --topics /planning/trajectory /localization/kinematic_state /localization/acceleration \
           /vehicle/status/steering_status /system/operation_mode/state

Only the inputs are played, so the recorded /control/trajectory_follower/control_cmd comes from the replayed controller.

c. Check.

  • Failed cycles = /control/trajectory_follower/lateral/diagnostic messages with empty data (the error log is throttled): 2993 before, 0 with this PR.
  • Compare /control/trajectory_follower/control_cmd lateral.steering_tire_angle in replay_out with /vehicle/status/steering_status from ~71 s into the bag (14:24:27): before it stays at 0.6908 rad (39.58 deg) until ~14:25:56; with this PR it follows the measured steering, max 0.6405 rad.

Notes for reviewers

  • Two adversarial review rounds (Codex). Fixed: the manual-steering fallback is published without the steering offset (auto offset removal is on for J6), clearer log, controller-level test. Out of scope (pre-existing, unchanged here): the manual initialisation of m_ctrl_cmd_prev mixes the offset-compensated and published frames.
  • Behavior changes only when the steering state would leave the steering limit. Under control with a solved QP, nothing changes.
  • The engage check did not catch the stale command: the lateral-acceleration-difference check uses the gate output (/control/command/control_cmd), which follows the measured steering in manual, not the MPC output. Not changed here.

Interface changes

None.

Effects on system behavior

After the driver steers beyond the limit in manual, the MPC no longer locks onto an out-of-range command, and engaging no longer steers toward it.

…ing limit

While Autoware is not in control, the controller starts from the measured steering. When the
driver steers past the steering limit, the low-confidence slew limit publishes a command beyond the
limit, and syncMpcSteerStateToCommand writes it into the MPC state (m_raw_steer_cmd_prev). The
first steering-rate constraint of the next QP then excludes every angle within the limit, so the QP
is infeasible ("qp solver error"). The failure path publishes the last command and syncs it again,
so the MPC stays failed with the same out-of-range command until the vehicle stops. When control is
enabled meanwhile, the command gate steers toward that stale command.

- resetSteeringCmdFilter clamps to the steering limit, as resetPrevResult already does.
- getInitialControlCommand clamps the measured steering to the limit.
- When the QP fails while Autoware is not in control, follow the measured steering instead of
  holding the last command.
- Test: a solve after syncing a command beyond the limit succeeds (primal infeasible before).

Signed-off-by: Daniel <daniel.sanchez@tier4.jp>
…hout the offset (review)

- On a QP failure while Autoware is not in control, the measured-steering
  command is offset-compensated like the MPC state; subtract the steering
  offset before publishing, as for every other command (with
  enable_auto_steering_offset_removal it was off by the offset).
- Log which fallback is used.
- Controller test: the driver steers beyond the limit in manual with a
  low-confidence reference, straightens the wheel and engages; the command
  must follow the wheel. Without the fix it stays at 0.767 rad.

Signed-off-by: Daniel <daniel.sanchez@tier4.jp>
…e latch test

Signed-off-by: Daniel <daniel.sanchez@tier4.jp>
@sonarqubecloud

sonarqubecloud Bot commented Oct 8, 2026

Copy link
Copy Markdown

@Owen-Liuyuxuan

Copy link
Copy Markdown

These three PRs are one vehicle-test set (MPC latch + ideal planner + its launch config), but the bases are not on one line:

PR Repo Current base
#3584 universe feat/v0.64/e2e
#3571 universe feat/v0.64/e2e-experiment-2026_09_29-AT
tier4/autoware_launch.x2#2976 launch feat/v4.4/e2e-experiment-2026_09_29-AT

Please retarget the whole set onto one line before the next test image is cut:

  • e2e: universe feat/v0.64/e2e and launch feat/v4.4/e2e
  • experiment: one matching experiment pair, not a mix of e2e and 2026_09_29-AT

2026_09_29-AT is older than the experiment checkout this workspace is on now (universe feat/v0.64/e2e-experiment-2026_10_06, launch feat/v4.4/e2e-experiment-2026_10_09). If the Tuesday test builds that image, the experiment option needs to be that pair.

Same note on the other two PRs.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants