auv-sim-bench 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- auv_sim_bench-0.1.0/PKG-INFO +203 -0
- auv_sim_bench-0.1.0/README.md +186 -0
- auv_sim_bench-0.1.0/auv_sim_bench/__init__.py +0 -0
- auv_sim_bench-0.1.0/auv_sim_bench/dynamics.py +94 -0
- auv_sim_bench-0.1.0/auv_sim_bench/sim_node.py +322 -0
- auv_sim_bench-0.1.0/auv_sim_bench.egg-info/PKG-INFO +203 -0
- auv_sim_bench-0.1.0/auv_sim_bench.egg-info/SOURCES.txt +20 -0
- auv_sim_bench-0.1.0/auv_sim_bench.egg-info/dependency_links.txt +1 -0
- auv_sim_bench-0.1.0/auv_sim_bench.egg-info/entry_points.txt +2 -0
- auv_sim_bench-0.1.0/auv_sim_bench.egg-info/requires.txt +1 -0
- auv_sim_bench-0.1.0/auv_sim_bench.egg-info/top_level.txt +1 -0
- auv_sim_bench-0.1.0/auv_sim_bench.egg-info/zip-safe +1 -0
- auv_sim_bench-0.1.0/config/barracuda.yaml +21 -0
- auv_sim_bench-0.1.0/config/sim.yaml +56 -0
- auv_sim_bench-0.1.0/launch/sim.launch.py +40 -0
- auv_sim_bench-0.1.0/package.xml +23 -0
- auv_sim_bench-0.1.0/resource/auv_sim_bench +0 -0
- auv_sim_bench-0.1.0/rviz/auv_sim.rviz +46 -0
- auv_sim_bench-0.1.0/setup.cfg +10 -0
- auv_sim_bench-0.1.0/setup.py +33 -0
- auv_sim_bench-0.1.0/test/test_dynamics.py +46 -0
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: auv_sim_bench
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Lightweight 6-DOF AUV physics sim for dry-testing state estimation.
|
|
5
|
+
Maintainer: wuisabel-gif
|
|
6
|
+
Maintainer-email: 231155141+wuisabel-gif@users.noreply.github.com
|
|
7
|
+
License: MIT
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
Requires-Dist: setuptools
|
|
10
|
+
Dynamic: description
|
|
11
|
+
Dynamic: description-content-type
|
|
12
|
+
Dynamic: license
|
|
13
|
+
Dynamic: maintainer
|
|
14
|
+
Dynamic: maintainer-email
|
|
15
|
+
Dynamic: requires-dist
|
|
16
|
+
Dynamic: summary
|
|
17
|
+
|
|
18
|
+
# AUVSimBench
|
|
19
|
+
|
|
20
|
+
A lightweight **physics twin** for autonomous underwater vehicles — a 6-DOF AUV
|
|
21
|
+
simulator as a single ROS 2 node, in pure Python/numpy. It runs the vehicle's
|
|
22
|
+
physics and publishes the **sensor topics a state estimator consumes, plus
|
|
23
|
+
ground-truth pose**, so you can develop and stress-test an underwater localization
|
|
24
|
+
or control stack **on the bench** — no water, no GPU, no Isaac Sim. It runs happily
|
|
25
|
+
on a Jetson.
|
|
26
|
+
|
|
27
|
+
## Why this is hard (and why the twin helps)
|
|
28
|
+
|
|
29
|
+
Validating underwater state estimation is uniquely painful:
|
|
30
|
+
|
|
31
|
+
- **Wet tests are expensive and rare.** Pool/ocean time is slow to schedule, and you
|
|
32
|
+
get only a handful of runs before everyone's tired and the battery's flat.
|
|
33
|
+
- **There is no ground truth underwater.** No GPS below the surface — so even when you
|
|
34
|
+
*do* get a dive, you often can't measure *how wrong* the estimate was.
|
|
35
|
+
- **Most failures aren't about the water.** In practice the things that break a dive are
|
|
36
|
+
wiring (a node subscribed to the wrong topic), frame conventions (axes flipped),
|
|
37
|
+
startup ordering (a sensor not powered on), or an estimator diverging — and you
|
|
38
|
+
usually discover them *mid-dive*, burning a wet test to debug a typo.
|
|
39
|
+
- **Recorded bags only go so far.** A bag replays exactly one motion; you can't steer
|
|
40
|
+
it, you can't make a sensor fail on cue, and replayed DVL/depth won't agree with a
|
|
41
|
+
real IMU sitting still on the bench.
|
|
42
|
+
|
|
43
|
+
A **physics twin** answers all four. It simulates the vehicle's dynamics, so every
|
|
44
|
+
sensor stream is derived from *one consistent truth*; it *knows* the true pose, so
|
|
45
|
+
estimator accuracy is measurable; and you can script scenarios, disturbances, and
|
|
46
|
+
on-demand sensor failures — all from a laptop or the robot's own compute.
|
|
47
|
+
|
|
48
|
+
```
|
|
49
|
+
wrench in ──► 6-DOF dynamics (thrust + buoyancy + drag + current) ──► IMU / DVL / depth + ground truth
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## What it gives you
|
|
53
|
+
|
|
54
|
+
- **Consistent sensors** — IMU, DVL and depth all generated from the same simulated
|
|
55
|
+
body, so they physically agree (the thing bag-replay can't do).
|
|
56
|
+
- **Ground truth** — `/auv_sim/ground_truth` is the exact pose, so you can plot
|
|
57
|
+
estimator error directly.
|
|
58
|
+
- **Scenarios & disturbances** — water current, and on-demand sensor failures (DVL
|
|
59
|
+
dropout, noise, NaN) to probe estimator robustness — the exact failure modes that
|
|
60
|
+
bite in real wet tests.
|
|
61
|
+
- **Driven by your real controller** — it takes a `geometry_msgs/Wrench` with a deadman,
|
|
62
|
+
the same contract a PID/thruster-allocation stack already produces.
|
|
63
|
+
|
|
64
|
+
## Physics model
|
|
65
|
+
|
|
66
|
+
An independent **6-DOF Newton-Euler** rigid body (FLU body frame, ENU world,
|
|
67
|
+
quaternion orientation):
|
|
68
|
+
|
|
69
|
+
- **Buoyancy** `ρ·V·g`, applied at a **center of buoyancy above the center of mass**, so
|
|
70
|
+
the vehicle self-rights — giving realistic roll/pitch, and therefore realistic IMU
|
|
71
|
+
output. Default displacement is exactly neutral.
|
|
72
|
+
- **Linear + quadratic drag** on both linear and angular velocity.
|
|
73
|
+
- **Water current** — drag acts on velocity *relative to the water*, so a current
|
|
74
|
+
actually pushes a passive vehicle (modeled properly, not faked through gravity).
|
|
75
|
+
- **Wrench input** in the body frame, with a 0.5 s command-timeout deadman.
|
|
76
|
+
|
|
77
|
+
The accelerometer output is the true specific force `(ΣF − gravity)/m` in the body
|
|
78
|
+
frame, so a level, neutrally-buoyant vehicle reads `az ≈ +9.81` — matching a real
|
|
79
|
+
ENU/FLU IMU.
|
|
80
|
+
|
|
81
|
+
## Vehicle-agnostic
|
|
82
|
+
|
|
83
|
+
It is **not tied to any one vehicle.** Every topic name, frame, rate and physics
|
|
84
|
+
constant is a parameter, and the defaults are generic:
|
|
85
|
+
|
|
86
|
+
| published topic (default) | type | content |
|
|
87
|
+
|---------------------------|------|---------|
|
|
88
|
+
| `imu/data` | `sensor_msgs/Imu` | orientation, body angular velocity, specific-force accel |
|
|
89
|
+
| `dvl/odometry` | `nav_msgs/Odometry` | body-frame velocity |
|
|
90
|
+
| `dvl/altitude`, `ping/range` | `sensor_msgs/Range` | height above the floor |
|
|
91
|
+
| `pressure` | `sensor_msgs/FluidPressure` | from depth |
|
|
92
|
+
| `ground_truth` | `geometry_msgs/PoseStamped` | exact sim pose |
|
|
93
|
+
|
|
94
|
+
Adapt it to a specific vehicle with a **profile** — a small YAML that remaps the
|
|
95
|
+
topics/frames and sets the mass/buoyancy. `config/barracuda.yaml` is one example
|
|
96
|
+
(Barracuda's real `/barracuda/...` topics); write your own for any other AUV.
|
|
97
|
+
|
|
98
|
+
Profiles can also avoid hardcoded robot names by using placeholders in topic and
|
|
99
|
+
frame strings:
|
|
100
|
+
|
|
101
|
+
| placeholder | expands to |
|
|
102
|
+
|-------------|------------|
|
|
103
|
+
| `{topic_prefix}` | the normalized `topic_prefix` parameter, such as `/barracuda` |
|
|
104
|
+
| `{frame_prefix}` | the normalized `frame_prefix` parameter, such as `barracuda` |
|
|
105
|
+
|
|
106
|
+
For example, `wrench_topic: "{topic_prefix}/wrench"` becomes
|
|
107
|
+
`/barracuda/wrench` when `topic_prefix: /barracuda`. Leave both prefixes empty
|
|
108
|
+
for generic relative names.
|
|
109
|
+
|
|
110
|
+
Profiles can also tune the vehicle dynamics and scenario start state:
|
|
111
|
+
|
|
112
|
+
| parameter | meaning |
|
|
113
|
+
|-----------|---------|
|
|
114
|
+
| `inertia` | diagonal body inertia `[Ixx, Iyy, Izz]` |
|
|
115
|
+
| `cob_offset` | center of buoyancy relative to center of mass, body frame |
|
|
116
|
+
| `linear_drag`, `quadratic_drag` | per-axis translational drag coefficients |
|
|
117
|
+
| `angular_drag`, `angular_quadratic_drag` | per-axis rotational drag coefficients |
|
|
118
|
+
| `initial_position` | starting world position |
|
|
119
|
+
| `initial_orientation_wxyz` | starting orientation quaternion |
|
|
120
|
+
| `initial_linear_velocity`, `initial_angular_velocity` | starting body velocities |
|
|
121
|
+
|
|
122
|
+
## See it in 3D
|
|
123
|
+
|
|
124
|
+
There's no built-in render, but it broadcasts **TF** and a **vehicle marker**, so it
|
|
125
|
+
shows up live in **RViz2** — which runs on a Jetson (plain OpenGL, no RTX/Isaac):
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
ros2 launch auv_sim_bench sim.launch.py rviz:=true
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
You get a 3D box for the vehicle flying around under thrust/current/buoyancy, the TF
|
|
132
|
+
frames, and — if you set `estimate_topic` to your estimator's pose — a second marker
|
|
133
|
+
for the **estimate**, so you can watch estimate-vs-truth drift in real time. It's
|
|
134
|
+
schematic (boxes + axes), not photoreal; photoreal water is the one thing that needs
|
|
135
|
+
Isaac Sim on an x86 box.
|
|
136
|
+
|
|
137
|
+
## Failure & disturbance injection
|
|
138
|
+
|
|
139
|
+
For the real reason to simulate — breaking the estimator on purpose:
|
|
140
|
+
|
|
141
|
+
```yaml
|
|
142
|
+
current: [0.3, 0.0, 0.0] # steady cross-current
|
|
143
|
+
dvl_dropout_start_sec: 10.0 # DVL goes silent at t=10s ...
|
|
144
|
+
dvl_dropout_duration_sec: 5.0 # ... for 5s
|
|
145
|
+
dvl_noise_stddev: 0.02 # velocity noise
|
|
146
|
+
dvl_emit_nan: false # or feed the estimator NaNs
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## Build & run
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
cd ~/ros2_ws/src && git clone <this repo> auv_sim_bench
|
|
153
|
+
cd ~/ros2_ws && colcon build --packages-select auv_sim_bench
|
|
154
|
+
source install/setup.bash
|
|
155
|
+
|
|
156
|
+
ros2 launch auv_sim_bench sim.launch.py # generic defaults
|
|
157
|
+
ros2 launch auv_sim_bench sim.launch.py rviz:=true # + 3D view
|
|
158
|
+
ros2 launch auv_sim_bench sim.launch.py config:=barracuda.yaml # Barracuda profile
|
|
159
|
+
# drive it by publishing geometry_msgs/Wrench on the wrench topic (default cmd_wrench)
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
### On a Jetson (AGX Orin)
|
|
163
|
+
|
|
164
|
+
Validated on a Jetson AGX Orin (JetPack 6 / ROS 2 Humble) — pure Python + numpy, so
|
|
165
|
+
no GPU or Isaac needed:
|
|
166
|
+
|
|
167
|
+
```bash
|
|
168
|
+
mkdir -p ~/auv_ws/src
|
|
169
|
+
cp -r AUVSimBench ~/auv_ws/src/auv_sim_bench
|
|
170
|
+
cd ~/auv_ws && colcon build --packages-select auv_sim_bench
|
|
171
|
+
source /opt/ros/humble/setup.bash
|
|
172
|
+
source ~/auv_ws/install/setup.bash # both source lines, in every new terminal
|
|
173
|
+
ros2 launch auv_sim_bench sim.launch.py
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
Headless over SSH there's no display for RViz, so drop `rviz:=true` and watch the
|
|
177
|
+
data instead (`ros2 topic echo /ground_truth`); driving it with a `/cmd_wrench`
|
|
178
|
+
publisher moves the body as expected. For the 3D view, run RViz on a monitor
|
|
179
|
+
attached to the Jetson.
|
|
180
|
+
|
|
181
|
+
Test your estimator: run the sim + your estimator, then compare
|
|
182
|
+
`/barracuda/estimated_pose` against `/auv_sim/ground_truth` — an accuracy check you
|
|
183
|
+
cannot get from a real dive (no underwater ground truth) or from bag replay.
|
|
184
|
+
|
|
185
|
+
## Tests
|
|
186
|
+
|
|
187
|
+
The physics core (`dynamics.py`) is plain numpy with no ROS dependency, so it is
|
|
188
|
+
unit-tested directly — buoyancy, gravity, drag, thrust response, and current:
|
|
189
|
+
|
|
190
|
+
```bash
|
|
191
|
+
pytest test/
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
## Credits & scope
|
|
195
|
+
|
|
196
|
+
Inspired by Leonardo Lima's [`isaac_underwater`](https://github.com/leonlime/isaac_underwater)
|
|
197
|
+
examples (MIT), which show buoyancy and drag in Isaac Sim. AUVSimBench takes that
|
|
198
|
+
idea in a different direction — its own 6-DOF dynamics in a standalone ROS 2 node, so
|
|
199
|
+
it runs anywhere ROS 2 does, including a Jetson.
|
|
200
|
+
|
|
201
|
+
It's a controls/estimation test rig, not a hydrodynamics research tool: drag and
|
|
202
|
+
inertia are simple tunable coefficients, not CFD. Added-mass and richer current
|
|
203
|
+
fields are natural next steps.
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
# AUVSimBench
|
|
2
|
+
|
|
3
|
+
A lightweight **physics twin** for autonomous underwater vehicles — a 6-DOF AUV
|
|
4
|
+
simulator as a single ROS 2 node, in pure Python/numpy. It runs the vehicle's
|
|
5
|
+
physics and publishes the **sensor topics a state estimator consumes, plus
|
|
6
|
+
ground-truth pose**, so you can develop and stress-test an underwater localization
|
|
7
|
+
or control stack **on the bench** — no water, no GPU, no Isaac Sim. It runs happily
|
|
8
|
+
on a Jetson.
|
|
9
|
+
|
|
10
|
+
## Why this is hard (and why the twin helps)
|
|
11
|
+
|
|
12
|
+
Validating underwater state estimation is uniquely painful:
|
|
13
|
+
|
|
14
|
+
- **Wet tests are expensive and rare.** Pool/ocean time is slow to schedule, and you
|
|
15
|
+
get only a handful of runs before everyone's tired and the battery's flat.
|
|
16
|
+
- **There is no ground truth underwater.** No GPS below the surface — so even when you
|
|
17
|
+
*do* get a dive, you often can't measure *how wrong* the estimate was.
|
|
18
|
+
- **Most failures aren't about the water.** In practice the things that break a dive are
|
|
19
|
+
wiring (a node subscribed to the wrong topic), frame conventions (axes flipped),
|
|
20
|
+
startup ordering (a sensor not powered on), or an estimator diverging — and you
|
|
21
|
+
usually discover them *mid-dive*, burning a wet test to debug a typo.
|
|
22
|
+
- **Recorded bags only go so far.** A bag replays exactly one motion; you can't steer
|
|
23
|
+
it, you can't make a sensor fail on cue, and replayed DVL/depth won't agree with a
|
|
24
|
+
real IMU sitting still on the bench.
|
|
25
|
+
|
|
26
|
+
A **physics twin** answers all four. It simulates the vehicle's dynamics, so every
|
|
27
|
+
sensor stream is derived from *one consistent truth*; it *knows* the true pose, so
|
|
28
|
+
estimator accuracy is measurable; and you can script scenarios, disturbances, and
|
|
29
|
+
on-demand sensor failures — all from a laptop or the robot's own compute.
|
|
30
|
+
|
|
31
|
+
```
|
|
32
|
+
wrench in ──► 6-DOF dynamics (thrust + buoyancy + drag + current) ──► IMU / DVL / depth + ground truth
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## What it gives you
|
|
36
|
+
|
|
37
|
+
- **Consistent sensors** — IMU, DVL and depth all generated from the same simulated
|
|
38
|
+
body, so they physically agree (the thing bag-replay can't do).
|
|
39
|
+
- **Ground truth** — `/auv_sim/ground_truth` is the exact pose, so you can plot
|
|
40
|
+
estimator error directly.
|
|
41
|
+
- **Scenarios & disturbances** — water current, and on-demand sensor failures (DVL
|
|
42
|
+
dropout, noise, NaN) to probe estimator robustness — the exact failure modes that
|
|
43
|
+
bite in real wet tests.
|
|
44
|
+
- **Driven by your real controller** — it takes a `geometry_msgs/Wrench` with a deadman,
|
|
45
|
+
the same contract a PID/thruster-allocation stack already produces.
|
|
46
|
+
|
|
47
|
+
## Physics model
|
|
48
|
+
|
|
49
|
+
An independent **6-DOF Newton-Euler** rigid body (FLU body frame, ENU world,
|
|
50
|
+
quaternion orientation):
|
|
51
|
+
|
|
52
|
+
- **Buoyancy** `ρ·V·g`, applied at a **center of buoyancy above the center of mass**, so
|
|
53
|
+
the vehicle self-rights — giving realistic roll/pitch, and therefore realistic IMU
|
|
54
|
+
output. Default displacement is exactly neutral.
|
|
55
|
+
- **Linear + quadratic drag** on both linear and angular velocity.
|
|
56
|
+
- **Water current** — drag acts on velocity *relative to the water*, so a current
|
|
57
|
+
actually pushes a passive vehicle (modeled properly, not faked through gravity).
|
|
58
|
+
- **Wrench input** in the body frame, with a 0.5 s command-timeout deadman.
|
|
59
|
+
|
|
60
|
+
The accelerometer output is the true specific force `(ΣF − gravity)/m` in the body
|
|
61
|
+
frame, so a level, neutrally-buoyant vehicle reads `az ≈ +9.81` — matching a real
|
|
62
|
+
ENU/FLU IMU.
|
|
63
|
+
|
|
64
|
+
## Vehicle-agnostic
|
|
65
|
+
|
|
66
|
+
It is **not tied to any one vehicle.** Every topic name, frame, rate and physics
|
|
67
|
+
constant is a parameter, and the defaults are generic:
|
|
68
|
+
|
|
69
|
+
| published topic (default) | type | content |
|
|
70
|
+
|---------------------------|------|---------|
|
|
71
|
+
| `imu/data` | `sensor_msgs/Imu` | orientation, body angular velocity, specific-force accel |
|
|
72
|
+
| `dvl/odometry` | `nav_msgs/Odometry` | body-frame velocity |
|
|
73
|
+
| `dvl/altitude`, `ping/range` | `sensor_msgs/Range` | height above the floor |
|
|
74
|
+
| `pressure` | `sensor_msgs/FluidPressure` | from depth |
|
|
75
|
+
| `ground_truth` | `geometry_msgs/PoseStamped` | exact sim pose |
|
|
76
|
+
|
|
77
|
+
Adapt it to a specific vehicle with a **profile** — a small YAML that remaps the
|
|
78
|
+
topics/frames and sets the mass/buoyancy. `config/barracuda.yaml` is one example
|
|
79
|
+
(Barracuda's real `/barracuda/...` topics); write your own for any other AUV.
|
|
80
|
+
|
|
81
|
+
Profiles can also avoid hardcoded robot names by using placeholders in topic and
|
|
82
|
+
frame strings:
|
|
83
|
+
|
|
84
|
+
| placeholder | expands to |
|
|
85
|
+
|-------------|------------|
|
|
86
|
+
| `{topic_prefix}` | the normalized `topic_prefix` parameter, such as `/barracuda` |
|
|
87
|
+
| `{frame_prefix}` | the normalized `frame_prefix` parameter, such as `barracuda` |
|
|
88
|
+
|
|
89
|
+
For example, `wrench_topic: "{topic_prefix}/wrench"` becomes
|
|
90
|
+
`/barracuda/wrench` when `topic_prefix: /barracuda`. Leave both prefixes empty
|
|
91
|
+
for generic relative names.
|
|
92
|
+
|
|
93
|
+
Profiles can also tune the vehicle dynamics and scenario start state:
|
|
94
|
+
|
|
95
|
+
| parameter | meaning |
|
|
96
|
+
|-----------|---------|
|
|
97
|
+
| `inertia` | diagonal body inertia `[Ixx, Iyy, Izz]` |
|
|
98
|
+
| `cob_offset` | center of buoyancy relative to center of mass, body frame |
|
|
99
|
+
| `linear_drag`, `quadratic_drag` | per-axis translational drag coefficients |
|
|
100
|
+
| `angular_drag`, `angular_quadratic_drag` | per-axis rotational drag coefficients |
|
|
101
|
+
| `initial_position` | starting world position |
|
|
102
|
+
| `initial_orientation_wxyz` | starting orientation quaternion |
|
|
103
|
+
| `initial_linear_velocity`, `initial_angular_velocity` | starting body velocities |
|
|
104
|
+
|
|
105
|
+
## See it in 3D
|
|
106
|
+
|
|
107
|
+
There's no built-in render, but it broadcasts **TF** and a **vehicle marker**, so it
|
|
108
|
+
shows up live in **RViz2** — which runs on a Jetson (plain OpenGL, no RTX/Isaac):
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
ros2 launch auv_sim_bench sim.launch.py rviz:=true
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
You get a 3D box for the vehicle flying around under thrust/current/buoyancy, the TF
|
|
115
|
+
frames, and — if you set `estimate_topic` to your estimator's pose — a second marker
|
|
116
|
+
for the **estimate**, so you can watch estimate-vs-truth drift in real time. It's
|
|
117
|
+
schematic (boxes + axes), not photoreal; photoreal water is the one thing that needs
|
|
118
|
+
Isaac Sim on an x86 box.
|
|
119
|
+
|
|
120
|
+
## Failure & disturbance injection
|
|
121
|
+
|
|
122
|
+
For the real reason to simulate — breaking the estimator on purpose:
|
|
123
|
+
|
|
124
|
+
```yaml
|
|
125
|
+
current: [0.3, 0.0, 0.0] # steady cross-current
|
|
126
|
+
dvl_dropout_start_sec: 10.0 # DVL goes silent at t=10s ...
|
|
127
|
+
dvl_dropout_duration_sec: 5.0 # ... for 5s
|
|
128
|
+
dvl_noise_stddev: 0.02 # velocity noise
|
|
129
|
+
dvl_emit_nan: false # or feed the estimator NaNs
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## Build & run
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
cd ~/ros2_ws/src && git clone <this repo> auv_sim_bench
|
|
136
|
+
cd ~/ros2_ws && colcon build --packages-select auv_sim_bench
|
|
137
|
+
source install/setup.bash
|
|
138
|
+
|
|
139
|
+
ros2 launch auv_sim_bench sim.launch.py # generic defaults
|
|
140
|
+
ros2 launch auv_sim_bench sim.launch.py rviz:=true # + 3D view
|
|
141
|
+
ros2 launch auv_sim_bench sim.launch.py config:=barracuda.yaml # Barracuda profile
|
|
142
|
+
# drive it by publishing geometry_msgs/Wrench on the wrench topic (default cmd_wrench)
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### On a Jetson (AGX Orin)
|
|
146
|
+
|
|
147
|
+
Validated on a Jetson AGX Orin (JetPack 6 / ROS 2 Humble) — pure Python + numpy, so
|
|
148
|
+
no GPU or Isaac needed:
|
|
149
|
+
|
|
150
|
+
```bash
|
|
151
|
+
mkdir -p ~/auv_ws/src
|
|
152
|
+
cp -r AUVSimBench ~/auv_ws/src/auv_sim_bench
|
|
153
|
+
cd ~/auv_ws && colcon build --packages-select auv_sim_bench
|
|
154
|
+
source /opt/ros/humble/setup.bash
|
|
155
|
+
source ~/auv_ws/install/setup.bash # both source lines, in every new terminal
|
|
156
|
+
ros2 launch auv_sim_bench sim.launch.py
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
Headless over SSH there's no display for RViz, so drop `rviz:=true` and watch the
|
|
160
|
+
data instead (`ros2 topic echo /ground_truth`); driving it with a `/cmd_wrench`
|
|
161
|
+
publisher moves the body as expected. For the 3D view, run RViz on a monitor
|
|
162
|
+
attached to the Jetson.
|
|
163
|
+
|
|
164
|
+
Test your estimator: run the sim + your estimator, then compare
|
|
165
|
+
`/barracuda/estimated_pose` against `/auv_sim/ground_truth` — an accuracy check you
|
|
166
|
+
cannot get from a real dive (no underwater ground truth) or from bag replay.
|
|
167
|
+
|
|
168
|
+
## Tests
|
|
169
|
+
|
|
170
|
+
The physics core (`dynamics.py`) is plain numpy with no ROS dependency, so it is
|
|
171
|
+
unit-tested directly — buoyancy, gravity, drag, thrust response, and current:
|
|
172
|
+
|
|
173
|
+
```bash
|
|
174
|
+
pytest test/
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
## Credits & scope
|
|
178
|
+
|
|
179
|
+
Inspired by Leonardo Lima's [`isaac_underwater`](https://github.com/leonlime/isaac_underwater)
|
|
180
|
+
examples (MIT), which show buoyancy and drag in Isaac Sim. AUVSimBench takes that
|
|
181
|
+
idea in a different direction — its own 6-DOF dynamics in a standalone ROS 2 node, so
|
|
182
|
+
it runs anywhere ROS 2 does, including a Jetson.
|
|
183
|
+
|
|
184
|
+
It's a controls/estimation test rig, not a hydrodynamics research tool: drag and
|
|
185
|
+
inertia are simple tunable coefficients, not CFD. Added-mass and richer current
|
|
186
|
+
fields are natural next steps.
|
|
File without changes
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"""6-DOF rigid-body AUV dynamics (pure numpy, no ROS).
|
|
2
|
+
|
|
3
|
+
Body frame is FLU, world frame is ENU (z up). State is integrated in the body
|
|
4
|
+
frame with Newton-Euler; orientation as a wxyz quaternion.
|
|
5
|
+
"""
|
|
6
|
+
import numpy as np
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def quat_mul(a, b):
|
|
10
|
+
aw, ax, ay, az = a
|
|
11
|
+
bw, bx, by, bz = b
|
|
12
|
+
return np.array([
|
|
13
|
+
aw * bw - ax * bx - ay * by - az * bz,
|
|
14
|
+
aw * bx + ax * bw + ay * bz - az * by,
|
|
15
|
+
aw * by - ax * bz + ay * bw + az * bx,
|
|
16
|
+
aw * bz + ax * by - ay * bx + az * bw,
|
|
17
|
+
])
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def quat_normalize(q):
|
|
21
|
+
n = np.linalg.norm(q)
|
|
22
|
+
return q / n if n > 0 else np.array([1.0, 0.0, 0.0, 0.0])
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def quat_to_rot(q):
|
|
26
|
+
"""Body -> world rotation matrix for a wxyz quaternion."""
|
|
27
|
+
w, x, y, z = q
|
|
28
|
+
return np.array([
|
|
29
|
+
[1 - 2 * (y * y + z * z), 2 * (x * y - z * w), 2 * (x * z + y * w)],
|
|
30
|
+
[2 * (x * y + z * w), 1 - 2 * (x * x + z * z), 2 * (y * z - x * w)],
|
|
31
|
+
[2 * (x * z - y * w), 2 * (y * z + x * w), 1 - 2 * (x * x + y * y)],
|
|
32
|
+
])
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class AUVParams:
|
|
36
|
+
def __init__(self):
|
|
37
|
+
self.mass = 15.0 # kg
|
|
38
|
+
self.water_density = 1025.0 # kg/m^3
|
|
39
|
+
self.gravity = 9.81 # m/s^2
|
|
40
|
+
# displaced volume; rho*V*g should be ~ weight for neutral buoyancy
|
|
41
|
+
self.volume = (self.mass) / self.water_density # exactly neutral by default
|
|
42
|
+
self.inertia = np.array([0.5, 0.8, 0.8]) # diag Ixx,Iyy,Izz
|
|
43
|
+
# center of buoyancy above the center of mass (body frame) -> righting moment
|
|
44
|
+
self.cob_offset = np.array([0.0, 0.0, 0.02])
|
|
45
|
+
# drag: F = -(lin * v + quad * v*|v|), per body axis
|
|
46
|
+
self.lin_drag = np.array([20.0, 40.0, 40.0])
|
|
47
|
+
self.quad_drag = np.array([30.0, 80.0, 80.0])
|
|
48
|
+
self.ang_drag = np.array([2.0, 4.0, 4.0])
|
|
49
|
+
self.ang_quad_drag = np.array([1.0, 2.0, 2.0])
|
|
50
|
+
# water current in the world frame (m/s); drag acts on velocity
|
|
51
|
+
# relative to the water, so a current pushes a passive vehicle.
|
|
52
|
+
self.current_world = np.zeros(3)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class AUVState:
|
|
56
|
+
def __init__(self):
|
|
57
|
+
self.p = np.zeros(3) # world position (ENU)
|
|
58
|
+
self.q = np.array([1.0, 0.0, 0.0, 0.0]) # world orientation (wxyz)
|
|
59
|
+
self.v = np.zeros(3) # body-frame linear velocity
|
|
60
|
+
self.w = np.zeros(3) # body-frame angular velocity
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class AUVDynamics:
|
|
64
|
+
def __init__(self, params=None, state=None):
|
|
65
|
+
self.p = params or AUVParams()
|
|
66
|
+
self.s = state or AUVState()
|
|
67
|
+
self.specific_force = np.zeros(3) # what the accelerometer would read (body)
|
|
68
|
+
|
|
69
|
+
def step(self, dt, force_body, torque_body):
|
|
70
|
+
p, s = self.p, self.s
|
|
71
|
+
R = quat_to_rot(s.q)
|
|
72
|
+
Rt = R.T
|
|
73
|
+
|
|
74
|
+
g_body = Rt @ np.array([0.0, 0.0, -p.mass * p.gravity])
|
|
75
|
+
b_body = Rt @ np.array([0.0, 0.0, p.water_density * p.volume * p.gravity])
|
|
76
|
+
|
|
77
|
+
v_rel = s.v - Rt @ p.current_world # velocity relative to the water
|
|
78
|
+
drag_f = -(p.lin_drag * v_rel + p.quad_drag * v_rel * np.abs(v_rel))
|
|
79
|
+
drag_t = -(p.ang_drag * s.w + p.ang_quad_drag * s.w * np.abs(s.w))
|
|
80
|
+
|
|
81
|
+
F = np.asarray(force_body, float) + g_body + b_body + drag_f
|
|
82
|
+
T = np.asarray(torque_body, float) + np.cross(p.cob_offset, b_body) + drag_t
|
|
83
|
+
|
|
84
|
+
v_dot = F / p.mass - np.cross(s.w, s.v)
|
|
85
|
+
w_dot = (T - np.cross(s.w, p.inertia * s.w)) / p.inertia
|
|
86
|
+
|
|
87
|
+
s.v = s.v + v_dot * dt
|
|
88
|
+
s.w = s.w + w_dot * dt
|
|
89
|
+
s.q = quat_normalize(s.q + 0.5 * quat_mul(s.q, np.array([0.0, *s.w])) * dt)
|
|
90
|
+
s.p = s.p + R @ s.v * dt
|
|
91
|
+
|
|
92
|
+
# accelerometer specific force = (total - gravity) / m, in body frame
|
|
93
|
+
self.specific_force = (F - g_body) / p.mass
|
|
94
|
+
return s
|
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
import random
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
import rclpy
|
|
5
|
+
from rclpy.node import Node
|
|
6
|
+
from geometry_msgs.msg import PoseStamped, TransformStamped, Wrench
|
|
7
|
+
from nav_msgs.msg import Odometry
|
|
8
|
+
from sensor_msgs.msg import FluidPressure, Imu, Range
|
|
9
|
+
from visualization_msgs.msg import Marker
|
|
10
|
+
from tf2_ros import StaticTransformBroadcaster, TransformBroadcaster
|
|
11
|
+
|
|
12
|
+
from .dynamics import AUVDynamics, AUVParams
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class AUVSimNode(Node):
|
|
16
|
+
def __init__(self):
|
|
17
|
+
super().__init__("auv_sim_bench")
|
|
18
|
+
|
|
19
|
+
self.declare_parameter("sim_rate_hz", 200.0)
|
|
20
|
+
self.declare_parameter("imu_rate_hz", 100.0)
|
|
21
|
+
self.declare_parameter("dvl_rate_hz", 8.0)
|
|
22
|
+
self.declare_parameter("depth_rate_hz", 5.0)
|
|
23
|
+
|
|
24
|
+
# generic defaults; remap or load a profile (e.g. config/barracuda.yaml)
|
|
25
|
+
self.declare_parameter("topic_prefix", "")
|
|
26
|
+
self.declare_parameter("frame_prefix", "")
|
|
27
|
+
self.declare_parameter("wrench_topic", "cmd_wrench")
|
|
28
|
+
self.declare_parameter("imu_topic", "imu/data")
|
|
29
|
+
self.declare_parameter("dvl_odom_topic", "dvl/odometry")
|
|
30
|
+
self.declare_parameter("dvl_alt_topic", "dvl/altitude")
|
|
31
|
+
self.declare_parameter("ping_range_topic", "ping/range")
|
|
32
|
+
self.declare_parameter("pressure_topic", "pressure")
|
|
33
|
+
self.declare_parameter("truth_topic", "ground_truth")
|
|
34
|
+
self.declare_parameter("estimate_topic", "") # "" disables the estimate marker
|
|
35
|
+
|
|
36
|
+
self.declare_parameter("odom_frame", "odom")
|
|
37
|
+
self.declare_parameter("base_frame", "base_link")
|
|
38
|
+
self.declare_parameter("imu_frame", "imu_link")
|
|
39
|
+
self.declare_parameter("dvl_frame", "dvl_link")
|
|
40
|
+
self.declare_parameter("ping_frame", "ping_link")
|
|
41
|
+
|
|
42
|
+
self.declare_parameter("wrench_timeout_sec", 0.5)
|
|
43
|
+
self.declare_parameter("floor_z", -10.0)
|
|
44
|
+
self.declare_parameter("surface_z", 0.0)
|
|
45
|
+
self.declare_parameter("atm_pressure", 101325.0)
|
|
46
|
+
|
|
47
|
+
self.declare_parameter("mass", 15.0)
|
|
48
|
+
self.declare_parameter("volume", 0.0)
|
|
49
|
+
self.declare_parameter("water_density", 1025.0)
|
|
50
|
+
self.declare_parameter("current", [0.0, 0.0, 0.0])
|
|
51
|
+
self.declare_parameter("inertia", [0.5, 0.8, 0.8])
|
|
52
|
+
self.declare_parameter("cob_offset", [0.0, 0.0, 0.02])
|
|
53
|
+
self.declare_parameter("linear_drag", [20.0, 40.0, 40.0])
|
|
54
|
+
self.declare_parameter("quadratic_drag", [30.0, 80.0, 80.0])
|
|
55
|
+
self.declare_parameter("angular_drag", [2.0, 4.0, 4.0])
|
|
56
|
+
self.declare_parameter("angular_quadratic_drag", [1.0, 2.0, 2.0])
|
|
57
|
+
self.declare_parameter("initial_position", [0.0, 0.0, 0.0])
|
|
58
|
+
self.declare_parameter("initial_orientation_wxyz", [1.0, 0.0, 0.0, 0.0])
|
|
59
|
+
self.declare_parameter("initial_linear_velocity", [0.0, 0.0, 0.0])
|
|
60
|
+
self.declare_parameter("initial_angular_velocity", [0.0, 0.0, 0.0])
|
|
61
|
+
|
|
62
|
+
self.declare_parameter("publish_tf", True)
|
|
63
|
+
self.declare_parameter("publish_markers", True)
|
|
64
|
+
self.declare_parameter("body_size", [0.6, 0.35, 0.25])
|
|
65
|
+
|
|
66
|
+
self.declare_parameter("dvl_noise_stddev", 0.0)
|
|
67
|
+
self.declare_parameter("dvl_dropout_start_sec", -1.0)
|
|
68
|
+
self.declare_parameter("dvl_dropout_duration_sec", 0.0)
|
|
69
|
+
self.declare_parameter("dvl_emit_nan", False)
|
|
70
|
+
self.declare_parameter("seed", 0)
|
|
71
|
+
|
|
72
|
+
def param_value(name):
|
|
73
|
+
return self.get_parameter(name).value
|
|
74
|
+
|
|
75
|
+
def vec_param(name, size=3):
|
|
76
|
+
value = [float(x) for x in param_value(name)]
|
|
77
|
+
if len(value) != size:
|
|
78
|
+
raise ValueError(f"{name} must contain {size} values")
|
|
79
|
+
return value
|
|
80
|
+
|
|
81
|
+
def normalize_prefix(prefix):
|
|
82
|
+
prefix = str(prefix).strip().strip("/")
|
|
83
|
+
return f"/{prefix}" if prefix else ""
|
|
84
|
+
|
|
85
|
+
topic_prefix = normalize_prefix(param_value("topic_prefix"))
|
|
86
|
+
frame_prefix = str(param_value("frame_prefix")).strip().strip("/")
|
|
87
|
+
|
|
88
|
+
def expand_name(value):
|
|
89
|
+
return str(value).format(
|
|
90
|
+
topic_prefix=topic_prefix,
|
|
91
|
+
frame_prefix=frame_prefix,
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
self.odom_frame = expand_name(param_value("odom_frame"))
|
|
95
|
+
self.base_frame = expand_name(param_value("base_frame"))
|
|
96
|
+
self.imu_frame = expand_name(param_value("imu_frame"))
|
|
97
|
+
self.dvl_frame = expand_name(param_value("dvl_frame"))
|
|
98
|
+
self.ping_frame = expand_name(param_value("ping_frame"))
|
|
99
|
+
self.wrench_timeout = float(param_value("wrench_timeout_sec"))
|
|
100
|
+
self.floor_z = float(param_value("floor_z"))
|
|
101
|
+
self.surface_z = float(param_value("surface_z"))
|
|
102
|
+
self.atm = float(param_value("atm_pressure"))
|
|
103
|
+
self.publish_tf = bool(param_value("publish_tf"))
|
|
104
|
+
self.publish_markers = bool(param_value("publish_markers"))
|
|
105
|
+
self.body_size = [float(x) for x in param_value("body_size")]
|
|
106
|
+
self.dvl_noise = float(param_value("dvl_noise_stddev"))
|
|
107
|
+
self.dvl_drop_start = float(param_value("dvl_dropout_start_sec"))
|
|
108
|
+
self.dvl_drop_dur = float(param_value("dvl_dropout_duration_sec"))
|
|
109
|
+
self.dvl_nan = bool(param_value("dvl_emit_nan"))
|
|
110
|
+
self.rng = random.Random(int(param_value("seed")))
|
|
111
|
+
|
|
112
|
+
params = AUVParams()
|
|
113
|
+
params.mass = float(param_value("mass"))
|
|
114
|
+
params.water_density = float(param_value("water_density"))
|
|
115
|
+
vol = float(param_value("volume"))
|
|
116
|
+
params.volume = vol if vol > 0.0 else params.mass / params.water_density
|
|
117
|
+
params.current_world = np.array(vec_param("current"))
|
|
118
|
+
params.inertia = np.array(vec_param("inertia"))
|
|
119
|
+
params.cob_offset = np.array(vec_param("cob_offset"))
|
|
120
|
+
params.lin_drag = np.array(vec_param("linear_drag"))
|
|
121
|
+
params.quad_drag = np.array(vec_param("quadratic_drag"))
|
|
122
|
+
params.ang_drag = np.array(vec_param("angular_drag"))
|
|
123
|
+
params.ang_quad_drag = np.array(vec_param("angular_quadratic_drag"))
|
|
124
|
+
self.sim = AUVDynamics(params)
|
|
125
|
+
self.sim.s.p = np.array(vec_param("initial_position"))
|
|
126
|
+
self.sim.s.q = np.array(vec_param("initial_orientation_wxyz", 4))
|
|
127
|
+
self.sim.s.v = np.array(vec_param("initial_linear_velocity"))
|
|
128
|
+
self.sim.s.w = np.array(vec_param("initial_angular_velocity"))
|
|
129
|
+
|
|
130
|
+
self.force = np.zeros(3)
|
|
131
|
+
self.torque = np.zeros(3)
|
|
132
|
+
self.last_wrench_t = self.now()
|
|
133
|
+
self.estimate = None
|
|
134
|
+
|
|
135
|
+
self.create_subscription(Wrench, expand_name(param_value("wrench_topic")), self.on_wrench, 10)
|
|
136
|
+
self.imu_pub = self.create_publisher(Imu, expand_name(param_value("imu_topic")), 10)
|
|
137
|
+
self.dvl_pub = self.create_publisher(Odometry, expand_name(param_value("dvl_odom_topic")), 10)
|
|
138
|
+
self.alt_pub = self.create_publisher(Range, expand_name(param_value("dvl_alt_topic")), 10)
|
|
139
|
+
self.ping_pub = self.create_publisher(Range, expand_name(param_value("ping_range_topic")), 10)
|
|
140
|
+
self.press_pub = self.create_publisher(FluidPressure, expand_name(param_value("pressure_topic")), 10)
|
|
141
|
+
self.truth_pub = self.create_publisher(PoseStamped, expand_name(param_value("truth_topic")), 10)
|
|
142
|
+
|
|
143
|
+
if self.publish_tf:
|
|
144
|
+
self.tf_bc = TransformBroadcaster(self)
|
|
145
|
+
self.static_bc = StaticTransformBroadcaster(self)
|
|
146
|
+
self._publish_static_frames()
|
|
147
|
+
if self.publish_markers:
|
|
148
|
+
self.marker_pub = self.create_publisher(Marker, "auv_sim/markers", 10)
|
|
149
|
+
est = expand_name(param_value("estimate_topic"))
|
|
150
|
+
if est:
|
|
151
|
+
self.create_subscription(PoseStamped, est, self.on_estimate, 10)
|
|
152
|
+
|
|
153
|
+
self.sim_dt = 1.0 / float(param_value("sim_rate_hz"))
|
|
154
|
+
self.acc = {"imu": 0.0, "dvl": 0.0, "depth": 0.0}
|
|
155
|
+
self.rates = {
|
|
156
|
+
"imu": 1.0 / float(param_value("imu_rate_hz")),
|
|
157
|
+
"dvl": 1.0 / float(param_value("dvl_rate_hz")),
|
|
158
|
+
"depth": 1.0 / float(param_value("depth_rate_hz")),
|
|
159
|
+
}
|
|
160
|
+
self.elapsed = 0.0
|
|
161
|
+
self.create_timer(self.sim_dt, self.step)
|
|
162
|
+
self.get_logger().info(
|
|
163
|
+
f"AUV sim bench @ {param_value('sim_rate_hz')} Hz, neutral V={params.volume:.4f} m^3; "
|
|
164
|
+
f"rviz fixed frame = {self.odom_frame}"
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
def now(self):
|
|
168
|
+
return self.get_clock().now().nanoseconds * 1e-9
|
|
169
|
+
|
|
170
|
+
def on_wrench(self, msg):
|
|
171
|
+
self.force = np.array([msg.force.x, msg.force.y, msg.force.z])
|
|
172
|
+
self.torque = np.array([msg.torque.x, msg.torque.y, msg.torque.z])
|
|
173
|
+
self.last_wrench_t = self.now()
|
|
174
|
+
|
|
175
|
+
def on_estimate(self, msg):
|
|
176
|
+
self.estimate = msg
|
|
177
|
+
|
|
178
|
+
def step(self):
|
|
179
|
+
if self.now() - self.last_wrench_t > self.wrench_timeout:
|
|
180
|
+
self.force = np.zeros(3)
|
|
181
|
+
self.torque = np.zeros(3)
|
|
182
|
+
|
|
183
|
+
s = self.sim.step(self.sim_dt, self.force, self.torque)
|
|
184
|
+
self.elapsed += self.sim_dt
|
|
185
|
+
for k in self.acc:
|
|
186
|
+
self.acc[k] += self.sim_dt
|
|
187
|
+
stamp = self.get_clock().now().to_msg()
|
|
188
|
+
|
|
189
|
+
if self.acc["imu"] >= self.rates["imu"]:
|
|
190
|
+
self.acc["imu"] = 0.0
|
|
191
|
+
self.publish_imu(stamp, s)
|
|
192
|
+
self.publish_truth(stamp, s)
|
|
193
|
+
if self.acc["dvl"] >= self.rates["dvl"]:
|
|
194
|
+
self.acc["dvl"] = 0.0
|
|
195
|
+
self.publish_dvl(stamp, s)
|
|
196
|
+
if self.acc["depth"] >= self.rates["depth"]:
|
|
197
|
+
self.acc["depth"] = 0.0
|
|
198
|
+
self.publish_depth(stamp, s)
|
|
199
|
+
|
|
200
|
+
def publish_imu(self, stamp, s):
|
|
201
|
+
f = self.sim.specific_force
|
|
202
|
+
msg = Imu()
|
|
203
|
+
msg.header.stamp = stamp
|
|
204
|
+
msg.header.frame_id = self.imu_frame
|
|
205
|
+
msg.orientation.w, msg.orientation.x, msg.orientation.y, msg.orientation.z = s.q
|
|
206
|
+
msg.angular_velocity.x, msg.angular_velocity.y, msg.angular_velocity.z = s.w
|
|
207
|
+
msg.linear_acceleration.x, msg.linear_acceleration.y, msg.linear_acceleration.z = f
|
|
208
|
+
self.imu_pub.publish(msg)
|
|
209
|
+
|
|
210
|
+
def publish_dvl(self, stamp, s):
|
|
211
|
+
if self.dvl_drop_start >= 0.0 and (
|
|
212
|
+
self.dvl_drop_start <= self.elapsed < self.dvl_drop_start + self.dvl_drop_dur
|
|
213
|
+
):
|
|
214
|
+
return
|
|
215
|
+
msg = Odometry()
|
|
216
|
+
msg.header.stamp = stamp
|
|
217
|
+
msg.header.frame_id = self.odom_frame
|
|
218
|
+
msg.child_frame_id = self.dvl_frame
|
|
219
|
+
if self.dvl_nan:
|
|
220
|
+
vx = vy = vz = float("nan")
|
|
221
|
+
else:
|
|
222
|
+
def noise():
|
|
223
|
+
return self.rng.gauss(0.0, self.dvl_noise) if self.dvl_noise > 0 else 0.0
|
|
224
|
+
|
|
225
|
+
vx, vy, vz = s.v[0] + noise(), s.v[1] + noise(), s.v[2] + noise()
|
|
226
|
+
msg.twist.twist.linear.x = vx
|
|
227
|
+
msg.twist.twist.linear.y = vy
|
|
228
|
+
msg.twist.twist.linear.z = vz
|
|
229
|
+
self.dvl_pub.publish(msg)
|
|
230
|
+
|
|
231
|
+
def publish_depth(self, stamp, s):
|
|
232
|
+
altitude = float(s.p[2] - self.floor_z)
|
|
233
|
+
for pub, frame in ((self.alt_pub, self.dvl_frame), (self.ping_pub, self.ping_frame)):
|
|
234
|
+
r = Range()
|
|
235
|
+
r.header.stamp = stamp
|
|
236
|
+
r.header.frame_id = frame
|
|
237
|
+
r.radiation_type = Range.ULTRASOUND
|
|
238
|
+
r.min_range = 0.1
|
|
239
|
+
r.max_range = 100.0
|
|
240
|
+
r.range = altitude
|
|
241
|
+
pub.publish(r)
|
|
242
|
+
|
|
243
|
+
depth = max(0.0, self.surface_z - float(s.p[2]))
|
|
244
|
+
pmsg = FluidPressure()
|
|
245
|
+
pmsg.header.stamp = stamp
|
|
246
|
+
pmsg.header.frame_id = self.imu_frame
|
|
247
|
+
pmsg.fluid_pressure = self.atm + self.sim.p.water_density * self.sim.p.gravity * depth
|
|
248
|
+
self.press_pub.publish(pmsg)
|
|
249
|
+
|
|
250
|
+
def publish_truth(self, stamp, s):
|
|
251
|
+
msg = PoseStamped()
|
|
252
|
+
msg.header.stamp = stamp
|
|
253
|
+
msg.header.frame_id = self.odom_frame
|
|
254
|
+
msg.pose.position.x, msg.pose.position.y, msg.pose.position.z = s.p
|
|
255
|
+
msg.pose.orientation.w, msg.pose.orientation.x, msg.pose.orientation.y, msg.pose.orientation.z = s.q
|
|
256
|
+
self.truth_pub.publish(msg)
|
|
257
|
+
|
|
258
|
+
if self.publish_tf:
|
|
259
|
+
tf = TransformStamped()
|
|
260
|
+
tf.header.stamp = stamp
|
|
261
|
+
tf.header.frame_id = self.odom_frame
|
|
262
|
+
tf.child_frame_id = self.base_frame
|
|
263
|
+
tf.transform.translation.x, tf.transform.translation.y, tf.transform.translation.z = s.p
|
|
264
|
+
tf.transform.rotation.w, tf.transform.rotation.x, tf.transform.rotation.y, tf.transform.rotation.z = s.q
|
|
265
|
+
self.tf_bc.sendTransform(tf)
|
|
266
|
+
|
|
267
|
+
if self.publish_markers:
|
|
268
|
+
self._publish_body_marker(stamp)
|
|
269
|
+
if self.estimate is not None:
|
|
270
|
+
self._publish_estimate_marker(stamp)
|
|
271
|
+
|
|
272
|
+
def _publish_static_frames(self):
|
|
273
|
+
tfs = []
|
|
274
|
+
for child in (self.imu_frame, self.dvl_frame, self.ping_frame):
|
|
275
|
+
tf = TransformStamped()
|
|
276
|
+
tf.header.stamp = self.get_clock().now().to_msg()
|
|
277
|
+
tf.header.frame_id = self.base_frame
|
|
278
|
+
tf.child_frame_id = child
|
|
279
|
+
tf.transform.rotation.w = 1.0
|
|
280
|
+
tfs.append(tf)
|
|
281
|
+
self.static_bc.sendTransform(tfs)
|
|
282
|
+
|
|
283
|
+
def _publish_body_marker(self, stamp):
|
|
284
|
+
m = Marker()
|
|
285
|
+
m.header.stamp = stamp
|
|
286
|
+
m.header.frame_id = self.base_frame
|
|
287
|
+
m.ns = "auv"
|
|
288
|
+
m.id = 0
|
|
289
|
+
m.type = Marker.CUBE
|
|
290
|
+
m.action = Marker.ADD
|
|
291
|
+
m.pose.orientation.w = 1.0
|
|
292
|
+
m.scale.x, m.scale.y, m.scale.z = self.body_size
|
|
293
|
+
m.color.r, m.color.g, m.color.b, m.color.a = 0.2, 0.5, 1.0, 0.85
|
|
294
|
+
self.marker_pub.publish(m)
|
|
295
|
+
|
|
296
|
+
def _publish_estimate_marker(self, stamp):
|
|
297
|
+
m = Marker()
|
|
298
|
+
m.header.stamp = stamp
|
|
299
|
+
m.header.frame_id = self.odom_frame
|
|
300
|
+
m.ns = "estimate"
|
|
301
|
+
m.id = 1
|
|
302
|
+
m.type = Marker.SPHERE
|
|
303
|
+
m.action = Marker.ADD
|
|
304
|
+
m.pose = self.estimate.pose
|
|
305
|
+
m.scale.x = m.scale.y = m.scale.z = 0.25
|
|
306
|
+
m.color.r, m.color.g, m.color.b, m.color.a = 0.2, 1.0, 0.3, 0.9
|
|
307
|
+
self.marker_pub.publish(m)
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def main():
|
|
311
|
+
rclpy.init()
|
|
312
|
+
node = AUVSimNode()
|
|
313
|
+
try:
|
|
314
|
+
rclpy.spin(node)
|
|
315
|
+
except KeyboardInterrupt:
|
|
316
|
+
pass
|
|
317
|
+
node.destroy_node()
|
|
318
|
+
rclpy.shutdown()
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
if __name__ == "__main__":
|
|
322
|
+
main()
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: auv_sim_bench
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Lightweight 6-DOF AUV physics sim for dry-testing state estimation.
|
|
5
|
+
Maintainer: wuisabel-gif
|
|
6
|
+
Maintainer-email: 231155141+wuisabel-gif@users.noreply.github.com
|
|
7
|
+
License: MIT
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
Requires-Dist: setuptools
|
|
10
|
+
Dynamic: description
|
|
11
|
+
Dynamic: description-content-type
|
|
12
|
+
Dynamic: license
|
|
13
|
+
Dynamic: maintainer
|
|
14
|
+
Dynamic: maintainer-email
|
|
15
|
+
Dynamic: requires-dist
|
|
16
|
+
Dynamic: summary
|
|
17
|
+
|
|
18
|
+
# AUVSimBench
|
|
19
|
+
|
|
20
|
+
A lightweight **physics twin** for autonomous underwater vehicles — a 6-DOF AUV
|
|
21
|
+
simulator as a single ROS 2 node, in pure Python/numpy. It runs the vehicle's
|
|
22
|
+
physics and publishes the **sensor topics a state estimator consumes, plus
|
|
23
|
+
ground-truth pose**, so you can develop and stress-test an underwater localization
|
|
24
|
+
or control stack **on the bench** — no water, no GPU, no Isaac Sim. It runs happily
|
|
25
|
+
on a Jetson.
|
|
26
|
+
|
|
27
|
+
## Why this is hard (and why the twin helps)
|
|
28
|
+
|
|
29
|
+
Validating underwater state estimation is uniquely painful:
|
|
30
|
+
|
|
31
|
+
- **Wet tests are expensive and rare.** Pool/ocean time is slow to schedule, and you
|
|
32
|
+
get only a handful of runs before everyone's tired and the battery's flat.
|
|
33
|
+
- **There is no ground truth underwater.** No GPS below the surface — so even when you
|
|
34
|
+
*do* get a dive, you often can't measure *how wrong* the estimate was.
|
|
35
|
+
- **Most failures aren't about the water.** In practice the things that break a dive are
|
|
36
|
+
wiring (a node subscribed to the wrong topic), frame conventions (axes flipped),
|
|
37
|
+
startup ordering (a sensor not powered on), or an estimator diverging — and you
|
|
38
|
+
usually discover them *mid-dive*, burning a wet test to debug a typo.
|
|
39
|
+
- **Recorded bags only go so far.** A bag replays exactly one motion; you can't steer
|
|
40
|
+
it, you can't make a sensor fail on cue, and replayed DVL/depth won't agree with a
|
|
41
|
+
real IMU sitting still on the bench.
|
|
42
|
+
|
|
43
|
+
A **physics twin** answers all four. It simulates the vehicle's dynamics, so every
|
|
44
|
+
sensor stream is derived from *one consistent truth*; it *knows* the true pose, so
|
|
45
|
+
estimator accuracy is measurable; and you can script scenarios, disturbances, and
|
|
46
|
+
on-demand sensor failures — all from a laptop or the robot's own compute.
|
|
47
|
+
|
|
48
|
+
```
|
|
49
|
+
wrench in ──► 6-DOF dynamics (thrust + buoyancy + drag + current) ──► IMU / DVL / depth + ground truth
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## What it gives you
|
|
53
|
+
|
|
54
|
+
- **Consistent sensors** — IMU, DVL and depth all generated from the same simulated
|
|
55
|
+
body, so they physically agree (the thing bag-replay can't do).
|
|
56
|
+
- **Ground truth** — `/auv_sim/ground_truth` is the exact pose, so you can plot
|
|
57
|
+
estimator error directly.
|
|
58
|
+
- **Scenarios & disturbances** — water current, and on-demand sensor failures (DVL
|
|
59
|
+
dropout, noise, NaN) to probe estimator robustness — the exact failure modes that
|
|
60
|
+
bite in real wet tests.
|
|
61
|
+
- **Driven by your real controller** — it takes a `geometry_msgs/Wrench` with a deadman,
|
|
62
|
+
the same contract a PID/thruster-allocation stack already produces.
|
|
63
|
+
|
|
64
|
+
## Physics model
|
|
65
|
+
|
|
66
|
+
An independent **6-DOF Newton-Euler** rigid body (FLU body frame, ENU world,
|
|
67
|
+
quaternion orientation):
|
|
68
|
+
|
|
69
|
+
- **Buoyancy** `ρ·V·g`, applied at a **center of buoyancy above the center of mass**, so
|
|
70
|
+
the vehicle self-rights — giving realistic roll/pitch, and therefore realistic IMU
|
|
71
|
+
output. Default displacement is exactly neutral.
|
|
72
|
+
- **Linear + quadratic drag** on both linear and angular velocity.
|
|
73
|
+
- **Water current** — drag acts on velocity *relative to the water*, so a current
|
|
74
|
+
actually pushes a passive vehicle (modeled properly, not faked through gravity).
|
|
75
|
+
- **Wrench input** in the body frame, with a 0.5 s command-timeout deadman.
|
|
76
|
+
|
|
77
|
+
The accelerometer output is the true specific force `(ΣF − gravity)/m` in the body
|
|
78
|
+
frame, so a level, neutrally-buoyant vehicle reads `az ≈ +9.81` — matching a real
|
|
79
|
+
ENU/FLU IMU.
|
|
80
|
+
|
|
81
|
+
## Vehicle-agnostic
|
|
82
|
+
|
|
83
|
+
It is **not tied to any one vehicle.** Every topic name, frame, rate and physics
|
|
84
|
+
constant is a parameter, and the defaults are generic:
|
|
85
|
+
|
|
86
|
+
| published topic (default) | type | content |
|
|
87
|
+
|---------------------------|------|---------|
|
|
88
|
+
| `imu/data` | `sensor_msgs/Imu` | orientation, body angular velocity, specific-force accel |
|
|
89
|
+
| `dvl/odometry` | `nav_msgs/Odometry` | body-frame velocity |
|
|
90
|
+
| `dvl/altitude`, `ping/range` | `sensor_msgs/Range` | height above the floor |
|
|
91
|
+
| `pressure` | `sensor_msgs/FluidPressure` | from depth |
|
|
92
|
+
| `ground_truth` | `geometry_msgs/PoseStamped` | exact sim pose |
|
|
93
|
+
|
|
94
|
+
Adapt it to a specific vehicle with a **profile** — a small YAML that remaps the
|
|
95
|
+
topics/frames and sets the mass/buoyancy. `config/barracuda.yaml` is one example
|
|
96
|
+
(Barracuda's real `/barracuda/...` topics); write your own for any other AUV.
|
|
97
|
+
|
|
98
|
+
Profiles can also avoid hardcoded robot names by using placeholders in topic and
|
|
99
|
+
frame strings:
|
|
100
|
+
|
|
101
|
+
| placeholder | expands to |
|
|
102
|
+
|-------------|------------|
|
|
103
|
+
| `{topic_prefix}` | the normalized `topic_prefix` parameter, such as `/barracuda` |
|
|
104
|
+
| `{frame_prefix}` | the normalized `frame_prefix` parameter, such as `barracuda` |
|
|
105
|
+
|
|
106
|
+
For example, `wrench_topic: "{topic_prefix}/wrench"` becomes
|
|
107
|
+
`/barracuda/wrench` when `topic_prefix: /barracuda`. Leave both prefixes empty
|
|
108
|
+
for generic relative names.
|
|
109
|
+
|
|
110
|
+
Profiles can also tune the vehicle dynamics and scenario start state:
|
|
111
|
+
|
|
112
|
+
| parameter | meaning |
|
|
113
|
+
|-----------|---------|
|
|
114
|
+
| `inertia` | diagonal body inertia `[Ixx, Iyy, Izz]` |
|
|
115
|
+
| `cob_offset` | center of buoyancy relative to center of mass, body frame |
|
|
116
|
+
| `linear_drag`, `quadratic_drag` | per-axis translational drag coefficients |
|
|
117
|
+
| `angular_drag`, `angular_quadratic_drag` | per-axis rotational drag coefficients |
|
|
118
|
+
| `initial_position` | starting world position |
|
|
119
|
+
| `initial_orientation_wxyz` | starting orientation quaternion |
|
|
120
|
+
| `initial_linear_velocity`, `initial_angular_velocity` | starting body velocities |
|
|
121
|
+
|
|
122
|
+
## See it in 3D
|
|
123
|
+
|
|
124
|
+
There's no built-in render, but it broadcasts **TF** and a **vehicle marker**, so it
|
|
125
|
+
shows up live in **RViz2** — which runs on a Jetson (plain OpenGL, no RTX/Isaac):
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
ros2 launch auv_sim_bench sim.launch.py rviz:=true
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
You get a 3D box for the vehicle flying around under thrust/current/buoyancy, the TF
|
|
132
|
+
frames, and — if you set `estimate_topic` to your estimator's pose — a second marker
|
|
133
|
+
for the **estimate**, so you can watch estimate-vs-truth drift in real time. It's
|
|
134
|
+
schematic (boxes + axes), not photoreal; photoreal water is the one thing that needs
|
|
135
|
+
Isaac Sim on an x86 box.
|
|
136
|
+
|
|
137
|
+
## Failure & disturbance injection
|
|
138
|
+
|
|
139
|
+
For the real reason to simulate — breaking the estimator on purpose:
|
|
140
|
+
|
|
141
|
+
```yaml
|
|
142
|
+
current: [0.3, 0.0, 0.0] # steady cross-current
|
|
143
|
+
dvl_dropout_start_sec: 10.0 # DVL goes silent at t=10s ...
|
|
144
|
+
dvl_dropout_duration_sec: 5.0 # ... for 5s
|
|
145
|
+
dvl_noise_stddev: 0.02 # velocity noise
|
|
146
|
+
dvl_emit_nan: false # or feed the estimator NaNs
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## Build & run
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
cd ~/ros2_ws/src && git clone <this repo> auv_sim_bench
|
|
153
|
+
cd ~/ros2_ws && colcon build --packages-select auv_sim_bench
|
|
154
|
+
source install/setup.bash
|
|
155
|
+
|
|
156
|
+
ros2 launch auv_sim_bench sim.launch.py # generic defaults
|
|
157
|
+
ros2 launch auv_sim_bench sim.launch.py rviz:=true # + 3D view
|
|
158
|
+
ros2 launch auv_sim_bench sim.launch.py config:=barracuda.yaml # Barracuda profile
|
|
159
|
+
# drive it by publishing geometry_msgs/Wrench on the wrench topic (default cmd_wrench)
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
### On a Jetson (AGX Orin)
|
|
163
|
+
|
|
164
|
+
Validated on a Jetson AGX Orin (JetPack 6 / ROS 2 Humble) — pure Python + numpy, so
|
|
165
|
+
no GPU or Isaac needed:
|
|
166
|
+
|
|
167
|
+
```bash
|
|
168
|
+
mkdir -p ~/auv_ws/src
|
|
169
|
+
cp -r AUVSimBench ~/auv_ws/src/auv_sim_bench
|
|
170
|
+
cd ~/auv_ws && colcon build --packages-select auv_sim_bench
|
|
171
|
+
source /opt/ros/humble/setup.bash
|
|
172
|
+
source ~/auv_ws/install/setup.bash # both source lines, in every new terminal
|
|
173
|
+
ros2 launch auv_sim_bench sim.launch.py
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
Headless over SSH there's no display for RViz, so drop `rviz:=true` and watch the
|
|
177
|
+
data instead (`ros2 topic echo /ground_truth`); driving it with a `/cmd_wrench`
|
|
178
|
+
publisher moves the body as expected. For the 3D view, run RViz on a monitor
|
|
179
|
+
attached to the Jetson.
|
|
180
|
+
|
|
181
|
+
Test your estimator: run the sim + your estimator, then compare
|
|
182
|
+
`/barracuda/estimated_pose` against `/auv_sim/ground_truth` — an accuracy check you
|
|
183
|
+
cannot get from a real dive (no underwater ground truth) or from bag replay.
|
|
184
|
+
|
|
185
|
+
## Tests
|
|
186
|
+
|
|
187
|
+
The physics core (`dynamics.py`) is plain numpy with no ROS dependency, so it is
|
|
188
|
+
unit-tested directly — buoyancy, gravity, drag, thrust response, and current:
|
|
189
|
+
|
|
190
|
+
```bash
|
|
191
|
+
pytest test/
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
## Credits & scope
|
|
195
|
+
|
|
196
|
+
Inspired by Leonardo Lima's [`isaac_underwater`](https://github.com/leonlime/isaac_underwater)
|
|
197
|
+
examples (MIT), which show buoyancy and drag in Isaac Sim. AUVSimBench takes that
|
|
198
|
+
idea in a different direction — its own 6-DOF dynamics in a standalone ROS 2 node, so
|
|
199
|
+
it runs anywhere ROS 2 does, including a Jetson.
|
|
200
|
+
|
|
201
|
+
It's a controls/estimation test rig, not a hydrodynamics research tool: drag and
|
|
202
|
+
inertia are simple tunable coefficients, not CFD. Added-mass and richer current
|
|
203
|
+
fields are natural next steps.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
package.xml
|
|
3
|
+
setup.cfg
|
|
4
|
+
setup.py
|
|
5
|
+
auv_sim_bench/__init__.py
|
|
6
|
+
auv_sim_bench/dynamics.py
|
|
7
|
+
auv_sim_bench/sim_node.py
|
|
8
|
+
auv_sim_bench.egg-info/PKG-INFO
|
|
9
|
+
auv_sim_bench.egg-info/SOURCES.txt
|
|
10
|
+
auv_sim_bench.egg-info/dependency_links.txt
|
|
11
|
+
auv_sim_bench.egg-info/entry_points.txt
|
|
12
|
+
auv_sim_bench.egg-info/requires.txt
|
|
13
|
+
auv_sim_bench.egg-info/top_level.txt
|
|
14
|
+
auv_sim_bench.egg-info/zip-safe
|
|
15
|
+
config/barracuda.yaml
|
|
16
|
+
config/sim.yaml
|
|
17
|
+
launch/sim.launch.py
|
|
18
|
+
resource/auv_sim_bench
|
|
19
|
+
rviz/auv_sim.rviz
|
|
20
|
+
test/test_dynamics.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
setuptools
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
auv_sim_bench
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Barracuda profile: maps the generic sim onto Barracuda's real topic + frame names.
|
|
2
|
+
# Run with: ros2 launch auv_sim_bench sim.launch.py config:=barracuda.yaml
|
|
3
|
+
auv_sim_bench:
|
|
4
|
+
ros__parameters:
|
|
5
|
+
wrench_topic: /barracuda/wrench
|
|
6
|
+
imu_topic: /barracuda/vectornav/imu
|
|
7
|
+
dvl_odom_topic: /barracuda/dvl/odometry
|
|
8
|
+
dvl_alt_topic: /barracuda/dvl/altitude
|
|
9
|
+
ping_range_topic: /barracuda/ping1d/range
|
|
10
|
+
pressure_topic: /barracuda/vectornav/pressure
|
|
11
|
+
truth_topic: /auv_sim/ground_truth
|
|
12
|
+
estimate_topic: /barracuda/estimated_pose
|
|
13
|
+
|
|
14
|
+
odom_frame: odom
|
|
15
|
+
base_frame: barracuda/base_link
|
|
16
|
+
imu_frame: barracuda/imu_link
|
|
17
|
+
dvl_frame: barracuda/dvl_link
|
|
18
|
+
ping_frame: barracuda/ping1d_link
|
|
19
|
+
|
|
20
|
+
mass: 15.0
|
|
21
|
+
water_density: 1025.0
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
auv_sim_bench:
|
|
2
|
+
ros__parameters:
|
|
3
|
+
sim_rate_hz: 200.0
|
|
4
|
+
imu_rate_hz: 100.0
|
|
5
|
+
dvl_rate_hz: 8.0
|
|
6
|
+
depth_rate_hz: 5.0
|
|
7
|
+
|
|
8
|
+
# generic topic names (load config/barracuda.yaml for the Barracuda profile)
|
|
9
|
+
topic_prefix: ""
|
|
10
|
+
frame_prefix: ""
|
|
11
|
+
wrench_topic: cmd_wrench
|
|
12
|
+
imu_topic: imu/data
|
|
13
|
+
dvl_odom_topic: dvl/odometry
|
|
14
|
+
dvl_alt_topic: dvl/altitude
|
|
15
|
+
ping_range_topic: ping/range
|
|
16
|
+
pressure_topic: pressure
|
|
17
|
+
truth_topic: ground_truth
|
|
18
|
+
estimate_topic: "" # set to your estimator's pose topic to show it in rviz
|
|
19
|
+
|
|
20
|
+
odom_frame: odom
|
|
21
|
+
base_frame: base_link
|
|
22
|
+
imu_frame: imu_link
|
|
23
|
+
dvl_frame: dvl_link
|
|
24
|
+
ping_frame: ping_link
|
|
25
|
+
|
|
26
|
+
wrench_timeout_sec: 0.5
|
|
27
|
+
floor_z: -10.0
|
|
28
|
+
surface_z: 0.0
|
|
29
|
+
atm_pressure: 101325.0
|
|
30
|
+
|
|
31
|
+
mass: 15.0
|
|
32
|
+
volume: 0.0 # 0 -> neutral buoyancy (mass / water_density)
|
|
33
|
+
water_density: 1025.0
|
|
34
|
+
current: [0.0, 0.0, 0.0] # world-frame water current (m/s)
|
|
35
|
+
inertia: [0.5, 0.8, 0.8] # diagonal Ixx,Iyy,Izz (kg*m^2)
|
|
36
|
+
cob_offset: [0.0, 0.0, 0.02] # center of buoyancy relative to COM, body frame (m)
|
|
37
|
+
linear_drag: [20.0, 40.0, 40.0]
|
|
38
|
+
quadratic_drag: [30.0, 80.0, 80.0]
|
|
39
|
+
angular_drag: [2.0, 4.0, 4.0]
|
|
40
|
+
angular_quadratic_drag: [1.0, 2.0, 2.0]
|
|
41
|
+
|
|
42
|
+
initial_position: [0.0, 0.0, 0.0]
|
|
43
|
+
initial_orientation_wxyz: [1.0, 0.0, 0.0, 0.0]
|
|
44
|
+
initial_linear_velocity: [0.0, 0.0, 0.0]
|
|
45
|
+
initial_angular_velocity: [0.0, 0.0, 0.0]
|
|
46
|
+
|
|
47
|
+
publish_tf: true
|
|
48
|
+
publish_markers: true
|
|
49
|
+
body_size: [0.6, 0.35, 0.25] # rviz box dimensions (m)
|
|
50
|
+
|
|
51
|
+
# failure injection (off by default)
|
|
52
|
+
dvl_noise_stddev: 0.0
|
|
53
|
+
dvl_dropout_start_sec: -1.0
|
|
54
|
+
dvl_dropout_duration_sec: 0.0
|
|
55
|
+
dvl_emit_nan: false
|
|
56
|
+
seed: 0
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
from ament_index_python.packages import get_package_share_directory
|
|
4
|
+
from launch import LaunchDescription
|
|
5
|
+
from launch.actions import DeclareLaunchArgument
|
|
6
|
+
from launch.conditions import IfCondition
|
|
7
|
+
from launch.substitutions import LaunchConfiguration, PathJoinSubstitution
|
|
8
|
+
from launch_ros.actions import Node
|
|
9
|
+
from launch_ros.substitutions import FindPackageShare
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def generate_launch_description():
|
|
13
|
+
share = get_package_share_directory("auv_sim_bench")
|
|
14
|
+
config_arg = DeclareLaunchArgument("config", default_value="sim.yaml")
|
|
15
|
+
rviz_arg = DeclareLaunchArgument("rviz", default_value="false")
|
|
16
|
+
|
|
17
|
+
config = PathJoinSubstitution([share, "config", LaunchConfiguration("config")])
|
|
18
|
+
rviz_cfg = PathJoinSubstitution(
|
|
19
|
+
[FindPackageShare("auv_sim_bench"), "rviz", "auv_sim.rviz"]
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
return LaunchDescription([
|
|
23
|
+
config_arg,
|
|
24
|
+
rviz_arg,
|
|
25
|
+
Node(
|
|
26
|
+
package="auv_sim_bench",
|
|
27
|
+
executable="sim",
|
|
28
|
+
name="auv_sim_bench",
|
|
29
|
+
parameters=[config],
|
|
30
|
+
output="screen",
|
|
31
|
+
),
|
|
32
|
+
Node(
|
|
33
|
+
package="rviz2",
|
|
34
|
+
executable="rviz2",
|
|
35
|
+
name="rviz2",
|
|
36
|
+
arguments=["-d", rviz_cfg],
|
|
37
|
+
condition=IfCondition(LaunchConfiguration("rviz")),
|
|
38
|
+
output="screen",
|
|
39
|
+
),
|
|
40
|
+
])
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
<?xml version="1.0"?>
|
|
2
|
+
<?xml-model href="http://download.ros.org/schema/package_format3.xsd" schematypens="http://www.w3.org/2001/XMLSchema"?>
|
|
3
|
+
<package format="3">
|
|
4
|
+
<name>auv_sim_bench</name>
|
|
5
|
+
<version>0.1.0</version>
|
|
6
|
+
<description>Lightweight 6-DOF AUV physics simulator that publishes consistent IMU, DVL, depth and ground-truth topics for dry-testing underwater state estimation. Pure ROS 2 / numpy, runs on a Jetson.</description>
|
|
7
|
+
<maintainer email="231155141+wuisabel-gif@users.noreply.github.com">wuisabel-gif</maintainer>
|
|
8
|
+
<license>MIT</license>
|
|
9
|
+
|
|
10
|
+
<exec_depend>rclpy</exec_depend>
|
|
11
|
+
<exec_depend>geometry_msgs</exec_depend>
|
|
12
|
+
<exec_depend>nav_msgs</exec_depend>
|
|
13
|
+
<exec_depend>sensor_msgs</exec_depend>
|
|
14
|
+
<exec_depend>visualization_msgs</exec_depend>
|
|
15
|
+
<exec_depend>tf2_ros</exec_depend>
|
|
16
|
+
<exec_depend>python3-numpy</exec_depend>
|
|
17
|
+
|
|
18
|
+
<test_depend>python3-pytest</test_depend>
|
|
19
|
+
|
|
20
|
+
<export>
|
|
21
|
+
<build_type>ament_python</build_type>
|
|
22
|
+
</export>
|
|
23
|
+
</package>
|
|
File without changes
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
Panels:
|
|
2
|
+
- Class: rviz_common/Displays
|
|
3
|
+
Name: Displays
|
|
4
|
+
Visualization Manager:
|
|
5
|
+
Global Options:
|
|
6
|
+
Fixed Frame: odom
|
|
7
|
+
Background Color: 20; 30; 40
|
|
8
|
+
Displays:
|
|
9
|
+
- Class: rviz_default_plugins/Grid
|
|
10
|
+
Name: Grid
|
|
11
|
+
Enabled: true
|
|
12
|
+
Plane: XY
|
|
13
|
+
Cell Size: 1
|
|
14
|
+
Plane Cell Count: 20
|
|
15
|
+
- Class: rviz_default_plugins/TF
|
|
16
|
+
Name: TF
|
|
17
|
+
Enabled: true
|
|
18
|
+
Show Names: true
|
|
19
|
+
Show Axes: true
|
|
20
|
+
Marker Scale: 1
|
|
21
|
+
- Class: rviz_default_plugins/Marker
|
|
22
|
+
Name: Vehicle
|
|
23
|
+
Enabled: true
|
|
24
|
+
Topic:
|
|
25
|
+
Value: /auv_sim/markers
|
|
26
|
+
Depth: 5
|
|
27
|
+
Reliability Policy: Reliable
|
|
28
|
+
- Class: rviz_default_plugins/Odometry
|
|
29
|
+
Name: DVL odometry
|
|
30
|
+
Enabled: false
|
|
31
|
+
Topic:
|
|
32
|
+
Value: /dvl/odometry
|
|
33
|
+
Tools:
|
|
34
|
+
- Class: rviz_default_plugins/MoveCamera
|
|
35
|
+
- Class: rviz_default_plugins/FocusCamera
|
|
36
|
+
Views:
|
|
37
|
+
Current:
|
|
38
|
+
Class: rviz_default_plugins/Orbit
|
|
39
|
+
Name: Current View
|
|
40
|
+
Distance: 12
|
|
41
|
+
Pitch: 0.4
|
|
42
|
+
Yaw: 0.8
|
|
43
|
+
Focal Point:
|
|
44
|
+
X: 0
|
|
45
|
+
Y: 0
|
|
46
|
+
Z: 0
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from glob import glob
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from setuptools import setup
|
|
6
|
+
|
|
7
|
+
package_name = "auv_sim_bench"
|
|
8
|
+
|
|
9
|
+
setup(
|
|
10
|
+
name=package_name,
|
|
11
|
+
version="0.1.0",
|
|
12
|
+
packages=[package_name],
|
|
13
|
+
data_files=[
|
|
14
|
+
("share/ament_index/resource_index/packages", ["resource/" + package_name]),
|
|
15
|
+
("share/" + package_name, ["package.xml"]),
|
|
16
|
+
(os.path.join("share", package_name, "launch"), glob("launch/*.launch.py")),
|
|
17
|
+
(os.path.join("share", package_name, "config"), glob("config/*.yaml")),
|
|
18
|
+
(os.path.join("share", package_name, "rviz"), glob("rviz/*.rviz")),
|
|
19
|
+
],
|
|
20
|
+
install_requires=["setuptools"],
|
|
21
|
+
zip_safe=True,
|
|
22
|
+
maintainer="wuisabel-gif",
|
|
23
|
+
maintainer_email="231155141+wuisabel-gif@users.noreply.github.com",
|
|
24
|
+
description="Lightweight 6-DOF AUV physics sim for dry-testing state estimation.",
|
|
25
|
+
long_description=Path("README.md").read_text(encoding="utf-8"),
|
|
26
|
+
long_description_content_type="text/markdown",
|
|
27
|
+
license="MIT",
|
|
28
|
+
entry_points={
|
|
29
|
+
"console_scripts": [
|
|
30
|
+
"sim = auv_sim_bench.sim_node:main",
|
|
31
|
+
],
|
|
32
|
+
},
|
|
33
|
+
)
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
|
|
3
|
+
from auv_sim_bench.dynamics import AUVDynamics, AUVParams
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def test_neutral_buoyancy_hovers():
|
|
7
|
+
sim = AUVDynamics(AUVParams()) # default volume = neutral
|
|
8
|
+
for _ in range(2000):
|
|
9
|
+
sim.step(0.005, [0, 0, 0], [0, 0, 0])
|
|
10
|
+
assert abs(sim.s.p[2]) < 0.05 # stays near its starting depth
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def test_accelerometer_reads_gravity_at_rest():
|
|
14
|
+
sim = AUVDynamics(AUVParams())
|
|
15
|
+
sim.step(0.005, [0, 0, 0], [0, 0, 0])
|
|
16
|
+
# level, neutrally buoyant: specific force is +g on body z
|
|
17
|
+
assert sim.specific_force[2] > 9.0
|
|
18
|
+
assert abs(sim.specific_force[0]) < 0.1
|
|
19
|
+
assert abs(sim.specific_force[1]) < 0.1
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def test_negative_buoyancy_sinks():
|
|
23
|
+
p = AUVParams()
|
|
24
|
+
p.volume = p.mass / p.water_density * 0.9 # under-displaced -> heavy
|
|
25
|
+
sim = AUVDynamics(p)
|
|
26
|
+
for _ in range(2000):
|
|
27
|
+
sim.step(0.005, [0, 0, 0], [0, 0, 0])
|
|
28
|
+
assert sim.s.p[2] < -0.1 # sinks
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def test_forward_thrust_moves_and_drag_limits_speed():
|
|
32
|
+
sim = AUVDynamics(AUVParams())
|
|
33
|
+
for _ in range(4000):
|
|
34
|
+
sim.step(0.005, [40, 0, 0], [0, 0, 0]) # steady forward force
|
|
35
|
+
assert sim.s.p[0] > 0.5 # moved forward
|
|
36
|
+
assert sim.s.v[0] < 5.0 # drag bounded the speed
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def test_current_pushes_passive_vehicle():
|
|
40
|
+
p = AUVParams()
|
|
41
|
+
p.current_world = np.array([0.5, 0.0, 0.0]) # 0.5 m/s current along +x
|
|
42
|
+
sim = AUVDynamics(p)
|
|
43
|
+
for _ in range(4000):
|
|
44
|
+
sim.step(0.005, [0, 0, 0], [0, 0, 0]) # no thrust
|
|
45
|
+
assert sim.s.p[0] > 0.2 # carried downstream
|
|
46
|
+
assert sim.s.v[0] > 0.1 # drifting toward the water velocity
|