|
54 | 54 | _decode_video_frames, |
55 | 55 | _decode_video_rows, |
56 | 56 | _decode_video_windows, |
| 57 | + _episode_ranges, |
| 58 | + _episode_tasks, |
57 | 59 | _image_tensor, |
58 | 60 | _index_names, |
59 | 61 | _open_video_decoder, |
@@ -165,6 +167,81 @@ def _configure_uint8_transform_worker(unused_worker_id): |
165 | 167 |
|
166 | 168 | class LeRobotValidationTest(unittest.TestCase): |
167 | 169 |
|
| 170 | + def test_episode_validation_projects_batched_hf_metadata(self): |
| 171 | + try: |
| 172 | + from datasets import Dataset |
| 173 | + except ImportError: |
| 174 | + self.skipTest("datasets is not installed") |
| 175 | + from pypaimon.multimodal.lerobot.dataset import ( |
| 176 | + _episode_ranges, _episode_tasks, |
| 177 | + ) |
| 178 | + |
| 179 | + rows = [{ |
| 180 | + "episode_index": index, |
| 181 | + "dataset_from_index": index * 2, |
| 182 | + "dataset_to_index": index * 2 + 2, |
| 183 | + "length": 2, |
| 184 | + "tasks": ["place", "pick"], |
| 185 | + "stats/camera/mean": [0.5] * 32, |
| 186 | + } for index in range(2050)] |
| 187 | + episodes = Dataset.from_list(rows) |
| 188 | + expected = [(index * 2, index * 2 + 2) for index in range(2050)] |
| 189 | + with patch.object( |
| 190 | + Dataset, "__getitem__", |
| 191 | + side_effect=AssertionError("Do not format individual rows")): |
| 192 | + self.assertEqual(expected, _episode_ranges( |
| 193 | + {"episodes": episodes}, 4100, 2050)) |
| 194 | + self.assertEqual((("pick", "place"),) * 2050, |
| 195 | + _episode_tasks({"episodes": episodes})) |
| 196 | + self.assertIn("stats/camera/mean", episodes.column_names) |
| 197 | + self.assertEqual(rows[0], episodes[0]) |
| 198 | + self.assertEqual((None,) * 2050, _episode_tasks({ |
| 199 | + "episodes": episodes.remove_columns("tasks"), |
| 200 | + })) |
| 201 | + formatted = episodes.select(range(2)).with_format( |
| 202 | + "numpy", |
| 203 | + columns=["episode_index", "dataset_from_index", |
| 204 | + "dataset_to_index", "length"], |
| 205 | + ) |
| 206 | + self.assertNotIn("tasks", formatted[0]) |
| 207 | + self.assertEqual([(0, 2), (2, 4)], _episode_ranges( |
| 208 | + {"episodes": formatted}, 4, 2)) |
| 209 | + self.assertEqual((None, None), _episode_tasks( |
| 210 | + {"episodes": formatted})) |
| 211 | + visible = formatted.with_format( |
| 212 | + "numpy", columns=formatted.format["columns"], |
| 213 | + output_all_columns=True) |
| 214 | + self.assertEqual((("pick", "place"),) * 2, _episode_tasks( |
| 215 | + {"episodes": visible})) |
| 216 | + # Preserve a selected Dataset's logical row order and range validation. |
| 217 | + with self.assertRaisesRegex(ValueError, "episode_index=1"): |
| 218 | + _episode_ranges({"episodes": episodes.select([1, 0])}, 4, 2) |
| 219 | + with self.assertRaisesRegex(ValueError, "integer controls"): |
| 220 | + _episode_ranges({"episodes": episodes.remove_columns("length")}, |
| 221 | + 4100, 2050) |
| 222 | + |
| 223 | + def test_episode_validation_preserves_metadata_representations(self): |
| 224 | + from pypaimon.multimodal.lerobot.dataset import ( |
| 225 | + _episode_ranges, _episode_tasks, |
| 226 | + ) |
| 227 | + import pandas as pd |
| 228 | + |
| 229 | + rows = [{ |
| 230 | + "episode_index": 0, "dataset_from_index": 0, |
| 231 | + "dataset_to_index": 2, "length": 2, "tasks": "pick", |
| 232 | + }, { |
| 233 | + "episode_index": 1, "dataset_from_index": 2, |
| 234 | + "dataset_to_index": 3, "length": 1, "tasks": None, |
| 235 | + }] |
| 236 | + for episodes in (rows, pd.DataFrame(rows)): |
| 237 | + with self.subTest(type=type(episodes).__name__): |
| 238 | + metadata = {"episodes": episodes} |
| 239 | + self.assertEqual([(0, 2), (2, 3)], |
| 240 | + _episode_ranges(metadata, 3, 2)) |
| 241 | + self.assertEqual((("pick",), None), _episode_tasks(metadata)) |
| 242 | + with self.assertRaisesRegex(ValueError, "expected 4"): |
| 243 | + _episode_ranges(metadata, 4, 2) |
| 244 | + |
168 | 245 | def test_episode_metadata_pickle_stays_small_and_usable(self): |
169 | 246 | try: |
170 | 247 | from datasets import Dataset |
@@ -931,6 +1008,35 @@ def test_torchcodec_batch_matches_single_frame_decoding(self): |
931 | 1008 | finally: |
932 | 1009 | collator.close() |
933 | 1010 |
|
| 1011 | + def test_custom_episode_transform_keeps_cross_column_inputs(self): |
| 1012 | + try: |
| 1013 | + from datasets import Dataset |
| 1014 | + except ImportError as error: |
| 1015 | + self.skipTest(str(error)) |
| 1016 | + |
| 1017 | + episodes = Dataset.from_dict({ |
| 1018 | + "episode_index": [0, 1], |
| 1019 | + "dataset_from_index": [0, 1], |
| 1020 | + "dataset_to_index": [1, 3], |
| 1021 | + "length": [0, 0], |
| 1022 | + "stats": [1, 2], |
| 1023 | + }) |
| 1024 | + |
| 1025 | + def transform(batch): |
| 1026 | + return dict(batch, length=batch["stats"], tasks=[ |
| 1027 | + ["pick" if index == 0 else "place"] |
| 1028 | + for index in batch["episode_index"] |
| 1029 | + ]) |
| 1030 | + |
| 1031 | + for source in (episodes, episodes.add_column( |
| 1032 | + "tasks", [["old"], ["old"]])): |
| 1033 | + with self.subTest(physical_tasks="tasks" in source.column_names): |
| 1034 | + metadata = {"episodes": source.with_transform(transform)} |
| 1035 | + self.assertEqual([(0, 1), (1, 3)], |
| 1036 | + _episode_ranges(metadata, 3, 2)) |
| 1037 | + self.assertEqual((("pick",), ("place",)), |
| 1038 | + _episode_tasks(metadata)) |
| 1039 | + |
934 | 1040 | def test_dataset_requires_supported_python(self): |
935 | 1041 | with patch( |
936 | 1042 | "pypaimon.multimodal.lerobot.dataset.sys.version_info", |
@@ -1038,6 +1144,19 @@ def close(self): |
1038 | 1144 | self.assertEqual([1.0], dataset.meta.stats["action"]["mean"].tolist()) |
1039 | 1145 | self.assertEqual(0, dataset.meta.get_task_index("pick")) |
1040 | 1146 | self.assertEqual("pick", sample["task"]) |
| 1147 | + try: |
| 1148 | + from datasets import Dataset |
| 1149 | + except ImportError: |
| 1150 | + pass |
| 1151 | + else: |
| 1152 | + formatted_metadata = dict(metadata) |
| 1153 | + formatted_metadata["episodes"] = Dataset.from_list( |
| 1154 | + metadata["episodes"]).with_format( |
| 1155 | + "numpy", columns=["episode_index", "dataset_from_index", |
| 1156 | + "dataset_to_index", "length"]) |
| 1157 | + formatted_reader = Reader(formatted_metadata) |
| 1158 | + self.assertEqual("pick", formatted_reader[1]["task"]) |
| 1159 | + formatted_reader.close() |
1041 | 1160 | torch.testing.assert_close( |
1042 | 1161 | sample["observation.state"], torch.tensor([1.0, 2.0])) |
1043 | 1162 | torch.testing.assert_close( |
|
0 commit comments