worldbench 0.2.0__py3-none-any.whl

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.
worldbench/__init__.py ADDED
@@ -0,0 +1,16 @@
1
+ """WorldBench: robotics world-model evaluation made practical."""
2
+
3
+ from worldbench.core import Metrics, WorldBench, WorldModelRun, evaluate, load_dataset
4
+ from worldbench.schemas import EvaluationResult, MetricResult
5
+
6
+ __all__ = [
7
+ "EvaluationResult",
8
+ "MetricResult",
9
+ "Metrics",
10
+ "WorldBench",
11
+ "WorldModelRun",
12
+ "evaluate",
13
+ "load_dataset",
14
+ ]
15
+
16
+ __version__ = "0.2.0"
@@ -0,0 +1,15 @@
1
+ """WorldBench backends."""
2
+
3
+ from worldbench.backends.benchmark import BenchmarkBackend
4
+ from worldbench.backends.demo import DemoBackend
5
+ from worldbench.backends.lerobot import create_lerobot_style_demo_source, import_lerobot_repo, import_lerobot_style
6
+ from worldbench.backends.local import LocalBackend
7
+
8
+ __all__ = [
9
+ "BenchmarkBackend",
10
+ "DemoBackend",
11
+ "LocalBackend",
12
+ "create_lerobot_style_demo_source",
13
+ "import_lerobot_repo",
14
+ "import_lerobot_style",
15
+ ]
@@ -0,0 +1,200 @@
1
+ """Synthetic benchmark scenario generator."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import shutil
6
+ from dataclasses import dataclass
7
+ from pathlib import Path
8
+
9
+ from PIL import Image, ImageDraw
10
+
11
+ from worldbench.utils import ensure_dir, write_json
12
+
13
+
14
+ SCENARIO_NAMES = [
15
+ "push_cube",
16
+ "action_mismatch",
17
+ "pre_contact_motion",
18
+ "object_disappears",
19
+ "temporal_flicker",
20
+ ]
21
+
22
+
23
+ @dataclass(frozen=True)
24
+ class ScenarioConfig:
25
+ name: str
26
+ failure_mode: str
27
+
28
+
29
+ class BenchmarkBackend:
30
+ """Generate lightweight synthetic benchmark scenarios."""
31
+
32
+ width = 128
33
+ height = 96
34
+
35
+ scenarios = [
36
+ ScenarioConfig("push_cube", "combined control/contact failures"),
37
+ ScenarioConfig("action_mismatch", "robot moves opposite the commanded action"),
38
+ ScenarioConfig("pre_contact_motion", "object moves before robot/object contact"),
39
+ ScenarioConfig("object_disappears", "object disappears during prediction"),
40
+ ScenarioConfig("temporal_flicker", "prediction flickers or jumps between frames"),
41
+ ]
42
+
43
+ def create(self, output_path: str | Path = "benchmarks", overwrite: bool = True) -> Path:
44
+ root = Path(output_path)
45
+ if root.exists() and overwrite:
46
+ shutil.rmtree(root)
47
+ ensure_dir(root)
48
+
49
+ for scenario in self.scenarios:
50
+ self._write_scenario(root / scenario.name, scenario)
51
+ return root
52
+
53
+ def _write_scenario(self, root: Path, scenario: ScenarioConfig) -> None:
54
+ ensure_dir(root)
55
+ states = _ground_truth_states()
56
+ actions = _actions()
57
+ self._write_episode(root, scenario, states, actions)
58
+ self._write_model(root, "good_model", scenario, states, quality="good")
59
+ self._write_model(root, "bad_model", scenario, states, quality="bad")
60
+
61
+ def _write_episode(
62
+ self,
63
+ root: Path,
64
+ scenario: ScenarioConfig,
65
+ states: list[dict[str, tuple[int, int]]],
66
+ actions: list[dict[str, object]],
67
+ ) -> None:
68
+ episode = root / "episode_001"
69
+ frames = ensure_dir(episode / "frames")
70
+ predictions = ensure_dir(episode / "predictions")
71
+ for idx, state in enumerate(states):
72
+ image = self._render_frame(state["robot"], state["object"], f"gt {idx:03d}")
73
+ image.save(frames / f"{idx:03d}.png")
74
+ image.save(predictions / f"{idx:03d}.png")
75
+
76
+ write_json(episode / "actions.json", actions)
77
+ write_json(
78
+ episode / "states.json",
79
+ [
80
+ {
81
+ "t": idx,
82
+ "robot_x": state["robot"][0],
83
+ "robot_y": state["robot"][1],
84
+ "object_x": state["object"][0],
85
+ "object_y": state["object"][1],
86
+ }
87
+ for idx, state in enumerate(states)
88
+ ],
89
+ )
90
+ write_json(
91
+ episode / "metadata.json",
92
+ {
93
+ "name": scenario.name,
94
+ "robot": "synthetic_2d_mobile_manipulator",
95
+ "task": "push cube",
96
+ "fps": 5,
97
+ "description": f"Synthetic benchmark scenario: {scenario.failure_mode}.",
98
+ },
99
+ )
100
+
101
+ def _write_model(
102
+ self,
103
+ root: Path,
104
+ model_name: str,
105
+ scenario: ScenarioConfig,
106
+ states: list[dict[str, tuple[int, int]]],
107
+ quality: str,
108
+ ) -> None:
109
+ model_dir = ensure_dir(root / model_name / "episode_001")
110
+ generated = states if quality == "good" else _bad_states_for_scenario(scenario.name, states)
111
+ for idx, state in enumerate(generated):
112
+ hide_object = quality == "bad" and scenario.name == "object_disappears" and idx in {2, 3, 4, 5, 6, 7, 8, 9}
113
+ flicker = quality == "bad" and scenario.name == "temporal_flicker" and idx in {4, 6, 8}
114
+ if quality == "bad" and scenario.name == "push_cube":
115
+ hide_object = idx == 4
116
+ flicker = idx == 6
117
+ label = f"{quality[:4]} {idx:03d}"
118
+ self._render_frame(
119
+ state["robot"],
120
+ state["object"],
121
+ label,
122
+ hide_object=hide_object,
123
+ flicker=flicker,
124
+ ).save(model_dir / f"{idx:03d}.png")
125
+
126
+ def _render_frame(
127
+ self,
128
+ robot: tuple[int, int],
129
+ obj: tuple[int, int],
130
+ label: str,
131
+ hide_object: bool = False,
132
+ flicker: bool = False,
133
+ ) -> Image.Image:
134
+ background = (10, 18, 28) if not flicker else (44, 17, 20)
135
+ image = Image.new("RGB", (self.width, self.height), background)
136
+ draw = ImageDraw.Draw(image)
137
+ for x in range(0, self.width, 16):
138
+ draw.line((x, 0, x, self.height), fill=(25, 38, 50), width=1)
139
+ for y in range(0, self.height, 16):
140
+ draw.line((0, y, self.width, y), fill=(25, 38, 50), width=1)
141
+ draw.rectangle((5, 5, self.width - 6, self.height - 6), outline=(54, 78, 96), width=1)
142
+ draw.text((8, 7), label, fill=(128, 152, 170))
143
+
144
+ rx, ry = robot
145
+ ox, oy = obj
146
+ draw.line((rx, ry, ox, oy), fill=(52, 72, 88), width=2)
147
+ draw.rounded_rectangle((rx - 10, ry - 8, rx + 10, ry + 8), radius=4, fill=(80, 172, 214), outline=(190, 236, 255), width=1)
148
+ draw.ellipse((rx - 5, ry - 5, rx + 5, ry + 5), fill=(231, 76, 60), outline=(255, 180, 160), width=1)
149
+ draw.rectangle((rx - 4, ry + 8, rx + 4, ry + 12), fill=(7, 14, 22))
150
+ if not hide_object:
151
+ draw.rectangle((ox - 8, oy - 8, ox + 8, oy + 8), fill=(46, 204, 113), outline=(179, 255, 204), width=2)
152
+ return image
153
+
154
+
155
+ def _ground_truth_states() -> list[dict[str, tuple[int, int]]]:
156
+ robot = (24, 50)
157
+ obj = (84, 50)
158
+ states = [{"robot": robot, "object": obj}]
159
+ for _ in range(9):
160
+ contact = ((robot[0] - obj[0]) ** 2 + (robot[1] - obj[1]) ** 2) ** 0.5 <= 18.0
161
+ robot = (robot[0] + 8, robot[1])
162
+ if contact:
163
+ obj = (obj[0] + 8, obj[1])
164
+ states.append({"robot": robot, "object": obj})
165
+ return states
166
+
167
+
168
+ def _actions() -> list[dict[str, object]]:
169
+ return [
170
+ {"t": idx, "action": "move_right", "dx": 1.0, "dy": 0.0, "gripper": "open"}
171
+ for idx in range(9)
172
+ ]
173
+
174
+
175
+ def _bad_states_for_scenario(name: str, states: list[dict[str, tuple[int, int]]]) -> list[dict[str, tuple[int, int]]]:
176
+ bad: list[dict[str, tuple[int, int]]] = []
177
+ start_robot = states[0]["robot"]
178
+ start_object = states[0]["object"]
179
+ for idx, state in enumerate(states):
180
+ robot = state["robot"]
181
+ obj = state["object"]
182
+ if name == "action_mismatch":
183
+ robot = (max(14, start_robot[0] - idx * 5), start_robot[1])
184
+ elif name == "pre_contact_motion":
185
+ if idx >= 1:
186
+ obj = (start_object[0] + idx * 7, start_object[1])
187
+ elif name == "object_disappears":
188
+ robot = state["robot"]
189
+ elif name == "temporal_flicker":
190
+ if idx in {4, 6, 8}:
191
+ robot = (112, 18)
192
+ obj = (34 + idx * 3, 76)
193
+ elif name == "push_cube":
194
+ robot = (max(14, start_robot[0] - idx * 5), start_robot[1])
195
+ if idx >= 2:
196
+ obj = (start_object[0] + idx * 4, start_object[1])
197
+ if idx == 6:
198
+ robot = (110, 18)
199
+ bad.append({"robot": robot, "object": obj})
200
+ return bad
@@ -0,0 +1,233 @@
1
+ """Synthetic demo dataset generator."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import shutil
6
+ from pathlib import Path
7
+
8
+ from PIL import Image, ImageDraw
9
+
10
+ from worldbench.utils import ensure_dir, write_json
11
+
12
+
13
+ class DemoBackend:
14
+ """Generate deterministic synthetic rollouts and model outputs."""
15
+
16
+ width = 128
17
+ height = 96
18
+
19
+ def create(self, output_path: str | Path = "examples/demo_dataset", overwrite: bool = True) -> Path:
20
+ root = Path(output_path)
21
+ if root.exists() and overwrite:
22
+ shutil.rmtree(root)
23
+ ensure_dir(root)
24
+
25
+ episodes = [
26
+ _episode_plan(
27
+ name="episode_001",
28
+ task="push cube right",
29
+ robot_start=(24, 50),
30
+ object_start=(84, 50),
31
+ delta=(8, 0),
32
+ actions=["move_right"] * 7 + ["close_gripper", "move_right"],
33
+ ),
34
+ _episode_plan(
35
+ name="episode_002",
36
+ task="push cube down",
37
+ robot_start=(58, 18),
38
+ object_start=(58, 68),
39
+ delta=(0, 7),
40
+ actions=["move_down"] * 7 + ["close_gripper", "move_down"],
41
+ ),
42
+ ]
43
+
44
+ for plan in episodes:
45
+ self._write_episode(root, plan)
46
+
47
+ self._write_model_outputs(root, episodes, "good_model", quality="good")
48
+ self._write_model_outputs(root, episodes, "bad_model", quality="bad")
49
+ self._mirror_example_outputs(root)
50
+ return root
51
+
52
+ def init_structure(self, output_path: str | Path) -> Path:
53
+ root = Path(output_path)
54
+ episode = root / "episode_001"
55
+ ensure_dir(episode / "frames")
56
+ ensure_dir(episode / "predictions")
57
+ write_json(
58
+ episode / "actions.json",
59
+ [
60
+ {"t": 0, "action": "move_right", "dx": 1.0, "dy": 0.0, "gripper": "open"},
61
+ {"t": 1, "action": "close_gripper", "dx": 0.0, "dy": 0.0, "gripper": "closed"},
62
+ ],
63
+ )
64
+ write_json(
65
+ episode / "states.json",
66
+ [
67
+ {"t": 0, "robot_x": 20, "robot_y": 50, "object_x": 80, "object_y": 50},
68
+ {"t": 1, "robot_x": 30, "robot_y": 50, "object_x": 80, "object_y": 50},
69
+ ],
70
+ )
71
+ write_json(
72
+ episode / "metadata.json",
73
+ {
74
+ "name": "push_cube_template",
75
+ "robot": "synthetic_2d_arm",
76
+ "task": "push cube",
77
+ "fps": 5,
78
+ "description": "Template WorldBench rollout. Add frames and predictions as numbered PNG files.",
79
+ },
80
+ )
81
+ return root
82
+
83
+ def _write_episode(self, root: Path, plan: dict[str, object]) -> None:
84
+ episode_dir = root / str(plan["name"])
85
+ frames_dir = episode_dir / "frames"
86
+ predictions_dir = episode_dir / "predictions"
87
+ ensure_dir(frames_dir)
88
+ ensure_dir(predictions_dir)
89
+ states = plan["states"]
90
+ actions = plan["actions_json"]
91
+
92
+ for idx, state in enumerate(states):
93
+ image = self._render_frame(state["robot"], state["object"], label=f"gt {idx:03d}")
94
+ image.save(frames_dir / f"{idx:03d}.png")
95
+ image.save(predictions_dir / f"{idx:03d}.png")
96
+
97
+ write_json(episode_dir / "actions.json", actions)
98
+ write_json(episode_dir / "states.json", plan["states_json"])
99
+ write_json(
100
+ episode_dir / "metadata.json",
101
+ {
102
+ "name": str(plan["name"]),
103
+ "robot": "synthetic_2d_arm",
104
+ "task": str(plan["task"]),
105
+ "fps": 5,
106
+ "description": "Synthetic robot rollout for world-model evaluation.",
107
+ },
108
+ )
109
+
110
+ def _write_model_outputs(self, root: Path, episodes: list[dict[str, object]], model_name: str, quality: str) -> None:
111
+ model_root = root / model_name
112
+ ensure_dir(model_root)
113
+ for plan in episodes:
114
+ episode_dir = model_root / str(plan["name"])
115
+ ensure_dir(episode_dir)
116
+ states = plan["states"]
117
+ bad_states = _bad_states(plan) if quality == "bad" else states
118
+ for idx, state in enumerate(bad_states):
119
+ hide_object = quality == "bad" and idx in {4}
120
+ flicker = quality == "bad" and idx in {6}
121
+ label = f"gt {idx:03d}" if quality == "good" else f"bad {idx:03d}"
122
+ image = self._render_frame(
123
+ state["robot"],
124
+ state["object"],
125
+ label=label,
126
+ hide_object=hide_object,
127
+ flicker=flicker,
128
+ )
129
+ image.save(episode_dir / f"{idx:03d}.png")
130
+
131
+ def _mirror_example_outputs(self, root: Path) -> None:
132
+ if root.name != "demo_dataset" or root.parent.name != "examples":
133
+ return
134
+ for model_name, folder_name in [("good_model", "good_model_outputs"), ("bad_model", "bad_model_outputs")]:
135
+ target = root.parent / folder_name
136
+ if target.exists():
137
+ shutil.rmtree(target)
138
+ shutil.copytree(root / model_name, target)
139
+
140
+ def _render_frame(
141
+ self,
142
+ robot: tuple[int, int],
143
+ obj: tuple[int, int],
144
+ label: str,
145
+ hide_object: bool = False,
146
+ flicker: bool = False,
147
+ ) -> Image.Image:
148
+ background = (14, 21, 28) if not flicker else (48, 20, 20)
149
+ image = Image.new("RGB", (self.width, self.height), background)
150
+ draw = ImageDraw.Draw(image)
151
+
152
+ for x in range(0, self.width, 16):
153
+ draw.line((x, 0, x, self.height), fill=(24, 34, 43), width=1)
154
+ for y in range(0, self.height, 16):
155
+ draw.line((0, y, self.width, y), fill=(24, 34, 43), width=1)
156
+
157
+ draw.rectangle((5, 5, self.width - 6, self.height - 6), outline=(58, 75, 88), width=1)
158
+ draw.text((8, 7), label, fill=(129, 152, 166))
159
+
160
+ rx, ry = robot
161
+ ox, oy = obj
162
+ draw.line((rx, ry, ox, oy), fill=(51, 68, 80), width=2)
163
+ draw.ellipse((rx - 8, ry - 8, rx + 8, ry + 8), fill=(231, 76, 60), outline=(255, 180, 160), width=2)
164
+ draw.rectangle((rx - 4, ry - 14, rx + 4, ry - 8), fill=(255, 202, 120))
165
+
166
+ if not hide_object:
167
+ draw.rectangle((ox - 8, oy - 8, ox + 8, oy + 8), fill=(46, 204, 113), outline=(179, 255, 204), width=2)
168
+ draw.line((ox - 8, oy, ox + 8, oy), fill=(22, 120, 75), width=1)
169
+ draw.line((ox, oy - 8, ox, oy + 8), fill=(22, 120, 75), width=1)
170
+
171
+ return image
172
+
173
+
174
+ def _episode_plan(
175
+ name: str,
176
+ task: str,
177
+ robot_start: tuple[int, int],
178
+ object_start: tuple[int, int],
179
+ delta: tuple[int, int],
180
+ actions: list[str],
181
+ ) -> dict[str, object]:
182
+ states = [{"robot": robot_start, "object": object_start}]
183
+ states_json = [{"t": 0, "robot_x": robot_start[0], "robot_y": robot_start[1], "object_x": object_start[0], "object_y": object_start[1]}]
184
+ actions_json = []
185
+ robot = robot_start
186
+ obj = object_start
187
+ for idx, action in enumerate(actions):
188
+ contact_before_action = ((robot[0] - obj[0]) ** 2 + (robot[1] - obj[1]) ** 2) ** 0.5 <= 18.0
189
+ if action.startswith("move"):
190
+ robot = (robot[0] + delta[0], robot[1] + delta[1])
191
+ if contact_before_action:
192
+ obj = (obj[0] + delta[0], obj[1] + delta[1])
193
+ states.append({"robot": robot, "object": obj})
194
+ states_json.append(
195
+ {"t": idx + 1, "robot_x": robot[0], "robot_y": robot[1], "object_x": obj[0], "object_y": obj[1]}
196
+ )
197
+
198
+ for idx, action in enumerate(actions):
199
+ dx = 1.0 if "right" in action else 0.0
200
+ dy = 1.0 if "down" in action else 0.0
201
+ actions_json.append(
202
+ {
203
+ "t": idx,
204
+ "action": action,
205
+ "dx": dx,
206
+ "dy": dy,
207
+ "gripper": "closed" if action == "close_gripper" else "open",
208
+ }
209
+ )
210
+ return {"name": name, "task": task, "states": states, "states_json": states_json, "actions_json": actions_json}
211
+
212
+
213
+ def _bad_states(plan: dict[str, object]) -> list[dict[str, tuple[int, int]]]:
214
+ states = plan["states"]
215
+ bad = []
216
+ first = states[0]
217
+ start_robot = first["robot"]
218
+ for idx, state in enumerate(states):
219
+ obj = state["object"]
220
+ if "right" in str(plan["task"]):
221
+ robot = (max(14, start_robot[0] - idx * 5), start_robot[1])
222
+ if idx >= 2:
223
+ obj = (obj[0] + idx * 4, obj[1])
224
+ if idx == 6:
225
+ robot = (110, 18)
226
+ else:
227
+ robot = (start_robot[0], max(14, start_robot[1] - idx * 4))
228
+ if idx >= 2:
229
+ obj = (obj[0], obj[1] + idx * 3)
230
+ if idx == 6:
231
+ robot = (104, 20)
232
+ bad.append({"robot": robot, "object": obj})
233
+ return bad
@@ -0,0 +1,117 @@
1
+ """Deterministic frame-freeze corruption helpers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ import random
7
+ import shutil
8
+ from pathlib import Path
9
+
10
+ from worldbench.utils import ensure_dir, list_image_files
11
+
12
+
13
+ @dataclass(frozen=True)
14
+ class FrameFreezeResult:
15
+ """Summary of a frozen prediction sequence."""
16
+
17
+ source_frames: int
18
+ frozen_frames: int
19
+ output_paths: list[Path]
20
+
21
+
22
+ def freeze_frames(
23
+ source_frames: str | Path,
24
+ output_frames: str | Path,
25
+ severity: float = 0.0,
26
+ seed: int = 42,
27
+ overwrite: bool = True,
28
+ ) -> FrameFreezeResult:
29
+ """Copy a frame sequence while freezing a deterministic subset of timesteps.
30
+
31
+ A frozen frame is copied from the most recent previous output frame, so the
32
+ visible timeline can repeat short spans without modifying the source data.
33
+ """
34
+
35
+ if not 0.0 <= severity <= 1.0:
36
+ raise ValueError(f"Severity must be between 0 and 1: {severity}")
37
+
38
+ source = Path(source_frames)
39
+ if not source.is_dir():
40
+ raise FileNotFoundError(f"Source frame directory does not exist: {source}")
41
+
42
+ frames = list_image_files(source)
43
+ if not frames:
44
+ raise ValueError(f"No image frames found in {source}")
45
+
46
+ destination = Path(output_frames)
47
+ if destination.exists() and overwrite:
48
+ shutil.rmtree(destination)
49
+ elif destination.exists():
50
+ raise FileExistsError(f"Output frame directory already exists: {destination}")
51
+ ensure_dir(destination)
52
+
53
+ frozen_positions = _frozen_positions(len(frames), severity, seed)
54
+ frozen_frames = 0
55
+ output_paths: list[Path] = []
56
+ previous_output: Path | None = None
57
+
58
+ for idx, source_path in enumerate(frames):
59
+ destination_path = destination / source_path.name
60
+ if idx == 0 or idx not in frozen_positions or previous_output is None:
61
+ shutil.copy2(source_path, destination_path)
62
+ else:
63
+ shutil.copy2(previous_output, destination_path)
64
+ frozen_frames += 1
65
+ previous_output = destination_path
66
+ output_paths.append(destination_path)
67
+
68
+ return FrameFreezeResult(
69
+ source_frames=len(frames),
70
+ frozen_frames=frozen_frames,
71
+ output_paths=output_paths,
72
+ )
73
+
74
+
75
+ def freeze_rollout_predictions(
76
+ source_dataset: str | Path,
77
+ output_root: str | Path,
78
+ severity: float = 0.0,
79
+ seed: int = 42,
80
+ overwrite: bool = True,
81
+ ) -> list[FrameFreezeResult]:
82
+ """Create a WorldBench-style prediction tree from a rollout dataset root."""
83
+
84
+ source_root = Path(source_dataset)
85
+ if not source_root.is_dir():
86
+ raise FileNotFoundError(f"Source dataset directory does not exist: {source_root}")
87
+
88
+ destination_root = Path(output_root)
89
+ if destination_root.exists() and overwrite:
90
+ shutil.rmtree(destination_root)
91
+ elif destination_root.exists():
92
+ raise FileExistsError(f"Output root already exists: {destination_root}")
93
+ ensure_dir(destination_root)
94
+
95
+ results: list[FrameFreezeResult] = []
96
+ for episode_dir in sorted(
97
+ path for path in source_root.iterdir() if path.is_dir() and path.name.startswith("episode_")
98
+ ):
99
+ frames_dir = episode_dir / "frames"
100
+ predictions_dir = destination_root / episode_dir.name / "predictions"
101
+ result = freeze_frames(frames_dir, predictions_dir, severity=severity, seed=seed, overwrite=overwrite)
102
+ results.append(result)
103
+ return results
104
+
105
+
106
+ def _frozen_positions(frame_count: int, severity: float, seed: int) -> set[int]:
107
+ if frame_count <= 1 or severity <= 0.0:
108
+ return set()
109
+
110
+ freeze_count = round((frame_count - 1) * severity)
111
+ freeze_count = max(0, min(frame_count - 1, freeze_count))
112
+ if freeze_count == 0:
113
+ return set()
114
+
115
+ positions = list(range(1, frame_count))
116
+ random.Random(seed).shuffle(positions)
117
+ return set(sorted(positions[:freeze_count]))