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 +16 -0
- worldbench/backends/__init__.py +15 -0
- worldbench/backends/benchmark.py +200 -0
- worldbench/backends/demo.py +233 -0
- worldbench/backends/frame_freeze.py +117 -0
- worldbench/backends/frame_scramble.py +139 -0
- worldbench/backends/lerobot.py +1225 -0
- worldbench/backends/local.py +23 -0
- worldbench/cli.py +463 -0
- worldbench/core.py +88 -0
- worldbench/dashboard.py +741 -0
- worldbench/dataset.py +161 -0
- worldbench/metrics/__init__.py +16 -0
- worldbench/metrics/action_consistency.py +177 -0
- worldbench/metrics/contact.py +118 -0
- worldbench/metrics/object_permanence.py +79 -0
- worldbench/metrics/temporal.py +59 -0
- worldbench/metrics/visual.py +95 -0
- worldbench/runners/__init__.py +17 -0
- worldbench/runners/benchmark.py +159 -0
- worldbench/runners/comparator.py +255 -0
- worldbench/runners/evaluator.py +228 -0
- worldbench/runners/reporter.py +145 -0
- worldbench/schemas.py +169 -0
- worldbench/utils.py +152 -0
- worldbench-0.2.0.dist-info/METADATA +438 -0
- worldbench-0.2.0.dist-info/RECORD +31 -0
- worldbench-0.2.0.dist-info/WHEEL +5 -0
- worldbench-0.2.0.dist-info/entry_points.txt +2 -0
- worldbench-0.2.0.dist-info/licenses/LICENSE +156 -0
- worldbench-0.2.0.dist-info/top_level.txt +1 -0
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]))
|