oopsie-data-tools 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.
- oopsie_data_tools/__init__.py +7 -0
- oopsie_data_tools/annotation_tool/__init__.py +5 -0
- oopsie_data_tools/annotation_tool/annotation_schema.py +266 -0
- oopsie_data_tools/annotation_tool/annotator_server.py +850 -0
- oopsie_data_tools/annotation_tool/episode_recorder.py +648 -0
- oopsie_data_tools/annotation_tool/rollout_annotator.py +333 -0
- oopsie_data_tools/annotation_tool/ui/annotator.html +2240 -0
- oopsie_data_tools/cli.py +894 -0
- oopsie_data_tools/init_wizard.py +329 -0
- oopsie_data_tools/skill/SKILL.md +98 -0
- oopsie_data_tools/skill/reference/conversion.md +257 -0
- oopsie_data_tools/skill/reference/format.md +112 -0
- oopsie_data_tools/skill/reference/robot-profile.md +104 -0
- oopsie_data_tools/skill/reference/setup.md +216 -0
- oopsie_data_tools/skill/reference/troubleshooting.md +97 -0
- oopsie_data_tools/test/__init__.py +1 -0
- oopsie_data_tools/test/conftest.py +174 -0
- oopsie_data_tools/test/fixtures/__init__.py +0 -0
- oopsie_data_tools/test/fixtures/make_invalid.py +633 -0
- oopsie_data_tools/test/fixtures/make_valid.py +406 -0
- oopsie_data_tools/test/test_annotation_completeness.py +154 -0
- oopsie_data_tools/test/test_annotation_schema.py +190 -0
- oopsie_data_tools/test/test_annotator_server.py +240 -0
- oopsie_data_tools/test/test_annotator_server_guards.py +146 -0
- oopsie_data_tools/test/test_bulk_inference_end_to_end.py +112 -0
- oopsie_data_tools/test/test_cli_config.py +111 -0
- oopsie_data_tools/test/test_cli_tools.py +438 -0
- oopsie_data_tools/test/test_contributor_config.py +38 -0
- oopsie_data_tools/test/test_conversion_utils_annotations.py +75 -0
- oopsie_data_tools/test/test_credentials_location.py +106 -0
- oopsie_data_tools/test/test_diversity.py +33 -0
- oopsie_data_tools/test/test_episode_recorder.py +335 -0
- oopsie_data_tools/test/test_episode_video_paths.py +173 -0
- oopsie_data_tools/test/test_hf_upload.py +234 -0
- oopsie_data_tools/test/test_init_wizard.py +193 -0
- oopsie_data_tools/test/test_install_skill.py +120 -0
- oopsie_data_tools/test/test_migrate_taxonomy_v2.py +255 -0
- oopsie_data_tools/test/test_new_profile.py +84 -0
- oopsie_data_tools/test/test_paths.py +137 -0
- oopsie_data_tools/test/test_python38_compat.py +79 -0
- oopsie_data_tools/test/test_record_step_purity.py +127 -0
- oopsie_data_tools/test/test_robot_setup.py +244 -0
- oopsie_data_tools/test/test_rollout_annotator.py +134 -0
- oopsie_data_tools/test/test_rotation_utils.py +126 -0
- oopsie_data_tools/test/test_validate.py +494 -0
- oopsie_data_tools/utils/__init__.py +1 -0
- oopsie_data_tools/utils/claude_skill.py +96 -0
- oopsie_data_tools/utils/contributor_config.py +91 -0
- oopsie_data_tools/utils/conversion_utils.py +220 -0
- oopsie_data_tools/utils/h5.py +60 -0
- oopsie_data_tools/utils/h5_inspect.py +167 -0
- oopsie_data_tools/utils/hf_limits.py +22 -0
- oopsie_data_tools/utils/hf_upload.py +235 -0
- oopsie_data_tools/utils/log.py +35 -0
- oopsie_data_tools/utils/migrate_taxonomy_v2.py +215 -0
- oopsie_data_tools/utils/paths.py +162 -0
- oopsie_data_tools/utils/restructure.py +402 -0
- oopsie_data_tools/utils/robot_profile/__init__.py +1 -0
- oopsie_data_tools/utils/robot_profile/robot_profile.py +240 -0
- oopsie_data_tools/utils/robot_profile/rotation_utils.py +119 -0
- oopsie_data_tools/utils/robot_profile/template.py +108 -0
- oopsie_data_tools/utils/validation/__init__.py +5 -0
- oopsie_data_tools/utils/validation/annotation_completeness.py +67 -0
- oopsie_data_tools/utils/validation/diversity.py +93 -0
- oopsie_data_tools/utils/validation/episode_data.py +54 -0
- oopsie_data_tools/utils/validation/episode_loader.py +201 -0
- oopsie_data_tools/utils/validation/episode_validator.py +315 -0
- oopsie_data_tools/utils/validation/errors.py +17 -0
- oopsie_data_tools/utils/validation/validation_utils.py +88 -0
- oopsie_data_tools-0.2.0.dist-info/METADATA +131 -0
- oopsie_data_tools-0.2.0.dist-info/RECORD +74 -0
- oopsie_data_tools-0.2.0.dist-info/WHEEL +4 -0
- oopsie_data_tools-0.2.0.dist-info/entry_points.txt +2 -0
- oopsie_data_tools-0.2.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,633 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Generate intentionally malformed HDF5 episodes for negative testing.
|
|
3
|
+
|
|
4
|
+
Each fixture targets one specific failure mode so tests can verify that the
|
|
5
|
+
annotator server, analysis scripts, and any other reader either degrade
|
|
6
|
+
gracefully (return defaults / empty results) or raise the expected error,
|
|
7
|
+
rather than crashing silently or producing wrong output.
|
|
8
|
+
|
|
9
|
+
Scenarios are grouped by which layer they exercise:
|
|
10
|
+
|
|
11
|
+
A – Annotator server file-level guards
|
|
12
|
+
invalid_not_h5 .h5 extension but file is plain text
|
|
13
|
+
invalid_empty_h5 valid HDF5 with no groups or attrs
|
|
14
|
+
invalid_missing_attrs correct structure, all root attrs absent
|
|
15
|
+
invalid_broken_video_ref image_observations/front → nonexistent file
|
|
16
|
+
invalid_no_video_group no image_observations and no <stem>_*.mp4 sibling
|
|
17
|
+
invalid_annotation_dataset episode_annotations is a scalar dataset, not a group
|
|
18
|
+
invalid_taxonomy_not_json annotation subgroup has malformed taxonomy JSON
|
|
19
|
+
|
|
20
|
+
B – HDF5 schema / data violations (structure)
|
|
21
|
+
invalid_mismatched_steps robot_states 20 steps, actions 5 steps
|
|
22
|
+
invalid_zero_steps robot_states and actions have 0 rows
|
|
23
|
+
invalid_actions_missing actions group entirely absent
|
|
24
|
+
invalid_robot_states_missing observations/robot_states group absent
|
|
25
|
+
invalid_image_obs_float image_observations/front stores a float array, not a path string
|
|
26
|
+
|
|
27
|
+
C – Tensor shape / dtype violations
|
|
28
|
+
invalid_joint_pos_wrong_dof joint_position is (20, 3) not (20, 7)
|
|
29
|
+
invalid_joint_vel_wrong_dof joint_velocity action is (20, 3) not (20, 7)
|
|
30
|
+
invalid_robot_state_extra_key robot_states has undeclared extra key
|
|
31
|
+
invalid_robot_state_missing_key gripper_position absent from robot_states group
|
|
32
|
+
|
|
33
|
+
D – Embedded robot_profile JSON violations
|
|
34
|
+
invalid_malformed_profile robot_profile attr is not valid JSON
|
|
35
|
+
invalid_profile_missing_key required key policy_name absent from profile JSON
|
|
36
|
+
invalid_profile_no_gripper action_space has no gripper key
|
|
37
|
+
invalid_profile_joint_no_names joint action_space but action_joint_names absent
|
|
38
|
+
invalid_profile_unsupported_action action_space contains unknown key "hand_position"
|
|
39
|
+
invalid_profile_empty_cameras camera_names is empty list
|
|
40
|
+
invalid_profile_missing_rs_key joint_velocity action_space but robot_state_keys omits
|
|
41
|
+
the "joint_position" state it requires
|
|
42
|
+
invalid_profile_biarm_mismatch is_biarm=True but cartesian_position is 7-DOF, not 14
|
|
43
|
+
|
|
44
|
+
E – Cross-consistency violations
|
|
45
|
+
invalid_joint_names_length_mismatch robot_state_joint_names has 7 entries, tensor has 5 cols
|
|
46
|
+
invalid_action_names_length_mismatch action_joint_names has 7 entries, tensor has 6 cols
|
|
47
|
+
invalid_profile_camera_not_in_obs profile declares wrist camera, image_observations omits it
|
|
48
|
+
invalid_profile_action_not_in_recorded profile declares joint_velocity, actions group omits it
|
|
49
|
+
invalid_profile_rs_key_not_in_recorded profile declares eef_pos, robot_states omits it
|
|
50
|
+
invalid_multiple_promised_fields_missing three fields promised by profile all absent
|
|
51
|
+
invalid_control_freq_zero control_freq=0 causes divide-by-zero in frame math
|
|
52
|
+
invalid_inconsistent_video_lengths front MP4 has 10 frames, wrist MP4 has 30 frames
|
|
53
|
+
invalid_video_length_step_mismatch MP4 has 10 frames, robot_states has 100 rows
|
|
54
|
+
|
|
55
|
+
Usage
|
|
56
|
+
-----
|
|
57
|
+
uv run python -m oopsie_data_tools.test.fixtures.make_invalid [OUTPUT_DIR]
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
from __future__ import annotations
|
|
61
|
+
|
|
62
|
+
import argparse
|
|
63
|
+
import json
|
|
64
|
+
import time
|
|
65
|
+
from pathlib import Path
|
|
66
|
+
from typing import Callable, Tuple
|
|
67
|
+
|
|
68
|
+
import h5py
|
|
69
|
+
import imageio
|
|
70
|
+
import numpy as np
|
|
71
|
+
|
|
72
|
+
from oopsie_data_tools.annotation_tool.annotation_schema import write_annotation_attrs
|
|
73
|
+
|
|
74
|
+
_VideoWriter = Callable[[Path, Tuple[int, int, int], int], None]
|
|
75
|
+
|
|
76
|
+
_STR_DTYPE = h5py.string_dtype(encoding="utf-8")
|
|
77
|
+
|
|
78
|
+
_VALID_ROBOT_PROFILE = {
|
|
79
|
+
"policy_name": "test_policy",
|
|
80
|
+
"robot_name": "test_robot",
|
|
81
|
+
"is_biarm": False,
|
|
82
|
+
"uses_mobile_base": False,
|
|
83
|
+
"gripper_name": "test_gripper",
|
|
84
|
+
"control_freq": 10,
|
|
85
|
+
"camera_names": ["front"],
|
|
86
|
+
"robot_state_keys": ["joint_position", "gripper_position"],
|
|
87
|
+
"robot_state_joint_names": ["j1", "j2", "j3", "j4", "j5", "j6", "j7"],
|
|
88
|
+
"action_space": ["joint_velocity", "gripper_position"],
|
|
89
|
+
"action_joint_names": ["j1", "j2", "j3", "j4", "j5", "j6", "j7"],
|
|
90
|
+
"orientation_representation": None,
|
|
91
|
+
"controller": None,
|
|
92
|
+
"gains": None,
|
|
93
|
+
"intrinsic_calibration_matrix": None,
|
|
94
|
+
"extrinsic_calibration_matrix": None,
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
# Defaults chosen so a video is *valid* unless a fixture deliberately makes it otherwise:
|
|
99
|
+
# 224px clears MIN_IMAGE_SIZE (180), and 20 frames at 10 fps matches the 20 recorded steps
|
|
100
|
+
# of the standard profile exactly. They used to be 64px/10 frames, which failed the size
|
|
101
|
+
# check before any fixture reached the defect it was written to demonstrate.
|
|
102
|
+
_VIDEO_FRAMES = 20
|
|
103
|
+
_VIDEO_SIZE = 224
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _write_video(
|
|
107
|
+
path: Path, color: tuple, n_frames: int = _VIDEO_FRAMES, size: int = _VIDEO_SIZE
|
|
108
|
+
) -> None:
|
|
109
|
+
frames = np.full((n_frames, size, size, 3), color, dtype=np.uint8)
|
|
110
|
+
with imageio.get_writer(
|
|
111
|
+
str(path), format="FFMPEG", mode="I", fps=10, codec="libx264",
|
|
112
|
+
output_params=["-crf", "28"],
|
|
113
|
+
) as writer:
|
|
114
|
+
for frame in frames:
|
|
115
|
+
writer.append_data(frame)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
# ---------------------------------------------------------------------------
|
|
119
|
+
# Shared builders
|
|
120
|
+
# ---------------------------------------------------------------------------
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _base_attrs(
|
|
124
|
+
f: h5py.File,
|
|
125
|
+
episode_id: str = "test_episode",
|
|
126
|
+
robot_profile: dict | str | None = None,
|
|
127
|
+
) -> None:
|
|
128
|
+
f.attrs["schema"] = "oopsiedata_format_v1"
|
|
129
|
+
f.attrs["episode_id"] = episode_id
|
|
130
|
+
if robot_profile is None:
|
|
131
|
+
robot_profile = _VALID_ROBOT_PROFILE
|
|
132
|
+
f.attrs["robot_profile"] = (
|
|
133
|
+
robot_profile if isinstance(robot_profile, str) else json.dumps(robot_profile)
|
|
134
|
+
)
|
|
135
|
+
f.attrs["language_instruction"] = "pick up the block"
|
|
136
|
+
f.attrs["operator_name"] = "test_operator"
|
|
137
|
+
f.attrs["lab_id"] = "test_lab"
|
|
138
|
+
f.attrs["timestamp"] = time.time()
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _robot_states(f: h5py.File, n: int = 20, joint_dof: int = 7, gripper_dof: int = 1) -> None:
|
|
142
|
+
rs = f.require_group("observations/robot_states")
|
|
143
|
+
rs.create_dataset("joint_position", data=np.zeros((n, joint_dof), dtype=np.float64))
|
|
144
|
+
rs.create_dataset("gripper_position", data=np.zeros((n, gripper_dof), dtype=np.float64))
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _actions(f: h5py.File, n: int = 20, joint_dof: int = 7) -> None:
|
|
148
|
+
ag = f.require_group("actions")
|
|
149
|
+
ag.create_dataset("joint_velocity", data=np.zeros((n, joint_dof), dtype=np.float64))
|
|
150
|
+
ag.create_dataset("gripper_position", data=np.zeros((n, 1), dtype=np.float64))
|
|
151
|
+
for key in (
|
|
152
|
+
"cartesian_position", "cartesian_velocity", "joint_position",
|
|
153
|
+
"base_position", "base_velocity", "gripper_velocity", "gripper_binary",
|
|
154
|
+
):
|
|
155
|
+
ag.create_dataset(key, data=h5py.Empty(dtype=np.float64))
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _video_paths(f: h5py.File, cam: str, rel_path: str, write: bool = True) -> None:
|
|
159
|
+
"""Point ``cam`` at ``rel_path`` and, unless told not to, write that video.
|
|
160
|
+
|
|
161
|
+
Declaring a video without creating it used to be the norm here, so nearly every
|
|
162
|
+
fixture failed on "Video file does not exist" rather than on the defect it was named
|
|
163
|
+
for. Pass ``write=False`` only where the missing file *is* the defect.
|
|
164
|
+
"""
|
|
165
|
+
vp = f.require_group("observations/video_paths")
|
|
166
|
+
vp.create_dataset(cam, data=rel_path, dtype=_STR_DTYPE)
|
|
167
|
+
if write:
|
|
168
|
+
_write_video(Path(f.filename).parent / rel_path, (120, 120, 120))
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def _full_valid_episode(f: h5py.File, episode_id: str, cam_path: str = "front.mp4") -> None:
|
|
172
|
+
_base_attrs(f, episode_id)
|
|
173
|
+
_robot_states(f)
|
|
174
|
+
_actions(f)
|
|
175
|
+
_video_paths(f, "front", cam_path)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
# ---------------------------------------------------------------------------
|
|
179
|
+
# A – Annotator server file-level guards
|
|
180
|
+
# ---------------------------------------------------------------------------
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def make_not_h5(out_dir: Path) -> None:
|
|
184
|
+
(out_dir / "invalid_not_h5.h5").write_text("this is not an HDF5 file\n")
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def make_empty_h5(out_dir: Path) -> None:
|
|
188
|
+
with h5py.File(out_dir / "invalid_empty_h5.h5", "w"):
|
|
189
|
+
pass
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def make_missing_attrs(out_dir: Path) -> None:
|
|
193
|
+
with h5py.File(out_dir / "invalid_missing_attrs.h5", "w") as f:
|
|
194
|
+
f.attrs["schema"] = "oopsiedata_format_v1"
|
|
195
|
+
_robot_states(f)
|
|
196
|
+
_actions(f)
|
|
197
|
+
_video_paths(f, "front", "invalid_missing_attrs_front.mp4")
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def make_broken_video_ref(out_dir: Path) -> None:
|
|
201
|
+
with h5py.File(out_dir / "invalid_broken_video_ref.h5", "w") as f:
|
|
202
|
+
_base_attrs(f, "invalid_broken_video_ref")
|
|
203
|
+
_robot_states(f)
|
|
204
|
+
_actions(f)
|
|
205
|
+
# The missing file is the defect here, so this one must not be written.
|
|
206
|
+
_video_paths(f, "front", "does_not_exist.mp4", write=False)
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def make_no_video_group(out_dir: Path) -> None:
|
|
210
|
+
with h5py.File(out_dir / "invalid_no_video_group.h5", "w") as f:
|
|
211
|
+
_base_attrs(f, "invalid_no_video_group")
|
|
212
|
+
_robot_states(f)
|
|
213
|
+
_actions(f)
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def make_annotation_dataset(out_dir: Path) -> None:
|
|
217
|
+
with h5py.File(out_dir / "invalid_annotation_dataset.h5", "w") as f:
|
|
218
|
+
_full_valid_episode(f, "invalid_annotation_dataset", "invalid_annotation_dataset_front.mp4")
|
|
219
|
+
f.create_dataset("episode_annotations", data="not a group", dtype=_STR_DTYPE)
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def make_taxonomy_not_json(out_dir: Path) -> None:
|
|
223
|
+
"""A complete, valid annotation whose ``taxonomy`` blob alone is corrupt.
|
|
224
|
+
|
|
225
|
+
Written through the real writer and then damaged, so the fixture fails on the defect
|
|
226
|
+
it is named for rather than on some unrelated attr the schema happens to have moved.
|
|
227
|
+
"""
|
|
228
|
+
with h5py.File(out_dir / "invalid_taxonomy_not_json.h5", "w") as f:
|
|
229
|
+
_full_valid_episode(f, "invalid_taxonomy_not_json", "invalid_taxonomy_not_json_front.mp4")
|
|
230
|
+
ag = f.require_group("episode_annotations/test_annotator")
|
|
231
|
+
write_annotation_attrs(
|
|
232
|
+
ag,
|
|
233
|
+
{
|
|
234
|
+
"outcome": "failure",
|
|
235
|
+
"timestamp": "2026-04-21T10:00:00+00:00",
|
|
236
|
+
"episode_description": "Robot dropped the object.",
|
|
237
|
+
"side_effect_category": ["grasp"],
|
|
238
|
+
"severity": "medium",
|
|
239
|
+
},
|
|
240
|
+
)
|
|
241
|
+
ag.attrs["taxonomy"] = "{ this is: not: valid json !!!"
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
# ---------------------------------------------------------------------------
|
|
245
|
+
# B – HDF5 schema / data violations (structure)
|
|
246
|
+
# ---------------------------------------------------------------------------
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def make_mismatched_steps(out_dir: Path) -> None:
|
|
250
|
+
_write_video(out_dir / "invalid_mismatched_steps_front.mp4", (100, 100, 200))
|
|
251
|
+
with h5py.File(out_dir / "invalid_mismatched_steps.h5", "w") as f:
|
|
252
|
+
_base_attrs(f, "invalid_mismatched_steps")
|
|
253
|
+
_robot_states(f, n=20)
|
|
254
|
+
_actions(f, n=5)
|
|
255
|
+
_video_paths(f, "front", "invalid_mismatched_steps_front.mp4")
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def make_zero_steps(out_dir: Path) -> None:
|
|
259
|
+
with h5py.File(out_dir / "invalid_zero_steps.h5", "w") as f:
|
|
260
|
+
_base_attrs(f, "invalid_zero_steps")
|
|
261
|
+
_robot_states(f, n=0)
|
|
262
|
+
_actions(f, n=0)
|
|
263
|
+
_video_paths(f, "front", "invalid_zero_steps_front.mp4")
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def make_actions_missing(out_dir: Path) -> None:
|
|
267
|
+
with h5py.File(out_dir / "invalid_actions_missing.h5", "w") as f:
|
|
268
|
+
_base_attrs(f, "invalid_actions_missing")
|
|
269
|
+
_robot_states(f)
|
|
270
|
+
_video_paths(f, "front", "invalid_actions_missing_front.mp4")
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def make_robot_states_missing(out_dir: Path) -> None:
|
|
274
|
+
with h5py.File(out_dir / "invalid_robot_states_missing.h5", "w") as f:
|
|
275
|
+
_base_attrs(f, "invalid_robot_states_missing")
|
|
276
|
+
_actions(f)
|
|
277
|
+
_video_paths(f, "front", "invalid_robot_states_missing_front.mp4")
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def make_image_obs_float(out_dir: Path) -> None:
|
|
281
|
+
with h5py.File(out_dir / "invalid_image_obs_float.h5", "w") as f:
|
|
282
|
+
_base_attrs(f, "invalid_image_obs_float")
|
|
283
|
+
_robot_states(f)
|
|
284
|
+
_actions(f)
|
|
285
|
+
# Store a float array instead of a path string. The loader will decode it
|
|
286
|
+
# to a garbage path and fail with "does not exist".
|
|
287
|
+
vp = f.require_group("observations/video_paths")
|
|
288
|
+
vp.create_dataset("front", data=np.zeros((10, 64, 64, 3), dtype=np.uint8))
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
# ---------------------------------------------------------------------------
|
|
292
|
+
# C – Tensor shape / dtype violations
|
|
293
|
+
# ---------------------------------------------------------------------------
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def make_joint_pos_wrong_dof(out_dir: Path) -> None:
|
|
297
|
+
with h5py.File(out_dir / "invalid_joint_pos_wrong_dof.h5", "w") as f:
|
|
298
|
+
_base_attrs(f, "invalid_joint_pos_wrong_dof")
|
|
299
|
+
rs = f.require_group("observations/robot_states")
|
|
300
|
+
rs.create_dataset("joint_position", data=np.zeros((20, 3), dtype=np.float64))
|
|
301
|
+
rs.create_dataset("gripper_position", data=np.zeros((20, 1), dtype=np.float64))
|
|
302
|
+
_actions(f)
|
|
303
|
+
_video_paths(f, "front", "invalid_joint_pos_wrong_dof_front.mp4")
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def make_joint_vel_wrong_dof(out_dir: Path) -> None:
|
|
307
|
+
with h5py.File(out_dir / "invalid_joint_vel_wrong_dof.h5", "w") as f:
|
|
308
|
+
_base_attrs(f, "invalid_joint_vel_wrong_dof")
|
|
309
|
+
_robot_states(f)
|
|
310
|
+
ag = f.require_group("actions")
|
|
311
|
+
ag.create_dataset("joint_velocity", data=np.zeros((20, 3), dtype=np.float64))
|
|
312
|
+
ag.create_dataset("gripper_position", data=np.zeros((20, 1), dtype=np.float64))
|
|
313
|
+
for key in (
|
|
314
|
+
"cartesian_position", "cartesian_velocity", "joint_position",
|
|
315
|
+
"base_position", "base_velocity", "gripper_velocity", "gripper_binary",
|
|
316
|
+
):
|
|
317
|
+
ag.create_dataset(key, data=h5py.Empty(dtype=np.float64))
|
|
318
|
+
_video_paths(f, "front", "invalid_joint_vel_wrong_dof_front.mp4")
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def make_robot_state_extra_key(out_dir: Path) -> None:
|
|
322
|
+
with h5py.File(out_dir / "invalid_robot_state_extra_key.h5", "w") as f:
|
|
323
|
+
_base_attrs(f, "invalid_robot_state_extra_key")
|
|
324
|
+
rs = f.require_group("observations/robot_states")
|
|
325
|
+
rs.create_dataset("joint_position", data=np.zeros((20, 7), dtype=np.float64))
|
|
326
|
+
rs.create_dataset("gripper_position", data=np.zeros((20, 1), dtype=np.float64))
|
|
327
|
+
rs.create_dataset("velocity_hack", data=np.zeros((20, 7), dtype=np.float64))
|
|
328
|
+
_actions(f)
|
|
329
|
+
_video_paths(f, "front", "invalid_robot_state_extra_key_front.mp4")
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def make_robot_state_missing_key(out_dir: Path) -> None:
|
|
333
|
+
with h5py.File(out_dir / "invalid_robot_state_missing_key.h5", "w") as f:
|
|
334
|
+
_base_attrs(f, "invalid_robot_state_missing_key")
|
|
335
|
+
rs = f.require_group("observations/robot_states")
|
|
336
|
+
rs.create_dataset("joint_position", data=np.zeros((20, 7), dtype=np.float64))
|
|
337
|
+
_actions(f)
|
|
338
|
+
_video_paths(f, "front", "invalid_robot_state_missing_key_front.mp4")
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
# ---------------------------------------------------------------------------
|
|
342
|
+
# D – Embedded robot_profile JSON violations
|
|
343
|
+
# ---------------------------------------------------------------------------
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def make_malformed_profile(out_dir: Path) -> None:
|
|
347
|
+
with h5py.File(out_dir / "invalid_malformed_profile.h5", "w") as f:
|
|
348
|
+
_base_attrs(f, "invalid_malformed_profile", robot_profile="{ broken json ...")
|
|
349
|
+
_robot_states(f)
|
|
350
|
+
_actions(f)
|
|
351
|
+
_video_paths(f, "front", "invalid_malformed_profile_front.mp4")
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
def make_profile_missing_key(out_dir: Path) -> None:
|
|
355
|
+
profile = {k: v for k, v in _VALID_ROBOT_PROFILE.items() if k != "policy_name"}
|
|
356
|
+
with h5py.File(out_dir / "invalid_profile_missing_key.h5", "w") as f:
|
|
357
|
+
_base_attrs(f, "invalid_profile_missing_key", robot_profile=profile)
|
|
358
|
+
_robot_states(f)
|
|
359
|
+
_actions(f)
|
|
360
|
+
_video_paths(f, "front", "invalid_profile_missing_key_front.mp4")
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
def make_profile_no_gripper(out_dir: Path) -> None:
|
|
364
|
+
profile = {**_VALID_ROBOT_PROFILE, "action_space": ["joint_velocity"]}
|
|
365
|
+
with h5py.File(out_dir / "invalid_profile_no_gripper.h5", "w") as f:
|
|
366
|
+
_base_attrs(f, "invalid_profile_no_gripper", robot_profile=profile)
|
|
367
|
+
_robot_states(f)
|
|
368
|
+
ag = f.require_group("actions")
|
|
369
|
+
ag.create_dataset("joint_velocity", data=np.zeros((20, 7), dtype=np.float64))
|
|
370
|
+
_video_paths(f, "front", "invalid_profile_no_gripper_front.mp4")
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
def make_profile_joint_no_names(out_dir: Path) -> None:
|
|
374
|
+
profile = {**_VALID_ROBOT_PROFILE, "action_joint_names": None}
|
|
375
|
+
with h5py.File(out_dir / "invalid_profile_joint_no_names.h5", "w") as f:
|
|
376
|
+
_base_attrs(f, "invalid_profile_joint_no_names", robot_profile=profile)
|
|
377
|
+
_robot_states(f)
|
|
378
|
+
_actions(f)
|
|
379
|
+
_video_paths(f, "front", "invalid_profile_joint_no_names_front.mp4")
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
def make_profile_unsupported_action(out_dir: Path) -> None:
|
|
383
|
+
profile = {
|
|
384
|
+
**_VALID_ROBOT_PROFILE,
|
|
385
|
+
"action_space": ["hand_position", "gripper_position"],
|
|
386
|
+
}
|
|
387
|
+
with h5py.File(out_dir / "invalid_profile_unsupported_action.h5", "w") as f:
|
|
388
|
+
_base_attrs(f, "invalid_profile_unsupported_action", robot_profile=profile)
|
|
389
|
+
_robot_states(f)
|
|
390
|
+
_actions(f)
|
|
391
|
+
_video_paths(f, "front", "invalid_profile_unsupported_action_front.mp4")
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
def make_profile_empty_cameras(out_dir: Path) -> None:
|
|
395
|
+
profile = {**_VALID_ROBOT_PROFILE, "camera_names": []}
|
|
396
|
+
with h5py.File(out_dir / "invalid_profile_empty_cameras.h5", "w") as f:
|
|
397
|
+
_base_attrs(f, "invalid_profile_empty_cameras", robot_profile=profile)
|
|
398
|
+
_robot_states(f)
|
|
399
|
+
_actions(f)
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
def make_profile_missing_rs_key(out_dir: Path) -> None:
|
|
403
|
+
profile = {
|
|
404
|
+
**_VALID_ROBOT_PROFILE,
|
|
405
|
+
"robot_state_keys": ["gripper_position"],
|
|
406
|
+
"robot_state_joint_names": [],
|
|
407
|
+
}
|
|
408
|
+
with h5py.File(out_dir / "invalid_profile_missing_rs_key.h5", "w") as f:
|
|
409
|
+
_base_attrs(f, "invalid_profile_missing_rs_key", robot_profile=profile)
|
|
410
|
+
rs = f.require_group("observations/robot_states")
|
|
411
|
+
rs.create_dataset("gripper_position", data=np.zeros((20, 1), dtype=np.float64))
|
|
412
|
+
_actions(f)
|
|
413
|
+
_video_paths(f, "front", "invalid_profile_missing_rs_key_front.mp4")
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
def make_profile_biarm_mismatch(out_dir: Path) -> None:
|
|
417
|
+
"""A bimanual profile recording a single arm's end-effector pose.
|
|
418
|
+
|
|
419
|
+
Joint counts cannot be constrained by is_biarm — two arms need not share a DOF count —
|
|
420
|
+
but a cartesian pose is [x, y, z, qx, qy, qz, qw] per arm, so a biarm robot records 14.
|
|
421
|
+
This one records 7, which means either an arm is missing or is_biarm is wrong.
|
|
422
|
+
|
|
423
|
+
The previous version of this fixture only set is_biarm=True with 7 joints and no
|
|
424
|
+
cartesian data at all, which is internally consistent and so demonstrated nothing.
|
|
425
|
+
"""
|
|
426
|
+
profile = {
|
|
427
|
+
**_VALID_ROBOT_PROFILE,
|
|
428
|
+
"is_biarm": True,
|
|
429
|
+
"robot_state_keys": ["joint_position", "gripper_position", "cartesian_position"],
|
|
430
|
+
}
|
|
431
|
+
with h5py.File(out_dir / "invalid_profile_biarm_mismatch.h5", "w") as f:
|
|
432
|
+
_base_attrs(f, "invalid_profile_biarm_mismatch", robot_profile=profile)
|
|
433
|
+
_robot_states(f, joint_dof=7)
|
|
434
|
+
rs = f["observations/robot_states"]
|
|
435
|
+
rs.create_dataset("cartesian_position", data=np.zeros((20, 7), dtype=np.float64))
|
|
436
|
+
_actions(f, joint_dof=7)
|
|
437
|
+
_video_paths(f, "front", "invalid_profile_biarm_mismatch_front.mp4")
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
# ---------------------------------------------------------------------------
|
|
441
|
+
# E – Cross-consistency violations
|
|
442
|
+
# ---------------------------------------------------------------------------
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
def make_joint_names_length_mismatch(out_dir: Path) -> None:
|
|
446
|
+
"""robot_state_joint_names declares 7 names but joint_position tensor has 5 columns."""
|
|
447
|
+
_write_video(out_dir / "invalid_joint_names_length_mismatch_front.mp4", (200, 100, 100))
|
|
448
|
+
profile = {
|
|
449
|
+
**_VALID_ROBOT_PROFILE,
|
|
450
|
+
"robot_state_joint_names": ["j1", "j2", "j3", "j4", "j5", "j6", "j7"],
|
|
451
|
+
}
|
|
452
|
+
with h5py.File(out_dir / "invalid_joint_names_length_mismatch.h5", "w") as f:
|
|
453
|
+
_base_attrs(f, "invalid_joint_names_length_mismatch", robot_profile=profile)
|
|
454
|
+
rs = f.require_group("observations/robot_states")
|
|
455
|
+
rs.create_dataset("joint_position", data=np.zeros((20, 5), dtype=np.float64))
|
|
456
|
+
rs.create_dataset("gripper_position", data=np.zeros((20, 1), dtype=np.float64))
|
|
457
|
+
_actions(f)
|
|
458
|
+
_video_paths(f, "front", "invalid_joint_names_length_mismatch_front.mp4")
|
|
459
|
+
|
|
460
|
+
|
|
461
|
+
def make_action_names_length_mismatch(out_dir: Path) -> None:
|
|
462
|
+
"""action_joint_names declares 7 names but joint_velocity action has 6 columns."""
|
|
463
|
+
_write_video(out_dir / "invalid_action_names_length_mismatch_front.mp4", (100, 200, 100))
|
|
464
|
+
profile = {
|
|
465
|
+
**_VALID_ROBOT_PROFILE,
|
|
466
|
+
"action_joint_names": ["j1", "j2", "j3", "j4", "j5", "j6", "j7"],
|
|
467
|
+
}
|
|
468
|
+
with h5py.File(out_dir / "invalid_action_names_length_mismatch.h5", "w") as f:
|
|
469
|
+
_base_attrs(f, "invalid_action_names_length_mismatch", robot_profile=profile)
|
|
470
|
+
_robot_states(f)
|
|
471
|
+
ag = f.require_group("actions")
|
|
472
|
+
ag.create_dataset("joint_velocity", data=np.zeros((20, 6), dtype=np.float64))
|
|
473
|
+
ag.create_dataset("gripper_position", data=np.zeros((20, 1), dtype=np.float64))
|
|
474
|
+
for key in (
|
|
475
|
+
"cartesian_position", "cartesian_velocity", "joint_position",
|
|
476
|
+
"base_position", "base_velocity", "gripper_velocity", "gripper_binary",
|
|
477
|
+
):
|
|
478
|
+
ag.create_dataset(key, data=h5py.Empty(dtype=np.float64))
|
|
479
|
+
_video_paths(f, "front", "invalid_action_names_length_mismatch_front.mp4")
|
|
480
|
+
|
|
481
|
+
|
|
482
|
+
def make_profile_camera_not_in_obs(out_dir: Path) -> None:
|
|
483
|
+
"""Profile declares two cameras but image_observations only has one."""
|
|
484
|
+
profile = {**_VALID_ROBOT_PROFILE, "camera_names": ["front", "wrist"]}
|
|
485
|
+
with h5py.File(out_dir / "invalid_profile_camera_not_in_obs.h5", "w") as f:
|
|
486
|
+
_base_attrs(f, "invalid_profile_camera_not_in_obs", robot_profile=profile)
|
|
487
|
+
_robot_states(f)
|
|
488
|
+
_actions(f)
|
|
489
|
+
_video_paths(f, "front", "invalid_profile_camera_not_in_obs_front.mp4")
|
|
490
|
+
|
|
491
|
+
|
|
492
|
+
def make_profile_action_not_in_recorded(out_dir: Path) -> None:
|
|
493
|
+
"""Profile declares joint_velocity + gripper_position but only gripper_position was recorded."""
|
|
494
|
+
with h5py.File(out_dir / "invalid_profile_action_not_in_recorded.h5", "w") as f:
|
|
495
|
+
_base_attrs(f, "invalid_profile_action_not_in_recorded")
|
|
496
|
+
_robot_states(f)
|
|
497
|
+
ag = f.require_group("actions")
|
|
498
|
+
ag.create_dataset("gripper_position", data=np.zeros((20, 1), dtype=np.float64))
|
|
499
|
+
_video_paths(f, "front", "invalid_profile_action_not_in_recorded_front.mp4")
|
|
500
|
+
|
|
501
|
+
|
|
502
|
+
def make_profile_rs_key_not_in_recorded(out_dir: Path) -> None:
|
|
503
|
+
"""Profile declares eef_cartesian_position in robot_state_keys but it's absent in the file."""
|
|
504
|
+
profile = {
|
|
505
|
+
**_VALID_ROBOT_PROFILE,
|
|
506
|
+
"robot_state_keys": ["joint_position", "gripper_position", "eef_cartesian_position"],
|
|
507
|
+
}
|
|
508
|
+
with h5py.File(out_dir / "invalid_profile_rs_key_not_in_recorded.h5", "w") as f:
|
|
509
|
+
_base_attrs(f, "invalid_profile_rs_key_not_in_recorded", robot_profile=profile)
|
|
510
|
+
rs = f.require_group("observations/robot_states")
|
|
511
|
+
rs.create_dataset("joint_position", data=np.zeros((20, 7), dtype=np.float64))
|
|
512
|
+
rs.create_dataset("gripper_position", data=np.zeros((20, 1), dtype=np.float64))
|
|
513
|
+
_actions(f)
|
|
514
|
+
_video_paths(f, "front", "invalid_profile_rs_key_not_in_recorded_front.mp4")
|
|
515
|
+
|
|
516
|
+
|
|
517
|
+
def make_multiple_promised_fields_missing(out_dir: Path) -> None:
|
|
518
|
+
"""Three fields promised by profile (eef state, joint_position action, wrist camera) all absent.
|
|
519
|
+
|
|
520
|
+
This catches validators that only check one missing field at a time and stop,
|
|
521
|
+
potentially masking the remaining violations.
|
|
522
|
+
"""
|
|
523
|
+
profile = {
|
|
524
|
+
**_VALID_ROBOT_PROFILE,
|
|
525
|
+
"camera_names": ["front", "wrist"],
|
|
526
|
+
"robot_state_keys": ["joint_position", "gripper_position", "eef_cartesian_position"],
|
|
527
|
+
"action_space": ["joint_position", "gripper_position", "gripper_velocity"],
|
|
528
|
+
}
|
|
529
|
+
with h5py.File(out_dir / "invalid_multiple_promised_fields_missing.h5", "w") as f:
|
|
530
|
+
_base_attrs(f, "invalid_multiple_promised_fields_missing", robot_profile=profile)
|
|
531
|
+
rs = f.require_group("observations/robot_states")
|
|
532
|
+
rs.create_dataset("joint_position", data=np.zeros((20, 7), dtype=np.float64))
|
|
533
|
+
rs.create_dataset("gripper_position", data=np.zeros((20, 1), dtype=np.float64))
|
|
534
|
+
ag = f.require_group("actions")
|
|
535
|
+
ag.create_dataset("gripper_position", data=np.zeros((20, 1), dtype=np.float64))
|
|
536
|
+
_video_paths(f, "front", "invalid_multiple_promised_fields_missing_front.mp4")
|
|
537
|
+
|
|
538
|
+
|
|
539
|
+
def make_control_freq_zero(out_dir: Path) -> None:
|
|
540
|
+
"""control_freq=0 causes divide-by-zero in any frame-to-timestep calculation."""
|
|
541
|
+
profile = {**_VALID_ROBOT_PROFILE, "control_freq": 0}
|
|
542
|
+
with h5py.File(out_dir / "invalid_control_freq_zero.h5", "w") as f:
|
|
543
|
+
_base_attrs(f, "invalid_control_freq_zero", robot_profile=profile)
|
|
544
|
+
_robot_states(f)
|
|
545
|
+
_actions(f)
|
|
546
|
+
_video_paths(f, "front", "invalid_control_freq_zero_front.mp4")
|
|
547
|
+
|
|
548
|
+
|
|
549
|
+
def make_inconsistent_video_lengths(out_dir: Path, video_writer: _VideoWriter) -> None:
|
|
550
|
+
"""front MP4 has 10 frames, wrist MP4 has 30 — they cannot represent the same episode."""
|
|
551
|
+
video_writer(out_dir / "invalid_inconsistent_video_lengths_front.mp4", (80, 120, 200), 10)
|
|
552
|
+
video_writer(out_dir / "invalid_inconsistent_video_lengths_wrist.mp4", (200, 80, 80), 30)
|
|
553
|
+
|
|
554
|
+
profile = {**_VALID_ROBOT_PROFILE, "camera_names": ["front", "wrist"]}
|
|
555
|
+
with h5py.File(out_dir / "invalid_inconsistent_video_lengths.h5", "w") as f:
|
|
556
|
+
_base_attrs(f, "invalid_inconsistent_video_lengths", robot_profile=profile)
|
|
557
|
+
_robot_states(f, n=20)
|
|
558
|
+
_actions(f, n=20)
|
|
559
|
+
vp = f.require_group("observations/video_paths")
|
|
560
|
+
vp.create_dataset("front", data="invalid_inconsistent_video_lengths_front.mp4", dtype=_STR_DTYPE)
|
|
561
|
+
vp.create_dataset("wrist", data="invalid_inconsistent_video_lengths_wrist.mp4", dtype=_STR_DTYPE)
|
|
562
|
+
|
|
563
|
+
|
|
564
|
+
def make_video_length_step_mismatch(out_dir: Path, video_writer: _VideoWriter) -> None:
|
|
565
|
+
"""MP4 has 10 frames but robot_states has 100 rows — video was truncated."""
|
|
566
|
+
video_writer(out_dir / "invalid_video_length_step_mismatch_front.mp4", (80, 200, 120), 10)
|
|
567
|
+
with h5py.File(out_dir / "invalid_video_length_step_mismatch.h5", "w") as f:
|
|
568
|
+
_base_attrs(f, "invalid_video_length_step_mismatch")
|
|
569
|
+
_robot_states(f, n=100)
|
|
570
|
+
_actions(f, n=100)
|
|
571
|
+
_video_paths(f, "front", "invalid_video_length_step_mismatch_front.mp4")
|
|
572
|
+
|
|
573
|
+
|
|
574
|
+
# ---------------------------------------------------------------------------
|
|
575
|
+
# Entry point
|
|
576
|
+
# ---------------------------------------------------------------------------
|
|
577
|
+
|
|
578
|
+
_DEFAULT_OUT = Path(__file__).resolve().parent / "samples"
|
|
579
|
+
|
|
580
|
+
_MAKERS_SIMPLE = [
|
|
581
|
+
make_not_h5, make_empty_h5, make_missing_attrs, make_broken_video_ref,
|
|
582
|
+
make_no_video_group, make_annotation_dataset, make_taxonomy_not_json,
|
|
583
|
+
make_mismatched_steps, make_zero_steps, make_actions_missing,
|
|
584
|
+
make_robot_states_missing, make_image_obs_float,
|
|
585
|
+
make_joint_pos_wrong_dof, make_joint_vel_wrong_dof,
|
|
586
|
+
make_robot_state_extra_key, make_robot_state_missing_key,
|
|
587
|
+
make_malformed_profile, make_profile_missing_key, make_profile_no_gripper,
|
|
588
|
+
make_profile_joint_no_names, make_profile_unsupported_action, make_profile_empty_cameras,
|
|
589
|
+
make_profile_missing_rs_key, make_profile_biarm_mismatch,
|
|
590
|
+
make_joint_names_length_mismatch, make_action_names_length_mismatch,
|
|
591
|
+
make_profile_camera_not_in_obs, make_profile_action_not_in_recorded,
|
|
592
|
+
make_profile_rs_key_not_in_recorded, make_multiple_promised_fields_missing,
|
|
593
|
+
make_control_freq_zero,
|
|
594
|
+
]
|
|
595
|
+
|
|
596
|
+
_MAKERS_WITH_VIDEO = [
|
|
597
|
+
make_inconsistent_video_lengths,
|
|
598
|
+
make_video_length_step_mismatch,
|
|
599
|
+
]
|
|
600
|
+
|
|
601
|
+
|
|
602
|
+
def main() -> None:
|
|
603
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
604
|
+
parser.add_argument(
|
|
605
|
+
"output_dir",
|
|
606
|
+
nargs="?",
|
|
607
|
+
type=Path,
|
|
608
|
+
default=_DEFAULT_OUT,
|
|
609
|
+
help=f"Destination directory (default: {_DEFAULT_OUT})",
|
|
610
|
+
)
|
|
611
|
+
args = parser.parse_args()
|
|
612
|
+
out: Path = args.output_dir
|
|
613
|
+
out.mkdir(parents=True, exist_ok=True)
|
|
614
|
+
|
|
615
|
+
for maker in _MAKERS_SIMPLE:
|
|
616
|
+
name = maker.__name__.replace("make_", "")
|
|
617
|
+
print(f" writing {name}...", end=" ", flush=True)
|
|
618
|
+
maker(out)
|
|
619
|
+
print("done")
|
|
620
|
+
|
|
621
|
+
for maker in _MAKERS_WITH_VIDEO:
|
|
622
|
+
name = maker.__name__.replace("make_", "")
|
|
623
|
+
print(f" writing {name}...", end=" ", flush=True)
|
|
624
|
+
maker(out, _write_video)
|
|
625
|
+
print("done")
|
|
626
|
+
|
|
627
|
+
h5_files = list(out.glob("invalid_*.h5"))
|
|
628
|
+
print(f"\nInvalid fixtures written to: {out}")
|
|
629
|
+
print(f" {len(h5_files)} invalid HDF5 files")
|
|
630
|
+
|
|
631
|
+
|
|
632
|
+
if __name__ == "__main__":
|
|
633
|
+
main()
|