Robot Operating System — Multiple Object Surface Inspection Survey — a ROS 2 Humble simulation workspace for UUV-based NBV inspection and Bayesian search missions using BT.cpp v4 behavior trees.
The system combines:
- Dubins-path vehicle simulation with idle drift dynamics
- Next-Best-View (NBV) inspection of detected targets on a cone or helix viewpoint manifold
- Bayesian occupancy search to find targets in unexplored space
- A boustrophedon (lawnmower) coverage baseline for non-adaptive comparison
- FLS (Forward-Looking Sonar) sensor simulation via ray casting
src/
├── demo_behaviors/ # BT.cpp v4 nodes, behavior trees, and full NBV mission launch
├── baseline_mission/ # Boustrophedon lawnmower coverage baseline (non-adaptive)
├── vista_sim/ # Dubins action server, vehicle kinematics, environment launch
├── sensor_model/ # FLS ray-cast sensor publisher and environment configs
├── uuv_interfaces/ # ROS 2 message, service, and action definitions
├── pose_generator/ # Python services: helix and cone viewpoint samplers
├── eca_a9_description/ # URDF/xacro for the ECA A9 UUV model
├── bayesian_search/ # Bayesian occupancy map and search pose server
├── nbv_cpp/ # TSDF + NBV scoring server (submodule)
├── sample_nbv_behaviors/ # NBV behavior tree nodes (submodule, rosmosis branch)
└── nrg_behaviors/ # Utility BT nodes: PublishTransform, RepeatWhile, etc. (submodule)
Clone the three external submodules into src/:
cd ~/projects/rosmosis_ws/src
# NBV behaviors
git clone git@github.com:UTNuclearRobotics/sample_nbv_behaviors.git
cd sample_nbv_behaviors && git checkout rosmosis && cd ..
# NRG utility behaviors
git clone git@github.com:UTNuclearRobotics/nrg_behaviors.git
# NBV TSDF server
git clone git@github.com:UTNuclearRobotics/nbv_cpp.gitFollow each submodule's README for any additional setup.
cd ~/projects/rosmosis_ws
colcon build --symlink-install --cmake-args -DCMAKE_BUILD_TYPE=Release
source install/setup.bash-DCMAKE_BUILD_TYPE=Release is required for nbv_cpp — its TSDF integration and ray-cast loops are significantly slower in debug mode. The Python packages (vista_sim, pose_generator, sensor_model) are unaffected.
For a partial build (sim + mission stack only, skipping nbv_cpp):
colcon build --packages-up-to demo_behaviors --symlink-install
source install/setup.bashros2 launch demo_behaviors demo_mission_launch.pyStarts: vehicle sim, FLS sensor, NBV server, helix service, Bayesian search server, behavior tree runner, RViz, rqt_console, rqt_graph.
ros2 launch demo_behaviors demo_mission_launch.py environment:=env_50x50_cluster_seabed| Environment | Description |
|---|---|
environment_basic |
Flat seabed, no objects (default) |
environment_seabed_basic |
Procedural rolling seabed |
environment_50x50 |
Flat 50×50m arena, four boxes at corners |
env_50x50_centroidBox |
Flat 50×50m arena, single box offset from centroid |
env_50x50_cluster |
Flat 50×50m arena, three boxes clustered mid-arena + one corner |
env_50x50_cluster_seabed |
Procedural seabed with divot at cluster site, same box layout |
ros2 launch demo_behaviors demo_mission_launch.py \
environment:=env_50x50_cluster_seabed \
drift_velocity:=0.1 \
constant_velocity:=0.8 \
log_level:=debug| Parameter | Default | Description |
|---|---|---|
environment |
environment_basic |
Environment yaml from sensor_model/config/ |
start_rviz |
true |
Launch RViz |
drift_velocity |
0.25 |
Idle drift speed between goals (m/s) |
constant_velocity |
0.5 |
Navigation speed along Dubins paths (m/s) |
time_step |
0.1 |
Simulation dt (s) |
log_level |
info |
ROS log level for all nodes (debug/info/warn/error/fatal) |
record |
false |
Record an MCAP bag of the mission (/face_hits, /detected_boxes, /tf, /tf_static) into data/bags/. Requires ros-humble-rosbag2-storage-mcap. |
bag_prefix |
nbv |
Run label for the output. A timestamp is appended automatically, and the same label tags both the bag (data/bags/) and the per-run reconstruction folder (data/reconstructions/). |
alpha |
0.5 |
CI-NBV cost weight in [0,1] for GetBestViewWithCost: utility = (1-alpha)·IG_norm − alpha·cost_norm. 0 = pure info-gain, 1 = pure cost. Plumbed CLI → run_bt → blackboard → BT, so it is the experiment-sweep knob (no XML edit). NBV mission only. |
debug_gui |
false |
Start the rqt_console / rqt_graph GUIs. Leave false for headless / batch / parallel runs (they need a display); true while debugging. Present in both the NBV and baseline launches. |
See all args: ros2 launch demo_behaviors demo_mission_launch.py --show-args
The baseline_mission package provides the non-adaptive comparison baseline: an
exhaustive boustrophedon (lawnmower) survey that sweeps the whole arena at a fixed
clearance, ignoring where the targets are. It reuses the same simulator, Dubins
action server, and sensor model as the NBV mission, and records the same topic
set and bag layout, so the two are directly comparable.
ros2 launch baseline_mission baseline_mission_launch.py \
environment:=env_50x50_cluster_seabed record:=true bag_prefix:=boustrophedon_cluster| Parameter | Default | Description |
|---|---|---|
environment |
environment_basic |
Environment yaml from sensor_model/config/ |
start_rviz |
true |
Launch RViz |
drift_velocity |
0.25 |
Idle drift speed between goals (m/s) |
constant_velocity |
0.5 |
Navigation speed along Dubins paths (m/s) |
time_step |
0.1 |
Simulation dt (s) |
log_level |
info |
ROS log level (debug/info/warn/error/fatal) |
record |
false |
Record an MCAP bag (/face_hits, /detected_boxes, /tf, /tf_static) into data/bags/ |
bag_prefix |
boustrophedon |
Run label; a timestamp is appended; tags the bag folder under data/bags/ |
debug_gui |
false |
Start the rqt_console / rqt_graph GUIs. Leave false for headless / batch / parallel runs |
The baseline takes the same launch args as the NBV mission except alpha (which is
NBV-only). The survey geometry (clearance, FOV, mount angle, ROI extents) is set as
members in boustrophedon_run.py, not via launch args — see the package README. See all
args: ros2 launch baseline_mission baseline_mission_launch.py --show-args.
It runs a standalone rclpy action client (no behavior tree, no NBV/search servers):
it computes the lawnmower waypoints in-file from the FLS swath geometry (lane spacing
= half the cross-track footprint for 50% overlap), then dispatches them one at a time
as PoseToPose goals. Lane spacing and the along-track running start are derived from
the sensor FOV and mount angle, so they stay consistent with the NBV sensor. The
launch self-terminates when the survey finishes. See
baseline_mission/README.md for the geometry and
tunable parameters.
MainTree — top-level mission loop (4 cycles):
DetectAndSortQueueruns continuously in parallel, accumulating detected targets sorted by distanceReactiveFallbackswitches between two branches each tick:- Inspection branch: if queue is non-empty, run
NBVOnTargeton the front target - Search branch: if queue is empty and within time budget, run
BayesianSearchto drive toward high-probability unexplored regions
- Inspection branch: if queue is non-empty, run
NBVOnTarget — per-target inspection:
- Publish ROI frame at target position
- Activate NBV policy, generate cone (or helix) viewpoints, score with TSDF
- Iterate: drive to best view → re-score → repeat until saturation
- Conclude policy, mark target complete
BayesianSearch — occupancy-driven exploration:
- Queries
bayesian_search_serverfor the next high-utility pose - Drives there via
DubinsClient - Sensor observations update the occupancy map, reducing uncertainty
vehicle_sim_server is the sole publisher of ned→base_link. It:
- Plans Dubins paths to action goals and follows them with an LOS controller
- Propagates idle drift between goals at
drift_velocity - Declares a goal complete when within 3.0m + 0.3 rad, or when the path is fully consumed and the vehicle is receding from closest approach (miss detection)
Ray-cast FLS sensor in sensor_model. Publishes:
/sonar/depth/image_rawand/sonar/depth/camera_info: synthetic depth image in thesonar_opticalframe/detected_boxes: detected object poses (box centroids) tagged with geometry IDs, in themapframe/face_hits: per-frame (geometry_id, primitive_id) hit pairs used for the CIR metric, in themapframe- Mesh markers for RViz visualization
The BT runner publishes its state on TCP port 1667 via BT::Groot2Publisher:
- Download Groot2 AppImage from behaviortree.dev
- Monitor → Connect to
localhost:1667 - Watch nodes light up live as the tree ticks
- vista_sim/README.md: vehicle sim parameters, environments, TF verification
- demo_behaviors/README.md: behavior node reference, custom behavior table, architecture details
- baseline_mission/README.md: boustrophedon lawnmower coverage baseline, including survey geometry, tunable parameters, and bag recording
This section lists the parameters that configure the mission outside of the NBV scoring algorithm itself (the TSDF resolution, viewpoint sampler geometry, convergence threshold, and cost-weighting are documented separately). The parameters below are grouped by concern: environment, arena, targets, vehicle, sensor, mission timing, the boustrophedon baseline, the Bayesian search, and output. Each entry gives the current value, the file it lives in, and what it controls. Several quantities are duplicated across files; the final subsection lists those and the values that must stay consistent for a valid comparison.
environment(launch argument, defaultenvironment_basic): selects the scene YAML fromsensor_model/config/. A single argument determines the terrain mesh, the target boxes, and the box count. Examples areenv_50x50_centroidBox(one box on a flat plane) andenv_50x50_cluster_seabed(four boxes on a procedural seabed). This is the primary switch for changing the test scenario.
- Seabed depth / NED anchor: the
maptonedstatic transform has z = 10 m (src/vista_sim/launch/vista_sim_launch.py, thestatic_transform_publisherline). This sets the seafloor depth used to resolve vehicle clearance, and it caps how deep the vehicle can fly, so any clearance value must be smaller than this. Increase it to model a deeper water column. - Arena size and origin: 50 by 50 m with origin at (0, 0)
(
src/bayesian_search/bayesian_search/bayesian_search_server.py, thearena_sizeandgrid_originmembers). The same extents are mirrored by the boustrophedon ROI and by the terrain mesh, and all three must agree. - Terrain mesh:
assets/flatPlane_50x50m.objfor a flat seabed orassets/seabed_50x50m.objfor procedural relief (env YAMLterrain.file_path).
box_count(env YAML): the number of pots in the scene. This must equal the mission'sRepeat num_cycles(see Mission timing), otherwise the mission either stops before all pots are inspected or searches out the remaining time budget.box_positions(env YAML): per-pot (x, y, z) position in the map frame.box_size(env YAML): 1.0 by 1.0 by 0.5 m for each pot.
These are shared by every mission because all of them drive through the same Dubins action server.
turn_radius_m= 5.0 m (launch argument): minimum turn radius. The path planner uses 1.2 times this value to leave yaw-rate headroom for cross-track correction.max_pitch_deg= 15 degrees (launch argument): the Dubins-airplane flight-path-angle bound used in both planning and dynamics.constant_velocity= 0.5 m/s (launch argument): navigation speed along Dubins paths.drift_velocity= 0.25 m/s (launch argument): idle drift speed applied between goals.time_step= 0.1 s (launch argument): simulation integration timestep.- Spawn pose =
Eta(-5, 0, 0, yaw=0), that is north = -5, east = 0, facing north (src/vista_sim/vista_sim/dubins_pose_to_pose_action_server.py, hardcoded). Shared by all missions. - Goal tolerance = within 3.0 m position and 0.3 rad yaw
(
dubins_pose_to_pose_action_server.py, hardcoded). Controls when a waypoint counts as reached. - Controller constants:
roll_ratio= 0.2,max_acceleration_mps2= 1.0,look_ahead_dist= 1.0 (dubins_pose_to_pose_action_server.py, hardcoded). These tune the line-of-sight follower and are rarely changed.
These describe the forward-looking sonar. Several values are duplicated across files (see Cross-file consistency).
- Mount angle = 20 degrees of downward tilt
(
src/eca_a9_description/urdf/eca_a9_visual.urdf.xacro, thefls_mount_angle_degproperty). This is the physical source of truth. The same angle is also hardcoded in the boustrophedon node, the NBV sampler, and the search server, all of which re-aim viewpoints assuming this tilt. - Field of view = 150 degrees horizontal by 25 degrees vertical
(
src/sensor_model/sensor_model/compute_intrinsics.py,FLS_CONFIG). The boustrophedon node hardcodes the same values for its swath math; the NBV and search read it fromcamera_info. - Beam count / image width = 512 px (
compute_intrinsics.py). - Maximum range = 100 m (
src/sensor_model/sensor_model/ray_casting_core.py,self.max_depth). Returns beyond this are discarded. - Bow offset: the sonar is mounted 0.7 m forward of
base_link(eca_a9_visual.urdf.xacro, thebase_to_sonarjoint origin).
Repeat num_cycles(src/demo_behaviors/behavior_trees/main_tree.xml, currently 4): the number of targets the mission inspects before it ends. Set this equal to the scenebox_count.isWithinTimeLimit hours(main_tree.xml, currently 2): the mission time budget. It guards the search branch, so it acts as a backstop that ends the mission if the targets are not all found. For short test runs, lower it.DubinsClient timeout_sec: 600 s in the NBV inspection subtree (nbv_on_target.xml) and 300 s in the search subtree (bayesian_search.xml). This is the per-maneuver timeout. For short time-boxed missions, consider lowering it so a single stalled maneuver cannot consume the whole budget.
Configured by the members at the top of
src/baseline_mission/baseline_mission/boustrophedon_run.py.
clearance= 5.0 m above the seabed. The survey depth is derived from this and the resolved seafloor depth.fov_x_deg/fov_y_deg= 150 / 25. These drive the swath and lane spacing and must match the sensor model.mount_angle_deg= 20. Used for the along-track running start, and must match the URDF.- ROI bounds (
roi_north_min/max,roi_east_min/max) = 0 to 50. The survey rectangle, which must match the arena.
Configured by the members of
src/bayesian_search/bayesian_search/bayesian_search_server.py.
grid_resolution= 1.0 m per cell.arena_size= (50, 50),grid_origin= (0, 0). Must match the arena.prior_sigma= 8.0 m: the width of the prior probability bump. The prior centroid defaults to the arena center.search_depth= 5.0 m NED, from which the search clearance is derived (currently 5 m, matching the baseline).mount_angle= 20 degrees. Must match the URDF.
record(launch argument, defaultfalse): toggles MCAP bag recording of/face_hits,/detected_boxes,/tf, and/tf_static.bag_prefix(launch argument, defaultnbvorboustrophedon): the run label. A timestamp is appended automatically. The same label tags both the bag directory (data/bags/) and the reconstruction directory (data/reconstructions/), so a run's bag and meshes are co-located.
Several quantities appear in more than one file with no single source of truth, so they can drift apart silently. For a valid method comparison, keep the following aligned.
- Mount angle (20 degrees) appears in four places: the URDF (the physical source of truth), the boustrophedon node, the NBV sampler, and the search server. Changing one means changing all four.
- Field of view (150 by 25 degrees) is hardcoded in the boustrophedon node
for its swath math, while the NBV and search read it from
camera_info. If the sensor FOV changes, update the boustrophedon node to match. - Arena size (50 by 50) and origin (0, 0) appear in the boustrophedon ROI, the search arena, and the terrain mesh, and all must agree.
- Clearance is currently 5 m for both the baseline and the search; keep them equal for a fair altitude comparison.
- Seabed depth (10 m) caps the clearance, so any clearance must be smaller than it.