A ROS 2 node that drives a robot to a goal by value iteration. It reads an occupancy grid, computes the cost of reaching the goal from every free cell and heading, and publishes the discrete action at the robot's current pose.
Author: nop (noplab90@gmail.com). License: BSD-3-Clause. Copyright: nop and CIT autonomous robot laboratory. See License.
Before the node can drive:
- A map server that serves
nav_msgs/GetMapon/map_server/map(the default). Cell value0is free. Any other value is occupied. For a raw cost grid, setmap_type: cost_addormap_type: cost_replaceand serve it on/cost_map. - A transform from
maptobase_link. - A
sensor_msgs/LaserScanon/scan. - A base that accepts
geometry_msgs/Twiston/cmd_vel.
The command is not a smooth velocity. While an action is selected, the node republishes that action at 10 Hz: linear.x is the action's forward step in meters, and angular.z is its rotation in radians. At the goal, and whenever the current cell has no action, it publishes zeros.
Place this directory in a workspace src folder, then:
colcon build --packages-select value_iteration3 --cmake-args -DCMAKE_BUILD_TYPE=Release
source install/setup.bash
ros2 launch value_iteration3 vi.launch.pySend a goal in the map frame on /goal_pose (geometry_msgs/PoseStamped). The node solves from that pose, then starts publishing /cmd_vel. Solving can take seconds on a large map. The robot holds still until a policy exists.
/value_function (nav_msgs/OccupancyGrid) shows the cost for the robot's current heading. The value is 0 at the goal and 100 at cost_drawing_threshold seconds or above. It is published about every 1.5 s.
| Name | Type | Role |
|---|---|---|
/map_server/map |
nav_msgs/GetMap |
Occupancy grid, read once at startup with map_type: occupancy |
/cost_map |
nav_msgs/GetMap |
Raw cost grid, read once at startup with map_type: cost_add or cost_replace |
/goal_pose |
geometry_msgs/PoseStamped |
Goal position and yaw in map |
/scan |
sensor_msgs/LaserScan |
Local obstacle update. Sensor data QoS |
/cmd_vel |
geometry_msgs/Twist |
Selected action, 10 Hz |
/value_function |
nav_msgs/OccupancyGrid |
Cost of the current heading |
map → base_link |
TF | Robot pose |
Set map_type to cost_add or cost_replace in config/params.yaml, or run ros2 run value_iteration3 vi_node --ros-args -p map_type:=cost_replace -p online:=true. Each data byte is interpreted as unsigned: 0–254 is a cost in seconds and 255 (signed -1) is blocked. For example, signed -2 represents cost 254. The two modes differ in how that value is used:
map_type |
Immediate cost of a free cell |
|---|---|
cost_add |
The 1 second base cost plus the cell value. A cell of 0 costs 1 second, as a free occupancy cell does |
cost_replace |
The cell value alone. It replaces the 1 second base cost, so a cell of 0 costs nothing |
Cost grids already contain their desired inflation, so safety_radius and safety_radius_penalty apply only to occupancy mode. Invalid map types are rejected, including the earlier ambiguous cost; use cost_replace for the behavior of value_iteration's cost.
Set online to false to load the map and publish nothing else. The launch file leaves it true.
A cell is a goal only when both of its checked corners lie inside goal_margin_radius of the goal position, and its heading bin lies inside goal_margin_theta degrees of the goal yaw. Goal cells have cost 0. Every other free cell starts unreachable.
Headings are theta_cell_num equal bins around the circle. With 60 bins, each bin is 6 degrees. The value must be from 1 to 64.
Every free cell already costs 1 second to enter. Cells within safety_radius of an occupied cell cost an extra safety_radius_penalty seconds.
If the log says that most free cells have no finite cost, the goal region is too small for the action steps. Increase goal_margin_radius (0.3 m is a useful next try on a 5 cm map).
/scan changes costs inside a square of radius local_xy_range around the robot (default 1 m).
Along each return, from 10% to 90% of the range, the local penalty is halved. The cells within two cells of the hit are then given a local penalty of 2048 seconds. Returns that are non-finite or outside the scan's range are ignored. The field is then updated from those cells until the change settles. A new goal clears local penalties.
Numeric defaults below are the values in config/params.yaml, which the launch file loads. If you run vi_node with no parameter file, online is false and global_thread_num is 1.
| Parameter | Default | Meaning |
|---|---|---|
map_type |
occupancy |
occupancy selects /map_server/map; cost_add and cost_replace select /cost_map |
online |
true |
Subscribe to the goal and the scan, and publish /cmd_vel |
global_thread_num |
2 |
Threads used while solving. 0 uses all logical cores |
theta_cell_num |
60 |
Number of heading bins |
safety_radius |
0.2 |
Meters around an occupied cell that receive the extra penalty |
safety_radius_penalty |
30.0 |
Extra seconds to enter a cell near an obstacle |
goal_margin_radius |
0.2 |
Meters. Both checked corners of a goal cell must lie inside this radius |
goal_margin_theta |
10 |
Degrees of heading that still count as the goal |
cost_drawing_threshold |
60 |
Seconds at which /value_function saturates at 100 |
local_xy_range |
1.0 |
Meters. Half-width of the square updated from the laser |
config_file |
set by the launch file | Path of the YAML file that contains action_list |
Actions live in config/params.yaml under action_list, next to ros__parameters. ROS 2 cannot store a list of maps as a parameter, so the node reads this list from the file. The number of entries is the number of actions. Add or remove entries to change the set. Each entry needs all three fields:
action_list:
- name: forward
onestep_forward_m: 0.3
onestep_rotation_deg: 0.0
- name: back
onestep_forward_m: -0.2
onestep_rotation_deg: 0.0
- name: right
onestep_forward_m: 0.0
onestep_rotation_deg: -20.0
- name: rightfw
onestep_forward_m: 0.2
onestep_rotation_deg: -20.0
- name: left
onestep_forward_m: 0.0
onestep_rotation_deg: 20.0
- name: leftfw
onestep_forward_m: 0.2
onestep_rotation_deg: 20.0onestep_forward_m is the forward step in meters. Negative is backward. onestep_rotation_deg is the rotation in degrees. Negative is clockwise when viewed from above. At most 127 actions.
The shipped list is six motions: 0.3 m forward, 0.2 m back, a turn of 20 degrees either way, and the same turn combined with 0.2 m forward.
CI builds and tests on ROS 2 Humble / Ubuntu 22.04. From a sourced ROS workspace:
rosdep install --from-paths src --ignore-src --rosdistro humble -y
colcon build --packages-select value_iteration3 --cmake-args -DCMAKE_BUILD_TYPE=Release
source install/setup.bash
colcon test --packages-select value_iteration3 --event-handlers console_direct+ --return-code-on-test-failure
colcon test-result --verboseTests use ament_cmake_test and ament_cmake_pytest. The solver checks unsigned cost decoding, blocked cells, and unchanged occupancy behavior. ROS integration tests launch the installed node with real nav_msgs/GetMap services, check both service routes and map metadata, and verify that raw 254 is traversable while 255 is blocked. Lint uses ament cppcheck (C++ static analysis), flake8 (Python), lint_cmake, and xmllint. CI uploads their result files.
This checks the solver without ROS. It needs a C++17 compiler and pthreads.
c++ -std=c++17 -O2 -pthread -I include \
src/planner.cpp src/actions.cpp src/global_planner.cpp src/local_planner.cpp \
src/test_planner.cpp -o /tmp/planner_test
/tmp/planner_testplanner_test ok means the action list parsed, two thread counts agreed, a far cell reached the goal, a walled-in cell stayed blocked, and a laser hit raised costs without lowering any.
value_iteration is the ROS 1 planner. value_iteration2 is its ROS 2 port. This package keeps the same kind of goal, scan, and velocity command, and changes when the field is solved and which inputs are accepted.
Shared with both of them:
- An occupancy grid, a pose in
map, and a laser scan. - A goal cell must lie inside
goal_margin_radiusandgoal_margin_theta. The node defaults are 0.2 m and 10 degrees. Manyvalue_iterationlaunch files override those to 0.3 m and 15 degrees. - The same six motions are shipped: 0.3 m forward, 0.2 m back, ±20 degrees, and ±20 degrees with 0.2 m forward.
- Free cells cost 1 second to enter. Cells within
safety_radius(0.2 m) cost an extrasafety_radius_penalty(30 seconds). - A laser hit inside 1 m adds a local penalty of 2048 seconds, and cells along the beam have that penalty halved.
/cmd_velrepeats the chosen step. It is not a smooth velocity.
Different in this package:
The cost-grid conversion matches value_iteration2 (ValueIterator::setMapWithCostGrid and the raw-cost State constructor). Its upstream ROS 2 /cost_map service branch is commented out; this package implements that service path with ROS 2 clients. cost_replace keeps that conversion; cost_add adds the 1 second base cost to each free cell. Neither cost mode applies occupancy safety inflation.
BSD-3-Clause. Copyright (c) 2026, nop and CIT autonomous robot laboratory.
This package refers to two earlier BSD-3-Clause programs by Ryuichi Ueda:
value_iteration, Copyright (c) 2021, CIT autonomous robot laboratoryvalue_iteration2, Copyright (c) 2024, Ryuichi Ueda and CIT autonomous robot laboratory
