Skip to content

Commit 8fe5c95

Browse files
committed
Add preemption guard for small areas
1 parent 4cc0d64 commit 8fe5c95

6 files changed

Lines changed: 152 additions & 5 deletions

File tree

include/frontier_exploration_ros2/frontier_search.hpp

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,7 @@ std::optional<VisibleRevealGain> compute_visible_reveal_gain(
219219
const std::optional<OccupancyGrid2d> & local_costmap,
220220
double range_m,
221221
double fov_deg,
222-
double ray_step_deg);
222+
double ray_step_deg,
223+
const std::optional<FrontierCandidate::CellBounds> & visible_reveal_bounds = std::nullopt);
223224

224225
} // namespace frontier_exploration_ros2

include/frontier_exploration_ros2/frontier_types.hpp

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,14 @@ class FrontierCache
266266
// Frontier output used by policy and goal dispatch.
267267
struct FrontierCandidate
268268
{
269+
struct CellBounds
270+
{
271+
int min_x{0};
272+
int min_y{0};
273+
int max_x{0};
274+
int max_y{0};
275+
};
276+
269277
FrontierCandidate() = default;
270278

271279
// Compatibility constructor for existing nearest-style tests and simple callers.
@@ -288,14 +296,16 @@ struct FrontierCandidate
288296
std::pair<int, int> start_cell_in,
289297
std::pair<double, double> start_world_point_in,
290298
std::optional<std::pair<double, double>> goal_point_in,
291-
int size_in)
299+
int size_in,
300+
std::optional<CellBounds> visible_reveal_bounds_in = std::nullopt)
292301
: centroid(std::move(centroid_in)),
293302
center_point(std::move(center_point_in)),
294303
center_cell(std::move(center_cell_in)),
295304
start_cell(std::move(start_cell_in)),
296305
start_world_point(std::move(start_world_point_in)),
297306
goal_point(std::move(goal_point_in)),
298-
size(size_in)
307+
size(size_in),
308+
visible_reveal_bounds(std::move(visible_reveal_bounds_in))
299309
{
300310
}
301311

@@ -313,6 +323,8 @@ struct FrontierCandidate
313323
std::optional<std::pair<double, double>> goal_point;
314324
// Number of cells in the underlying frontier cluster.
315325
int size{0};
326+
// Conservative local bounds used to limit target-pose visible-reveal estimation around this cluster.
327+
std::optional<CellBounds> visible_reveal_bounds;
316328
};
317329

318330
enum class FrontierStrategy

src/frontier_explorer_core_dispatch.cpp

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -284,14 +284,26 @@ std::optional<double> FrontierExplorerCore::active_goal_visible_reveal_length()
284284
sensor_pose.orientation = detail::quaternion_from_yaw(heading + yaw_offset_rad);
285285

286286
// The helper returns a local, occlusion-aware reveal length estimate around the target pose.
287+
const auto visible_reveal_bounds =
288+
std::visit(
289+
[](const auto & frontier) -> std::optional<FrontierCandidate::CellBounds> {
290+
using FrontierT = std::decay_t<decltype(frontier)>;
291+
if constexpr (std::is_same_v<FrontierT, FrontierCandidate>) {
292+
return frontier.visible_reveal_bounds;
293+
}
294+
return std::nullopt;
295+
},
296+
*active_goal_frontier);
297+
287298
const auto visible_gain = compute_visible_reveal_gain(
288299
sensor_pose,
289300
*map,
290301
*costmap,
291302
local_costmap,
292303
params.goal_preemption_lidar_range_m,
293304
params.goal_preemption_lidar_fov_deg,
294-
params.goal_preemption_lidar_ray_step_deg);
305+
params.goal_preemption_lidar_ray_step_deg,
306+
visible_reveal_bounds);
295307
if (!visible_gain.has_value()) {
296308
return std::nullopt;
297309
}
@@ -441,6 +453,15 @@ void FrontierExplorerCore::consider_preempt_active_goal(const std::string & trig
441453
if (!visible_reveal_length.has_value()) {
442454
callbacks.log_warn(
443455
"Skipping visible reveal gain gate for the active goal; falling back to frontier snapshot reselection");
456+
} else if (
457+
map.has_value() &&
458+
static_cast<double>(frontier_size(*active_goal_frontier)) * map->map().info.resolution <=
459+
*visible_reveal_length)
460+
{
461+
// Small frontier clusters can look fully revealable even with low absolute gain thresholds.
462+
// If the current visible reveal already covers the whole cluster-size proxy, skip preemption.
463+
reset_replacement_candidate_tracking();
464+
return;
444465
} else if (*visible_reveal_length >= params.goal_preemption_lidar_min_reveal_length_m) {
445466
// Keep current goal while target pose can still reveal enough unexplored boundary on arrival.
446467
reset_replacement_candidate_tracking();

src/frontier_search.cpp

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -407,6 +407,23 @@ std::optional<FrontierCandidate> build_frontier_candidate(
407407
centroid_sum_y / static_cast<double>(new_frontier.size()),
408408
};
409409

410+
int min_frontier_x = new_frontier.front()->mapX;
411+
int max_frontier_x = new_frontier.front()->mapX;
412+
int min_frontier_y = new_frontier.front()->mapY;
413+
int max_frontier_y = new_frontier.front()->mapY;
414+
for (auto * frontier_point : new_frontier) {
415+
min_frontier_x = std::min(min_frontier_x, frontier_point->mapX);
416+
max_frontier_x = std::max(max_frontier_x, frontier_point->mapX);
417+
min_frontier_y = std::min(min_frontier_y, frontier_point->mapY);
418+
max_frontier_y = std::max(max_frontier_y, frontier_point->mapY);
419+
}
420+
const FrontierCandidate::CellBounds visible_reveal_bounds{
421+
std::max(0, min_frontier_x - 1),
422+
std::max(0, min_frontier_y - 1),
423+
std::min(occupancy_map.getSizeX() - 1, max_frontier_x + 1),
424+
std::min(occupancy_map.getSizeY() - 1, max_frontier_y + 1),
425+
};
426+
410427
std::pair<int, int> center_cell = {new_frontier.front()->mapX, new_frontier.front()->mapY};
411428
std::pair<double, double> center_point = frontier_world_points.front();
412429
double center_distance_sq = std::numeric_limits<double>::infinity();
@@ -454,6 +471,7 @@ std::optional<FrontierCandidate> build_frontier_candidate(
454471
start_world_point,
455472
std::nullopt,
456473
static_cast<int>(new_frontier.size()),
474+
visible_reveal_bounds,
457475
};
458476
}
459477

@@ -524,6 +542,7 @@ std::optional<FrontierCandidate> build_frontier_candidate(
524542
start_world_point,
525543
*goal_point,
526544
static_cast<int>(new_frontier.size()),
545+
visible_reveal_bounds,
527546
};
528547
}
529548

@@ -803,7 +822,8 @@ std::optional<VisibleRevealGain> compute_visible_reveal_gain(
803822
const std::optional<OccupancyGrid2d> & local_costmap,
804823
double range_m,
805824
double fov_deg,
806-
double ray_step_deg)
825+
double ray_step_deg,
826+
const std::optional<FrontierCandidate::CellBounds> & visible_reveal_bounds)
807827
{
808828
// Invalid geometry means the caller should skip this optimization and keep the fallback path.
809829
if (range_m < 0.1 || fov_deg < 1.0 || fov_deg > 360.0 || ray_step_deg < 0.25 || ray_step_deg > 45.0) {
@@ -859,6 +879,15 @@ std::optional<VisibleRevealGain> compute_visible_reveal_gain(
859879
// Leaving the map bounds is equivalent to exhausting that ray.
860880
break;
861881
}
882+
if (visible_reveal_bounds.has_value() &&
883+
(map_x < visible_reveal_bounds->min_x ||
884+
map_x > visible_reveal_bounds->max_x ||
885+
map_y < visible_reveal_bounds->min_y ||
886+
map_y > visible_reveal_bounds->max_y))
887+
{
888+
// Treat the local frontier-cluster bounds as a virtual wall for visible-gain estimation.
889+
break;
890+
}
862891

863892
if (map_x == last_map_x && map_y == last_map_y) {
864893
continue;

test/test_frontier_snapshot_and_search.cpp

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -318,6 +318,42 @@ TEST(FrontierSearchTests, VisibleRevealGainRespectsYawAndFov)
318318
EXPECT_EQ(north_gain->visible_reveal_cell_count, 0);
319319
}
320320

321+
TEST(FrontierSearchTests, VisibleRevealGainStopsAtVirtualClusterBounds)
322+
{
323+
auto map_msg = build_grid(12, 12, -1);
324+
for (int x = 2; x <= 4; ++x) {
325+
set_cells(map_msg, {{x, 6}}, 0);
326+
}
327+
328+
auto costmap_msg = build_grid(12, 12, 0);
329+
const OccupancyGrid2d occupancy_map(map_msg);
330+
const OccupancyGrid2d costmap(costmap_msg);
331+
332+
const auto unclipped_gain = compute_visible_reveal_gain(
333+
make_pose(3.0, 6.0, 0.0),
334+
occupancy_map,
335+
costmap,
336+
std::nullopt,
337+
6.0,
338+
1.0,
339+
1.0);
340+
const auto clipped_gain = compute_visible_reveal_gain(
341+
make_pose(3.0, 6.0, 0.0),
342+
occupancy_map,
343+
costmap,
344+
std::nullopt,
345+
6.0,
346+
1.0,
347+
1.0,
348+
FrontierCandidate::CellBounds{2, 6, 4, 6});
349+
350+
ASSERT_TRUE(unclipped_gain.has_value());
351+
ASSERT_TRUE(clipped_gain.has_value());
352+
EXPECT_GT(unclipped_gain->visible_reveal_cell_count, 0);
353+
EXPECT_EQ(clipped_gain->visible_reveal_cell_count, 0);
354+
EXPECT_DOUBLE_EQ(clipped_gain->visible_reveal_length_m, 0.0);
355+
}
356+
321357
TEST(FrontierSearchTests, ExplorationCompleteCallbackRunsWhenNoFrontiersRemain)
322358
{
323359
auto core = make_snapshot_core();

test/test_preemption_flow.cpp

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -616,6 +616,54 @@ TEST(PreemptionFlowTests, VisibleGainGateSkipsSnapshotSearchWhenGoalStillHasVisi
616616
EXPECT_TRUE(core->pending_frontier_sequence.empty());
617617
}
618618

619+
TEST(PreemptionFlowTests, VisibleGainGateSkipsSmallClusterEvenBelowMinRevealThreshold)
620+
{
621+
auto core = make_preemption_core();
622+
auto fake_handle = std::make_shared<FakeGoalHandle>();
623+
core->goal_handle = fake_handle;
624+
core->params.goal_preemption_enabled = true;
625+
core->params.goal_preemption_lidar_range_m = 6.0;
626+
core->params.goal_preemption_lidar_fov_deg = 90.0;
627+
core->params.goal_preemption_lidar_ray_step_deg = 1.0;
628+
core->params.goal_preemption_lidar_min_reveal_length_m = 2.0;
629+
core->active_goal_frontier = FrontierCandidate{{4.0, 5.0}, {3.0, 5.0}, 1};
630+
core->active_goal_frontiers = {*core->active_goal_frontier};
631+
632+
auto map_msg = build_grid(12, 12, -1);
633+
for (int x = 0; x <= 4; ++x) {
634+
set_cell(map_msg, x, 5, 0);
635+
}
636+
core->map = OccupancyGrid2d(map_msg);
637+
638+
int frontier_search_calls = 0;
639+
int dispatch_calls = 0;
640+
core->callbacks.wait_for_action_server = [](double) {return true;};
641+
core->callbacks.dispatch_goal_request = [&dispatch_calls](const GoalDispatchRequest &) {
642+
dispatch_calls += 1;
643+
};
644+
core->callbacks.frontier_search = [&frontier_search_calls](
645+
const geometry_msgs::msg::Pose &,
646+
const OccupancyGrid2d &,
647+
const OccupancyGrid2d &,
648+
const std::optional<OccupancyGrid2d> &,
649+
double,
650+
bool)
651+
{
652+
frontier_search_calls += 1;
653+
FrontierSearchResult result;
654+
result.frontiers = {FrontierCandidate{{8.0, 8.0}, {8.0, 8.0}, 8}};
655+
result.robot_map_cell = {0, 0};
656+
return result;
657+
};
658+
659+
core->consider_preempt_active_goal("map");
660+
661+
EXPECT_EQ(frontier_search_calls, 0);
662+
EXPECT_EQ(dispatch_calls, 0);
663+
EXPECT_EQ(fake_handle->cancel_calls, 0);
664+
EXPECT_TRUE(core->pending_frontier_sequence.empty());
665+
}
666+
619667
TEST(PreemptionFlowTests, VisibleGainGateFallsBackToSnapshotReselectionWhenGoalGainIsExhausted)
620668
{
621669
auto core = make_preemption_core();

0 commit comments

Comments
 (0)