Engineering Guides · Module 13
Tuning costmaps, inflation and footprint
Most “the robot will not go through that door” and “it scrapes the wall” problems are costmap numbers. Start from the official values, then change one parameter from evidence.
Assumptions: Nav2 on ROS 2 Jazzy, planar robot with a LaserScan. The YAML is the costmap section of the official nav2_params.yaml (jazzy branch, fetched 2026-11-04) and is parsed by the site’s tests with every key checked against that file. The inflation-cost formula is the layer’s documented exponential model as recalled by the author and was not re-fetched; the example is executed by the tests but does not prove the formula matches your Nav2 build. Nothing here has been run in Nav2 by the site’s tests.
1. Engineering problem
The costmap is the planner’s picture of where the robot may go and how close to obstacles it should stay. If the robot is modelled too large, or the inflation too wide, doors become impassable; too small and it brushes obstacles.
2. When is this needed?
- Bringing up Nav2 on a new robot.
- Robot refuses narrow passages, hesitates near obstacles, or clips corners.
- Changing the LiDAR, its mounting, or the robot’s size.
3. Selection and design decisions
Parameters (names confirmed from the official file)
| Parameter | Role | Tune when |
|---|---|---|
robot_radius | Circular robot footprint radius; cells closer than this to an obstacle are impassable | Always set to the real circumscribed radius plus a small padding; wrong values dominate everything |
inflation_layer.inflation_radius | How far from an obstacle cost is spread | Robot hugs walls (increase) or cannot pass doors (decrease) |
inflation_layer.cost_scaling_factor | Exponential decay of cost with distance: larger → cost drops faster, planner may run closer to obstacles | Path keeps too close to walls (decrease) or detours too much (increase) |
resolution | Metres per cell | Finer grids resolve narrow gaps but cost CPU and memory |
update_frequency, publish_frequency | Costmap update and publish rates (local is faster than global) | Latency to see new obstacles vs CPU |
observation_sources and scan.* | Which sensors mark and clear obstacles; obstacle_max_range limits marking, raytrace_max_range limits clearing | Ghost obstacles remain (clearing range too small) or distant clutter appears (marking range too large) |
rolling_window, width, height | Local costmap follows the robot in a window of this size (metres) | Controller needs to see farther ahead |
track_unknown_space | Keep unknown cells distinct from free | Planning through unexplored space is wrong for your application |
4. Advantages and disadvantages
- Wide inflation: keeps the robot away from walls (safer, smoother), but narrows or closes corridors.
- Fine resolution: accurate near obstacles, more CPU. 5 cm is the official default; coarser is cheaper and rounds gaps closed.
- Long marking range: early obstacle awareness, more clutter and false obstacles.
5. Requirements and constraints
- A corridor narrower than
2 × robot_radiusis impassable in the model; the planner can use it, with cost, only if wider than that. - The footprint (radius) must cover the robot at every orientation, including sensor masts and bumpers.
- Sensor frames and the LiDAR height must be right for the scan to mark obstacles at all.
6. Architecture
/scan ──▶ obstacle/voxel layer ──┐
/map ──▶ static layer ──────────┼──▶ inflation layer ──▶ costmap ──▶ planner / controller
(odom frame: local costmap; map frame: global costmap)7. Implementation
The costmap section of the official Jazzy parameter file (parsed and name-checked by the site’s tests; global costmap has a static and obstacle layer, local costmap a voxel layer, both inflate):
local_costmap:
local_costmap:
ros__parameters:
update_frequency: 5.0
publish_frequency: 2.0
global_frame: odom
robot_base_frame: base_link
rolling_window: true
width: 3
height: 3
resolution: 0.05
robot_radius: 0.22
plugins: ["voxel_layer", "inflation_layer"]
inflation_layer:
plugin: "nav2_costmap_2d::InflationLayer"
cost_scaling_factor: 3.0
inflation_radius: 0.70
voxel_layer:
plugin: "nav2_costmap_2d::VoxelLayer"
enabled: true
publish_voxel_map: true
origin_z: 0.0
z_resolution: 0.05
z_voxels: 16
max_obstacle_height: 2.0
mark_threshold: 0
observation_sources: scan
scan:
topic: /scan
max_obstacle_height: 2.0
clearing: true
marking: true
data_type: "LaserScan"
raytrace_max_range: 3.0
raytrace_min_range: 0.0
obstacle_max_range: 2.5
obstacle_min_range: 0.0
always_send_full_costmap: true
global_costmap:
global_costmap:
ros__parameters:
update_frequency: 1.0
publish_frequency: 1.0
global_frame: map
robot_base_frame: base_link
robot_radius: 0.22
resolution: 0.05
track_unknown_space: true
plugins: ["static_layer", "obstacle_layer", "inflation_layer"]
obstacle_layer:
plugin: "nav2_costmap_2d::ObstacleLayer"
enabled: true
observation_sources: scan
scan:
topic: /scan
max_obstacle_height: 2.0
clearing: true
marking: true
data_type: "LaserScan"
raytrace_max_range: 3.0
raytrace_min_range: 0.0
obstacle_max_range: 2.5
obstacle_min_range: 0.0
static_layer:
plugin: "nav2_costmap_2d::StaticLayer"
map_subscribe_transient_local: true
inflation_layer:
plugin: "nav2_costmap_2d::InflationLayer"
cost_scaling_factor: 3.0
inflation_radius: 0.7
always_send_full_costmap: true
What the inflation numbers mean
import math
robot_radius, inflation_radius, scale = 0.22, 0.70, 3.0
INSCRIBED = 253 # cost of a cell the robot cannot enter without touching
def cost(d): # d: distance from the obstacle to the cell centre, metres
if d <= robot_radius:
return INSCRIBED
if d > inflation_radius:
return 0
return int(math.exp(-scale * (d - robot_radius)) * (INSCRIBED - 1))
for d in (0.20, 0.22, 0.30, 0.45, 0.70, 0.75):
print(d, cost(d))
# narrowest corridor the planner can use, and the narrowest with a free (zero-cost) lane:
print(round(2 * robot_radius, 2), round(2 * inflation_radius, 2))Printed (checked by the site’s tests):
0.2 253
0.22 253
0.3 198
0.45 126
0.7 59
0.75 0
0.44 1.4With robot_radius 0.22 m, inflation 0.70 m and scaling 3.0, cost is 253 (inscribed: forbidden) up to 0.22 m, falls through 198 at 0.30 m and 126 at 0.45 m, and ends at 59 at the inflation radius before dropping to 0. The last line gives the key corridor widths: 0.44 m is the narrowest the model allows; a corridor must be wider than 1.4 m (2 × inflation radius) to contain a lane of zero inflated cost.
Inspect and change on the running system
Not executed by the site’s automated tests (they need a ROS 2 installation).
ros2 param get /local_costmap/local_costmap inflation_layer.inflation_radius
ros2 param set /local_costmap/local_costmap inflation_layer.inflation_radius 0.5
ros2 topic hz /local_costmap/costmap
# in RViz: add the costmap displays and compare them with the real wallsA ros2 param set is a temporary test; write the final value into your parameter file.
8. Real examples
- Robot will not enter an 0.8 m doorway: 2 × inflation radius (1.4 m) is larger than the door, so the whole doorway is inflated; the planner cost there is high but not lethal. Lower the inflation radius or the cost scaling so the centre line is cheap.
- Ghost obstacles trail behind the robot: clearing needs a long enough
raytrace_max_rangeand theclearing: trueflag. - Robot cuts corners:
robot_radiussmaller than the real robot.
9. Failure scenarios
- Setting
robot_radiusto the body radius but forgetting a protruding sensor. - Inflation so wide that every corridor looks closed.
- LiDAR frame wrong or scan below the obstacle height, so nothing is marked.
- Different
resolutionin local and global costmaps causing disagreements near obstacles.
10. Troubleshooting
- View both costmaps in RViz against the real environment: measure a doorway and compare.
- Compute your corridor numbers:
2 × robot_radiusand2 × inflation_radiusagainst the real width. - Change one parameter at a time with
ros2 param setand repeat the same manoeuvre. - Check the scan frame and range parameters if obstacles are missing or never clear.
- Record the final values and re-test the full route (a bag of the route for regression).
11. Validation
- The robot passes the narrowest real doorway on its route with the margin you require, repeatedly (at least 20 attempts, count successes).
- Minimum clearance to walls along the route, measured, stays above your requirement.
- No ghost obstacles remain after a person walks through.
- The YAML parses and every key is one confirmed from the official file (checked by the site’s tests).
12. Engineering checklist
13. Further references
- Nav2 default parameter file, jazzy branch (costmap section confirmed on 2026-11-04): nav2_bringup/params/nav2_params.yaml.
- Nav2 documentation for the full parameter reference and the inflation formula: docs.nav2.org. The inflation formula in this guide was verified against the source (read 2026-11-06): inflation_layer.hpp,
computeCost: cost is LETHAL_OBSTACLE (254) at distance 0, INSCRIBED_INFLATED_OBSTACLE (253) at or inside the inscribed radius, and otherwise (253 − 1)·exp(−cost_scaling_factor·(distance − inscribed_radius)) truncated to an integer; the constants are in cost_values.hpp (NO_INFORMATION 255, LETHAL_OBSTACLE 254, INSCRIBED_INFLATED_OBSTACLE 253, MAX_NON_OBSTACLE 252, FREE_SPACE 0).
Related on FixTheRobot
- Nav2 Architecture Navigation · Lesson
- Tuning a Navigation Stack Navigation · Lesson
- Layered Costmaps Navigation · Lesson
- Tune the Inflation Radius Navigation · Lab
- Lint a Nav2 Parameter File Navigation · Lab