A lightweight physics twin for autonomous underwater vehicles — a 6-DOF AUV simulator as a single ROS 2 node, in pure Python/numpy. It runs the vehicle’s physics and publishes the sensor topics a state estimator consumes, plus ground-truth pose, so you can develop and stress-test an underwater localization or control stack on the bench — no water, no GPU, no Isaac Sim. It runs happily on a Jetson.
Validating underwater state estimation is uniquely painful:
A physics twin answers all four. It simulates the vehicle’s dynamics, so every sensor stream is derived from one consistent truth; it knows the true pose, so estimator accuracy is measurable; and you can script scenarios, disturbances, and on-demand sensor failures — all from a laptop or the robot’s own compute.
wrench in ──► 6-DOF dynamics (thrust + buoyancy + drag + current) ──► IMU / DVL / depth + ground truth
/auv_sim/ground_truth is the exact pose, so you can plot
estimator error directly.geometry_msgs/Wrench with a deadman,
the same contract a PID/thruster-allocation stack already produces.An independent 6-DOF Newton-Euler rigid body (FLU body frame, ENU world, quaternion orientation):
ρ·V·g, applied at a center of buoyancy above the center of mass, so
the vehicle self-rights — giving realistic roll/pitch, and therefore realistic IMU
output. Default displacement is exactly neutral.The accelerometer output is the true specific force (ΣF − gravity)/m in the body
frame, so a level, neutrally-buoyant vehicle reads az ≈ +9.81 — matching a real
ENU/FLU IMU.
It is not tied to any one vehicle. Every topic name, frame, rate and physics constant is a parameter, and the defaults are generic:
| published topic (default) | type | content |
|---|---|---|
imu/data |
sensor_msgs/Imu |
orientation, body angular velocity, specific-force accel |
dvl/odometry |
nav_msgs/Odometry |
body-frame velocity |
dvl/altitude, ping/range |
sensor_msgs/Range |
height above the floor |
pressure |
sensor_msgs/FluidPressure |
from depth |
ground_truth |
geometry_msgs/PoseStamped |
exact sim pose |
Adapt it to a specific vehicle with a profile — a small YAML that remaps the
topics/frames and sets the mass/buoyancy. config/barracuda.yaml is one example
(Barracuda’s real /barracuda/... topics); write your own for any other AUV.
Profiles can also avoid hardcoded robot names by using placeholders in topic and frame strings:
| placeholder | expands to |
|---|---|
{topic_prefix} |
the normalized topic_prefix parameter, such as /barracuda |
{frame_prefix} |
the normalized frame_prefix parameter, such as barracuda |
For example, wrench_topic: "{topic_prefix}/wrench" becomes
/barracuda/wrench when topic_prefix: /barracuda. Leave both prefixes empty
for generic relative names.
Profiles can also tune the vehicle dynamics and scenario start state:
| parameter | meaning |
|---|---|
inertia |
diagonal body inertia [Ixx, Iyy, Izz] |
cob_offset |
center of buoyancy relative to center of mass, body frame |
linear_drag, quadratic_drag |
per-axis translational drag coefficients |
angular_drag, angular_quadratic_drag |
per-axis rotational drag coefficients |
initial_position |
starting world position |
initial_orientation_wxyz |
starting orientation quaternion |
initial_linear_velocity, initial_angular_velocity |
starting body velocities |
There’s no built-in render, but it broadcasts TF and a vehicle marker, so it shows up live in RViz2 — which runs on a Jetson (plain OpenGL, no RTX/Isaac):
ros2 launch auv_sim_bench sim.launch.py rviz:=true
You get a 3D box for the vehicle flying around under thrust/current/buoyancy, the TF
frames, and — if you set estimate_topic to your estimator’s pose — a second marker
for the estimate, so you can watch estimate-vs-truth drift in real time. It’s
schematic (boxes + axes), not photoreal; photoreal water is the one thing that needs
Isaac Sim on an x86 box.
For the real reason to simulate — breaking the estimator on purpose:
current: [0.3, 0.0, 0.0] # steady cross-current
dvl_dropout_start_sec: 10.0 # DVL goes silent at t=10s ...
dvl_dropout_duration_sec: 5.0 # ... for 5s
dvl_noise_stddev: 0.02 # velocity noise
dvl_emit_nan: false # or feed the estimator NaNs
When an estimate topic is set, the node compares the estimate against ground
truth and publishes the error on auv_sim/estimator_error
(geometry_msgs/Vector3Stamped: x = position error [m], y = attitude error
[deg], z = NEES), plus a periodic RMS/max summary in the log. This works with
the built-in physics and against an external simulator (below).
Two estimate inputs:
estimate_topic (PoseStamped) — position + attitude error only.estimate_odom_topic (Odometry) — also carries pose.covariance, which
enables NEES (normalized estimation error squared). For a consistent
filter position NEES averages to ~3; much larger means the covariance is
over-confident — the exact failure the IEKF’s mislabeled-covariance bugs
produced. This is the check that turns “is the estimate close?” into “is the
estimate’s uncertainty honest?”.Set physics_enabled: false and the node stops simulating — instead it
subscribes to truth_topic and becomes a pure estimator evaluator. Point it
at a higher-fidelity simulator’s ground truth and you score the estimator against
that physics, with the same metrics and the same topic contract.
# 1. bring up Isaac Sim (barracuda scene) publishing /barracuda/* + /barracuda/pose
# 2. run the evaluator against it:
ros2 launch auv_sim_bench sim.launch.py config:=isaac.yaml
# 3. watch the error:
ros2 topic echo /auv_sim/estimator_error
Same benchmark, two backends: the built-in physics for a GPU-free bench/CI check,
Isaac Sim for the high-fidelity pre-wet-test check. config/isaac.yaml is
barracuda.yaml with physics_enabled: false and truth_topic:
/barracuda/pose.
cd ~/ros2_ws/src && git clone <this repo> auv_sim_bench
cd ~/ros2_ws && colcon build --packages-select auv_sim_bench
source install/setup.bash
ros2 launch auv_sim_bench sim.launch.py # generic defaults
ros2 launch auv_sim_bench sim.launch.py rviz:=true # + 3D view
ros2 launch auv_sim_bench sim.launch.py config:=barracuda.yaml # Barracuda profile
# drive it by publishing geometry_msgs/Wrench on the wrench topic (default cmd_wrench)
Validated on a Jetson AGX Orin (JetPack 6 / ROS 2 Humble) — pure Python + numpy, so no GPU or Isaac needed:
mkdir -p ~/auv_ws/src
cp -r AUVSimBench ~/auv_ws/src/auv_sim_bench
cd ~/auv_ws && colcon build --packages-select auv_sim_bench
source /opt/ros/humble/setup.bash
source ~/auv_ws/install/setup.bash # both source lines, in every new terminal
ros2 launch auv_sim_bench sim.launch.py
Headless over SSH there’s no display for RViz, so drop rviz:=true and watch the
data instead (ros2 topic echo /ground_truth); driving it with a /cmd_wrench
publisher moves the body as expected. For the 3D view, run RViz on a monitor
attached to the Jetson.
Test your estimator: run the sim + your estimator, then compare
/barracuda/estimated_pose against /auv_sim/ground_truth — an accuracy check you
cannot get from a real dive (no underwater ground truth) or from bag replay.
The physics core (dynamics.py) is plain numpy with no ROS dependency, so it is
unit-tested directly — buoyancy, gravity, drag, thrust response, and current:
pytest test/
Inspired by Leonardo Lima’s isaac_underwater
examples (MIT), which show buoyancy and drag in Isaac Sim. AUVSimBench takes that
idea in a different direction — its own 6-DOF dynamics in a standalone ROS 2 node, so
it runs anywhere ROS 2 does, including a Jetson.
It’s a controls/estimation test rig, not a hydrodynamics research tool: drag and inertia are simple tunable coefficients, not CFD. Added-mass and richer current fields are natural next steps.