Skip to content

Commit cb6e253

Browse files
Release v0.7.1: rosbag replay, counterfactual demo, Nav2 plugins, LeRobot validation.
Adds the full rosbag-to-imagination loop, bundled counterfactual demo, Nav2 avoidance preview, compiled costmap layer and DWB critic with loopback smoke, and optional LeRobot loader validation after v2.1→v3.0 convert. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 5ab178e commit cb6e253

48 files changed

Lines changed: 2860 additions & 31 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,5 +55,5 @@ jobs:
5555
run: |
5656
source /opt/ros/jazzy/setup.bash
5757
source install/setup.bash
58-
colcon test --packages-select world_model_py world_model_msgs world_model_bringup world_model_viz world_model_datasets world_model_nav2
58+
colcon test --packages-select world_model_py world_model_msgs world_model_bringup world_model_viz world_model_datasets world_model_nav2 world_model_costmap world_model_dwb_critics
5959
colcon test-result --verbose

CHANGELOG.md

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,49 @@ All notable changes to this project are documented in this file. The format is
44
based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this
55
project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
66

7+
## [0.7.1] - 2026-06-08
8+
9+
LeRobot loader validation and a compiled Nav2 DWB critic plugin.
10+
11+
### Added
12+
- **LeRobot loader validation**`test/validate_lerobot_loader.py` exports a toy
13+
dataset, converts v2.1 → v3.0, and loads it with `LeRobotDataset` when
14+
`lerobot` is installed. Parquet writer fix: 1-D state/action columns are stored
15+
as scalars so v3 loading succeeds.
16+
- **`world_model_dwb_critics`** — compiled `dwb_core::TrajectoryCritic` plugin
17+
(`WorldModelOccupancyCritic`) that scores DWB rollouts against predicted
18+
`FutureOccupancy` using the same lethal-union rule as `world_model_costmap`.
19+
- **Nav2 loopback integration**`nav2_loopback_world_model.launch.py` patches
20+
Nav2 bringup params with the costmap layer + DWB critic; `smoke_nav2_loopback.py`
21+
verifies both plugins initialise in a live stack (requires
22+
`ros-jazzy-nav2-loopback-sim`).
23+
24+
### Notes
25+
- Loopback smoke confirms plugin load + subscription, not full navigation to a goal.
26+
LeRobot validation is optional (`pip install lerobot`).
27+
28+
## [0.7.0] - 2026-06-08
29+
30+
From rosbag replay to counterfactual imagination and Nav2-style avoidance — the
31+
full “use your own data” loop, plus one-command demos.
32+
33+
### Added
34+
- **Rosbag replay**`bag_relay` node + `replay_imagination.launch.py`: play a
35+
bag through the World Model runtime and RViz imagination viewer.
36+
- **Experience recording**`experience_recorder` writes `experience.npz` when a
37+
replay finishes; `planning_node` hot-reloads the memory for `ImagineFutures`.
38+
- **Counterfactual demo** — bundled `drive_demo.mcap`,
39+
`replay_counterfactual_demo.launch.py`, `counterfactual_marker_node` (mosaic +
40+
MarkerArray), and Foxglove layout `foxglove/counterfactual.json`.
41+
- **Nav2 avoidance preview**`costmap_preview_node` (predicted lethal union),
42+
`avoidance_demo_node` (pick a detour path using the same rule as
43+
`world_model_costmap::WorldModelLayer`), `nav2_avoidance_demo.launch.py`, and
44+
`docs/nav2_avoidance.gif`.
45+
46+
### Notes
47+
- Nav2 avoidance demo visualises the costmap-layer logic in Python + RViz; closed-
48+
loop Nav2 in Gazebo is still future work (see `world_model_costmap/README.md`).
49+
750
## [0.6.0] - 2026-06-08
851

952
Generate the future instead of retrieving it: a learned `latent → pixel` decoder

README.md

Lines changed: 118 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,10 @@ diverge by action. Learning-free episodic planning — see
3030
over a shared JSON wire; ROS 2 clients are unchanged.
3131
- **Compiled Nav2 costmap layer** — predicted occupancy flows straight into the
3232
Nav2 costmap (real `nav2_costmap_2d::Layer`, not a mock).
33-
- **rosbag2 → LeRobot** dataset export, **GPU-free**.
33+
- **Compiled Nav2 DWB critic** — score DWB rollouts against the same predicted
34+
occupancy (`world_model_dwb_critics`).
35+
- **rosbag2 → LeRobot** dataset export, **GPU-free** (optional `lerobot` loader
36+
validation test included).
3437
- **Counterfactual imagination & planning (learning-free)** — imagine *"what if I
3538
steer left / straight / right"* and plan to an image goal from an episodic
3639
retrieval world model, **no dynamics training**; validated on real public data.
@@ -152,16 +155,80 @@ ros2 bag record -s mcap -o my_run \
152155
panel and enable the `/world_model_viz/imagination` topic — markers carry their
153156
own colors.)
154157

158+
### 5. Replay your own rosbag2
159+
160+
Feed a recorded drive through the World Model — no robot, no synthetic publisher.
161+
The `bag_relay` node joins camera + `cmd_vel` + `odom` into
162+
`world_model_msgs/Observation`; the launch file starts bag play, the runtime,
163+
the imagination viewer and RViz.
164+
165+
```bash
166+
ros2 launch world_model_bringup replay_imagination.launch.py \
167+
bag:=/path/to/my_drive \
168+
adapter:=dummy # or ijepa / vjepa2 on a GPU box
169+
170+
# topic names differ in your bag? override them:
171+
# image_topic:=/front_camera/image_raw action_topic:=/control/cmd state_topic:=/localization/odom
172+
# headless:
173+
# rviz:=false
174+
```
175+
176+
Record your own bag first:
177+
178+
```bash
179+
ros2 bag record -s mcap -o my_drive /camera/image_raw /cmd_vel /odom
180+
```
181+
182+
**Record an experience memory while replaying** (for learning-free counterfactual
183+
planning). The `experience_recorder` encodes each frame, pairs consecutive
184+
latents with steering (`cmd_vel.angular.z`), and writes `experience.npz` when
185+
the bag finishes. `planning_node` reloads the file automatically.
186+
187+
```bash
188+
ros2 launch world_model_bringup replay_imagination.launch.py \
189+
bag:=/path/to/my_drive \
190+
record_experience:=true \
191+
experience_out:=/tmp/experience.npz
192+
193+
# after bag play ends (~3s idle), imagine L / straight / R:
194+
ros2 service call /world_model_planning/imagine_futures \
195+
world_model_msgs/srv/ImagineFutures \
196+
"{steering_options: [-0.7, 0.0, 0.7], horizon: 12}"
197+
```
198+
199+
Use `adapter:=ijepa` on a GPU box for real latent encoding; `dummy` works
200+
GPU-free for pipeline smoke tests.
201+
202+
### 6. Counterfactual replay demo (bundled bag, one command)
203+
204+
A synthetic driving bag ships in the repo. This replays it, records
205+
`experience.npz`, auto-calls `ImagineFutures`, and shows the L / straight / R
206+
mosaic in RViz — no robot, no dataset download, GPU-free with `dummy`.
207+
208+
```bash
209+
ros2 launch world_model_bringup replay_counterfactual_demo.launch.py
210+
# faster replay: rate:=3.0 headless: rviz:=false
211+
```
212+
213+
Topics: `/counterfactual_viz/counterfactual/mosaic` (Image) and
214+
`…/markers` (MarkerArray). Rebuild the bundled bag with
215+
`python3 world_model_bringup/scripts/build_demo_bag.py`.
216+
217+
Open the counterfactual clip in [Foxglove](https://foxglove.dev/) with layout
218+
[`world_model_viz/foxglove/counterfactual.json`](world_model_viz/foxglove/counterfactual.json)
219+
(record `/counterfactual_viz/counterfactual/mosaic` during a replay).
220+
155221
## Packages
156222

157223
| package | type | what |
158224
|---|---|---|
159225
| `world_model_msgs` | ament_cmake | the message/action **contract**: `Observation`, `ActionCondition`, `FutureState`, `FutureOccupancy`, `LatentState`, `RiskScore`, `Rollout`, `PredictFuture.action` |
160-
| `world_model_py` | ament_python | adapter SDK (`load_model`), `dummy` / `ijepa` / `remote` adapters, lifecycle runtime node, reference `world-model-server`, benchmark, `world-model` CLI |
161-
| `world_model_viz` | ament_python | imagination viewer: imagined `FutureOccupancy` + `RiskScore` → RViz `MarkerArray` |
226+
| `world_model_py` | ament_python | adapter SDK (`load_model`), `dummy` / `ijepa` / `remote` adapters, lifecycle runtime node, `bag_relay` (rosbag2 → Observation), reference `world-model-server`, benchmark, `world-model` CLI |
227+
| `world_model_viz` | ament_python | imagination viewer (`FutureOccupancy` + `RiskScore` → RViz); counterfactual mosaic viewer (`ImagineFutures` → Image + MarkerArray) |
162228
| `world_model_datasets` | ament_python | `export_lerobot`: rosbag2 → LeRobot-compatible dataset (parquet + mp4 + meta), GPU-free |
163229
| `world_model_nav2` | ament_python | score Nav2 candidate trajectories by model-based risk (`ScoreTrajectories` service + risk-coloured path markers) |
164230
| `world_model_costmap` | ament_cmake (C++) | **compiled** `nav2_costmap_2d::Layer` that stamps predicted `FutureOccupancy` into the costmap |
231+
| `world_model_dwb_critics` | ament_cmake (C++) | **compiled** `dwb_core::TrajectoryCritic` that scores DWB rollouts against predicted occupancy |
165232
| `world_model_bringup` | ament_cmake | launch files + demo config |
166233

167234
## Architecture
@@ -295,9 +362,18 @@ Output (v2.1 layout): `meta/{info,episodes,tasks,stats}.json[l]`,
295362
Supported state/action types: `nav_msgs/Odometry`, `sensor_msgs/JointState`,
296363
`geometry_msgs/Twist[Stamped]`, `std_msgs/Float{32,64}MultiArray`.
297364

298-
> Honesty: validated structurally (parquet round-trips, mp4 is ffprobe-readable,
299-
> counts consistent), **not** against the `lerobot` loader (not a dependency
300-
> here). Verify against your `lerobot` version before training.
365+
**Loader validation (optional).** With `pip install lerobot`, convert v2.1 → v3.0
366+
and open the dataset:
367+
368+
```bash
369+
pip install lerobot
370+
cd world_model_datasets
371+
python3 -m pytest test/validate_lerobot_loader.py -q
372+
```
373+
374+
The test exports a toy dataset, runs `lerobot.scripts.convert_dataset_v21_to_v30`,
375+
and loads it with `LeRobotDataset`. Structural checks (parquet round-trip, ffprobe-
376+
readable mp4, counts) run in CI without `lerobot`.
301377

302378
**Validated on real public data.** `test/validate_real_lerobot.py` builds a
303379
rosbag2 from a public **LeRobot SO-101** episode (real camera video + real 6-DoF
@@ -522,6 +598,34 @@ unit-tested. This follows recent World-Model failure/OOD-monitoring work, e.g.
522598
[failure detection without failure data](https://arxiv.org/html/2503.08558v1),
523599
and [foundation world models detecting manipulation failures](https://arxiv.org/pdf/2603.06987).
524600

601+
## Nav2 avoidance preview (predicted occupancy → detour)
602+
603+
![World Model predicted occupancy steers around the lethal union](docs/nav2_avoidance.gif)
604+
605+
<sub>**Predicted occupancy → avoid.** The dummy World Model imagines a moving
606+
obstacle blob; `costmap_preview_node` shows the lethal union that
607+
`world_model_costmap::WorldModelLayer` would stamp into Nav2, and
608+
`avoidance_demo_node` picks a detour path that misses it (naive straight in red,
609+
safe arc in green). GPU-free, no Gazebo.</sub>
610+
611+
```bash
612+
ros2 launch world_model_bringup nav2_avoidance_demo.launch.py
613+
```
614+
615+
Honest scope: this previews the **costmap layer** logic in RViz; for a **live Nav2
616+
stack** (loopback sim, not Gazebo) with both plugins loaded:
617+
618+
```bash
619+
sudo apt install ros-jazzy-nav2-loopback-sim # once
620+
ros2 launch world_model_bringup nav2_loopback_world_model.launch.py use_rviz:=false
621+
# optional headless smoke:
622+
python3 $(ros2 pkg prefix world_model_bringup)/share/world_model_bringup/scripts/smoke_nav2_loopback.py
623+
```
624+
625+
For trajectory *ranking* by model risk, see the scorer mock below. For a **compiled
626+
DWB critic**, see
627+
[world_model_dwb_critics/README.md](world_model_dwb_critics/README.md).
628+
525629
## Nav2 trajectory scoring (mock)
526630

527631
![Nav2 trajectory scoring by World Model risk](docs/nav2_scoring.gif)
@@ -551,16 +655,20 @@ For the **production path**, `world_model_costmap` is a *compiled*
551655
`nav2_costmap_2d::Layer` that stamps the model's predicted `FutureOccupancy`
552656
straight into the Nav2 costmap — so any planner/controller avoids predicted
553657
obstacles. See [world_model_costmap/README.md](world_model_costmap/README.md).
658+
`world_model_dwb_critics` adds the same rule as a `dwb_core::TrajectoryCritic`
659+
for DWB local planners.
554660

555661
## Roadmap (90-day MVP)
556662

557663
- **0–30d (this scaffold):** msgs, adapter SDK, dummy + remote adapters,
558664
lifecycle node, CLI, HTML smoke/bench report — **all GPU-free**. ✅
559665
- **31–60d:** JEPA latent adapter (image→latent→surprise) ✅, RViz imagination
560-
markers ✅, rosbag2 replay demo (next).
561-
- **61–90d:** rosbag2 → LeRobotDataset converter ✅, Nav2 trajectory-scoring
562-
mock ✅, compiled Nav2 costmap layer ✅, remote adapter + reference server
563-
(Cosmos/DreamZero-ready) ✅, benchmark dashboard, VLA Zoo / Walking Zoo examples.
666+
markers ✅, rosbag2 replay demo ✅, counterfactual replay demo ✅, Nav2
667+
avoidance preview ✅.
668+
- **61–90d:** rosbag2 → LeRobotDataset converter ✅ (optional `lerobot` loader
669+
validation ✅), Nav2 trajectory-scoring mock ✅, compiled Nav2 costmap layer ✅,
670+
compiled DWB critic ✅, remote adapter + reference server (Cosmos/DreamZero-ready)
671+
✅, benchmark dashboard. VLA Zoo / Walking Zoo — out of scope for now.
564672

565673
## Contributing
566674

docs/nav2_avoidance.gif

3.35 KB
Loading

world_model_bringup/CMakeLists.txt

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,15 @@ project(world_model_bringup)
33

44
find_package(ament_cmake REQUIRED)
55

6-
install(DIRECTORY launch config
6+
install(DIRECTORY launch config demo
77
DESTINATION share/${PROJECT_NAME}
88
)
99

10+
install(PROGRAMS
11+
scripts/nav2_world_model_params.py
12+
scripts/smoke_nav2_loopback.py
13+
scripts/build_demo_bag.py
14+
DESTINATION share/${PROJECT_NAME}/scripts
15+
)
16+
1017
ament_package()
637 KB
Binary file not shown.
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
rosbag2_bagfile_information:
2+
version: 9
3+
storage_identifier: mcap
4+
duration:
5+
nanoseconds: 4702000000
6+
starting_time:
7+
nanoseconds_since_epoch: 1000000000
8+
message_count: 144
9+
topics_with_message_count:
10+
- topic_metadata:
11+
name: /cmd_vel
12+
type: geometry_msgs/msg/Twist
13+
serialization_format: cdr
14+
offered_qos_profiles:
15+
[]
16+
type_description_hash: ""
17+
message_count: 48
18+
- topic_metadata:
19+
name: /odom
20+
type: nav_msgs/msg/Odometry
21+
serialization_format: cdr
22+
offered_qos_profiles:
23+
[]
24+
type_description_hash: ""
25+
message_count: 48
26+
- topic_metadata:
27+
name: /camera/image_raw
28+
type: sensor_msgs/msg/Image
29+
serialization_format: cdr
30+
offered_qos_profiles:
31+
[]
32+
type_description_hash: ""
33+
message_count: 48
34+
compression_format: ""
35+
compression_mode: ""
36+
relative_file_paths:
37+
- drive_demo.mcap_0.mcap
38+
files:
39+
- path: drive_demo.mcap_0.mcap
40+
starting_time:
41+
nanoseconds_since_epoch: 1000000000
42+
duration:
43+
nanoseconds: 4702000000
44+
message_count: 288
45+
custom_data: ~
46+
ros_distro: jazzy
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
"""World Model predicted occupancy -> avoid the lethal union (GPU-free).
2+
3+
Shows what ``world_model_costmap::WorldModelLayer`` would stamp into Nav2, then
4+
picks a detour path that misses the predicted lethal cells. No Gazebo / full
5+
Nav2 stack required.
6+
7+
ros2 launch world_model_bringup nav2_avoidance_demo.launch.py
8+
# rviz:=false on headless
9+
"""
10+
import os
11+
12+
from ament_index_python.packages import get_package_share_directory
13+
from launch import LaunchDescription
14+
from launch.actions import DeclareLaunchArgument
15+
from launch.conditions import IfCondition
16+
from launch.substitutions import LaunchConfiguration
17+
from launch_ros.actions import LifecycleNode, Node
18+
19+
20+
def generate_launch_description() -> LaunchDescription:
21+
viz_share = get_package_share_directory("world_model_viz")
22+
rviz_cfg = os.path.join(viz_share, "rviz", "nav2_avoidance.rviz")
23+
24+
runtime = LifecycleNode(
25+
package="world_model_py",
26+
executable="runtime_node",
27+
name="world_model_runtime",
28+
namespace="",
29+
parameters=[{"adapter": LaunchConfiguration("adapter"), "horizon": 8, "autostart": True}],
30+
output="screen",
31+
)
32+
sample = Node(
33+
package="world_model_py",
34+
executable="sample_publisher",
35+
name="sample_observation_publisher",
36+
remappings=[("observation", "/world_model_runtime/observation")],
37+
parameters=[{"rate_hz": 4.0}],
38+
output="screen",
39+
)
40+
preview = Node(
41+
package="world_model_viz",
42+
executable="costmap_preview_node",
43+
name="costmap_preview",
44+
output="screen",
45+
)
46+
avoidance = Node(
47+
package="world_model_nav2",
48+
executable="avoidance_demo_node",
49+
name="avoidance_demo",
50+
output="screen",
51+
)
52+
rviz = Node(
53+
package="rviz2",
54+
executable="rviz2",
55+
name="rviz2",
56+
arguments=["-d", rviz_cfg],
57+
condition=IfCondition(LaunchConfiguration("rviz")),
58+
output="screen",
59+
)
60+
61+
return LaunchDescription(
62+
[
63+
DeclareLaunchArgument("adapter", default_value="dummy"),
64+
DeclareLaunchArgument("rviz", default_value="true"),
65+
runtime,
66+
sample,
67+
preview,
68+
avoidance,
69+
rviz,
70+
]
71+
)

0 commit comments

Comments
 (0)