Skip to content
alexbeh.me
Log in

Tuning log

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.

#SourceTodayNeededAddressed 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 cm5 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

  1. 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 /odom at the same station, and cross-correlate the commanded angular rate against the measured one.

    The chain is MPPI at 20 Hz → velocity_smoother at 20 Hz → collision_monitor → base driver, so expect 60–150 ms.

  2. 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.

  3. 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_markers publisher in path_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.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 / wz are all 0.0, but the fork already implements them — motion_models.hpp maintains a command history and offsets it by round(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

    TargetFromToFile

    FollowPath.model_delay_vx

    0.0measured τhmc_nav2_params_tuning_04Sep-2026-09-04-v1.yaml

    FollowPath.model_delay_wz

    0.0measured τ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.

  2. 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_furthest from 12 to 25. These remain:

    The max_path_occupancy_ratio gate matters most in a factory. A path point is invalid at cost ≥ INSCRIBED_INFLATED_OBSTACLE (253) — see findPathCosts. With footprint_padding: 0.25 the 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 return with 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-critic gate_ramp_band parameter defaulting to 0. Zero is today’s behaviour, so the change is opt-in and bisectable.

    Starting bands: 5 indices for the offset_from_furthest gate, 0.05 for max_path_occupancy_ratio, 0.15 rad for max_angle_to_furthest, 0.3 m for CostCritic.near_goal.

    GateWhereWeight it switches
    path_segments_count < offset_from_furthest_ → returnpath_align_critic.cpp:5925.0
    path_segments_count < offset_from_furthest_ → returncurvature_only_path_align_critic.cpp:1328.0
    invalid_ctr / path_segments_flt > max_path_occupancy_ratio_ → returnboth align critics25.0 and 8.0
    local_path_length < near_goal_distance_ → drop preferential termcost_critic.cpp:1543.0
    posePointAngle(…) < max_angle_to_furthest_ → returnpath_angle_critic.cpp:88-1072.0

    Changes

    TargetFromToFile

    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 returnweight ramppath_align_critic.cpp

    gate_ramp_band

    hard returnweight rampcurvature_only_path_align_critic.cpp

    gate_ramp_band

    hard returnweight ramppath_angle_critic.cpp

    gate_ramp_band

    dropped termweight rampcost_critic.cpp

    Where it lives utils.hpp:430-459 (findPathCosts)

  3. 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_layer instead.

    Then revisit inflation_radius: 1.7. With cost_scaling_factor: 5.0 it spans a whole factory aisle, so CostCritic (weight 3.0) applies a lateral bias almost everywhere and fights PathAlignCritic. 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

    TargetFromToFile

    footprint_padding

    Both costmaps.

    0.250.05hmc_nav2_params_tuning_04Sep-2026-09-04-v1.yaml

    inflation_layer.inflation_radius

    Both costmaps. Reconcile with the BT comment either way.

    1.71.0–1.2hmc_nav2_params_tuning_04Sep-2026-09-04-v1.yaml
  4. 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.

    CostCritic reads the costmap directly, so a 10 Hz ripple is injected into a 20 Hz loop. Raise the local costmap’s update_frequency to 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

    TargetFromToFile

    local_costmap.update_frequency

    10.020.0hmc_nav2_params_tuning_04Sep-2026-09-04-v1.yaml

    Verified by 1 Weave metric

  5. 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_steps 70 × model_dt 0.05 = 3.5 s, which at vx_max 1.0 covers 3.5 m — but PathHandler.prune_distance is 3.0 m. findClosestPathPt clamps to size - 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_distance to 4.0 — at least vx_max × time_steps × model_dt, plus margin. It stays well inside the 15 m local costmap and inside transformLocalPlan’s costmap-bounds break.

    Changes

    TargetFromToFile

    PathHandler.prune_distance

    3.04.0hmc_nav2_params_tuning_04Sep-2026-09-04-v1.yaml

    Where it lives utils.hpp:675-692 (findClosestPathPt), feasible_path_handler.cpp:200-220 (transformLocalPlan)

  6. 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

    TargetFromToFile

    velocity_smoother.scale_velocities

    FalseTruehmc_nav2_params_tuning_04Sep-2026-09-04-v1.yaml

    Verified by 1 Weave metric

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

    PathAlignCritic runs at weight 25, step 4, offset 10; CurvatureOnlyPathAlignCritic at weight 8, step 2, offset 8. Both score lateral deviation.

    After the gate ramps land, re-measure costs_std / cost_weight for both. If they are still fighting, fold the curvature term into PathAlignCritic as a curvature-dependent weight and disable the second critic, rather than keeping two gated terms on one physical quantity.

    Changes

    TargetFromToFile

    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.

    truefalsehmc_nav2_params_tuning_04Sep-2026-09-04-v1.yaml
  8. 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_std is 0.25, with the note that “0.14 is the magic number”, while advanced.wz_std_decay_strength is -1.0 (disabled) and wz_std_decay_to is 0.06. Try a decay strength of 2.0, so angular exploration decays on the straights while corner authority is retained.

    Changes

    TargetFromToFile

    advanced.wz_std_decay_strength

    -1.02.0hmc_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.

  1. 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 through CornerFilletSmoother with simplify_tolerance: 0.10 (RDP) and smoothing_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 a goal_approach_start pose; plan last_mile_path to that pose, then append the straight segment verbatim. The approach line must be excluded from filleting: the smoother already has the preserve_authored_curves machinery and the soft vertex 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

    TargetFromToFile

    route_smoother.simplify_tolerance

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

  2. 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.

    GoalCritic takes .rowwise().mean() over the horizon and GoalAngleCritic does the same for yaw error. Near the goal, with near_goal_vx_max: 0.20 and 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 on goal_critic.cpp, that when local_path_length falls 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_tolerance semantics, so it stays compatible with BidirectionalGoalChecker.

    Keep the existing GoalCritic for 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_weight against T = 0.4608.

    Changes

    TargetFromToFile

    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

  3. 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, a slowdown_radius, and it pairs with BaseGoal at a 0.001 m tolerance.

    The BT declares a ControllerSelector at line 86, but FollowPath hardcodes goal_checker_id="general_goal_checker" at line 647. Add a GoalCheckerSelector — the node already exists and is registered in nav2_tree_nodes.xml:258 — and bind goal_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

    TargetFromToFile

    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

  4. 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::isGoalReached re-checks within_xy after the stop check and resets check_xy_ = true when 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

    TargetFromToFile

    general_goal_checker.xy_goal_tolerance

    0.150.05hmc_nav2_params_tuning_04Sep-2026-09-04-v1.yaml

    general_goal_checker.yaw_goal_tolerance

    0.0880.035hmc_nav2_params_tuning_04Sep-2026-09-04-v1.yaml

    general_goal_checker.trans_stopped_velocity

    0.080.02hmc_nav2_params_tuning_04Sep-2026-09-04-v1.yaml

    general_goal_checker.rot_stopped_velocity

    0.080.02hmc_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.

Read, not changed

Where the behaviour described above actually lives. Nothing in the plan edits these.

  • hmgics_navigation2/nav2_bt_navigator/behavior_trees/navigate_on_route_graph_stop_and_wait.xml

    The repo copy of the same tree — the reference, not the file the robot loads. Cited by 2.1.

  • 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. 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. 2 Critic balance

    With publish_critics_stats: true, re-derive costs_std / cost_weight for 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. 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.

    Covers 1.2 Remove the remaining relay-style critic gates

  4. 4 Goal accuracy

    20 repeats to the same physical goal from at least three approach directions, including one requiring a reversal — enforce_path_inversion: True is 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. 5 Terminal-rotation drift

    Log xy displacement during the shim’s ROTATING_TO_GOAL_HEADING phase — logControlMode already 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. 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 (bag nav2_full_20260805_115905).

    prune_distance and 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. 7 Aisle clearance

    After reducing footprint_padding and inflation_radius, re-run the narrowest aisle and the tightest corner in the plant with consider_footprint: false still 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.