Design changes
What has to change, and in what order, to take a general goal from 15 cm / 5° to 5 cm / 2° and stop the robot weaving along the path.
Route-graph BT: NavFn first mile + authored route middle + NavFn last mile → CornerFilletSmoother → FeasiblePathHandler → RotationShimController wrapping MPPI → BidirectionalGoalChecker.
Goal accuracy
0.15 m / 0.088 rad0.05 m / 0.035 rad
General goals, confirmed with the user. 0.088 rad is 5.0°; 0.035 rad is 2.0°.
Path alignment
Oscillation and weaving along the pathp95 lateral error ≤ 2 cm, no dominant spectral peak
The dominant field symptom is weave, not steady offset and not corner cutting.
Budget
5 cm
2° of heading, in the map frame
Changes
28
across 15 items in 3 phases
No rebuild
8
of 12 changing items; 4 need a build
Files touched
12
8 more are read, not changed
Error budget
5 cm total in the map frame is a hard target, and the terms are listed in the order the error enters. Every term has to land under its ceiling for the total to close, and the two sums under the table say what "close" depends on.
| # | Source | Today | Needed | Addressed by |
|---|---|---|---|---|
| 0 | Localization repeatability at the goalgates the plan The goal is expressed in map, so goal accuracy can never beat localization repeatability at the station. Nothing below this line can recover what this term loses. | unmeasured | ≤ 2 cm, 1σ | |
| 1 | Path geometry near the goal — NavFn staircase → RDP → 2.0 m fillet | up to 10 cm | ≤ 1 cm | |
| 2 | MPPI terminal convergence — mean-over-horizon goal cost | tolerance-limited | ≤ 2 cm | |
| 3 | Terminal in-place rotation drift | unmeasured | ≤ 1 cm | |
| 4 | Goal-checker acceptance bandnot additive The window the four terms above have to fit inside, not a fifth error added to them: if the controller converges to 2 cm the band never binds. Counted as a contributor it would be the whole budget on its own. | 15 cm | 5 cm |
Does it close?
- Target
- 5.0 cm
- Worst case — 4 terms, added
- 6.0 cm
- Independent — root-sum-square
- 3.2 cm
The budget closes only if the terms are independent. All four at their ceiling at once is 6.0 cm, over the 5.0 cm target; combined in quadrature they are 3.2 cm.
3 of the 4 contributing terms have never been measured, so the figures above are what the plan is aiming at rather than what it has observed. The heading budget is 2°; both are in the map frame.
The plan
Ordered, and the order is the argument: measure first, then the weave, then the last five centimetres. Apply and measure one item at a time.
Phase 0
Measure before changing anything
Three numbers decide how much of the rest of this plan is worth doing. Publish them before touching a parameter.
Expected effort: half a day
0.1 Actuation dead time τ
measurement
Cross-correlate commanded wz against measured wz. The lag peak is τ, and it is the single most important number in the plan.
Log
/cmd_vel— the controller’s output — and/odomat the same station, and cross-correlate the commanded angular rate against the measured one.The chain is MPPI at 20 Hz →
velocity_smootherat 20 Hz →collision_monitor→ base driver, so expect 60–150 ms.0.2 Localization repeatability
measurement
Drive to the same physical goal about 20 times and report the 1σ and the max. This one gates the whole plan.
Record the
map-frame pose reported at goal-reached and the physical position, measured with tape or a laser. Also record AMCL pose-jump magnitude and rate while driving a straight aisle.If AMCL’s lateral 1σ at a goal is 3 cm, no controller change reaches 5 cm and the answer becomes a local-feature fine-positioning stage instead — the QR/dock path already in
FollowPathWithBaseController. Everything in Phase 2 assumes this number came back under 2 cm.0.3 Weave signature
measurement
FFT the lateral error from a straight-lane run. The peak frequency says which mechanism dominates.
From a straight-lane run, log lateral error — robot pose against the nearest point on
/controller_path_bt— and take its FFT. The peak tells you which of the changes below to expect a result from:- ~10 Hz → costmap or critic ripple: the costmap-rate and rollout-overrun items.
- 1–3 Hz → a dead-time limit cycle: the latency model.
- matches the AMCL update rate → localization, not control, and the repeatability measurement above is the whole answer.
Existing instrumentation covers most of this:
publish_critics_stats: true,publish_optimal_trajectory: true, and the~/path_follow_debug_markerspublisher inpath_follow_critic.cpp.Where it lives path_follow_critic.cpp (~/path_follow_debug_markers)
Phase 1
Kill the weave
Ordered by expected effect. Apply and measure one at a time; the FFT peak from the measurements says which to expect a result from.
1.1 Model the actuation latency
parameters
The highest-leverage change here, and it costs no code: MPPI's rollouts currently assume a commanded wz takes effect instantly.
model_delay_vx / vy / wzare all 0.0, but the fork already implements them —motion_models.hppmaintains a command history and offsets it byround(delay / model_dt)steps.Lateral error is corrected through
wz, so the alignment loop is closed around an unmodelled pure dead time. At 1 m/s with an alignment loop crossing near 2 rad/s, τ = 0.1 s costs about 11° of phase margin — enough to turn a well-damped loop into a limit cycle. That is the classic signature of the reported weave.Set the delays to the τ measured in Phase 0, rounded to a multiple of
model_dt(0.05). Start at the measured value.Changes
Target From To File FollowPath.model_delay_vx
0.0 measured τ hmc_nav2_params_tuning_04Sep-2026-09-04-v1.yaml FollowPath.model_delay_wz
0.0 measured τ hmc_nav2_params_tuning_04Sep-2026-09-04-v1.yaml Where it lives motion_models.hpp:76-86 (command history), motion_models.hpp:158-160
After 0.1 Actuation dead time τ
Verified by 1 Weave metric
Watch If the robot becomes sluggish at corners, back off one step — 0.05 s.
1.2 Remove the remaining relay-style critic gates
C++
Four high-weight critic terms still switch fully on or off between consecutive control cycles with no hysteresis. This is the one place where the code, not a parameter, is the oscillation source.
The parameter file’s comments already diagnose one relay and work around it by moving
PathAngleCritic.offset_from_furthestfrom 12 to 25. These remain:The
max_path_occupancy_ratiogate matters most in a factory. A path point is invalid at cost ≥INSCRIBED_INFLATED_OBSTACLE(253) — seefindPathCosts. Withfootprint_padding: 0.25the inflation layer’s inscribed radius is 0.38 + 0.25 = 0.63 m, so every authored-lane point within 0.63 m of a rack, a wall or a parked pallet is invalid. In a narrow aisle the ratio hovers right at the 0.10 / 0.07 thresholds, and the weight-25 alignment term blinks on and off as the robot advances.Worse, invalid points are also skipped inside the per-sample accumulation —
if (path_pts_valid[path_pt])— so the alignment signal’s composition changes on every 10 Hz costmap update even while the ratio stays below the threshold.The change: replace each hard
returnwith a continuous weight ramp. Add one shared helper —mppi::utils::gateRamp(value, threshold, band), returning a smoothstep in [0, 1] — and multiply the critic’s contribution by it, behind a new per-criticgate_ramp_bandparameter defaulting to 0. Zero is today’s behaviour, so the change is opt-in and bisectable.Starting bands: 5 indices for the
offset_from_furthestgate, 0.05 formax_path_occupancy_ratio, 0.15 rad formax_angle_to_furthest, 0.3 m forCostCritic.near_goal.Gate Where Weight it switches path_segments_count < offset_from_furthest_ → return path_align_critic.cpp:59 25.0 path_segments_count < offset_from_furthest_ → return curvature_only_path_align_critic.cpp:132 8.0 invalid_ctr / path_segments_flt > max_path_occupancy_ratio_ → return both align critics 25.0 and 8.0 local_path_length < near_goal_distance_ → drop preferential term cost_critic.cpp:154 3.0 posePointAngle(…) < max_angle_to_furthest_ → return path_angle_critic.cpp:88-107 2.0 Changes
Target From To File mppi::utils::gateRamp
New shared helper: a smoothstep in [0, 1] over a band either side of the threshold.
— — utils.hpp gate_ramp_band
Both the offset and the occupancy gate. Defaults to 0 — today's behaviour.
hard return weight ramp path_align_critic.cpp gate_ramp_band
hard return weight ramp curvature_only_path_align_critic.cpp gate_ramp_band
hard return weight ramp path_angle_critic.cpp gate_ramp_band
dropped term weight ramp cost_critic.cpp Where it lives utils.hpp:430-459 (findPathCosts)
Verified by 2 Critic balance, 3 Gate-chatter check
1.3 Reduce how often the occupancy gate can fire at all
parameters
footprint_padding is 0.25 for a 1.144 × 0.76 m robot, and inflation_radius 1.7 spans a whole aisle.
Independently of the gate ramps, the padding inflates every obstacle by an extra 25 cm on both costmaps. Drop it to 0.05 and carry the real uncertainty in
inflation_layerinstead.Then revisit
inflation_radius: 1.7. Withcost_scaling_factor: 5.0it spans a whole factory aisle, soCostCritic(weight 3.0) applies a lateral bias almost everywhere and fightsPathAlignCritic. 1.0–1.2 m is the range the BT’s own comment assumes — “inflation_radius is 1.2 m”, line 268. The parameter file and the BT documentation have drifted apart, which is worth reconciling either way.Changes
Target From To File footprint_padding
Both costmaps.
0.25 0.05 hmc_nav2_params_tuning_04Sep-2026-09-04-v1.yaml inflation_layer.inflation_radius
Both costmaps. Reconcile with the BT comment either way.
1.7 1.0–1.2 hmc_nav2_params_tuning_04Sep-2026-09-04-v1.yaml Verified by 1 Weave metric, 7 Aisle clearance
1.4 Match the costmap rate to the control rate
parameters
The controller runs at 20 Hz and the local costmap updates at 10, so CostCritic's contribution is piecewise-constant across pairs of control cycles.
CostCriticreads the costmap directly, so a 10 Hz ripple is injected into a 20 Hz loop. Raise the local costmap’supdate_frequencyto 20.0. A 15 × 15 m rolling window at 5 cm is 300 × 300 cells; this is cheap.If the FFT peak from the weave measurement is at 10 Hz, do this one first.
Changes
Target From To File local_costmap.update_frequency
10.0 20.0 hmc_nav2_params_tuning_04Sep-2026-09-04-v1.yaml Verified by 1 Weave metric
1.5 Stop the rollouts overrunning the pruned path
parameters
The horizon covers 3.5 m and the path handler prunes at 3.0, so the last stretch of every rollout scores against one path point.
Horizon =
time_steps70 ×model_dt0.05 = 3.5 s, which atvx_max1.0 covers 3.5 m — butPathHandler.prune_distanceis 3.0 m.findClosestPathPtclamps tosize - 1, so the last ~14% of every rollout maps onto the same final path point. That both dilutes the lateral-error signal and couples longitudinal to lateral motion in the cost.Set
prune_distanceto 4.0 — at leastvx_max × time_steps × model_dt, plus margin. It stays well inside the 15 m local costmap and insidetransformLocalPlan’s costmap-bounds break.Changes
Target From To File PathHandler.prune_distance
3.0 4.0 hmc_nav2_params_tuning_04Sep-2026-09-04-v1.yaml Where it lives utils.hpp:675-692 (findClosestPathPt), feasible_path_handler.cpp:200-220 (transformLocalPlan)
Verified by 1 Weave metric, 6 Regression
1.6 Preserve commanded curvature through the velocity smoother
parameters
With scale_velocities false, a wz clipped by the accel limit leaves vx untouched — so the executed arc is not the arc MPPI optimised.
The difference between the optimised curvature and the executed one shows up directly as lateral error. Set it True, so the
v/ωratio is preserved under limiting.Changes
Target From To File velocity_smoother.scale_velocities
False True hmc_nav2_params_tuning_04Sep-2026-09-04-v1.yaml Verified by 1 Weave metric
1.7 Two overlapping alignment critics
parameters + C++
PathAlignCritic and CurvatureOnlyPathAlignCritic score the same physical quantity over different windows behind different gates. Their sum is a staircase whose steps land at different moments.
PathAlignCriticruns at weight 25, step 4, offset 10;CurvatureOnlyPathAlignCriticat weight 8, step 2, offset 8. Both score lateral deviation.After the gate ramps land, re-measure
costs_std / cost_weightfor both. If they are still fighting, fold the curvature term intoPathAlignCriticas a curvature-dependent weight and disable the second critic, rather than keeping two gated terms on one physical quantity.Changes
Target From To File curvature-dependent weight
Only if the re-measure shows the two critics still fighting.
— — path_align_critic.cpp CurvatureOnlyPathAlignCritic.enabled
Conditional on the same re-measure.
true false hmc_nav2_params_tuning_04Sep-2026-09-04-v1.yaml After 1.2 Remove the remaining relay-style critic gates
Verified by 2 Critic balance
1.8 Damp the angular exploration
parameters
A cheap A/B: wz_std_decay_strength is -1.0 — a mechanism built for exactly this problem, currently switched off.
wz_stdis 0.25, with the note that “0.14 is the magic number”, whileadvanced.wz_std_decay_strengthis-1.0(disabled) andwz_std_decay_tois 0.06. Try a decay strength of 2.0, so angular exploration decays on the straights while corner authority is retained.Changes
Target From To File advanced.wz_std_decay_strength
-1.0 2.0 hmc_nav2_params_tuning_04Sep-2026-09-04-v1.yaml After 1.1 Model the actuation latency
Verified by 1 Weave metric
Watch Low risk and easily reverted — but do it after the latency model, so it is not masking the dead-time fix.
Phase 2
Goal accuracy to 5 cm / 2°
Make the final approach deterministic, give the optimiser something to converge onto, and only then tighten what the checker will accept.
Assumes the localization repeatability measured in Phase 0 came back at 2 cm or better. If it did not, stop here and build the local-feature fine-positioning stage instead.
2.1 Make the final approach geometry deterministic
parameters + behaviour tree + C++
The biggest single win: 10 cm of RDP tolerance is larger than the entire 5 cm error budget.
Today the last mile is planned by
AstarGridBased(NavFn) — an 8-connected grid path with 45°-quantised headings and a staircase — then passed throughCornerFilletSmootherwithsimplify_tolerance: 0.10(RDP) andsmoothing_radius: 2.0. The BT itself flags this at line 524: “SmoothPath will cause the path slightly deviated from route_graph (about max 5cm) … lower the RDP tolerance”.Lower the tolerance to 0.02, as that comment already recommends. Verify the A* connector staircase does not survive as false corners; if it does, straighten the graph rather than raise ε back up.
Author a straight approach segment into the last mile. In
ComputeAndPublishRoute, back-project a fixed length L — start with 1.5 m — from the goal along the goal yaw to form agoal_approach_startpose; planlast_mile_pathto that pose, then append the straight segment verbatim. The approach line must be excluded from filleting: the smoother already has thepreserve_authored_curvesmachinery and thesoftvertex concept to build on.The final approach is then a straight line at exactly the goal heading, so the goal yaw is achieved by driving rather than by spinning in place — which removes the terminal-rotation term almost entirely.
Changes
Target From To File route_smoother.simplify_tolerance
0.10 0.02 hmc_nav2_params_tuning_04Sep-2026-09-04-v1.yaml ComputeAndPublishRoute
Back-project goal_approach_start from the goal, plan the last mile to it, append the straight segment verbatim.
— — navigate_on_route_graph_stop_and_wait.xml approach-segment preservation
Optional: extend preserve_authored_curves so the appended line is never filleted.
— — corner_fillet_smoother.cpp Where it lives corner_fillet_smoother.cpp:173-250 (preserve_authored_curves), navigate_on_route_graph_stop_and_wait.xml:524 (SmoothPath comment)
Verified by 4 Goal accuracy, 5 Terminal-rotation drift, 6 Regression
2.2 Give MPPI a terminal gradient
parameters + C++ + build
GoalCritic scores the mean distance to goal over the whole horizon, so near the goal the cost surface is nearly flat. This is the structural reason 5 cm is out of reach with the current critic set.
GoalCritictakes.rowwise().mean()over the horizon andGoalAngleCriticdoes the same for yaw error. Near the goal, withnear_goal_vx_max: 0.20and a 3.5 s horizon, most of the horizon sits past the goal, so both means are dominated by the overshoot region. No amount of weight tuning creates a gradient that is not there.Add a
TerminalGoalCritic, modelled ongoal_critic.cpp, that whenlocal_path_lengthfalls below a threshold:- scores the closest approach to the goal over the rollout —
.rowwise().minCoeff()rather than the mean — so parking on the goal is strictly cheaper than passing near it; - adds a stop term penalising residual speed at the point of closest approach, so the optimum is stopped at the goal rather than passing through it;
- keeps
symmetric_yaw_tolerancesemantics, so it stays compatible withBidirectionalGoalChecker.
Keep the existing
GoalCriticfor the 1.4 m → 0.3 m band and hand over to the terminal critic inside about 0.3 m. Balance it the way the others are balanced:costs_std / cost_weightagainst T = 0.4608.Changes
Target From To File TerminalGoalCritic
New critic: closest approach plus a stop term, inside a distance threshold.
— — terminal_goal_critic.hppnew TerminalGoalCritic::score
New.
— — terminal_goal_critic.cppnew plugin declaration
— — critics.xml source list
— — CMakeLists.txt FollowPath.critics
Add TerminalGoalCritic and its weight, balanced against T = 0.4608.
— — hmc_nav2_params_tuning_04Sep-2026-09-04-v1.yaml Where it lives goal_critic.cpp:53-57 (.rowwise().mean()), goal_angle_critic.cpp:56-59 (.rowwise().mean())
Verified by 4 Goal accuracy
- scores the closest approach to the goal over the rollout —
2.3 Reuse the existing pose regulator for the terminal leg
behaviour tree
FollowPathWithBaseController already regulates x, y and θ together — which the shim's in-place rotation cannot — and the BT plumbing to select it is declared and unused.
The GA-MPC controller carries a terminal pose regulator:
k_phi: 6.0,k_delta: 2.0,final_rotation: true, aslowdown_radius, and it pairs withBaseGoalat a 0.001 m tolerance.The BT declares a
ControllerSelectorat line 86, butFollowPathhardcodesgoal_checker_id="general_goal_checker"at line 647. Add aGoalCheckerSelector— the node already exists and is registered innav2_tree_nodes.xml:258— and bindgoal_checker_id="{selected_goal_checker}". A fleet manager or a route operation can then select{FollowPathWithBaseController, BaseGoal}for the last ~1.5 m of a precise goal, and{FollowPath, general_goal_checker}everywhere else.Prefer this over the new critic if it proves sufficient in trials — reusing the tuned GA-MPC regulator is less new code than a new critic. Run both in the goal-accuracy trials and keep whichever closes the budget.
Changes
Target From To File GoalCheckerSelector
Add the node and bind FollowPath.goal_checker_id to {selected_goal_checker}.
— — navigate_on_route_graph_stop_and_wait.xml Where it lives goal_checker_selector_node.cpp (GoalCheckerSelector)
Verified by 4 Goal accuracy
2.4 Tighten the tolerances last, not first
parameters
Only once the geometry and the terminal leg measure inside budget. Tightening first turns a control problem into a hunting problem.
The stopped-velocity terms matter at this scale: at 0.08 m/s the robot is still covering 4 mm per control cycle while it is called stopped.
Watch for a hunting mode here.
BidirectionalGoalChecker::isGoalReachedre-checkswithin_xyafter the stop check and resetscheck_xy_ = truewhen it fails. If the shim’s in-place terminal rotation (rotate_to_goal_heading: true,rotate_to_heading_angular_vel: 0.25) drags the robot even 2 cm through wheel slip under payload, the goal is rejected and control returns to MPPI — a cycle. At 15 cm this is masked; at 5 cm it becomes the dominant failure. The straight approach segment is the real fix, since it arrives already aligned; log terminal-rotation xy drift explicitly to confirm.Leave
bypass_y_axis: False. The parameter file already marks it “Disable this during deployment”, and at a 5 cm target, accepting a 0.5 m lateral error would defeat the purpose.Changes
Target From To File general_goal_checker.xy_goal_tolerance
0.15 0.05 hmc_nav2_params_tuning_04Sep-2026-09-04-v1.yaml general_goal_checker.yaw_goal_tolerance
0.088 0.035 hmc_nav2_params_tuning_04Sep-2026-09-04-v1.yaml general_goal_checker.trans_stopped_velocity
0.08 0.02 hmc_nav2_params_tuning_04Sep-2026-09-04-v1.yaml general_goal_checker.rot_stopped_velocity
0.08 0.02 hmc_nav2_params_tuning_04Sep-2026-09-04-v1.yaml Where it lives bidirectional_goal_checker.cpp:264-282 (isGoalReached)
Files to change
12 files — parameters (1), behaviour tree (1), C++ (8), build (2). Not a list: every entry below is a file some change above lands in, and the items under it are that relation read backwards. Nothing here is written down twice.
hmgics_navigation2/nav2_mppi_controller/include/nav2_mppi_controller/tools/utils.hpp
C++
MPPI's shared helpers: findPathCosts, findClosestPathPt, and the home for the gate-ramp helper.
- 1.2 Remove the remaining relay-style critic gates — mppi::utils::gateRamp
Also cited by 1.5
hmgics_navigation2/nav2_mppi_controller/src/critics/path_align_critic.cpp
C++
The weight-25 alignment term, and the critic with two of the four relay gates in it.
- 1.2 Remove the remaining relay-style critic gates — gate_ramp_band
- 1.7 Two overlapping alignment critics — curvature-dependent weight
hmgics_navigation2/nav2_mppi_controller/src/critics/curvature_only_path_align_critic.cpp
C++
The second alignment term, weight 8, over a different window.
- 1.2 Remove the remaining relay-style critic gates — gate_ramp_band
hmgics_navigation2/nav2_mppi_controller/src/critics/path_angle_critic.cpp
C++
The heading term, weight 2, gated on the angle to the furthest path point.
- 1.2 Remove the remaining relay-style critic gates — gate_ramp_band
hmgics_navigation2/nav2_mppi_controller/src/critics/cost_critic.cpp
C++
The costmap term, weight 3, whose preferential part drops out near the goal.
- 1.2 Remove the remaining relay-style critic gates — gate_ramp_band
hmgics_navigation2/nav2_mppi_controller/include/nav2_mppi_controller/critics/terminal_goal_critic.hpp
C++new
The terminal-gradient critic's header.
- 2.2 Give MPPI a terminal gradient — TerminalGoalCritic
hmgics_navigation2/nav2_mppi_controller/src/critics/terminal_goal_critic.cpp
C++new
The terminal-gradient critic, modelled on goal_critic.cpp.
- 2.2 Give MPPI a terminal gradient — TerminalGoalCritic::score
hmgics_navigation2/nav2_mppi_controller/critics.xml
build
The pluginlib manifest. A critic nobody declares here cannot be loaded by name.
- 2.2 Give MPPI a terminal gradient — plugin declaration
hmgics_navigation2/nav2_mppi_controller/CMakeLists.txt
build
The critic library's source list.
- 2.2 Give MPPI a terminal gradient — source list
hmgics_navigation2/nav2_smoother/src/corner_fillet_smoother.cpp
C++
The fillet smoother, which already has the preserve_authored_curves machinery an approach segment needs.
- 2.1 Make the final approach geometry deterministic — approach-segment preservation
Read, not changed
Where the behaviour described above actually lives. Nothing in the plan edits these.
hmgics_navigation2/nav2_mppi_controller/include/nav2_mppi_controller/motion_models.hpp
The motion models — where the command history behind model_delay_* is already implemented. Cited by 1.1.
hmgics_navigation2/nav2_mppi_controller/src/critics/goal_critic.cpp
The existing goal term, scoring the mean distance to goal over the whole horizon. Cited by 2.2.
hmgics_navigation2/nav2_mppi_controller/src/critics/goal_angle_critic.cpp
The existing goal-yaw term, scoring the mean yaw error over the whole horizon. Cited by 2.2.
hmgics_navigation2/nav2_mppi_controller/src/critics/path_follow_critic.cpp
Carries the ~/path_follow_debug_markers publisher the weave measurement reads. Cited by 0.3.
navigation2_common/nav2_controller/plugins/feasible_path_handler.cpp
Prunes and transforms the local plan, and breaks on the costmap bounds. Cited by 1.5.
navigation2_common/nav2_controller/plugins/bidirectional_goal_checker.cpp
The goal checker in force, including the re-check that can hunt at a tight tolerance. Cited by 2.4.
navigation2_common/nav2_behavior_tree/plugins/action/goal_checker_selector_node.cpp
The GoalCheckerSelector node — already written and already registered, just never bound. Cited by 2.3.
Verification
Per change, on the bench or in simulation first. Every parameter above is dynamic-reconfigurable — each critic has a dynamicParametersCallback — so most of Phase 1 can be swept live.
1 Weave metric
Drive a fixed 30 m straight aisle at 1.0 m/s. Record lateral error against
/controller_path_bt; report RMS, p95 and the FFT peak. Baseline first, then after each Phase 1 item.Success p95 lateral error ≤ 2 cm, and no dominant spectral peak.
Covers 1.1 Model the actuation latency, 1.3 Reduce how often the occupancy gate can fire at all, 1.4 Match the costmap rate to the control rate, 1.5 Stop the rollouts overrunning the pruned path, 1.6 Preserve commanded curvature through the velocity smoother, 1.8 Damp the angular exploration
2 Critic balance
With
publish_critics_stats: true, re-derivecosts_std / cost_weightfor every critic once the gate ramps land, and re-balance to std/T ∈ [0.5, 2.0] with T = 0.4608 — exactly as the parameter file’s comments do today.The ramps change the measured spread, so the existing weights (25.0 / 8.0 / 4.5 / 2.0) must be re-derived rather than carried over.
Covers 1.2 Remove the remaining relay-style critic gates, 1.7 Two overlapping alignment critics
3 Gate-chatter check
Count per-minute transitions of each gated critic’s contribution between zero and non-zero, before and after the ramps.
Success Transitions go to approximately zero.
4 Goal accuracy
20 repeats to the same physical goal from at least three approach directions, including one requiring a reversal —
enforce_path_inversion: Trueis on. Measure the physical pose, not the reported map pose.Success p95 ≤ 5 cm / 2°, max ≤ 8 cm.
Covers 2.1 Make the final approach geometry deterministic, 2.2 Give MPPI a terminal gradient, 2.3 Reuse the existing pose regulator for the terminal leg, 2.4 Tighten the tolerances last, not first
5 Terminal-rotation drift
Log xy displacement during the shim’s
ROTATING_TO_GOAL_HEADINGphase —logControlModealready emits the mode.Success ≤ 1 cm, or the approach segment has not done its job.
Covers 2.1 Make the final approach geometry deterministic, 2.4 Tighten the tolerances last, not first
6 Regression
Re-run the two scenarios the parameter file and BT comments record as previously broken: the out-and-back route leg-selection case (bag
nav2_full_20260731_115322) and the route-rebuild limit cycle (bagnav2_full_20260805_115905).prune_distanceand the smoother tolerance both touch that machinery.Covers 1.5 Stop the rollouts overrunning the pruned path, 2.1 Make the final approach geometry deterministic
7 Aisle clearance
After reducing
footprint_paddingandinflation_radius, re-run the narrowest aisle and the tightest corner in the plant withconsider_footprint: falsestill set, and confirm the collision monitor never fires.Covers 1.3 Reduce how often the occupancy gate can fire at all
What the robot runs today
The AMR navigates on a route graph: a NavFn first mile onto the graph, the
authored route through the middle, a NavFn last mile off it, and then
CornerFilletSmoother → FeasiblePathHandler → RotationShimController
wrapping MPPI → BidirectionalGoalChecker. Every number the controller reads is
in the parameter file tracked on this site, and every version of that file is in
the log beside this page.
Two targets, confirmed with the user. Tighten general goals from 0.15 m / 0.088 rad to about 0.05 m / 0.035 rad, and fix the path alignment — where the dominant field symptom is oscillation and weaving along the path, not a steady offset and not corner cutting.
Scope is open to new plugins where a parameter cannot express the fix, and two of the changes here take it.
How to read it
The parameter file’s own comments already establish the right methodology:
balance the critics on costs_std / cost_weight against temperature (0.4608),
targeting a spread of roughly 0.5 to 2.0, and they already name one relay-style
gate as a source of oscillation. This plan continues that line of reasoning
rather than restarting it — the conclusion is that the weave is a control-loop
problem, dead time plus on/off gates, and not a critic-weight problem. Weight
tuning has been pushed about as far as it can go.
So the order matters more than the list. Phase 0 is three measurements, and one
of them decides how much of the rest is worth doing at all: the goal is
expressed in the map frame, so goal accuracy can never beat localization
repeatability at the station. Phase 1 is the weave. Phase 2 is the last five
centimetres, and it assumes Phase 0 came back clean.
Everything below the error budget is derived from one file. An item is numbered by its position, its “parameters only” or “needs code” badge comes from the files its own changes touch, the list of files to change is a join back over those changes, and each item’s verification is the other side of the check that names it. Nothing on this page is stated in two places, which is the one property that makes a plan of this size worth keeping as data rather than as a document.
What this is not
It is not a changelog. Nothing here has been applied, and an item on this page is a proposal with an argument attached, not a record of something the robot has run — the versions in the tuning log are that record, and the two should be read against each other.
It is also not a promise that the budget closes. Three of the four error terms have never been measured, and the arithmetic under the table says plainly what has to be true for five centimetres to be reachable: the terms have to be independent, and the localization term has to come back under two. If it does not, the honest answer is a local-feature fine-positioning stage rather than any amount of tuning, and that is written into the plan rather than discovered halfway through it.