Skip to content

Commit 7ecd193

Browse files
committed
Add Bounded-Horizon Dynamic Programming ordering to MRTSP
1 parent ccd8877 commit 7ecd193

15 files changed

Lines changed: 1243 additions & 114 deletions

CMakeLists.txt

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ add_library(${PROJECT_NAME}_core
3333
src/frontier_explorer_core_suppression.cpp
3434
src/frontier_explorer_core_dispatch.cpp
3535
src/mrtsp_ordering.cpp
36+
src/mrtsp_solver.cpp
3637
)
3738
target_include_directories(${PROJECT_NAME}_core PUBLIC
3839
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
@@ -213,6 +214,21 @@ if(BUILD_TESTING)
213214
nav_msgs
214215
)
215216

217+
ament_add_gtest(test_mrtsp_solver
218+
test/test_mrtsp_solver.cpp
219+
)
220+
target_link_libraries(test_mrtsp_solver
221+
${PROJECT_NAME}_core
222+
)
223+
target_include_directories(test_mrtsp_solver PRIVATE
224+
${CMAKE_CURRENT_SOURCE_DIR}/include
225+
)
226+
ament_target_dependencies(test_mrtsp_solver
227+
action_msgs
228+
geometry_msgs
229+
nav_msgs
230+
)
231+
216232
ament_add_gtest(test_control_service_and_idle
217233
test/test_control_service_and_idle.cpp
218234
)

README.md

Lines changed: 289 additions & 82 deletions
Large diffs are not rendered by default.

config/params.yaml

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,17 @@ frontier_explorer:
1818
autostart: true
1919
control_service_enabled: true
2020

21-
# Frontier Selection Strategy
21+
# Frontier selection strategy. Use "mrtsp" for MRTSP ordering or "nearest" for distance policy.
2222
strategy: mrtsp
2323

24+
# MRTSP solver mode. "dp" scores all optimized frontiers, keeps the best candidate pool,
25+
# and searches a bounded sequence; "greedy" traverses the full MRTSP matrix one step at a time.
26+
mrtsp_solver: dp
27+
# Maximum number of scored frontier candidates passed into the bounded-horizon solver.
28+
dp_solver_candidate_limit: 15
29+
# Number of distinct frontier visits evaluated in the candidate pool before dispatching sequence[0].
30+
dp_planning_horizon: 10
31+
2432
# QoS
2533
map_qos_durability: transient_local
2634
map_qos_reliability: reliable

include/frontier_exploration_ros2/frontier_explorer_core.hpp

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ limitations under the License.
1616

1717
#pragma once
1818

19+
#include <cstddef>
1920
#include <functional>
2021
#include <memory>
2122
#include <optional>
@@ -81,6 +82,11 @@ struct FrontierExplorerCoreParams
8182
double weight_gain_ws{1.0};
8283
double max_linear_speed_vmax{0.5};
8384
double max_angular_speed_wmax{1.0};
85+
// MRTSP can traverse its cost matrix greedily or with bounded dynamic programming.
86+
std::string mrtsp_solver{"dp"};
87+
// DP limits are algorithm-level controls, so they are not tied to one strategy name.
88+
std::size_t dp_solver_candidate_limit{15};
89+
std::size_t dp_planning_horizon{10};
8490
int occ_threshold{OCC_THRESHOLD};
8591
int min_frontier_size_cells{MIN_FRONTIER_SIZE};
8692
double frontier_candidate_min_goal_distance_m{0.0};
@@ -379,6 +385,11 @@ class FrontierExplorerCore
379385
double weight_gain_ws{0.0};
380386
double max_linear_speed_vmax{0.0};
381387
double max_angular_speed_wmax{0.0};
388+
// Solver settings are part of the order cache key because they can change the
389+
// selected sequence without any map, frontier, or robot-pose change.
390+
std::string mrtsp_solver;
391+
std::size_t dp_solver_candidate_limit{0};
392+
std::size_t dp_planning_horizon{0};
382393
FrontierSequence frontier_sequence;
383394
};
384395
std::optional<MrtspOrderCacheEntry> mrtsp_order_cache;

include/frontier_exploration_ros2/mrtsp_ordering.hpp

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,16 @@ double lower_bound_time_cost(
7575
double max_linear_speed_vmax,
7676
double max_angular_speed_wmax);
7777

78+
// Computes the exact robot-start row cost used by build_cost_matrix().
79+
// Candidate pruning calls this helper to stay aligned with MRTSP matrix semantics.
80+
double compute_mrtsp_start_cost(
81+
const FrontierCandidate & candidate,
82+
const RobotState & robot_state,
83+
const CostWeights & weights,
84+
double sensor_effective_range_m,
85+
double max_linear_speed_vmax,
86+
double max_angular_speed_wmax);
87+
7888
// Builds the directed MRTSP heuristic matrix consumed by the greedy ordering step.
7989
MrtspCostMatrix build_cost_matrix(
8090
const std::vector<FrontierCandidate> & frontiers,
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
/*
2+
Copyright 2026 Mert Güler
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
#pragma once
18+
19+
#include <cstddef>
20+
#include <vector>
21+
22+
#include "frontier_exploration_ros2/frontier_types.hpp"
23+
#include "frontier_exploration_ros2/mrtsp_ordering.hpp"
24+
25+
namespace frontier_exploration_ros2
26+
{
27+
28+
constexpr std::size_t kMaxDpSolverCandidateLimit{60U};
29+
30+
struct MrtspSolverConfig
31+
{
32+
// Upper bound for the candidate pool passed to the bounded solver.
33+
std::size_t candidate_limit{15};
34+
// Number of distinct frontier visits evaluated in each receding-horizon plan.
35+
std::size_t planning_horizon{10};
36+
};
37+
38+
struct MrtspPrunedCandidate
39+
{
40+
// Index in the frontier vector received from the core. The DP solver works on the
41+
// pruned vector, then callers use this field to map the result back to dispatch data.
42+
std::size_t original_index{};
43+
FrontierCandidate candidate{};
44+
// MRTSP start-row cost used for deterministic top-N candidate pruning.
45+
double score{0.0};
46+
};
47+
48+
// Scores candidates with the same robot-to-frontier cost used by the MRTSP matrix,
49+
// sorts them deterministically, and returns the best bounded candidate pool.
50+
std::vector<MrtspPrunedCandidate> prune_mrtsp_candidates(
51+
const std::vector<FrontierCandidate> & candidates,
52+
const RobotState & robot_state,
53+
const CostWeights & weights,
54+
double sensor_effective_range_m,
55+
double max_linear_speed_vmax,
56+
double max_angular_speed_wmax,
57+
const MrtspSolverConfig & config);
58+
59+
// Searches the lowest-cost K-frontier sequence in the given matrix.
60+
// Matrix node 0 is the robot start node, and nodes 1..M are pruned frontiers.
61+
// Returned indices are relative to the pruned candidate list.
62+
std::vector<std::size_t> solve_bounded_horizon_mrtsp_order(
63+
const MrtspCostMatrix & cost_matrix,
64+
std::size_t planning_horizon);
65+
66+
} // namespace frontier_exploration_ros2

package.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
<?xml-model href="http://download.ros.org/schema/package_format3.xsd" schematypens="http://www.w3.org/2001/XMLSchema"?>
33
<package format="3">
44
<name>frontier_exploration_ros2</name>
5-
<version>1.4.1</version>
5+
<version>1.5.0</version>
66
<description>Modern C++ frontier exploration package for autonomous mobile robots, with a core C++ library for custom integrations alongside ROS 2 Jazzy / Nav2 integration.</description>
77
<maintainer email="support.mertgulerx@gmail.com">mertgulerx</maintainer>
88
<license>Apache-2.0</license>

src/frontier_explorer_core_dispatch.cpp

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -772,19 +772,52 @@ bool FrontierExplorerCore::send_frontier_goal(
772772
if (frontier_sequence.empty()) {
773773
return false;
774774
}
775+
776+
std::size_t dispatch_index = 0U;
777+
for (; dispatch_index < frontier_sequence.size(); ++dispatch_index) {
778+
const auto & candidate_frontier = frontier_sequence[dispatch_index];
779+
if (!params.goal_skip_on_blocked_goal) {
780+
break;
781+
}
782+
783+
const auto cost_status = frontier_cost_status(std::optional<FrontierLike>{candidate_frontier});
784+
if (!cost_status.has_value()) {
785+
break;
786+
}
787+
788+
callbacks.log_info(
789+
"Skipping blocked frontier goal before dispatch: " + *cost_status +
790+
"; " + describe_frontier(candidate_frontier));
791+
}
792+
793+
if (dispatch_index >= frontier_sequence.size()) {
794+
callbacks.log_info(
795+
"All selected frontier goals are blocked before dispatch; waiting for updated costmap or frontier data");
796+
return false;
797+
}
798+
799+
FrontierSequence dispatch_sequence;
800+
dispatch_sequence.reserve(frontier_sequence.size() - dispatch_index);
801+
for (std::size_t index = dispatch_index; index < frontier_sequence.size(); ++index) {
802+
dispatch_sequence.push_back(frontier_sequence[index]);
803+
}
804+
775805
const auto goal_pose = build_goal_pose(
776-
frontier_sequence.front(),
806+
dispatch_sequence.front(),
777807
current_pose);
778808
if (debug_outputs_enabled()) {
779809
callbacks.publish_selected_frontier_pose(goal_pose);
780810
}
811+
const std::string dispatch_description = dispatch_index == 0U ?
812+
description :
813+
"Sending frontier goal after blocked-goal skip: " + describe_frontier(dispatch_sequence.front());
781814
// Frontier mode dispatches only the first element from the selected sequence.
782815
return send_pose_goal(
783816
goal_pose,
784817
"frontier",
785-
frontier_sequence.front(),
786-
frontier_sequence,
787-
description);
818+
dispatch_sequence.front(),
819+
dispatch_sequence,
820+
dispatch_description);
788821
}
789822

790823
void FrontierExplorerCore::dispatch_goal_request(

0 commit comments

Comments
 (0)