blob: a52d6246b376a5c515d38dae3774187af8461751 [file]
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import builtins
from array import array
from fractions import Fraction
import importlib.util
import io
import json
import pickle
import shutil
import sys
import tempfile
import threading
import unittest
import weakref
from concurrent.futures import ThreadPoolExecutor
from pathlib import Path
from types import SimpleNamespace
from unittest.mock import Mock, patch
import numpy as np
import pyarrow as pa
import pyarrow.fs as pafs
import pyarrow.parquet as pq
from pypaimon.catalog.catalog_exception import TableNotExistException
from pypaimon.catalog.table_query_auth import TableQueryAuthResult
import pypaimon.multimodal as pmm
from pypaimon.common.identifier import Identifier
from pypaimon.common.options import Options
from pypaimon.common.options.core_options import CoreOptions
from pypaimon.multimodal.source_utils import _SourceFileIO
from pypaimon.multimodal.connection import MultimodalConnection
from pypaimon.multimodal.lerobot import load_from_lerobot
from pypaimon.multimodal.lerobot.dataset import (
_PaimonLeRobotMetadata,
_PyAVVideoDecoder,
_RangeBackedVideo,
_arrow_rows,
_decode_video_frames,
_decode_video_rows,
_decode_video_windows,
_episode_ranges,
_episode_tasks,
_image_tensor,
_index_names,
_open_video_decoder,
_reader_metadata,
_selected_episodes,
_stack_visual_windows,
_torch_row,
)
from pypaimon.multimodal.lerobot.api import _create_target_table
from pypaimon.multimodal.lerobot.metadata import (
_append_arrow_tables,
_companion_identifier,
_dataset_fps,
_integer,
_load_dataset_metadata,
_managed_table_options,
_metadata_table,
_subtask_indices,
_validated_episode_tables,
)
from pypaimon.multimodal.lerobot.loader import (
_image_bytes,
_read_batch,
_video_frame_ordinal,
_video_sample_timestamps,
_validate_frame_controls,
)
from pypaimon.multimodal.lerobot.schema import (
_schema_from_info,
_validate_lerobot_schema,
_validate_v3_required_features,
)
from pypaimon.multimodal.lerobot.source import (
_pandas_index_column,
_LeRobotSource,
_RemoteLeRobotDataset,
_import_lerobot_dataset,
_open_dataset,
_remote_source_path,
_validate_info_paths,
)
from pypaimon.multimodal.table import _target_schema
from pypaimon.tests.video_frame_index_utils import FrameIndex
try:
from lerobot.datasets.lerobot_dataset import LeRobotDataset
except ImportError:
LeRobotDataset = None
try:
import av
except ImportError:
av = None
class _ManualDatasetReader(pmm.PaimonDatasetReader):
def read_indices(self, indices, columns):
raise NotImplementedError
class _NullableLabelReader(pmm.PaimonDatasetReader):
def read_indices(self, indices, columns):
return pa.Table.from_pylist([
{"index": i, "episode_index": 0, "frame_index": i,
"timestamp": i / 10.0, "task_index": 0,
"subtask_index": None if i % 16 == 15 else 0}
for i in indices], schema=self.schema).select(columns)
class _PickleDatasetReader(_ManualDatasetReader):
def __init__(self, value):
self.value = value
def __getstate__(self):
return {"value": self.value}
def _replaced_contract(field, old, new):
description = field.metadata[b"description"].decode("utf-8")
if old not in description:
raise AssertionError("%r is missing from %r" % (old, description))
return {
b"description": description.replace(old, new).encode("utf-8"),
}
def _catalog_rows(connection, name):
try:
table = connection.catalog.get_table(connection._identifier(name))
except TableNotExistException:
prefix, suffix = name.rsplit("_", 1)
if suffix not in ("info", "stats", "tasks", "subtasks"):
raise
sources = _catalog_rows(connection, prefix + "_datasets")
if not sources:
return []
source = sources[0]
if suffix in ("tasks", "subtasks"):
return source[suffix] or []
value = source[suffix]
return ([] if value is None else [
{"key": key, "value": json.dumps(item)}
for key, item in json.loads(value).items()])
builder = table.new_read_builder()
plan = builder.new_scan().plan()
return builder.new_read().to_arrow(plan.splits()).to_pylist()
def _catalog_arrow(connection, name):
table = connection.catalog.get_table(connection._identifier(name))
builder = table.new_read_builder()
plan = builder.new_scan().plan()
return table, builder.new_read().to_arrow(plan.splits())
def _catalog_metadata(connection, name):
return {
row["key"]: json.loads(row["value"])
for row in _catalog_rows(connection, name)
}
def _increment_image(value):
return value + 1
def _configure_uint8_transform_worker(unused_worker_id):
from torch.utils.data import get_worker_info
dataset = get_worker_info().dataset
dataset.return_uint8 = True
dataset.image_transforms = _increment_image
class LeRobotValidationTest(unittest.TestCase):
def test_dataset_fps_accepts_integral_numbers(self):
for value in (30, 30.0, np.int64(30), np.float32(30), np.float64(30), np.longdouble(30)):
with self.subTest(value=value):
self.assertEqual(30, _dataset_fps(value))
self.assertIsInstance(_dataset_fps(value), int)
for value in (True, np.bool_(True), "30", None, 0, -1, 0.0, -30.0,
29.97, float("nan"), float("inf"), -float("inf")):
with self.subTest(value=value):
with self.assertRaises(ValueError):
_dataset_fps(value)
with self.assertRaises(ValueError):
_integer(30.0, "episode_index")
def test_dataset_fps_preserves_extended_precision(self):
if np.finfo(np.longdouble).eps >= np.finfo(float).eps:
self.skipTest("longdouble has no additional precision on this platform")
value = np.longdouble("30.000000000000001")
self.assertNotEqual(value, 30)
self.assertEqual(float(value), 30.0)
with self.assertRaises(ValueError):
_dataset_fps(value)
def test_arrow_task_columns_without_pandas_metadata(self):
for component in ("task", "subtask"):
for text_type in (pa.string(), pa.large_string()):
schema = pa.schema([(component, text_type), (component + "_index", pa.int64())])
self.assertEqual(component, _pandas_index_column(schema, component))
for fields in (
[(component + "_index", pa.int64())],
[(component + "_index", pa.int64()), ("description", pa.string())],
[(component + "_index", pa.int64()), (component, pa.int64())]):
with self.subTest(component=component, fields=fields):
with self.assertRaises(ValueError):
_pandas_index_column(pa.schema(fields), component)
schema = pa.schema([(component + "_index", pa.int64()), (component, pa.string())])
with self.assertRaises(ValueError):
_pandas_index_column(schema.with_metadata({b"pandas": b"invalid"}), component)
def test_episode_validation_projects_batched_hf_metadata(self):
try:
from datasets import Dataset
except ImportError:
self.skipTest("datasets is not installed")
from pypaimon.multimodal.lerobot.dataset import (
_episode_ranges, _episode_tasks,
)
rows = [{
"episode_index": index,
"dataset_from_index": index * 2,
"dataset_to_index": index * 2 + 2,
"length": 2,
"tasks": ["place", "pick"],
"stats/camera/mean": [0.5] * 32,
} for index in range(2050)]
episodes = Dataset.from_list(rows)
expected = [(index * 2, index * 2 + 2) for index in range(2050)]
with patch.object(
Dataset, "__getitem__",
side_effect=AssertionError("Do not format individual rows")):
self.assertEqual(expected, _episode_ranges(
{"episodes": episodes}, 4100, 2050))
self.assertEqual((("pick", "place"),) * 2050,
_episode_tasks({"episodes": episodes}))
self.assertIn("stats/camera/mean", episodes.column_names)
self.assertEqual(rows[0], episodes[0])
self.assertEqual((None,) * 2050, _episode_tasks({
"episodes": episodes.remove_columns("tasks"),
}))
formatted = episodes.select(range(2)).with_format(
"numpy",
columns=["episode_index", "dataset_from_index",
"dataset_to_index", "length"],
)
self.assertNotIn("tasks", formatted[0])
self.assertEqual([(0, 2), (2, 4)], _episode_ranges(
{"episodes": formatted}, 4, 2))
self.assertEqual((None, None), _episode_tasks(
{"episodes": formatted}))
visible = formatted.with_format(
"numpy", columns=formatted.format["columns"],
output_all_columns=True)
self.assertEqual((("pick", "place"),) * 2, _episode_tasks(
{"episodes": visible}))
# Preserve a selected Dataset's logical row order and range validation.
with self.assertRaisesRegex(ValueError, "episode_index=1"):
_episode_ranges({"episodes": episodes.select([1, 0])}, 4, 2)
with self.assertRaisesRegex(ValueError, "integer controls"):
_episode_ranges({"episodes": episodes.remove_columns("length")},
4100, 2050)
def test_episode_validation_preserves_metadata_representations(self):
from pypaimon.multimodal.lerobot.dataset import (
_episode_ranges, _episode_tasks,
)
import pandas as pd
rows = [{
"episode_index": 0, "dataset_from_index": 0,
"dataset_to_index": 2, "length": 2, "tasks": "pick",
}, {
"episode_index": 1, "dataset_from_index": 2,
"dataset_to_index": 3, "length": 1, "tasks": None,
}]
for episodes in (rows, pd.DataFrame(rows)):
with self.subTest(type=type(episodes).__name__):
metadata = {"episodes": episodes}
self.assertEqual([(0, 2), (2, 3)],
_episode_ranges(metadata, 3, 2))
self.assertEqual((("pick",), None), _episode_tasks(metadata))
with self.assertRaisesRegex(ValueError, "expected 4"):
_episode_ranges(metadata, 4, 2)
def test_episode_metadata_pickle_stays_small_and_usable(self):
try:
from datasets import Dataset
except ImportError:
self.skipTest("datasets is not installed")
rows = [{
"episode_index": index,
"dataset_from_index": index * 400,
"dataset_to_index": (index + 1) * 400,
"length": 400,
"tasks": ["pick", "place"],
} for index in range(50)]
episodes_arrow = pa.Table.from_pylist(rows)
fingerprint = "0123456789abcdef"
episodes = Dataset(episodes_arrow, fingerprint=fingerprint)
metadata = _PaimonLeRobotMetadata(
"robot", "tag", {"fps": 50}, None, episodes, ["pick", "place"],
None)
metadata._compress_episodes = True
payload = pickle.dumps(metadata)
self.assertLess(len(payload), len(pickle.dumps(episodes)) * 3 // 4)
restored = pickle.loads(payload)
self.assertIsInstance(restored.episodes, Dataset)
self.assertEqual(episodes[:], restored.episodes[:])
self.assertEqual(episodes.features, restored.episodes.features)
self.assertEqual(episodes._fingerprint, restored.episodes._fingerprint)
self.assertEqual("tag", restored.revision)
self.assertEqual(50, restored.fps)
episodes.set_format("numpy")
restored = pickle.loads(pickle.dumps(metadata))
self.assertEqual("numpy", restored.episodes.format["type"])
episodes.reset_format()
metadata.episodes = episodes.with_format("numpy")
restored = pickle.loads(pickle.dumps(metadata))
self.assertEqual("numpy", restored.episodes.format["type"])
with tempfile.TemporaryDirectory() as directory:
path = str(Path(directory) / "episodes.arrow")
with pa.OSFile(path, "wb") as output:
with pa.ipc.new_stream(
output, episodes.data.table.schema) as writer:
writer.write_table(episodes.data.table)
metadata.episodes = Dataset.from_file(path)
restored = pickle.loads(pickle.dumps(metadata))
self.assertEqual(
metadata.episodes.cache_files,
restored.episodes.cache_files,
)
def test_video_columns_decode_in_parallel(self):
barrier = threading.Barrier(2)
class Collator:
def __init__(self, video_column):
self.video_column = video_column
self.output_column = video_column + "_decoded"
def __call__(self, rows):
barrier.wait(timeout=5)
return [dict(
row,
**{self.output_column: row[self.video_column] + 10},
) for row in rows]
rows = {
0: {"camera_a": 1, "camera_b": 2},
1: {"camera_a": 3, "camera_b": 4},
}
_decode_video_rows(
[rows], [Collator("camera_a"), Collator("camera_b")])
self.assertEqual(11, rows[0]["camera_a_decoded"])
self.assertEqual(12, rows[0]["camera_b_decoded"])
self.assertEqual(13, rows[1]["camera_a_decoded"])
self.assertEqual(14, rows[1]["camera_b_decoded"])
def test_video_column_decode_parallelism_is_bounded(self):
worker_counts = []
def executor(*args, **kwargs):
worker_counts.append(kwargs["max_workers"])
return ThreadPoolExecutor(*args, **kwargs)
class Collator:
def __init__(self, index):
self.video_column = "camera_%d" % index
self.output_column = self.video_column
def __call__(self, rows):
return rows
rows = {
0: {"camera_%d" % index: index for index in range(9)},
}
with patch(
"pypaimon.multimodal.video.ThreadPoolExecutor",
side_effect=executor):
_decode_video_rows(
[rows], [Collator(index) for index in range(9)])
self.assertEqual([8], worker_counts)
def test_video_row_fallback_preserves_torch_mode(self):
try:
import torch
except ImportError as error:
self.skipTest(str(error))
from pypaimon.multimodal.video import _VideoDecodePool
class Collator:
video_column = "camera"
output_column = "frame"
def __call__(self, rows):
mode = (torch.is_grad_enabled(),
torch.is_inference_mode_enabled())
return [dict(row, frame=mode) for row in rows]
pool = _VideoDecodePool()
try:
for context, expected in (
(torch.enable_grad(), (True, False)),
(torch.no_grad(), (False, False)),
(torch.inference_mode(), (False, True))):
with self.subTest(expected=expected), context:
rows = {0: {"camera": 1}}
_decode_video_rows([rows], [Collator()], decode_pool=pool)
self.assertEqual(expected, rows[0]["frame"])
finally:
pool.close()
@unittest.skipUnless(
av is not None and importlib.util.find_spec("torch") is not None,
"PyAV and Torch are required for video decoding",
)
def test_pyav_decoder_reuses_windows_and_seeks_known_frames(self):
class Frame:
time_base = Fraction(1, 10)
def __init__(self, pts):
self.pts = pts
self.key_frame = pts % 10 == 0
def to_ndarray(self, format):
assert format == "rgb24"
return np.full((2, 2, 3), self.pts, dtype=np.uint8)
class Container:
def __init__(self):
self.stream = SimpleNamespace(time_base=Fraction(1, 10))
self.streams = SimpleNamespace(video=[self.stream])
self.position = 0
self.decoded = 0
self.seeks = []
def decode(self, stream):
assert stream is self.stream
while self.position < 120:
index = self.position
self.position += 1
self.decoded += 1
yield Frame(index)
def seek(self, offset, *, backward, any_frame, stream):
assert backward
assert not any_frame
assert stream is self.stream
self.seeks.append(offset)
self.position = offset
def close(self):
pass
container = Container()
with patch("av.open", return_value=container):
decoder = _PyAVVideoDecoder(io.BytesIO())
try:
for index in range(119):
decoder[index]
decoder[index + 1]
self.assertEqual(120, container.decoded)
self.assertEqual([], container.seeks)
decoder[5]
decoder[90]
decoder[8]
self.assertEqual(136, container.decoded)
self.assertEqual([0, 90, 0], container.seeks)
finally:
decoder.close()
def test_pyav_decoder_indexes_cold_random_reads(self):
class Frame:
time_base = Fraction(1, 10)
def __init__(self, pts):
self.pts = pts
self.key_frame = pts % 10 == 0
def to_ndarray(self, format):
assert format == "rgb24"
return np.full((2, 2, 3), self.pts, dtype=np.uint8)
class Container:
def __init__(self):
self.stream = SimpleNamespace(time_base=Fraction(1, 10))
self.streams = SimpleNamespace(video=[self.stream])
self.position = 0
self.decoded = 0
self.demuxed = 0
self.seeks = []
def demux(self, stream):
assert stream is self.stream
for index in range(120):
self.demuxed += 1
yield SimpleNamespace(
pts=index, time_base=Fraction(1, 10),
is_discard=False, is_keyframe=index % 10 == 0,
)
def decode(self, stream):
assert stream is self.stream
while self.position < 120:
index = self.position
self.position += 1
self.decoded += 1
yield Frame(index)
def seek(self, offset, *, backward, any_frame, stream):
assert backward
assert not any_frame
assert stream is self.stream
self.seeks.append(offset)
self.position = offset
def close(self):
pass
container = Container()
fake_av = SimpleNamespace(open=lambda unused_stream: container)
tensor = staticmethod(
lambda frame: frame.to_ndarray(format="rgb24"))
with patch.dict(sys.modules, {"av": fake_av}), patch.object(
_PyAVVideoDecoder, "_tensor", tensor):
decoder = _PyAVVideoDecoder(io.BytesIO())
try:
frame = decoder[95]
self.assertEqual(120, container.demuxed)
self.assertEqual([90], container.seeks)
self.assertEqual(6, container.decoded)
self.assertTrue((frame == 95).all())
finally:
decoder.close()
def test_range_backed_video_bounds_prefetch_and_cache(self):
payload = bytes(range(256))
calls = []
def read_ranges(ranges):
self.assertLessEqual(sum(length for _, length in ranges), 16)
calls.append(list(ranges))
return [payload[offset:offset + length] for offset, length in ranges]
with patch.object(_RangeBackedVideo, '_MAX_CACHE_BYTES', 16):
reader = _RangeBackedVideo(len(payload), read_ranges)
reader.prefetch([(0, 8), (64, 8), (128, 8), (192, 8)])
self.assertEqual([[(0, 8), (64, 8)]], calls)
for offset in (192, 0, 128, 64, 192):
reader.prefetch([(offset, 48)])
reader.seek(offset)
self.assertEqual(payload[offset:offset + 32], reader.read(32))
self.assertLessEqual(sum(end - start for start, end, _ in reader._segments), 16)
reader.seek(0)
self.assertEqual(payload, reader.read())
reader.close()
self.assertEqual([], reader._segments)
def test_range_backed_video_fetches_only_missing_bytes(self):
payload = bytes(range(32))
calls = []
def read_ranges(ranges):
calls.append(list(ranges))
return [
payload[offset:offset + length]
for offset, length in ranges
]
reader = _RangeBackedVideo(len(payload), read_ranges)
reader.prefetch([(0, 4), (12, 4)])
self.assertEqual([[(0, 4), (12, 4)]], calls)
calls.clear()
reader.seek(2)
self.assertEqual(payload[2:4], reader.read(2))
self.assertEqual([], calls)
reader.seek(6)
self.assertEqual(payload[6:14], reader.read(8))
self.assertEqual([[(6, 6)]], calls)
calls.clear()
reader.seek(18)
output = bytearray(4)
self.assertEqual(4, reader.readinto(output))
self.assertEqual(payload[18:22], bytes(output))
self.assertEqual([[(18, 4)]], calls)
self.assertEqual(len(payload) - 3, reader.seek(-3, io.SEEK_END))
self.assertEqual(payload[-3:], reader.read())
with self.assertRaisesRegex(ValueError, "Negative seek"):
reader.seek(-1)
reader.close()
@unittest.skipUnless(
av is not None and importlib.util.find_spec("torch") is not None,
"PyAV and Torch are required for video decoding",
)
@patch.object(_RangeBackedVideo, '_MAX_CACHE_BYTES', 1024)
def test_indexed_pyav_decoder_handles_b_frames_and_fragmented_video(self):
cases = (
("mpeg4", None, None),
(
"libx264",
{"movflags": "frag_keyframe+empty_moov+default_base_moof"},
None,
),
(
"libx265",
None,
{"x265-params":
"keyint=12:min-keyint=12:scenecut=0:log-level=error"},
),
)
for codec, container_options, stream_options in cases:
with self.subTest(codec=codec):
output = io.BytesIO()
with av.open(
output,
mode="w",
format="mp4",
options=container_options) as container:
stream = container.add_stream(codec, rate=30)
stream.width = 16
stream.height = 16
stream.pix_fmt = "yuv420p"
stream.gop_size = 12
stream.codec_context.max_b_frames = 2
if stream_options is not None:
stream.options = stream_options
for index in range(60):
image = np.full(
(16, 16, 3), index + 24, dtype=np.uint8)
frame = av.VideoFrame.from_ndarray(
image, format="rgb24")
frame.pts = index
frame.time_base = Fraction(1, 30)
for packet in stream.encode(frame):
container.mux(packet)
for packet in stream.encode():
container.mux(packet)
payload = output.getvalue()
index = None if container_options else FrameIndex.inspect(
io.BytesIO(payload), len(payload))
with av.open(io.BytesIO(payload)) as container:
expected = np.stack([
np.array(frame.to_ndarray(format="rgb24"), copy=True)
for frame in container.decode(video=0)
])
source = io.BytesIO(payload)
source.video_length = len(payload)
range_calls = []
def read_ranges(ranges):
self.assertLessEqual(
sum(length for _, length in ranges),
_RangeBackedVideo._MAX_CACHE_BYTES)
range_calls.append(list(ranges))
return [
payload[offset:offset + length]
for offset, length in ranges
]
source.video_read_ranges = read_ranges
decoder = _PyAVVideoDecoder(source, index)
try:
indices = list(range(len(expected)))
if codec == "mpeg4":
plan = ({0: indices}, [])
with patch.object(
decoder, "_indexed_plan", return_value=plan):
actual = decoder.get_frames_at(
indices=indices
).data.permute(0, 2, 3, 1).numpy()
self.assertGreater(len(range_calls), 1)
else:
actual = decoder.get_frames_at(
indices=indices
).data.permute(0, 2, 3, 1).numpy()
np.testing.assert_array_equal(expected, actual)
if codec == "libx264":
decoder[0]
actual = decoder.get_frames_at(
indices=list(range(16))
).data.permute(0, 2, 3, 1).numpy()
np.testing.assert_array_equal(expected[:16], actual)
finally:
decoder.close()
@unittest.skipUnless(
av is not None and importlib.util.find_spec("torch") is not None,
"PyAV and Torch are required for video decoding",
)
def test_indexed_pyav_decoder_reuses_session_across_batches(self):
output = io.BytesIO()
with av.open(output, mode="w", format="mp4") as container:
stream = container.add_stream("libx264", rate=30)
stream.width = stream.height = 32
stream.pix_fmt = "yuv420p"
stream.options = {"x264-params":
"keyint=12:min-keyint=12:scenecut=0:bframes=2:threads=1"}
for ordinal in range(48):
frame = av.VideoFrame.from_ndarray(
np.full((32, 32, 3), ordinal * 4, dtype=np.uint8),
format="rgb24")
for packet in stream.encode(frame):
container.mux(packet)
for packet in stream.encode():
container.mux(packet)
payload = output.getvalue()
index = FrameIndex.inspect(io.BytesIO(payload), len(payload))
with av.open(io.BytesIO(payload)) as container:
expected = [frame.to_ndarray(format="rgb24")
for frame in container.decode(video=0)]
source = io.BytesIO(payload)
decoder = _PyAVVideoDecoder(source, index, thread_count=1)
inputs = []
original_open = av.open
def open_container(value):
inputs.append(value)
return original_open(value)
try:
with patch.object(av, "open", side_effect=open_container):
for indices in ([31, 0, 12, 31], [45, 2, 17, 45], [0, 47, 1]):
actual = decoder.get_frames_at(indices=indices).data
np.testing.assert_array_equal(
np.stack([expected[i] for i in indices]),
actual.permute(0, 2, 3, 1).numpy())
self.assertEqual(1, len(inputs))
self.assertFalse(inputs[0].closed)
self.assertEqual([], inputs[0]._segments)
finally:
decoder.close()
self.assertTrue(inputs[0].closed)
self.assertEqual([], inputs[0]._segments)
self.assertIsNone(decoder._stream)
self.assertFalse(source.closed)
@unittest.skipUnless(av is not None, "PyAV is required")
def test_indexed_pyav_decoder_closes_failed_session_before_retry(self):
source = io.BytesIO(b"0123456789abcdef")
index = FrameIndex([(0, 4)], [(0, 0, 4)])
decoder = _PyAVVideoDecoder(source, index)
inputs = []
containers = []
def open_container(value):
inputs.append(value)
container = Mock()
container.streams.video = [SimpleNamespace(thread_count=0)]
containers.append(container)
return container
with patch.object(av, "open", side_effect=open_container), patch.object(
decoder, "_decode_indexed_group", side_effect=ValueError("decode failed")) as decode:
with self.assertRaisesRegex(ValueError, "decode failed"):
decoder._decode_indexed([0])
self.assertTrue(inputs[0].closed)
containers[0].close.assert_called_once_with()
def read_group(container, stream, anchor, targets, decoded):
decoded[0] = b"frame"
decode.side_effect = read_group
self.assertEqual({0: b"frame"}, decoder._decode_indexed([0]))
self.assertEqual(2, len(inputs))
self.assertFalse(inputs[1].closed)
decoder.close()
decoder.close()
self.assertTrue(inputs[1].closed)
containers[1].close.assert_called_once_with()
@unittest.skipUnless(av is not None, "PyAV is required")
def test_indexed_plan_prefetches_requested_gops_with_bounded_lookahead(self):
source = io.BytesIO(b"")
source.video_length = 1000000
index = FrameIndex(
[(900000, 100000)],
[(0, 0, 100000), (10, 10, 300000),
(20, 20, 500000), (30, 30, 700000)])
decoder = _PyAVVideoDecoder(source, index)
try:
self.assertEqual(({1: [12]}, [(300000, 232768)]),
decoder._indexed_plan([12]))
self.assertEqual(({1: [19], 2: [20]}, [(300000, 432768)]),
decoder._indexed_plan([19, 20]))
self.assertEqual(({0: [0], 3: [39]}, [(100000, 232768), (700000, 300000)]),
decoder._indexed_plan([0, 39]))
finally:
decoder.close()
@unittest.skipUnless(av is not None, "PyAV is required")
def test_indexed_gop_boundary_avoids_extra_range_read(self):
output = io.BytesIO()
with av.open(output, mode="w", format="mp4") as container:
stream = container.add_stream("libx264", rate=30)
stream.width, stream.height = 160, 120
stream.pix_fmt = "yuv420p"
stream.options = {"x264-params":
"keyint=12:min-keyint=12:scenecut=0:bframes=2:threads=1"}
rng = np.random.RandomState(12)
for ordinal in range(60):
frame = av.VideoFrame.from_ndarray(
rng.randint(0, 256, (120, 160, 3), dtype=np.uint8),
format="rgb24")
frame.pts, frame.time_base = ordinal, Fraction(1, 30)
for packet in stream.encode(frame):
container.mux(packet)
for packet in stream.encode():
container.mux(packet)
payload = output.getvalue()
index = FrameIndex.inspect(io.BytesIO(payload), len(payload))
with av.open(io.BytesIO(payload)) as container:
expected = [frame.to_ndarray(format="rgb24")
for frame in container.decode(video=0)]
for target in (23, 35):
counts = []
for lookahead in (False, True):
source = io.BytesIO(payload)
source.video_length = len(payload)
calls = []
def read_ranges(ranges):
calls.extend(ranges)
return [payload[start:start + size] for start, size in ranges]
source.video_read_ranges = read_ranges
decoder = _PyAVVideoDecoder(source, index)
if not lookahead:
anchor = next(i for i, entry in enumerate(index.keyframes)
if entry[0] <= target < index.keyframes[i + 1][0])
start = index.keyframes[anchor][2]
end = index.keyframes[anchor + 1][2]
decoder._indexed_plan = lambda indices: (
{anchor: indices}, [(start, end - start)])
try:
actual = decoder.get_frames_at(indices=[target]).data
np.testing.assert_array_equal(
expected[target], actual[0].permute(1, 2, 0).numpy())
counts.append(len(calls))
finally:
decoder.close()
self.assertLess(counts[1], counts[0])
def test_default_video_backend_falls_back_on_os_error(self):
stream = Mock(spec=["seek"])
decoder = object()
with patch(
"pypaimon.multimodal.lerobot.dataset."
"_open_torchcodec_decoder",
side_effect=OSError("unavailable")), patch(
"pypaimon.multimodal.lerobot.dataset._PyAVVideoDecoder",
return_value=decoder) as pyav:
self.assertIs(decoder, _open_video_decoder(stream))
stream.seek.assert_called_once_with(0)
pyav.assert_called_once_with(stream)
with patch(
"pypaimon.multimodal.lerobot.dataset."
"_open_torchcodec_decoder",
side_effect=OSError("unavailable")):
with self.assertRaises(OSError):
_open_video_decoder(stream, backend="torchcodec")
def test_indexed_default_backend_falls_back_to_torchcodec(self):
stream = Mock()
stream.video_keyframe_index = FrameIndex(
[], [(0, 0, 0)])
decoder = object()
module = "pypaimon.multimodal.lerobot.dataset."
with patch(
module + "_PyAVVideoDecoder",
side_effect=ImportError("no av")) as pyav, patch(
module + "_open_torchcodec_decoder",
return_value=decoder) as torchcodec:
self.assertIs(decoder, _open_video_decoder(stream))
stream.seek.assert_called_once_with(0)
pyav.assert_called_once_with(
stream, stream.video_keyframe_index)
torchcodec.assert_called_once_with(stream)
stream.reset_mock()
with patch(
module + "_PyAVVideoDecoder",
side_effect=ImportError("no av")), patch(
module + "_open_torchcodec_decoder") as torchcodec:
with self.assertRaisesRegex(ImportError, "no av"):
_open_video_decoder(stream, backend="pyav")
torchcodec.assert_not_called()
def test_indexed_default_backend_falls_back_after_lazy_failure(self):
if av is None:
self.skipTest("PyAV is required for its real exception types")
try:
av.open(io.BytesIO(b"invalid video"))
except av.error.InvalidDataError as error:
invalid_data_error = error
else:
self.fail("Invalid video did not raise InvalidDataError")
stream = Mock()
stream.video_keyframe_index = FrameIndex(
[], [(0, 0, 0)])
pyav_decoder = Mock()
pyav_decoder.get_frames_at.side_effect = invalid_data_error
torchcodec_decoder = Mock()
expected = object()
torchcodec_decoder.get_frames_at.return_value = expected
module = "pypaimon.multimodal.lerobot.dataset."
with patch(
module + "_PyAVVideoDecoder",
return_value=pyav_decoder), patch(
module + "_open_torchcodec_decoder",
return_value=torchcodec_decoder) as torchcodec:
decoder = _open_video_decoder(stream)
self.assertIs(expected, decoder.get_frames_at(indices=[3]))
pyav_decoder.close.assert_called_once_with()
stream.seek.assert_called_once_with(0)
torchcodec.assert_called_once_with(stream)
decoder.close()
torchcodec_decoder.close.assert_called_once_with()
stream.reset_mock()
with patch(
module + "_PyAVVideoDecoder",
return_value=pyav_decoder), patch(
module + "_open_torchcodec_decoder") as torchcodec:
decoder = _open_video_decoder(stream, backend="pyav")
with self.assertRaises(av.error.InvalidDataError):
decoder.get_frames_at(indices=[3])
stream.seek.assert_not_called()
torchcodec.assert_not_called()
def test_indexed_default_backend_falls_back_on_ffmpeg_error(self):
if av is None:
self.skipTest("PyAV is required for its real exception types")
stream = Mock()
stream.video_keyframe_index = FrameIndex(
[], [(0, 0, 0)])
pyav_decoder = Mock()
pyav_decoder.get_frames_at.side_effect = av.error.FFmpegError(
1, "cannot open", "video")
torchcodec_decoder = Mock()
expected = object()
torchcodec_decoder.get_frames_at.return_value = expected
module = "pypaimon.multimodal.lerobot.dataset."
with patch(
module + "_PyAVVideoDecoder",
return_value=pyav_decoder), patch(
module + "_open_torchcodec_decoder",
return_value=torchcodec_decoder):
decoder = _open_video_decoder(stream)
self.assertIs(expected, decoder.get_frames_at(indices=[3]))
stream.seek.assert_called_once_with(0)
def test_table_option_limits_video_decoders(self):
from pypaimon.filesystem.local_file_io import LocalFileIO
info = {
"codebase_version": "v3.0", "fps": 10,
"total_frames": 1, "total_episodes": 1, "total_tasks": 1,
"features": {
"index": {"dtype": "int64", "shape": [1]},
"episode_index": {"dtype": "int64", "shape": [1]},
"frame_index": {"dtype": "int64", "shape": [1]},
"timestamp": {"dtype": "float32", "shape": [1]},
"task_index": {"dtype": "int64", "shape": [1]},
"camera": {"dtype": "video", "shape": [2, 2, 3],
"names": ["height", "width", "channels"]},
"camera_b": {"dtype": "video", "shape": [2, 2, 3],
"names": ["height", "width", "channels"]},
},
}
metadata = _reader_metadata({
"repo_id": "test/video-cache-option", "info": info,
"episodes": [{"episode_index": 0, "dataset_from_index": 0,
"dataset_to_index": 1, "length": 1,
"tasks": ["pick"]}],
"tasks": ["pick"],
})
module = "pypaimon.multimodal.lerobot.dataset."
option = "read.video.max-open-decoders"
for configured, expected in ((None, 16), ("2", 2)):
with self.subTest(configured=configured):
options = {} if configured is None else {
option: configured, "read.video.codec-threads": configured,
"read.video.prefetch-max-bytes": configured}
frames = SimpleNamespace(
options=CoreOptions(Options(options)))
rows = SimpleNamespace(
_table=frames, num_rows=1, file_io=LocalFileIO(),
close=lambda: None)
with patch(module + "_load_dataset", return_value=(frames, metadata)), \
patch(module + "_target_schema",
return_value=_schema_from_info(info)), \
patch(module + "_PaimonTableFrameReader",
return_value=rows):
dataset = pmm.PaimonLeRobotDataset(Mock())
try:
self.assertEqual([expected, expected], [
c.max_open_videos for c in dataset.reader._video_collators])
self.assertEqual([0 if configured is None else 2] * 2, [
c.prefetch_max_bytes for c in dataset.reader._video_collators])
self.assertEqual([1 if configured is None else 2] * 2, [
c.decoder_factory.keywords["codec_threads"]
for c in dataset.reader._video_collators])
finally:
dataset.reader.close()
for configured in ("0", "bad", True, 1.5):
with self.subTest(configured=configured):
frames = SimpleNamespace(
options=CoreOptions(Options({option: configured})))
rows = SimpleNamespace(
_table=frames, num_rows=1, file_io=LocalFileIO(),
close=lambda: None)
with patch(module + "_load_dataset", return_value=(frames, metadata)), \
patch(module + "_target_schema",
return_value=_schema_from_info(info)), \
patch(module + "_PaimonTableFrameReader",
return_value=rows):
with self.assertRaisesRegex(ValueError, option):
pmm.PaimonLeRobotDataset(Mock())
def test_nullable_windows_share_one_storage_across_workers(self):
try:
import torch
except ImportError as error:
self.skipTest(str(error))
metadata = {
"info": {
"codebase_version": "v3.0", "fps": 10,
"total_frames": 128, "total_episodes": 1, "total_tasks": 1,
"features": {
key: {"dtype": dtype, "shape": [1]}
for key, dtype in (("index", "int64"), ("episode_index", "int64"),
("frame_index", "int64"), ("timestamp", "float32"),
("task_index", "int64"), ("subtask_index", "int64"))
},
},
"tasks": ["pick"], "subtasks": ["reach"],
"episodes": [{"episode_index": 0, "dataset_from_index": 0,
"dataset_to_index": 128, "length": 128, "tasks": ["pick"]}],
}
reader = _NullableLabelReader(
metadata, delta_timestamps={"subtask_index": [i / 10.0 for i in range(-1, 31)]})
dataset = pmm.PaimonLeRobotDataset(reader)
try:
samples = dataset.get_items([0, 0])
samples[0]["subtask_index"][0].fill_(-100)
self.assertEqual(0, samples[0]["subtask_index"][1].item())
self.assertEqual(0, samples[1]["subtask_index"][0].item())
loader = dataset.to_dataloader(
batch_size=64, num_workers=1, multiprocessing_context="spawn")
for batch in loader:
labels = batch["subtask_index"]
mask = batch["subtask_index_is_valid"]
self.assertTrue(labels.is_shared())
self.assertTrue(mask.is_shared())
self.assertEqual((64, 32), tuple(labels.shape))
self.assertEqual(labels.shape, mask.shape)
self.assertEqual(torch.int64, labels.dtype)
self.assertEqual(torch.bool, mask.dtype)
for offset, index in enumerate(batch["index"].tolist()):
for column, delta in enumerate(range(-1, 31)):
position = min(127, max(0, index + delta))
expected = position % 16 != 15
self.assertEqual(expected, mask[offset, column].item())
self.assertEqual(0 if expected else -1, labels[offset, column].item())
before = labels.clone()
labels[0, 0] = -100
self.assertTrue(torch.equal(before.flatten()[1:], labels.flatten()[1:]))
finally:
dataset.close()
def test_nullable_batch_contract_is_stable(self):
import pytest
torch = pytest.importorskip("torch")
from pypaimon.multimodal.lerobot.dataset import _collate_lerobot_batch
for labels, expected, validity in (
([torch.tensor(0), None], [0, -1], [True, False]),
([torch.tensor(0), torch.tensor(1)], [0, 1], [True, True]),
([None, None], [-1, -1], [False, False]),
([[None, torch.tensor(1)], torch.tensor([2, 3])],
[[-1, 1], [2, 3]], [[False, True], [True, True]])):
batch = _collate_lerobot_batch([{"subtask_index": label} for label in labels], {})
self.assertEqual(expected, batch["subtask_index"].tolist())
self.assertEqual(validity, batch["subtask_index_is_valid"].tolist())
self.assertEqual(torch.int64, batch["subtask_index"].dtype)
self.assertEqual(torch.bool, batch["subtask_index_is_valid"].dtype)
def test_nullable_label_storage_preserves_mixed_shapes(self):
try:
import torch
except ImportError as error:
self.skipTest(str(error))
from pypaimon.multimodal.lerobot.dataset import _pack_nullable_labels
scalar = torch.tensor(3)
rows = [{"subtask_index": [scalar, scalar, None]},
{"subtask_index": torch.tensor([4, 5, 6])},
{"subtask_index": scalar}, {"subtask_index": None}]
_pack_nullable_labels(rows)
first = rows[0]["subtask_index"]
self.assertIsNone(first[2])
self.assertIsNone(rows[3]["subtask_index"])
tensors = first[:2] + [rows[1]["subtask_index"], rows[2]["subtask_index"]]
self.assertEqual(1, len({value.untyped_storage().data_ptr() for value in tensors}))
first[0].fill_(-100)
self.assertEqual(3, first[1].item())
self.assertEqual(3, rows[2]["subtask_index"].item())
self.assertEqual([4, 5, 6], rows[1]["subtask_index"].tolist())
self.assertEqual(3, scalar.item())
valid = [{"subtask_index": scalar}]
_pack_nullable_labels(valid)
self.assertIs(scalar, valid[0]["subtask_index"])
empty = [{"subtask_index": [None, None]}]
_pack_nullable_labels(empty)
self.assertEqual([None, None], empty[0]["subtask_index"])
def test_custom_reader_empty_subtask_vocabulary(self):
try:
import torch # noqa: F401
except ImportError as error:
self.skipTest(str(error))
metadata = {
"info": {
"codebase_version": "v3.0", "fps": 10,
"total_frames": 1, "total_episodes": 1, "total_tasks": 1,
"features": {
key: {"dtype": dtype, "shape": [1]}
for key, dtype in (("index", "int64"), ("episode_index", "int64"),
("frame_index", "int64"), ("timestamp", "float32"),
("task_index", "int64"), ("subtask_index", "int64"))
},
},
"tasks": ["pick"], "subtasks": [],
"episodes": [{"episode_index": 0, "dataset_from_index": 0,
"dataset_to_index": 1, "length": 1, "tasks": ["pick"]}],
}
class Reader(pmm.PaimonDatasetReader):
label = None
def read_indices(self, indices, columns):
return pa.Table.from_pylist([
{"index": 0, "episode_index": 0, "frame_index": 0,
"timestamp": 0.0, "task_index": 0, "subtask_index": self.label}
for unused in indices], schema=self.schema).select(columns)
reader = Reader(metadata)
self.assertIsNone(reader[0]["subtask"])
batch = next(iter(pmm.PaimonLeRobotDataset(reader).to_dataloader(
batch_size=1, num_workers=0)))
self.assertEqual([None], batch["subtask"])
reader.label = 0
with self.assertRaises(ValueError):
reader[0]
def test_video_batches_include_delta_frames_and_preserve_backends(self):
try:
import torch
except ImportError as error:
self.skipTest(str(error))
metadata = {
"repo_id": "test/video-batches",
"info": {
"codebase_version": "v3.0", "fps": 10,
"total_frames": 3, "total_episodes": 1, "total_tasks": 1,
"features": {
"index": {"dtype": "int64", "shape": [1]},
"episode_index": {"dtype": "int64", "shape": [1]},
"frame_index": {"dtype": "int64", "shape": [1]},
"timestamp": {"dtype": "float32", "shape": [1]},
"task_index": {"dtype": "int64", "shape": [1]},
"camera": {"dtype": "video", "shape": [2, 2, 3],
"names": ["height", "width", "channels"]},
},
},
"episodes": [{"episode_index": 0, "dataset_from_index": 0,
"dataset_to_index": 3, "length": 3,
"tasks": ["pick"]}],
"tasks": ["pick"],
}
class Reader(pmm.PaimonDatasetReader):
# This test checks batching and backend cache reuse with one decoder.
# Replica splitting is covered in video_decode_parallelism_test.py.
_table = SimpleNamespace(options=CoreOptions(Options({
"read.video.decoders-per-payload": "1",
})))
def read_indices(self, indices, columns):
return pa.Table.from_pylist([{
"index": index, "episode_index": 0,
"frame_index": index, "timestamp": index / 10,
"task_index": 0,
"camera": pmm.VideoFrameDescriptor(
"file:///shared.video", 0, 5, index + 3
).serialize(),
} for index in indices], schema=self.schema).select(columns)
for backend, batch in (("torchcodec", True), (None, True),
("pyav", False), (None, False)):
with self.subTest(backend=backend, batch=batch):
calls = []
class Decoder:
def __getitem__(self, index):
calls.append(index)
return torch.full((3, 2, 2), index, dtype=torch.uint8)
class BatchDecoder(Decoder):
def __getitem__(self, index):
raise AssertionError("unexpected single-frame call")
def get_frames_at(self, *, indices):
calls.append(indices)
return SimpleNamespace(data=torch.stack([
torch.full((3, 2, 2), i, dtype=torch.uint8)
for i in indices
]))
decoder = BatchDecoder() if batch else Decoder()
module = "pypaimon.multimodal.lerobot.dataset."
with patch(
module + "_open_torchcodec_decoder",
return_value=decoder,
side_effect=None if batch else OSError("unavailable"),
) as open_torchcodec, patch(
module + "_PyAVVideoDecoder", return_value=decoder,
) as open_pyav:
file_io = SimpleNamespace(
new_input_stream=lambda path: io.BytesIO(b"video"))
reader = Reader(
metadata, file_io=file_io, video_backend=backend,
delta_timestamps={"camera": [-0.1, 0.0, 0.1]},
)
try:
last, first, duplicate = reader.get_items([2, 0, 2])
self.assertEqual([[3, 4, 5]] if batch else [3, 4, 5], calls)
self.assertEqual([3, 3, 2, 2], list(last["camera"].shape))
torch.testing.assert_close(
last["camera"][:, 0, 0, 0],
torch.tensor([4, 5, 5], dtype=torch.float32) / 255)
torch.testing.assert_close(
first["camera"][:, 0, 0, 0],
torch.tensor([3, 3, 4], dtype=torch.float32) / 255)
self.assertEqual([False, False, True],
last["camera_is_pad"].tolist())
self.assertEqual([True, False, False],
first["camera_is_pad"].tolist())
last["camera"].zero_()
self.assertGreater(float(duplicate["camera"].sum()), 0)
reader.get_items([1])
self.assertEqual(
[[3, 4, 5]] * 2 if batch else [3, 4, 5] * 2, calls)
self.assertEqual(0 if backend == "pyav" else 1,
open_torchcodec.call_count)
self.assertEqual(0 if batch else 1, open_pyav.call_count)
finally:
reader.close()
def test_video_batch_decode_propagates_errors(self):
decoder = Mock()
decoder.get_frames_at.side_effect = RuntimeError("decode failed")
with self.assertRaisesRegex(RuntimeError, "decode failed"):
_decode_video_frames(decoder, [0, 1], [{}, {}])
decoder.get_frames_at.assert_called_once_with(indices=[0, 1])
@unittest.skipUnless(
av is not None and importlib.util.find_spec("torchcodec") is not None,
"PyAV and TorchCodec are required for batch video decoding",
)
def test_torchcodec_batch_matches_single_frame_decoding(self):
import torch
try:
from torchcodec.decoders import VideoDecoder
except (ImportError, OSError, RuntimeError) as error:
self.skipTest(str(error))
output = io.BytesIO()
with av.open(output, mode="w", format="mp4") as container:
stream = container.add_stream("libx264", rate=10)
stream.width = stream.height = 16
stream.pix_fmt = "yuv420p"
stream.gop_size = 4
stream.codec_context.max_b_frames = 2
for index in range(12):
frame = av.VideoFrame.from_ndarray(
np.full((16, 16, 3), index * 16, dtype=np.uint8),
format="rgb24")
for packet in stream.encode(frame):
container.mux(packet)
for packet in stream.encode():
container.mux(packet)
payload = output.getvalue()
decoder = _open_video_decoder(io.BytesIO(payload), backend="torchcodec")
self.assertIsInstance(decoder, VideoDecoder)
indices = [9, 1, 9, 5]
expected = torch.stack([decoder[index] for index in indices])
file_io = SimpleNamespace(
new_input_stream=lambda path: io.BytesIO(payload))
collator = pmm.VideoFrameCollator(
SimpleNamespace(file_io=file_io), video_column="video",
decoder_factory=lambda source: decoder,
decode_batch_fn=_decode_video_frames,
collate_fn=lambda rows: rows,
)
try:
with patch.object(
decoder, "get_frames_at", wraps=decoder.get_frames_at,
) as decode_batch:
result = collator([{
"video": pmm.VideoFrameDescriptor(
"file:///episode.mp4", 0, len(payload), index
).serialize(),
} for index in indices])
decode_batch.assert_called_once_with(indices=[1, 5, 9, 9])
actual = torch.stack([row["frame"] for row in result])
self.assertEqual(torch.uint8, actual.dtype)
self.assertEqual((4, 3, 16, 16), tuple(actual.shape))
torch.testing.assert_close(actual, expected, rtol=0, atol=0)
finally:
collator.close()
def test_custom_episode_transform_keeps_cross_column_inputs(self):
try:
from datasets import Dataset
except ImportError as error:
self.skipTest(str(error))
episodes = Dataset.from_dict({
"episode_index": [0, 1],
"dataset_from_index": [0, 1],
"dataset_to_index": [1, 3],
"length": [0, 0],
"stats": [1, 2],
})
def transform(batch):
return dict(batch, length=batch["stats"], tasks=[
["pick" if index == 0 else "place"]
for index in batch["episode_index"]
])
for source in (episodes, episodes.add_column(
"tasks", [["old"], ["old"]])):
with self.subTest(physical_tasks="tasks" in source.column_names):
metadata = {"episodes": source.with_transform(transform)}
self.assertEqual([(0, 1), (1, 3)],
_episode_ranges(metadata, 3, 2))
self.assertEqual((("pick",), ("place",)),
_episode_tasks(metadata))
def test_dataset_requires_supported_python(self):
with patch(
"pypaimon.multimodal.lerobot.dataset.sys.version_info",
(3, 9)), patch(
"pypaimon.multimodal.lerobot.dataset._load_dataset") as load:
with self.assertRaisesRegex(RuntimeError, "Python 3.10"):
pmm.PaimonLeRobotDataset(Mock())
load.assert_not_called()
def test_dataset_reads_one_batch_from_custom_reader(self):
try:
import torch
except ImportError as error:
self.skipTest(str(error))
info = {
"codebase_version": "v3.0",
"total_frames": 3,
"total_episodes": 1,
"total_tasks": 1,
"fps": 10,
"features": {
"index": {"dtype": "int64", "shape": [1]},
"episode_index": {"dtype": "int64", "shape": [1]},
"frame_index": {"dtype": "int64", "shape": [1]},
"timestamp": {"dtype": "float32", "shape": [1]},
"task_index": {"dtype": "int64", "shape": [1]},
"observation.state": {"dtype": "float32", "shape": [2]},
"action": {"dtype": "float32", "shape": [1]},
},
}
rows = {
index: {
"index": index,
"episode_index": 0,
"frame_index": index,
"timestamp": index / 10,
"task_index": 0,
"observation.state": [index, index + 1],
"action": float(index),
}
for index in range(3)
}
class Reader(pmm.PaimonDatasetReader):
def __init__(self, metadata, **kwargs):
self.calls = []
self.closed = False
super().__init__(metadata, **kwargs)
def read_indices(self, indices, columns):
self.calls.append((indices, columns))
return pa.Table.from_pylist([
{name: rows[index][name] for name in columns}
for index in indices
], schema=self.schema)
def close(self):
super().close()
self.closed = True
metadata = {
"repo_id": "logical/multi-table",
"revision": "dataset-version-12",
"info": info,
"episodes": [{
"episode_index": 0,
"dataset_from_index": 0,
"dataset_to_index": 3,
"length": 3,
"tasks": ["pick"],
}],
"tasks": ["pick"],
"stats": {"action": {"mean": [1.0]}},
}
missing_episodes = dict(metadata)
missing_episodes.pop("episodes")
with self.assertRaisesRegex(ValueError, "must define episodes"):
Reader(missing_episodes)
reader = Reader(
metadata,
delta_timestamps={"action": [-0.1, 0.0, 0.1]},
)
with self.assertRaisesRegex(TypeError, "tag_name"):
Reader(metadata, tag_name="snapshot-b")
dataset = pmm.PaimonLeRobotDataset(reader)
self.assertIsInstance(dataset.reader, pmm.PaimonDatasetReader)
self.assertEqual(_schema_from_info(info), dataset.reader.schema)
self.assertIsNone(dataset.reader.absolute_to_relative_idx)
self.assertTrue(repr(dataset).startswith("PaimonLeRobotDataset("))
with patch(
"pypaimon.multimodal.lerobot.dataset._torch_row",
wraps=_torch_row) as convert:
sample, _ = dataset.__getitems__([1, 2])
self.assertEqual(3, convert.call_count)
self.assertEqual({"action"}, set(convert.call_args_list[-1][0][0]))
self.assertEqual([((0, 1, 2), tuple(info["features"]))],
reader.calls)
self.assertFalse(hasattr(dataset, "tag_name"))
self.assertEqual("dataset-version-12", dataset.meta.revision)
self.assertEqual((2,), dataset.features["observation.state"]["shape"])
self.assertEqual([1.0], dataset.meta.stats["action"]["mean"].tolist())
self.assertEqual(0, dataset.meta.get_task_index("pick"))
self.assertEqual("pick", sample["task"])
try:
from datasets import Dataset
except ImportError:
pass
else:
formatted_metadata = dict(metadata)
formatted_metadata["episodes"] = Dataset.from_list(
metadata["episodes"]).with_format(
"numpy", columns=["episode_index", "dataset_from_index",
"dataset_to_index", "length"])
formatted_reader = Reader(formatted_metadata)
self.assertEqual("pick", formatted_reader[1]["task"])
formatted_reader.close()
torch.testing.assert_close(
sample["observation.state"], torch.tensor([1.0, 2.0]))
torch.testing.assert_close(
sample["action"], torch.tensor([0.0, 1.0, 2.0]))
self.assertEqual([False, False, False],
sample["action_is_pad"].tolist())
batch = next(iter(dataset.to_dataloader(
batch_size=2, shuffle=False, num_workers=0)))
self.assertEqual([0, 1], batch["index"].tolist())
self.assertEqual([2, 3], list(batch["action"].shape))
control_reader = Reader(
metadata,
delta_timestamps={"timestamp": [-0.1, 0.0, 0.1]},
)
torch.testing.assert_close(
control_reader[1]["timestamp"], torch.tensor([0.0, 0.1, 0.2]))
control_reader.close()
rows[0]["episode_index"] = 1
try:
with self.assertRaisesRegex(ValueError, "episode_index"):
dataset[1]
finally:
rows[0]["episode_index"] = 0
with self.assertRaisesRegex(ValueError, "manages collate_fn"):
dataset.to_dataloader(batch_size=2, collate_fn=lambda rows: rows)
dataset.return_uint8 = True
self.assertTrue(reader.return_uint8)
with self.assertRaisesRegex(TypeError, "return_uint8"):
dataset.return_uint8 = 1
image_transforms = Mock()
dataset.image_transforms = image_transforms
self.assertIs(image_transforms, reader.image_transforms)
dataset.image_transforms = None
self.assertIsNone(reader.image_transforms)
with self.assertRaisesRegex(TypeError, "image_transforms"):
dataset.image_transforms = 1
wrong_schema = reader.schema.set(
reader.schema.get_field_index("action"),
pa.field("action", pa.int64()),
)
with patch.object(reader, "read_indices") as read:
read.return_value = pa.Table.from_pylist(
[{name: rows[0][name] for name in info["features"]}],
schema=wrong_schema,
)
with self.assertRaisesRegex(
ValueError, "field action expects float, found int64"):
dataset[0]
dataset.close()
self.assertTrue(reader.closed)
def test_dataset_does_not_proxy_pickle_protocol(self):
dataset = pmm.PaimonLeRobotDataset(_PickleDatasetReader(7))
with self.assertRaises(AttributeError):
dataset.__getattr__("__getstate__")
restored = pickle.loads(pickle.dumps(dataset))
self.assertIsInstance(restored.reader, _PickleDatasetReader)
self.assertEqual(7, restored.reader.value)
def test_metadata_json_preserves_nested_values(self):
values = {
"name": "机器人",
"count": 2 ** 64,
"custom": {"labels": ["pick", None], "enabled": True},
}
table = _metadata_table(values)
self.assertEqual(pa.schema([("key", pa.string()), ("value", pa.string())]),
table.schema)
self.assertEqual(values, {
row["key"]: json.loads(row["value"])
for row in table.to_pylist()
})
def test_invalid_training_tag_fails_before_catalog_access(self):
for tag_name in (None, 1, "", " ", "a/b", "a\\b", "a\x00b"):
with self.subTest(tag_name=tag_name):
connection = Mock()
with self.assertRaisesRegex(ValueError, "tag_name"):
MultimodalConnection.create_lerobot_tag(
connection, "robot", tag_name)
self.assertEqual([], connection.mock_calls)
def test_import_validates_tag_before_source_access(self):
connection = Mock()
with patch("pypaimon.multimodal.lerobot.api._resolved_source",
side_effect=RuntimeError("source accessed")) as resolve:
for tag_name in (1, "", " ", "a/b", "a\\b", "a\x00b"):
with self.subTest(tag_name=tag_name):
with self.assertRaisesRegex(ValueError, "tag_name"):
load_from_lerobot(connection, "robot", "source",
tag_name=tag_name)
resolve.assert_not_called()
self.assertEqual([], connection.mock_calls)
with self.assertRaisesRegex(RuntimeError, "source accessed"):
load_from_lerobot(connection, "robot", "source", tag_name=None)
resolve.assert_called_once()
@patch("pypaimon.multimodal.lerobot.api._import_lerobot_dataset",
return_value=Mock())
def test_training_tag_uses_current_component_snapshots(self, _):
import pandas as pd
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
source = root / "source"
(source / "meta" / "episodes").mkdir(parents=True)
(source / "data").mkdir()
info = {
"codebase_version": "v3.0",
"total_frames": 1,
"total_episodes": 1,
"total_tasks": 1,
"fps": 30,
"data_path": "data/file.parquet",
"features": {
name: {"dtype": dtype, "shape": [1]}
for name, dtype in (
("index", "int64"), ("episode_index", "int64"),
("frame_index", "int64"), ("task_index", "int64"),
("subtask_index", "int64"),
("timestamp", "float32"),
)
},
"custom": {"labels": ["pick", None], "enabled": True},
}
(source / "meta" / "info.json").write_text(json.dumps(info))
stats = {"timestamp": {"min": [0.0], "max": [0.0]}}
(source / "meta" / "stats.json").write_text(json.dumps(stats))
pq.write_table(pa.Table.from_pandas(pd.DataFrame(
{"task_index": [0]}, index=pd.Index(["pick"], name="task"),
)), source / "meta" / "tasks.parquet")
subtasks = pa.Table.from_pandas(pd.DataFrame(
{"subtask_index": [0]}, index=pd.Index(["reach"], name="instruction"),
))
pq.write_table(subtasks, source / "meta" / "subtasks.parquet")
pq.write_table(pa.table({
"episode_index": [0], "dataset_from_index": [0],
"dataset_to_index": [1], "tasks": [["pick"]], "length": [1],
"data/chunk_index": [0], "data/file_index": [0],
}), source / "meta" / "episodes" / "file.parquet")
frames = pa.table({
"index": [0], "episode_index": [0], "frame_index": [0],
"task_index": [0], "subtask_index": [0], "timestamp": pa.array([0], pa.float32()),
})
pq.write_table(frames, source / "data" / "file.parquet")
connection = pmm.connect(options={"warehouse": str(root / "wh")})
remote = "oss://source-bucket/robot"
with patch(
"pypaimon.multimodal.lerobot.source._SourceFileIO",
return_value=_RemoteLeRobotFileIO(source, remote)):
self.assertIsNone(connection.load_from_lerobot("robot", remote))
for suffix in ("_frames", "_episodes", "_datasets"):
with self.subTest(table="robot" + suffix):
component = connection.get_table("robot" + suffix)
schema = component.raw_table.table_schema
self.assertEqual("true", schema.options["data-evolution.enabled"])
self.assertEqual("true", schema.options["row-tracking.enabled"])
self.assertEqual("-1", schema.options.get("bucket", "-1"))
self.assertFalse(schema.primary_keys)
self.assertGreater(component.scan().to_arrow().num_rows, 0)
self.assertEqual([{"subtask_index": 0, "subtask": "reach"}],
_catalog_rows(connection, "robot_subtasks"))
self.assertEqual(info, _catalog_metadata(connection, "robot_info"))
self.assertEqual(stats, _catalog_metadata(connection, "robot_stats"))
table = connection.get_table("robot_frames")
self.assertEqual([], table.raw_table.tag_manager().list_tags())
table.add(frames)
snapshots = connection.create_lerobot_tag("robot_frames", "training")
self.assertEqual({"frames": 3, "episodes": 1, "datasets": 1}, snapshots)
table.add(frames)
self.assertEqual(2, table.scan(tag_name="training").to_arrow().num_rows)
self.assertEqual(3, table.scan().to_arrow().num_rows)
for component, snapshot_id in snapshots.items():
name = "robot_frames" if component == "frames" else "robot_" + component
self.assertEqual(snapshot_id, connection.catalog.get_tag(
connection._identifier(name), "training").snapshot.id)
with patch.object(connection.catalog, "create_tag") as create_tag:
with self.assertRaisesRegex(ValueError, "already points"):
connection.create_lerobot_tag("robot_frames", "training")
create_tag.assert_not_called()
create_tag = connection.catalog.create_tag
attempts = []
def fail_second_component(*args, **kwargs):
attempts.append(args[0])
if len(attempts) == 2:
raise RuntimeError("tag failed")
return create_tag(*args, **kwargs)
with patch.object(connection.catalog, "create_tag",
side_effect=fail_second_component):
with self.assertRaisesRegex(RuntimeError, "tag failed"):
connection.create_lerobot_tag("robot_frames", "retry")
self.assertFalse(table.raw_table.tag_manager().tag_exists("retry"))
self.assertEqual(4, connection.create_lerobot_tag(
"robot_frames", "retry")["frames"])
self.assertEqual(3, table.scan(tag_name="retry").to_arrow().num_rows)
connection.catalog.drop_table(connection._identifier("robot_datasets"))
with patch.object(connection.catalog, "create_tag") as create_tag:
with self.assertRaises(TableNotExistException):
connection.create_lerobot_tag("robot_frames", "incomplete")
create_tag.assert_not_called()
(source / "meta" / "stats.json").unlink()
with patch(
"pypaimon.multimodal.lerobot.source._SourceFileIO",
return_value=_RemoteLeRobotFileIO(source, remote)):
connection.load_from_lerobot("no_stats", remote, tag_name="ready")
with self.assertRaises(TableNotExistException):
connection.get_table("no_stats_stats")
self.assertEqual({"frames": 2, "episodes": 1, "datasets": 1},
connection.create_lerobot_tag("no_stats_frames", "training"))
def test_self_contained_import_rejects_table_branches(self):
with self.assertRaisesRegex(ValueError, "does not support"):
_managed_table_options("db.robot$branch_dev")
def test_companion_identifier_preserves_quoted_components(self):
name = _companion_identifier(
"`db.name`.`robot.data`", "_tasks")
identifier = Identifier.from_string(name)
self.assertEqual("db.name", identifier.get_database_name())
self.assertEqual("robot.data_tasks", identifier.get_table_name())
def test_image_tensor_preserves_declared_channels(self):
try:
from PIL import Image
import torch
except ImportError as error:
self.skipTest(str(error))
cases = [
("L", np.full((4, 5), 64, dtype=np.uint8),
[4, 5, 1], [64]),
("RGB", np.tile(
np.array([32, 64, 96], dtype=np.uint8), (4, 5, 1)),
[4, 5, 3], [32, 64, 96]),
("RGBA", np.tile(
np.array([32, 64, 96, 128], dtype=np.uint8), (4, 5, 1)),
[4, 5, 4], [32, 64, 96, 128]),
]
for mode, values, shape, expected in cases:
with self.subTest(mode=mode):
output = io.BytesIO()
Image.fromarray(values, mode=mode).save(output, format="PNG")
feature = {"dtype": "image", "shape": shape}
tensor = _image_tensor(output.getvalue(), feature)
uint8_tensor = _image_tensor(
output.getvalue(), feature, return_uint8=True)
self.assertEqual(torch.float32, tensor.dtype)
self.assertEqual(torch.uint8, uint8_tensor.dtype)
self.assertEqual(
[shape[2], shape[0], shape[1]], list(tensor.shape))
self.assertEqual(list(tensor.shape), list(uint8_tensor.shape))
self.assertGreaterEqual(float(tensor.min()), 0.0)
self.assertLessEqual(float(tensor.max()), 1.0)
self.assertEqual(expected, uint8_tensor[:, 0, 0].tolist())
torch.testing.assert_close(
tensor, uint8_tensor.float().div(255))
output = io.BytesIO()
Image.fromarray(cases[1][1], mode="RGB").save(output, format="PNG")
tensor = _image_tensor(output.getvalue(), {
"dtype": "image",
"shape": [3, 4, 5],
"names": ["channels", "height", "width"],
})
self.assertEqual([3, 4, 5], list(tensor.shape))
uint8_tensor = _image_tensor(
output.getvalue(),
{
"dtype": "image",
"shape": [3, 4, 5],
"names": ["channels", "height", "width"],
},
return_uint8=True,
)
self.assertEqual(torch.uint8, uint8_tensor.dtype)
self.assertEqual([32, 64, 96], uint8_tensor[:, 0, 0].tolist())
original = uint8_tensor.clone()
uint8_tensor.zero_()
reread = _image_tensor(
output.getvalue(),
{
"dtype": "image",
"shape": [3, 4, 5],
"names": ["channels", "height", "width"],
},
return_uint8=True,
)
self.assertTrue(torch.equal(original, reread))
self.assertNotEqual(uint8_tensor.data_ptr(), reread.data_ptr())
depth = np.array([
[0, 1000, 4095],
[8192, 32768, 65535],
], dtype=np.uint16)
output = io.BytesIO()
Image.fromarray(depth).save(output, format="PNG")
feature = {
"dtype": "image",
"shape": [2, 3, 1],
"info": {
"is_depth_map": True,
"depth_unit": "mm",
},
}
tensor = _image_tensor(output.getvalue(), feature)
uint8_requested = _image_tensor(
output.getvalue(),
feature,
return_uint8=True,
)
self.assertEqual([1, 2, 3], list(tensor.shape))
self.assertEqual(torch.float32, tensor.dtype)
self.assertEqual(depth.astype(np.float32).tolist(), tensor[0].tolist())
self.assertEqual(torch.float32, uint8_requested.dtype)
self.assertTrue(torch.equal(tensor, uint8_requested))
def test_image_tensor_applies_exif_orientation(self):
try:
from PIL import Image
import torch
except ImportError as error:
self.skipTest(str(error))
output = io.BytesIO()
image = Image.fromarray(
np.arange(18, dtype=np.uint8).reshape(2, 3, 3), mode="RGB")
exif = image.getexif()
exif[274] = 6
image.save(output, format="JPEG", exif=exif)
tensor = _image_tensor(output.getvalue(), {
"dtype": "image",
"shape": [3, 2, 3],
}, return_uint8=True)
self.assertEqual(torch.uint8, tensor.dtype)
self.assertEqual([3, 3, 2], list(tensor.shape))
def test_dataset_uint8_getitem_matches_getitems(self):
try:
from PIL import Image
import torch
except ImportError as error:
self.skipTest(str(error))
features = {
"index": {"dtype": "int64", "shape": [1]},
"task_index": {"dtype": "int64", "shape": [1]},
"observation.left": {
"dtype": "image", "shape": [4, 5, 3]},
"observation.wrist": {
"dtype": "image", "shape": [4, 5, 1]},
}
def jpeg(mode, values):
output = io.BytesIO()
Image.fromarray(values, mode=mode).save(
output, format="JPEG", quality=100)
return output.getvalue()
rows = []
for index in range(2):
rows.append({
"index": index,
"task_index": 0,
"observation.left": jpeg(
"RGB", np.full((4, 5, 3), 40 + index, np.uint8)),
"observation.wrist": jpeg(
"L", np.full((4, 5), 80 + index, np.uint8)),
})
dataset = object.__new__(_ManualDatasetReader)
dataset._total_frames = 2
dataset.episodes = None
dataset._selected_ranges = None
dataset._delta_indices = {}
dataset._read_table = Mock()
dataset._frame_locator = Mock()
dataset._frame_locator.locate.return_value = ([], False)
def read_rows(indices, projection, splits=None, needs_filter=True):
return {
index: {
key: value for key, value in rows[index].items()
if key in projection
}
for index in indices
}
dataset._read_rows = read_rows
dataset._file_io = Mock()
dataset._image_keys = [
"observation.left", "observation.wrist"]
dataset.blob_parallelism = 1
dataset._task_names = ["task"]
dataset._subtask_names = None
dataset._features = features
dataset._projection = list(features)
dataset._delta_projection = None
dataset.return_uint8 = True
dataset.image_transforms = None
with patch(
"pypaimon.multimodal.lerobot.dataset._resolve_image_blobs"):
single = dataset[1]
batched = dataset.__getitems__([1, 0])
dataset.return_uint8 = False
normalized = dataset[1]
for key in dataset._image_keys:
self.assertEqual(torch.uint8, single[key].dtype)
self.assertTrue(torch.equal(single[key], batched[0][key]))
torch.testing.assert_close(
normalized[key], single[key].float().div(255))
def test_dataset_retries_image_fetch_and_decode_together(self):
try:
from PIL import Image
import torch
except ImportError as error:
self.skipTest(str(error))
output = io.BytesIO()
Image.fromarray(
np.full((4, 5, 3), 64, dtype=np.uint8), mode="RGB"
).save(output, format="JPEG")
descriptor = b"serialized blob descriptor"
rows = [{
"index": 0,
"task_index": 0,
"observation.image": descriptor,
}]
dataset = object.__new__(_ManualDatasetReader)
dataset._total_frames = 1
dataset.episodes = None
dataset._selected_ranges = None
dataset._delta_indices = {}
dataset._read_table = Mock()
dataset._frame_locator = Mock()
dataset._frame_locator.locate.return_value = ([], False)
def read_rows(indices, projection, splits=None, needs_filter=True):
return {
index: {
key: value for key, value in rows[index].items()
if key in projection
}
for index in indices
}
dataset._read_rows = read_rows
dataset._file_io = Mock()
dataset._image_keys = ["observation.image"]
dataset.blob_parallelism = 1
dataset._task_names = {0: "task"}
dataset._subtask_names = None
dataset._features = {
"index": {"dtype": "int64", "shape": [1]},
"task_index": {"dtype": "int64", "shape": [1]},
"observation.image": {
"dtype": "image", "shape": [4, 5, 3]},
}
dataset._projection = list(dataset._features)
dataset._delta_projection = None
dataset.return_uint8 = True
dataset.image_transforms = None
sources = []
def resolve(_file_io, row_groups, image_keys, _parallelism):
sources.append(row_groups[0][0][image_keys[0]])
row_groups[0][0][image_keys[0]] = \
b"not a JPEG" if len(sources) == 1 else output.getvalue()
with patch(
"pypaimon.multimodal.lerobot.dataset._resolve_image_blobs",
side_effect=resolve) as fetch:
sample = dataset[0]
self.assertEqual(2, fetch.call_count)
self.assertEqual([descriptor, descriptor], sources)
self.assertEqual(torch.uint8, sample["observation.image"].dtype)
self.assertEqual([3, 4, 5], list(
sample["observation.image"].shape))
def test_transformed_windows_are_assembled_per_sample(self):
try:
import torch
except ImportError as error:
self.skipTest(str(error))
keys = ["left", "right"]
original_stack = torch.stack
for kind in ("image", "video"):
with self.subTest(kind=kind):
reader = object.__new__(_ManualDatasetReader)
reader._total_frames = 33
reader.episodes = None
reader._selected_ranges = None
reader._features = {
key: {"dtype": kind, "shape": [4, 5, 3]}
for key in keys
}
reader._projection = keys
reader._delta_projection = keys
reader._image_keys = []
reader._visual_keys = keys
reader._video_collators = []
reader._file_io = None
reader._task_names = ["task"]
reader._subtask_names = None
reader.return_uint8 = True
reader._plan = lambda i: {
"index": i, "windows": {key: [i, i, i + 1] for key in keys},
"padding": {},
}
reader._read_rows = lambda indices, projection: {
i: dict(task_index=0, **{
key: torch.full((3, 4, 5), i, dtype=torch.uint8)
for key in keys
}) for i in indices
}
windows = []
peak = []
def stack(*args, **kwargs):
value = original_stack(*args, **kwargs)
windows.append(weakref.ref(value))
peak.append(sum(ref() is not None for ref in windows))
return value
def resize(value):
return torch.nn.functional.interpolate(
value.float(), size=(2, 2), mode="nearest")
reader.image_transforms = resize
module = "pypaimon.multimodal.lerobot.dataset."
with patch("torch.get_num_threads", return_value=1), \
patch("torch.stack", side_effect=stack), \
patch(module + "_decode_video_windows",
wraps=_decode_video_windows) as decode, \
patch(module + "_stack_visual_windows",
wraps=_stack_visual_windows) as assemble:
result = reader.get_items(list(range(32)))
self.assertEqual(64, len(windows))
self.assertLessEqual(max(peak), len(keys))
decode.assert_not_called()
assemble.assert_not_called()
self.assertTrue(all(ref() is None for ref in windows))
for i, item in enumerate(result):
for key in keys:
expected = torch.tensor([i, i, i + 1]).reshape(3, 1, 1, 1)
self.assertTrue(torch.equal(
item[key], expected.expand(3, 3, 2, 2).float()))
result[0][keys[0]].zero_()
self.assertEqual(1, result[0][keys[1]][-1, 0, 0, 0].item())
batch = reader._get_batch([0, 1], share_memory=False)
for key in keys:
self.assertEqual(
[2, 3, 3, 2, 2], list(batch[key].shape))
def test_video_windows_decode_directly_and_fall_back(self):
try:
import torch
except ImportError as error:
self.skipTest(str(error))
from pypaimon.multimodal.video import VideoFrameCollator
from pypaimon.table.row.blob import VideoFrameDescriptor
key = "camera"
feature = {key: {"dtype": "video", "shape": [4, 5, 3]}}
rows = {i: {key: VideoFrameDescriptor("a.video", 0, 1, i).serialize()}
for i in range(2)}
plans = [{"windows": {key: [1, 0, 0]}}] * 2
pixels = torch.arange(120).reshape(2, 4, 5, 3).to(torch.uint8)
frames = pixels.permute(0, 3, 1, 2)
batch = frames.contiguous()
decoder = SimpleNamespace(get_frames_at=Mock(
return_value=SimpleNamespace(data=batch)))
collator = VideoFrameCollator(
SimpleNamespace(file_io=Mock()), video_column=key,
decoder_factory=Mock(), decode_fn=Mock())
with patch.object(collator, "_decoder", return_value=decoder) as open_decoder, \
patch("torch.get_num_threads", return_value=1):
for uint8, batch in ((True, frames), (False, frames),
(True, frames.contiguous()),
(False, frames.contiguous())):
decoder.get_frames_at.reset_mock()
decoder.get_frames_at.return_value = SimpleNamespace(data=batch)
result = _decode_video_windows(
plans, rows, [collator], feature, uint8)[key]
expected = batch[[1, 0, 0]]
if not uint8:
expected = expected.float().div(255)
self.assertTrue(torch.equal(result[0], expected))
self.assertTrue(result[0].is_contiguous())
result[0][1].zero_()
self.assertTrue(torch.equal(result[0][2], expected[2]))
self.assertTrue(torch.equal(result[1], expected))
self.assertTrue(torch.equal(batch, frames))
decoder.get_frames_at.assert_called_once_with(indices=[0, 1])
decoder.get_frames_at.reset_mock()
decoder.get_frames_at.return_value = SimpleNamespace(data=frames)
result = _decode_video_windows(
plans, rows, [collator], feature, False,
return_batch=True, share_memory=True)[key]
expected = torch.stack([
frames[[1, 0, 0]], frames[[1, 0, 0]]
]).float().div(255)
self.assertTrue(result.is_shared())
self.assertTrue(torch.equal(result, expected))
result[0, 1].zero_()
self.assertTrue(torch.equal(result[0, 2], expected[0, 2]))
self.assertTrue(torch.equal(result[1], expected[1]))
self.assertTrue(torch.equal(frames, batch))
decoder.get_frames_at.assert_called_once_with(indices=[0, 1])
for indices in ([0, 1], [1, 0, 0]):
decoder.get_frames_at.reset_mock()
decoder.get_frames_at.return_value = SimpleNamespace(data=frames)
result = _decode_video_windows(
[{"windows": {key: indices}}], rows, [collator], feature, True)[key][0]
decoder.get_frames_at.assert_called_once_with(indices=[0, 1])
self.assertTrue(torch.equal(result, batch[indices]))
self.assertTrue(result.is_contiguous())
result.zero_()
self.assertTrue(torch.equal(batch, frames))
decoder.get_frames_at.reset_mock()
separate = dict(rows)
separate[2] = {key: VideoFrameDescriptor("b.video", 0, 1, 0).serialize()}
separate[3] = {key: VideoFrameDescriptor("b.video", 0, 1, 1).serialize()}
interleaved = [{"windows": {key: window}}
for window in ([1, 0, 0], [2, 3], [0, 1], [3, 2, 2])]
result = _decode_video_windows(
interleaved, separate, [collator], feature, True)[key]
self.assertEqual(2, decoder.get_frames_at.call_count)
self.assertEqual(
[[0, 1], [0, 1]],
[c.kwargs["indices"] for c in decoder.get_frames_at.call_args_list])
for actual, indices in zip(result, ([1, 0, 0], [0, 1], [0, 1], [1, 0, 0])):
self.assertTrue(torch.equal(actual, batch[indices]))
self.assertTrue(actual.is_contiguous())
result[2].zero_()
self.assertTrue(torch.equal(result[1], batch))
self.assertTrue(torch.equal(batch, frames))
fixed = [{"windows": {key: window}}
for window in ([1, 0], [2, 3], [0, 1], [3, 2])]
result = _decode_video_windows(
fixed, separate, [collator], feature, True,
return_batch=True, share_memory=True)[key]
expected = torch.stack([
batch[[1, 0]], batch[[0, 1]],
batch[[0, 1]], batch[[1, 0]],
])
self.assertTrue(result.is_shared())
self.assertTrue(torch.equal(result, expected))
open_decoder.reset_mock()
mixed = dict(rows)
mixed[1] = {key: VideoFrameDescriptor("b.video", 0, 1, 0).serialize()}
self.assertEqual({}, _decode_video_windows(
plans, mixed, [collator], feature, True))
open_decoder.assert_not_called()
self.assertEqual({}, _decode_video_windows(
plans, {0: {key: None}, 1: rows[1]}, [collator], feature, True))
open_decoder.return_value = SimpleNamespace()
self.assertEqual({}, _decode_video_windows(
plans, rows, [collator], feature, True))
open_decoder.return_value = decoder
decoder.get_frames_at.return_value = SimpleNamespace(data=batch[:1])
with self.assertRaisesRegex(ValueError, "one frame per index"):
_decode_video_windows(plans, rows, [collator], feature, True)
def test_visual_windows_preserve_order_padding_and_isolation(self):
try:
import torch
from torch.utils.data import default_collate
except ImportError as error:
self.skipTest(str(error))
keys = ["left", "right"]
plans = [{"windows": {key: indices for key in keys}}
for indices in ([1, 0, 0], [0, 1, 1], [1, 0, 0])]
for dtype in (torch.uint8, torch.float32):
frames = torch.arange(120).reshape(2, 4, 5, 3).to(dtype)
frames = frames.permute(0, 3, 1, 2)
rows = {i: {key: frames[i] for key in keys} for i in range(2)}
with patch("torch.get_num_threads", return_value=1):
actual = _stack_visual_windows(plans, rows, keys)
for key in keys:
expected = [torch.stack([rows[i][key] for i in
plan["windows"][key]]) for plan in plans]
self.assertTrue(torch.equal(
default_collate(actual[key]), default_collate(expected)))
self.assertTrue(all(value.is_contiguous()
for value in actual[key]))
actual[key][0][1].zero_()
self.assertTrue(torch.equal(actual[key][0][2], expected[0][2]))
self.assertTrue(torch.equal(actual[key][2], expected[2]))
self.assertTrue(torch.equal(frames[0], expected[0][1]))
with patch("torch.get_num_threads", return_value=2):
self.assertEqual({}, _stack_visual_windows(plans, rows, keys))
self.assertEqual({}, _stack_visual_windows(plans, rows, ["left"]))
rows = {i: {key: torch.ones(2, requires_grad=True) for key in keys}
for i in range(2)}
with patch("torch.get_num_threads", return_value=1), torch.no_grad():
result = _stack_visual_windows(plans, rows, keys)
self.assertFalse(result["left"][0].requires_grad)
with patch("torch.get_num_threads", return_value=1), \
torch.inference_mode():
result = _stack_visual_windows(plans, rows, keys)
self.assertTrue(result["left"][0].is_inference())
def test_dataset_return_uint8_requires_bool(self):
loaded = (
Mock(),
Mock(repo_id="pypaimon/invalid-return-uint8"),
)
with patch(
"pypaimon.multimodal.lerobot.dataset."
"_load_dataset",
return_value=loaded), patch(
"pypaimon.multimodal.lerobot.dataset._target_schema",
return_value=pa.schema([])), patch(
"pypaimon.multimodal.lerobot.dataset.sys",
SimpleNamespace(version_info=(3, 10), modules=sys.modules)):
for invalid in (0, 1, None, "true"):
with self.subTest(return_uint8=invalid):
with self.assertRaisesRegex(
TypeError, "return_uint8 must be a boolean"):
pmm.PaimonLeRobotDataset(
Mock(), return_uint8=invalid)
def test_arrow_rows_converts_numeric_features_by_column(self):
try:
import torch
except ImportError as error:
self.skipTest(str(error))
features = {
"index": {"dtype": "int64", "shape": [1]},
"state": {"dtype": "float64", "shape": [3]},
"matrix": {"dtype": "float32", "shape": [2, 2]},
"reward": {"dtype": "float32", "shape": [1]},
"label": {"dtype": "string", "shape": [1]},
"image": {"dtype": "image", "shape": [1, 1, 3]},
}
arrow = pa.table({
"index": pa.array([0, 1], type=pa.int64()),
"state": pa.array(
[[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]],
type=pa.list_(pa.float64(), 3),
),
"matrix": pa.array(
[
[[1.0, 2.0], [3.0, 4.0]],
[[5.0, 6.0], [7.0, 8.0]],
],
type=pa.list_(pa.list_(pa.float32(), 2)),
),
"reward": pa.array([0.0, 1.0], type=pa.float32()),
"label": ["pick", "place"],
"image": [b"left", b"right"],
})
rows = _arrow_rows(arrow, features)
converted = [
_torch_row(
{key: value for key, value in row.items() if key != "image"},
features,
)
for row in rows
]
self.assertEqual(0, rows[0]["index"])
self.assertEqual(torch.float64, converted[0]["state"].dtype)
self.assertEqual([3], list(converted[0]["state"].shape))
self.assertEqual(torch.float32, converted[0]["matrix"].dtype)
self.assertEqual([2, 2], list(converted[0]["matrix"].shape))
self.assertEqual([], list(converted[0]["reward"].shape))
self.assertEqual("pick", converted[0]["label"])
self.assertEqual(b"right", rows[1]["image"])
converted[0]["state"].zero_()
self.assertEqual([4.0, 5.0, 6.0], converted[1]["state"].tolist())
def test_arrow_rows_rejects_lossy_numeric_narrowing(self):
try:
import torch # noqa: F401
except ImportError as error:
self.skipTest(str(error))
cases = [
("uint8", pa.int16(), [-1, 256]),
("uint16", pa.int32(), [-1, 65536]),
("uint32", pa.int64(), [-1, 4294967296]),
("float16", pa.float32(), [-70000.0, 70000.0]),
]
for dtype, arrow_type, values in cases:
with self.subTest(dtype=dtype):
table = pa.table({
"value": pa.array(values, type=arrow_type),
})
features = {
"value": {"dtype": dtype, "shape": [1]},
}
with self.assertRaisesRegex(
ValueError, "outside the %s range" % dtype):
_arrow_rows(table, features)
def test_selected_episodes_preserves_caller_order(self):
self.assertEqual([1, 0], _selected_episodes([1, 0], 2))
with self.assertRaisesRegex(ValueError, "duplicate"):
_selected_episodes([1, 1], 2)
with self.assertRaisesRegex(ValueError, "indices in"):
_selected_episodes([2], 2)
def test_metadata_indices_keep_integer_dtype(self):
import pandas as pd
for field in ("task_index", "subtask_index"):
with self.subTest(field=field):
values = pd.DataFrame(
{
field: np.array([0, 1], dtype=np.int64),
"quality": [0.9, 0.8],
},
index=pd.Index(["pick", "place"], name="instruction"),
)
self.assertEqual(
{0: "pick", 1: "place"},
_index_names(values, field),
)
def test_dataset_open_never_downloads_videos(self):
calls = []
class Dataset:
def __init__(self, **kwargs):
calls.append(kwargs)
_open_dataset(Dataset, _LeRobotSource(
path="lerobot/example",
root=None,
repo_id="lerobot/example",
))
self.assertFalse(calls[0]["download_videos"])
def test_dataset_paths_cannot_escape_source(self):
temp_dir = Path(tempfile.mkdtemp(prefix="pypaimon_lerobot_paths_"))
try:
root = temp_dir / "source"
root.mkdir()
inside = root / "frame.png"
inside.write_bytes(b"frame")
outside = temp_dir / "secret"
outside.write_bytes(b"secret")
self.assertEqual(
b"frame",
_image_bytes({"path": str(inside)}, root),
)
for path in ("../secret", str(outside)):
with self.assertRaisesRegex(ValueError, "within the source"):
_image_bytes({"path": path}, root)
_validate_info_paths({
"data_path": "data/chunk-{chunk_index:03d}/file.parquet",
})
for path in (
"../secret.parquet",
"%2e%2e/secret.parquet",
"%252e%252e/secret.parquet"):
with self.assertRaisesRegex(ValueError, "info.data_path"):
_validate_info_paths({"data_path": path})
finally:
shutil.rmtree(temp_dir, ignore_errors=True)
def test_remote_image_paths_cannot_escape_source(self):
root = "oss://bucket/datasets/robot"
self.assertEqual(
root + "/images/frame.png",
_remote_source_path(root, "images/frame.png", "image path"),
)
self.assertEqual(
root + "/images/frame.png",
_remote_source_path(
root,
root + "/images/frame.png",
"image path",
),
)
for path in (
"../private",
"%2e%2e/private",
"%252e%252e/private",
"oss://other/private",
"oss://bucket/datasets/robot/../../private"):
with self.assertRaisesRegex(ValueError, "within the source"):
_remote_source_path(root, path, "image path")
def test_double_encoded_file_uri_cannot_escape_source(self):
temp_dir = Path(tempfile.mkdtemp(prefix="pypaimon_lerobot_uri_"))
source_file_io = _SourceFileIO(Options({}))
try:
root = temp_dir / "source"
root.mkdir()
(root / "frame one").write_bytes(b"frame")
(temp_dir / "secret").write_bytes(b"secret")
source_path = _remote_source_path(
root.as_uri(),
"frame%20one",
"image path",
source_file_io,
)
with source_file_io.new_input_stream(source_path) as stream:
self.assertEqual(b"frame", stream.read())
with self.assertRaisesRegex(ValueError, "within the source"):
_remote_source_path(
root.as_uri(),
"%252e%252e/secret",
"image path",
source_file_io,
)
finally:
source_file_io.close()
shutil.rmtree(temp_dir, ignore_errors=True)
def test_hdfs_source_rejects_explicit_keytab_before_resolution(self):
with patch(
"pypaimon.multimodal.lerobot.source._SourceFileIO"
) as source_file_io:
with self.assertRaisesRegex(ValueError, "process-isolated"):
load_from_lerobot(
Mock(),
"frames",
"hdfs://source-ns/robot",
source_options={
"security.kerberos.login.principal": "source@REALM",
"security.kerberos.login.keytab": "/source.keytab",
},
)
source_file_io.assert_not_called()
def test_timestamp_validation_quantizes_float32(self):
frame_index = 61441
batch = pa.table({
"index": pa.array([frame_index], type=pa.int64()),
"episode_index": pa.array([0], type=pa.int64()),
"frame_index": pa.array([frame_index], type=pa.int64()),
"timestamp": pa.array([frame_index / 30], type=pa.float32()),
"task_index": pa.array([0], type=pa.int64()),
})
self.assertEqual(
{0},
_validate_frame_controls(
batch, 30, 0, 0, frame_index, [0]),
)
def test_subtask_index_must_reference_metadata(self):
batch = pa.table({
"index": pa.array([0], type=pa.int64()),
"episode_index": pa.array([0], type=pa.int64()),
"frame_index": pa.array([0], type=pa.int64()),
"timestamp": pa.array([0], type=pa.float32()),
"task_index": pa.array([0], type=pa.int64()),
"subtask_index": pa.array([2], type=pa.int64()),
})
with self.assertRaisesRegex(ValueError, "subtask_index 2 outside"):
_validate_frame_controls(
batch, 30, 0, 0, 0, [0], range(2))
nullable = batch.set_column(
batch.schema.get_field_index("subtask_index"),
"subtask_index", pa.array([None], type=pa.int64()))
self.assertEqual(
{0}, _validate_frame_controls(
nullable, 30, 0, 0, 0, [0], range(2)))
def test_metadata_writer_closes_after_commit_creation_failure(self):
table = Mock()
builder = table.new_batch_write_builder.return_value
writer = builder.new_write.return_value
builder.new_commit.side_effect = RuntimeError("commit init failed")
with self.assertRaisesRegex(RuntimeError, "commit init failed"):
_append_arrow_tables(table, [])
writer.abort.assert_called_once_with()
writer.close.assert_called_once_with()
def test_episode_shards_use_normal_batch_rolling(self):
data = pa.table({"episode_index": [0]})
table = Mock()
builder = table.new_batch_write_builder.return_value
writer = builder.new_write.return_value
def shards():
yield data
yield data
raise RuntimeError("stop after two shards")
with patch(
"pypaimon.multimodal.lerobot.metadata._target_schema",
return_value=data.schema):
with self.assertRaisesRegex(RuntimeError, "two shards"):
_append_arrow_tables(table, shards())
self.assertEqual(2, writer.write_arrow.call_count)
writer.prepare_commit.assert_not_called()
writer.abort.assert_called_once_with()
writer.close.assert_called_once_with()
def test_optional_dependency_error_is_actionable(self):
original_import = builtins.__import__
def reject_lerobot(name, *args, **kwargs):
if name.startswith("lerobot"):
raise ImportError("missing for test")
return original_import(name, *args, **kwargs)
with patch("builtins.__import__", side_effect=reject_lerobot):
with self.assertRaisesRegex(
ImportError, r"install 'pypaimon\[lerobot\]'"):
_import_lerobot_dataset()
def test_schema_comes_from_metadata_and_rejects_unsupported_types(self):
info = {
"features": {
"scalar": {"dtype": "uint16", "shape": [1]},
"vector": {"dtype": "float32", "shape": [3]},
"tensor": {"dtype": "float64", "shape": [2, 3]},
"image": {"dtype": "image", "shape": [8, 10, 3]},
}
}
schema = _schema_from_info(info)
self.assertEqual(
["scalar", "vector", "tensor", "image"],
schema.names,
)
self.assertEqual(pa.int32(), schema.field("scalar").type)
self.assertEqual(pa.list_(pa.float32()), schema.field("vector").type)
self.assertEqual(
pa.list_(pa.list_(pa.float64())),
schema.field("tensor").type,
)
self.assertEqual(pa.large_binary(), schema.field("image").type)
info["features"]["scalar"]["dtype"] = "uint64"
with self.assertRaisesRegex(ValueError, "no lossless Paimon integer"):
_schema_from_info(info)
info["features"] = {
"camera": {
"dtype": "video",
"shape": [8, 10, 3],
}
}
self.assertEqual(
pa.large_binary(),
_schema_from_info(info).field("camera").type,
)
def test_tensor_schema_uses_arrays_and_preserves_shape(self):
for shape in ([3], [2, 3], [2, 1, 3]):
with self.subTest(shape=shape):
field = _schema_from_info({"features": {
"state": {"dtype": "float32", "shape": shape}
}}).field("state")
expected = pa.float32()
for unused_size in shape:
expected = pa.list_(expected)
self.assertEqual(expected, field.type)
self.assertIn(
"shape=" + json.dumps(shape, separators=(",", ":")),
field.metadata[b"description"].decode("utf-8"))
def test_existing_schema_preserves_lerobot_feature_contract(self):
source = _schema_from_info({
"features": {
"scalar": {"dtype": "float32", "shape": [1]},
"vector": {
"dtype": "float32",
"shape": [3],
"names": ["x", "y", "z"],
},
"tensor": {"dtype": "float32", "shape": [2, 3]},
"image": {"dtype": "image", "shape": [8, 10, 3]},
}
})
replacements = {
"shape": pa.field(
"tensor",
source.field("tensor").type,
nullable=False,
metadata=_replaced_contract(
source.field("tensor"), "shape=[2,3]", "shape=[5,3]"),
),
"dtype": pa.field(
"scalar",
pa.float64(),
nullable=False,
metadata=_replaced_contract(
source.field("scalar"),
"dtype=float32",
"dtype=float64",
),
),
"names": pa.field(
"vector",
source.field("vector").type,
nullable=False,
metadata=_replaced_contract(
source.field("vector"),
'names=["x","y","z"]',
'names=["z","y","x"]',
),
),
"array": pa.field(
"vector",
pa.list_(pa.float64()),
nullable=False,
metadata=source.field("vector").metadata,
),
"bytes": pa.field(
"image",
pa.binary(),
nullable=False,
metadata=source.field("image").metadata,
),
}
for name, replacement in replacements.items():
with self.subTest(name=name):
target = pa.schema([
replacement if field.name == replacement.name else field
for field in source
])
with self.assertRaisesRegex(
ValueError, "cannot be converted"):
_validate_lerobot_schema(source, target, "dataset")
def test_v3_required_features_have_native_types(self):
features = {
"timestamp": {"dtype": "float32", "shape": [1]},
"frame_index": {"dtype": "int64", "shape": [1]},
"episode_index": {"dtype": "int64", "shape": [1]},
"index": {"dtype": "int64", "shape": [1]},
"task_index": {"dtype": "int64", "shape": [1]},
}
_validate_v3_required_features({"features": features})
for name, replacement in (
("timestamp", {"dtype": "float64", "shape": [1]}),
("frame_index", {"dtype": "int32", "shape": [1]}),
("episode_index", {"dtype": "int64", "shape": [2]})):
with self.subTest(name=name):
invalid = dict(features)
invalid[name] = replacement
with self.assertRaisesRegex(
ValueError, "required feature %s" % name):
_validate_v3_required_features({"features": invalid})
missing = dict(features)
del missing["task_index"]
with self.assertRaisesRegex(ValueError, "required feature task_index"):
_validate_v3_required_features({"features": missing})
def test_remote_episode_metadata_projects_stats_columns(self):
source = _LeRobotSource(
path="oss://bucket/robot",
root=None,
repo_id="",
file_io=Mock(),
)
info = {
"total_frames": 1,
"total_episodes": 1,
"total_tasks": 0,
"data_path": "data/chunk-{chunk_index:03d}/file-{file_index:03d}.parquet",
}
episode_table = pa.table({
"episode_index": [0],
"dataset_from_index": [0],
"dataset_to_index": [1],
"length": [1],
"data/chunk_index": [0],
"data/file_index": [0],
})
with patch(
"pypaimon.multimodal.lerobot.source._remote_parquet_files",
return_value=["oss://bucket/robot/meta/episodes/file.parquet"]):
with patch(
"pypaimon.multimodal.lerobot.source._read_remote_parquet",
return_value=episode_table,
) as read_parquet:
dataset = _RemoteLeRobotDataset(source, info)
read_parquet.assert_called_once_with(
source.file_io,
"oss://bucket/robot/meta/episodes/file.parquet",
columns=_RemoteLeRobotDataset._EPISODE_COLUMNS,
)
self.assertIsInstance(dataset._episode_starts, array)
self.assertNotIsInstance(dataset.meta.episodes, list)
def test_empty_local_dataset_is_rejected_before_opening_lerobot(self):
temp_dir = Path(tempfile.mkdtemp(prefix="pypaimon_lerobot_empty_"))
try:
source = temp_dir / "source"
(source / "meta").mkdir(parents=True)
(source / "meta" / "info.json").write_text(json.dumps({
"codebase_version": "v3.0",
"total_frames": 0,
"total_episodes": 0,
"total_tasks": 0,
"fps": 30,
"features": {
"index": {"dtype": "int64", "shape": [1]},
"timestamp": {
"dtype": "float32",
"shape": [1],
"fps": 10.0,
},
"camera": {
"dtype": "video",
"shape": [8, 10, 3],
"video_info": {"video.fps": 10.0},
},
},
}))
connection = pmm.connect(options={
"warehouse": str(temp_dir / "warehouse"),
})
with patch(
"pypaimon.multimodal.lerobot.api._import_lerobot_dataset"
) as import_lerobot:
with self.assertRaisesRegex(ValueError, "non-empty"):
connection.load_from_lerobot("empty_frames", source)
import_lerobot.assert_not_called()
with self.assertRaises(TableNotExistException):
connection.get_table("empty_frames_frames")
finally:
shutil.rmtree(temp_dir, ignore_errors=True)
def test_video_options_must_match_metadata(self):
temp_dir = Path(tempfile.mkdtemp(prefix="pypaimon_lerobot_options_"))
try:
info = {
"features": {
"index": {"dtype": "int64", "shape": [1]},
"camera_a": {"dtype": "video", "shape": [8, 10, 3]},
"camera_b": {"dtype": "video", "shape": [8, 10, 3]},
},
}
connection = pmm.connect(options={
"warehouse": str(temp_dir / "warehouse"),
})
schema = _schema_from_info(info)
metadata = {"stats_table": None, "subtasks_table": None}
with self.assertRaisesRegex(ValueError, "do not match"):
_create_target_table(
connection,
"conflict",
schema,
options={"video-frame-field": "camera_a"},
metadata=metadata,
video_fields=("camera_a", "camera_b"),
)
table = _create_target_table(
connection,
"reordered",
schema,
options={"video-frame-field": "camera_b,camera_a"},
metadata=metadata,
video_fields=("camera_a", "camera_b"),
)
self.assertEqual(
{"camera_a", "camera_b"},
table.raw_table.options.video_frame_fields(),
)
finally:
shutil.rmtree(temp_dir, ignore_errors=True)
def test_video_import_requires_single_writer_layout(self):
temp_dir = Path(tempfile.mkdtemp(prefix="pypaimon_lerobot_layout_"))
try:
info = {
"features": {
"episode_index": {"dtype": "int64", "shape": [1]},
"camera": {"dtype": "video", "shape": [8, 10, 3]},
},
}
schema = _schema_from_info(info)
connection = pmm.connect(options={
"warehouse": str(temp_dir / "warehouse"),
})
with self.assertRaisesRegex(ValueError, "bucket-unaware"):
_create_target_table(
connection,
"bucketed",
schema,
options={"bucket": "1"},
metadata={"stats_table": None, "subtasks_table": None},
video_fields=("camera",),
)
finally:
shutil.rmtree(temp_dir, ignore_errors=True)
def test_empty_fast_path_validates_required_counts(self):
temp_dir = Path(tempfile.mkdtemp(prefix="pypaimon_lerobot_counts_"))
try:
connection = pmm.connect(options={
"warehouse": str(temp_dir / "warehouse"),
})
base_info = {
"codebase_version": "v3.0",
"total_frames": 0,
"total_episodes": 0,
"total_tasks": 0,
"fps": 30,
"features": {
"index": {"dtype": "int64", "shape": [1]},
},
}
cases = [
("missing_frames", {}, "total_frames",
"missing required field total_frames"),
("inconsistent_episodes", {"total_episodes": 1}, None,
"must both be zero or both be positive"),
("negative_tasks", {"total_tasks": -1}, None,
"must be a non-negative integer"),
]
for name, updates, missing_field, message in cases:
with self.subTest(name=name):
info = dict(base_info)
info.update(updates)
if missing_field is not None:
info.pop(missing_field)
source = temp_dir / name
(source / "meta").mkdir(parents=True)
(source / "meta" / "info.json").write_text(
json.dumps(info))
with patch(
"pypaimon.multimodal.lerobot.api."
"_import_lerobot_dataset"
) as import_lerobot:
with self.assertRaisesRegex(ValueError, message):
connection.load_from_lerobot(name, source)
import_lerobot.assert_not_called()
finally:
shutil.rmtree(temp_dir, ignore_errors=True)
def test_empty_dataset_with_tasks_is_rejected(self):
temp_dir = Path(tempfile.mkdtemp(prefix="pypaimon_lerobot_empty_meta_"))
try:
source = temp_dir / "source"
(source / "meta").mkdir(parents=True)
info = {
"codebase_version": "v3.0",
"total_frames": 0,
"total_episodes": 0,
"total_tasks": 1,
"fps": 30,
"features": {
"index": {"dtype": "int64", "shape": [1]},
"task_index": {"dtype": "int64", "shape": [1]},
},
}
(source / "meta" / "info.json").write_text(json.dumps(info))
(source / "meta" / "stats.json").write_text(json.dumps({
"index": {"min": [0], "max": [0]},
}))
pq.write_table(pa.table({
"task_index": [0],
"task": ["pick"],
}), source / "meta" / "tasks.parquet")
connection = pmm.connect(options={
"warehouse": str(temp_dir / "warehouse"),
})
with self.assertRaisesRegex(ValueError, "non-empty"):
connection.load_from_lerobot("frames", source)
with self.assertRaises(TableNotExistException):
connection.get_table("frames_frames")
finally:
shutil.rmtree(temp_dir, ignore_errors=True)
def test_optional_subtasks_keep_their_native_schema(self):
import pandas as pd
temp_dir = Path(tempfile.mkdtemp(prefix="pypaimon_lerobot_subtasks_"))
try:
source = temp_dir / "source"
(source / "meta").mkdir(parents=True)
(source / "meta" / "info.json").write_text(json.dumps({
"codebase_version": "v3.0",
"total_frames": 0,
"total_episodes": 0,
"total_tasks": 0,
"fps": 30,
"features": {
"index": {"dtype": "int64", "shape": [1]},
"subtask_index": {"dtype": "int64", "shape": [1]},
},
}))
pq.write_table(pa.Table.from_pandas(pd.DataFrame(
{"subtask_index": [0]},
index=pd.Index(["reach"], name="instruction"),
)), source / "meta" / "subtasks.parquet")
source_info = json.loads(
(source / "meta" / "info.json").read_text())
metadata = _load_dataset_metadata(
None,
source_info,
_LeRobotSource(
path=str(source),
root=source,
repo_id="local/subtasks",
),
)
expected = pq.read_table(source / "meta" / "subtasks.parquet")
self.assertTrue(metadata["subtasks_table"].equals(expected))
self.assertEqual([0], list(metadata["subtask_indices"]))
finally:
shutil.rmtree(temp_dir, ignore_errors=True)
def test_subtask_metadata_must_match_frame_feature(self):
import pandas as pd
info = {"features": {"subtask_index": {}}}
with self.assertRaisesRegex(ValueError, "subtasks.parquet is missing"):
_subtask_indices(None, info)
reordered = pa.Table.from_pandas(pd.DataFrame(
{"subtask_index": [1, 0]},
index=pd.Index(["reach", "grasp"], name="instruction"),
))
with self.assertRaisesRegex(ValueError, "numeric and text mappings"):
_subtask_indices(reordered, info)
with self.assertRaisesRegex(ValueError, "Pandas index"):
_subtask_indices(pa.table({
"subtask_index": [0],
}), info)
def test_native_metadata_does_not_require_json_values(self):
import pandas as pd
temp_dir = Path(tempfile.mkdtemp(prefix="pypaimon_lerobot_native_"))
try:
source = temp_dir / "source"
(source / "meta" / "episodes").mkdir(parents=True)
info = {
"codebase_version": "v3.0",
"total_frames": 1,
"total_episodes": 1,
"total_tasks": 1,
"fps": 30,
"features": {
"index": {"dtype": "int64", "shape": [1]},
},
}
pq.write_table(pa.Table.from_pandas(pd.DataFrame(
{
"task_index": [0],
"native_bytes": [b"\xff"],
},
index=pd.Index(["pick"], name="instruction"),
)), source / "meta" / "tasks.parquet")
episode_table = pa.table({
"episode_index": [0],
"dataset_from_index": [0],
"dataset_to_index": [1],
"tasks": [["pick"]],
"length": [1],
"native_bytes": [b"\xff"],
"stats/value/mean": [float("nan")],
})
pq.write_table(
episode_table,
source / "meta" / "episodes" / "part.parquet",
)
(source / "meta" / "stats.json").write_text(json.dumps({
"mean": float("nan"),
"max": float("inf"),
}))
metadata = _load_dataset_metadata(
None,
info,
_LeRobotSource(
path=str(source),
root=source,
repo_id="local/native-metadata",
),
)
self.assertIsNone(metadata["episodes"])
self.assertEqual(1, len(metadata["episode_paths"]))
stored_episode = list(_validated_episode_tables(metadata))[0]
self.assertEqual(1, len(metadata["episodes"]))
self.assertEqual(
b"\xff",
stored_episode.column("native_bytes")[0].as_py(),
)
self.assertTrue(np.isnan(
stored_episode.column("stats/value/mean")[0].as_py()))
self.assertEqual(
b"\xff",
metadata["tasks_table"].column("native_bytes")[0].as_py(),
)
stored_stats = {
row["key"]: json.loads(row["value"])
for row in metadata["stats_table"].to_pylist()
}
self.assertTrue(np.isnan(stored_stats["mean"]))
self.assertTrue(np.isinf(stored_stats["max"]))
finally:
shutil.rmtree(temp_dir, ignore_errors=True)
def test_invalid_fps_creates_no_table(self):
temp_dir = Path(tempfile.mkdtemp(prefix="pypaimon_lerobot_fps_"))
try:
source = temp_dir / "source"
(source / "meta").mkdir(parents=True)
(source / "meta" / "info.json").write_text(json.dumps({
"codebase_version": "v3.0",
"total_frames": 0,
"total_episodes": 0,
"total_tasks": 0,
"fps": 0,
"features": {
"index": {"dtype": "int64", "shape": [1]},
},
}))
connection = pmm.connect(options={
"warehouse": str(temp_dir / "warehouse"),
})
with self.assertRaisesRegex(ValueError, "fps must be positive"):
connection.load_from_lerobot("frames", source)
with self.assertRaises(TableNotExistException):
connection.get_table("frames_frames")
finally:
shutil.rmtree(temp_dir, ignore_errors=True)
def test_source_values_are_safely_converted(self):
class Dataset:
def __init__(self, value):
self.value = value
def read_batch(self, unused_begin, unused_end):
return pa.table({"value": [self.value]})
cases = [
({"dtype": "int32", "shape": [1]}, 1.5, "safely converted"),
({"dtype": "float32", "shape": [1]}, 1e100, "float32 range"),
({"dtype": "uint8", "shape": [1]}, -1, "uint8 range"),
({"dtype": "uint8", "shape": [2]}, [0, 256], "uint8 range"),
({"dtype": "uint16", "shape": [1]}, 65536, "uint16 range"),
({"dtype": "uint32", "shape": [1]}, -1, "uint32 range"),
({"dtype": "float16", "shape": [1]}, 70000, "float16 range"),
({"dtype": "uint8", "shape": [1]}, "256", "non-numeric"),
({"dtype": "float32", "shape": [1]}, "1e100", "non-numeric"),
({"dtype": "bool", "shape": [1]}, 2, "non-boolean"),
({"dtype": "string", "shape": [1]}, 123, "non-string"),
]
for feature, value, message in cases:
with self.subTest(feature=feature, value=value):
info = {"features": {"value": feature}}
schema = _schema_from_info(info)
with self.assertRaisesRegex(ValueError, message):
_read_batch(Dataset(value), info, 0, 1, schema)
boundary_cases = [
({"dtype": "uint8", "shape": [1]}, 255),
({"dtype": "uint16", "shape": [1]}, 65535),
({"dtype": "uint32", "shape": [1]}, 4294967295),
({"dtype": "float16", "shape": [1]}, 65504),
({"dtype": "bool", "shape": [1]}, True),
({"dtype": "string", "shape": [1]}, "pick"),
]
for feature, value in boundary_cases:
with self.subTest(feature=feature, value=value):
info = {"features": {"value": feature}}
schema = _schema_from_info(info)
result = _read_batch(Dataset(value), info, 0, 1, schema)
self.assertEqual(value, result.column("value")[0].as_py())
def test_local_v2_is_rejected_before_opening(self):
temp_dir = Path(tempfile.mkdtemp(prefix="pypaimon_lerobot_v2_"))
try:
info_dir = temp_dir / "meta"
info_dir.mkdir()
(info_dir / "info.json").write_text(json.dumps({
"codebase_version": "v2.1",
"features": {"index": {"dtype": "int64", "shape": [1]}},
}))
connection = pmm.connect(options={
"warehouse": str(temp_dir / "warehouse"),
})
with self.assertRaisesRegex(ValueError, "supports LeRobot Dataset v3 only"):
connection.load_from_lerobot("frames", temp_dir)
finally:
shutil.rmtree(temp_dir, ignore_errors=True)
def test_episode_aware_multi_video_import(self):
import pandas as pd
temp_dir = Path(tempfile.mkdtemp(prefix="pypaimon_lerobot_video_"))
try:
info_dir = temp_dir / "meta"
info_dir.mkdir()
info = {
"codebase_version": "v3.0",
"fps": 10,
"total_frames": 5,
"total_episodes": 2,
"total_tasks": 1,
"data_path": (
"data/chunk-{chunk_index:03d}/"
"file-{file_index:03d}.parquet"
),
"video_path": (
"videos/{video_key}/chunk-{chunk_index:03d}/"
"file-{file_index:03d}.mp4"
),
"features": {
"index": {"dtype": "int64", "shape": [1]},
"episode_index": {"dtype": "int64", "shape": [1]},
"frame_index": {"dtype": "int64", "shape": [1]},
"timestamp": {
"dtype": "float32",
"shape": [1],
"fps": 10.0,
},
"task_index": {"dtype": "int64", "shape": [1]},
"observation.state": {
"dtype": "float32",
"shape": [3],
},
"camera_a": {
"dtype": "video",
"shape": [8, 10, 3],
"video_info": {"video.fps": 10.0},
},
"camera_b": {
"dtype": "video",
"shape": [8, 10, 3],
"video_info": {"video.fps": 10.0},
},
},
}
(info_dir / "info.json").write_text(json.dumps(info))
payloads = {
"camera_a/chunk-000/file-000.mp4": b"camera-a",
"camera_b/chunk-000/file-000.mp4": b"camera-b-0",
"camera_b/chunk-000/file-001.mp4": b"camera-b-1",
}
for relative, payload in payloads.items():
path = temp_dir / "videos" / relative
path.parent.mkdir(parents=True, exist_ok=True)
path.write_bytes(payload)
episodes = [
{
"episode_index": 0,
"dataset_from_index": 0,
"dataset_to_index": 2,
"length": 2,
"data/chunk_index": 0,
"data/file_index": 0,
"tasks": ["pick"],
"videos/camera_a/chunk_index": 0,
"videos/camera_a/file_index": 0,
"videos/camera_a/from_timestamp": 0.5,
"videos/camera_a/to_timestamp": 0.7,
"videos/camera_b/chunk_index": 0,
"videos/camera_b/file_index": 0,
"videos/camera_b/from_timestamp": 0.0,
"videos/camera_b/to_timestamp": 0.2,
},
{
"episode_index": 1,
"dataset_from_index": 2,
"dataset_to_index": 5,
"length": 3,
"data/chunk_index": 0,
"data/file_index": 0,
"tasks": ["pick"],
"videos/camera_a/chunk_index": 0,
"videos/camera_a/file_index": 0,
"videos/camera_a/from_timestamp": 0.1,
"videos/camera_a/to_timestamp": 0.4,
"videos/camera_b/chunk_index": 0,
"videos/camera_b/file_index": 1,
"videos/camera_b/from_timestamp": 0.0,
"videos/camera_b/to_timestamp": 0.3,
},
]
# The published Episode Parquet, not the dataset object's stale
# metadata cache, must determine the imported video descriptors.
cached_episodes = [dict(episode) for episode in episodes]
cached_episodes[1].update({
"videos/camera_b/file_index": 0,
"videos/camera_b/from_timestamp": 0.2,
"videos/camera_b/to_timestamp": 0.5,
})
class Dataset:
root = temp_dir
meta = SimpleNamespace(
info=info, episodes=cached_episodes, tasks=["pick"])
rows = pa.table({
"index": pa.array(range(5), type=pa.int64()),
"episode_index": pa.array(
[0, 0, 1, 1, 1], type=pa.int64()),
"frame_index": pa.array(
[0, 1, 0, 1, 2], type=pa.int64()),
"timestamp": pa.array(
[0.0, 0.1, 0.0, 0.1, 0.2],
type=pa.float32(),
),
"task_index": pa.array([0] * 5, type=pa.int64()),
"observation.state": pa.array(
[[float(index), 0.0, 1.0] for index in range(5)],
type=pa.list_(pa.float32(), 3),
),
})
def __len__(self):
return 5
def read_batch(self, begin, end):
return self.rows.slice(begin, end - begin)
connection = pmm.connect(options={
"warehouse": str(temp_dir / "warehouse"),
})
episodes_path = (
temp_dir / "meta/episodes/chunk-000/file-000.parquet")
episodes_path.parent.mkdir(parents=True)
pq.write_table(pa.Table.from_pylist(episodes), episodes_path)
pq.write_table(pa.Table.from_pandas(pd.DataFrame(
{"task_index": [0]},
index=pd.Index(["pick"], name="task"),
)), temp_dir / "meta/tasks.parquet")
def sample_timestamps(unused_dataset, uri):
return (
[0.0, 0.1, 0.2, 0.3, 0.5, 0.6]
if "camera_a" in uri else
[0.0, 0.1, 0.2, 0.3, 0.4]
)
with patch(
"pypaimon.multimodal.lerobot.api."
"_import_lerobot_dataset",
return_value=object,
), patch(
"pypaimon.multimodal.lerobot.api."
"_open_resolved_dataset",
return_value=Dataset(),
), patch(
"pypaimon.multimodal.lerobot.loader."
"_video_sample_timestamps",
side_effect=sample_timestamps,
):
result = connection.load_from_lerobot(
"frames", temp_dir, batch_size=1)
self.assertIsNone(result)
table = connection.get_table("frames_frames")
self.assertEqual(
{"camera_a", "camera_b"},
table.raw_table.options.video_frame_fields(),
)
field_types = {
field.name: str(field.type)
for field in table.raw_table.fields
}
self.assertEqual(
"ARRAY<FLOAT> NOT NULL",
field_types["observation.state"],
)
rows = table.scan().select([
"index", "camera_a", "camera_b"
]).to_arrow().sort_by("index").to_pylist()
camera_a = [
pmm.VideoFrameDescriptor.deserialize(row["camera_a"])
for row in rows
]
camera_b = [
pmm.VideoFrameDescriptor.deserialize(row["camera_b"])
for row in rows
]
self.assertEqual(
[4, 5, 1, 2, 3],
[descriptor.frame_index for descriptor in camera_a],
)
self.assertEqual(
[0, 1, 0, 1, 2],
[descriptor.frame_index for descriptor in camera_b],
)
_, bodies = table.scan().select([
"index", "camera_a", "camera_b"
]).read_blobs()
self.assertEqual(
[payloads["camera_a/chunk-000/file-000.mp4"]] * 5,
bodies["camera_a"],
)
self.assertEqual(
[payloads["camera_b/chunk-000/file-000.mp4"]] * 2
+ [payloads["camera_b/chunk-000/file-001.mp4"]] * 3,
bodies["camera_b"],
)
data_path = temp_dir / "data/chunk-000/file-000.parquet"
data_path.parent.mkdir(parents=True)
pq.write_table(Dataset.rows, data_path)
remote = "oss://source-bucket/robot-videos"
source_file_io = _RemoteLeRobotFileIO(temp_dir, remote)
with patch(
"pypaimon.multimodal.lerobot.source._SourceFileIO",
return_value=source_file_io,
), patch(
"pypaimon.multimodal.lerobot.api."
"_import_lerobot_dataset",
return_value=object,
), patch(
"pypaimon.multimodal.lerobot.loader."
"_video_sample_timestamps",
side_effect=sample_timestamps,
):
remote_result = connection.load_from_lerobot(
"remote_frames", remote, batch_size=1)
self.assertIsNone(remote_result)
opened_videos = [
path for path in source_file_io.opened_paths
if path.endswith(".mp4")
]
self.assertEqual(
{
remote + "/videos/" + relative
for relative in payloads
},
set(opened_videos),
)
self.assertEqual(1, source_file_io.close_count)
_, remote_bodies = connection.get_table(
"remote_frames_frames").scan().select([
"index", "camera_a", "camera_b"
]).read_blobs()
self.assertEqual(bodies, remote_bodies)
# Both cameras now share one physical MP4 across Episodes. The
# logical Episode boundary must still control normal-file rolling.
episodes[1].update({
"videos/camera_b/file_index": 0,
"videos/camera_b/from_timestamp": 0.2,
"videos/camera_b/to_timestamp": 0.5,
})
pq.write_table(pa.Table.from_pylist(episodes), episodes_path)
with patch(
"pypaimon.multimodal.lerobot.api."
"_import_lerobot_dataset",
return_value=object,
), patch(
"pypaimon.multimodal.lerobot.api."
"_open_resolved_dataset",
return_value=Dataset(),
), patch(
"pypaimon.multimodal.lerobot.loader."
"_video_sample_timestamps",
side_effect=sample_timestamps,
):
connection.load_from_lerobot(
"shared_video_frames",
temp_dir,
batch_size=1,
options={"target-file-row-num": "1"},
)
raw_table = connection.get_table(
"shared_video_frames_frames").raw_table
files = {
file.file_name: file
for split in raw_table.new_read_builder().new_scan().plan().splits()
for file in split.files
}.values()
self.assertEqual(
[2, 3],
sorted(
file.row_count for file in files
if not file.file_name.endswith(".video")
and ".vector." not in file.file_name
),
)
self.assertFalse(any(".vector." in file.file_name for file in files))
finally:
shutil.rmtree(temp_dir, ignore_errors=True)
@unittest.skipUnless(av is not None, "PyAV is required for MP4 decoding")
def test_imported_video_payload_can_be_decoded(self):
self._assert_imported_video_payload_can_be_decoded(False)
@unittest.skipUnless(
av is not None
and sys.version_info >= (3, 10)
and importlib.util.find_spec("datasets") is not None
and importlib.util.find_spec("torch") is not None,
"Video training reads require Python 3.10+, PyAV, datasets, and Torch",
)
def test_imported_video_payload_supports_training_reads(self):
self._assert_imported_video_payload_can_be_decoded(True)
@unittest.skipUnless(av is not None, "PyAV is required for MP4 decoding")
def test_remuxed_episode_payload_can_be_decoded(self):
self._assert_imported_video_payload_can_be_decoded(False, remux=True)
@unittest.skipUnless(
av is not None
and sys.version_info >= (3, 10)
and importlib.util.find_spec("datasets") is not None
and importlib.util.find_spec("torch") is not None,
"Video training reads require Python 3.10+, PyAV, datasets, and Torch",
)
def test_remuxed_episode_payload_supports_training_reads(self):
self._assert_imported_video_payload_can_be_decoded(True, remux=True)
@unittest.skipUnless(av is not None, "PyAV is required for MP4 decoding")
def test_remux_local_source_does_not_use_destination_file_io(self):
self._assert_imported_video_payload_can_be_decoded(
False, remux=True, remote_destination=True)
def _assert_imported_video_payload_can_be_decoded(
self, training_reads, remux=False, remote_destination=False):
import pandas as pd
temp_dir = Path(tempfile.mkdtemp(prefix="pypaimon_lerobot_mp4_"))
try:
info = {
"codebase_version": "v3.0",
"fps": 10,
"total_frames": 5,
"total_episodes": 2,
"total_tasks": 1,
"data_path": (
"data/chunk-{chunk_index:03d}/"
"file-{file_index:03d}.parquet"
),
"video_path": (
"videos/{video_key}/chunk-{chunk_index:03d}/"
"file-{file_index:03d}.mp4"
),
"features": {
"index": {"dtype": "int64", "shape": [1]},
"episode_index": {"dtype": "int64", "shape": [1]},
"frame_index": {"dtype": "int64", "shape": [1]},
"timestamp": {
"dtype": "float32",
"shape": [1],
"fps": 10.0,
},
"task_index": {"dtype": "int64", "shape": [1]},
"action": {"dtype": "float32", "shape": [1]},
"camera": {
"dtype": "video",
"shape": [128, 128, 3],
"video_info": {"video.fps": 10.0},
},
"camera_b": {
"dtype": "video",
"shape": [128, 128, 3],
"video_info": {"video.fps": 10.0},
},
},
}
episodes = [
{
"episode_index": 0,
"dataset_from_index": 0,
"dataset_to_index": 2,
"length": 2,
"data/chunk_index": 0,
"data/file_index": 0,
"tasks": ["pick"],
"videos/camera/chunk_index": 0,
"videos/camera/file_index": 0,
"videos/camera/from_timestamp": 5.5,
"videos/camera/to_timestamp": 5.7,
"videos/camera_b/chunk_index": 0,
"videos/camera_b/file_index": 0,
"videos/camera_b/from_timestamp": 5.5,
"videos/camera_b/to_timestamp": 5.7,
},
{
"episode_index": 1,
"dataset_from_index": 2,
"dataset_to_index": 5,
"length": 3,
"data/chunk_index": 0,
"data/file_index": 0,
"tasks": ["pick"],
"videos/camera/chunk_index": 0,
"videos/camera/file_index": 0,
"videos/camera/from_timestamp": 6.1,
"videos/camera/to_timestamp": 6.4,
"videos/camera_b/chunk_index": 0,
"videos/camera_b/file_index": 0,
"videos/camera_b/from_timestamp": 6.1,
"videos/camera_b/to_timestamp": 6.4,
},
]
target_frame_values = {
55: 168, 56: 216, 61: 56, 62: 88, 63: 120,
}
info_dir = temp_dir / "meta"
info_dir.mkdir()
(info_dir / "info.json").write_text(json.dumps(info))
episodes_path = (
info_dir / "episodes/chunk-000/file-000.parquet")
episodes_path.parent.mkdir(parents=True)
pq.write_table(pa.Table.from_pylist(episodes), episodes_path)
pq.write_table(pa.Table.from_pandas(pd.DataFrame(
{"task_index": [0]},
index=pd.Index(["pick"], name="task"),
)), info_dir / "tasks.parquet")
video_path = (
temp_dir / "videos/camera/chunk-000/file-000.mp4")
video_path.parent.mkdir(parents=True)
with av.open(str(video_path), mode="w") as container:
stream = container.add_stream("mpeg4", rate=10)
stream.width = 128
stream.height = 128
stream.pix_fmt = "yuv420p"
stream.time_base = Fraction(1, 10)
stream.gop_size = 12
stream.codec_context.max_b_frames = 2
for pts in range(120):
value = target_frame_values.get(pts)
if value is None:
noise = np.random.RandomState(pts).randint(
0, 256, size=(128, 128, 3))
image = noise.astype(np.uint8)
else:
image = np.full(
(128, 128, 3), value, dtype=np.uint8)
frame = av.VideoFrame.from_ndarray(image, format="rgb24")
frame.pts = pts
frame.time_base = Fraction(1, 10)
for packet in stream.encode(frame):
container.mux(packet)
for packet in stream.encode():
container.mux(packet)
# MPEG4 is lossy: compare imported frames with the source video's
# decoded pixels rather than its uncompressed input images.
with av.open(str(video_path)) as container:
expected_frame_values = [
float(frame.to_ndarray(format="rgb24").mean())
for index, frame in enumerate(container.decode(video=0))
if index in target_frame_values
]
camera_b_path = (
temp_dir / "videos/camera_b/chunk-000/file-000.mp4")
camera_b_path.parent.mkdir(parents=True)
shutil.copy2(video_path, camera_b_path)
class Dataset:
root = temp_dir
meta = SimpleNamespace(
info=info, episodes=episodes, tasks=["pick"])
rows = pa.table({
"index": pa.array(range(5), type=pa.int64()),
"episode_index": pa.array(
[0, 0, 1, 1, 1], type=pa.int64()),
"frame_index": pa.array(
[0, 1, 0, 1, 2], type=pa.int64()),
"timestamp": pa.array(
[0.0, 0.1, 0.0, 0.1, 0.2],
type=pa.float32(),
),
"task_index": pa.array([0] * 5, type=pa.int64()),
"action": pa.array(
[0.0, 1.0, 2.0, 3.0, 4.0], type=pa.float32()),
})
def __len__(self):
return 5
def read_batch(self, begin, end):
return self.rows.slice(begin, end - begin)
connection = pmm.connect(options={
"warehouse": str(temp_dir / "warehouse"),
"file-io.read-coalesce.max-gap": "0 b",
})
from pypaimon.multimodal.lerobot.loader import _prepare_dataset
def prepare(table, *args, **kwargs):
if not remote_destination:
return _prepare_dataset(table, *args, **kwargs)
open_destination = table.file_io.new_input_stream
def open_file(path):
if str(path).endswith(".mp4"):
raise IOError("Destination FileIO cannot open local source videos")
return open_destination(path)
with patch.object(table.file_io, "new_input_stream", side_effect=open_file):
return _prepare_dataset(table, *args, **kwargs)
with patch(
"pypaimon.multimodal.lerobot.api."
"_import_lerobot_dataset",
return_value=object,
), patch(
"pypaimon.multimodal.lerobot.api."
"_open_resolved_dataset",
return_value=Dataset(),
), patch(
"pypaimon.multimodal.lerobot.loader._prepare_dataset",
side_effect=prepare,
):
connection.load_from_lerobot(
"frames", temp_dir, batch_size=1,
options={"write.lerobot.remux-episodes": str(remux).lower()})
table = connection.get_table("frames_frames")
rows = table.scan().select([
"index", "camera", "camera_b"
]).to_arrow().sort_by("index").to_pylist()
descriptors = [
pmm.VideoFrameDescriptor.deserialize(row["camera"])
for row in rows
]
camera_b_descriptors = [
pmm.VideoFrameDescriptor.deserialize(row["camera_b"])
for row in rows
]
if not remux:
self.assertEqual(
[55, 56, 61, 62, 63],
[descriptor.frame_index for descriptor in descriptors],
)
else:
self.assertEqual(2, len(set(
d.payload_descriptor for d in descriptors)))
class Decoder:
def __init__(self, source):
self.container = av.open(source)
self.frames = list(self.container.decode(video=0))
def value(self, frame_index):
frame = self.frames[frame_index]
return float(frame.to_ndarray(
format="rgb24").mean()), float(frame.time)
def close(self):
self.container.close()
collator = pmm.VideoFrameCollator(
table,
video_column="camera",
decoder_factory=Decoder,
decode_fn=lambda decoder, frame_index, row: decoder.value(
frame_index),
output_column="decoded",
collate_fn=lambda decoded_rows: decoded_rows,
)
try:
decoded_rows = collator(rows)
self.assertEqual(2 if remux else 1, len(collator._decoders))
finally:
collator.close()
np.testing.assert_allclose(
[row["decoded"][0] for row in decoded_rows],
expected_frame_values,
atol=1e-6,
)
if not remux:
np.testing.assert_allclose(
[row["decoded"][1] for row in decoded_rows],
[5.5, 5.6, 6.1, 6.2, 6.3],
atol=1e-6,
)
if not training_reads:
return
dataset = pmm.PaimonLeRobotDataset(
table,
delta_timestamps={"camera": [0.0, 0.1]},
)
try:
transferred = []
file_io = table.raw_table.file_io
read_ranges = file_io.read_ranges_coalesced
read_file_range = file_io.read_file_range
selected = [descriptors[2], camera_b_descriptors[2]]
tracked = []
for descriptor in selected:
tracked.append((
descriptor.uri,
descriptor.offset,
descriptor.offset + descriptor.length,
))
def track(path, offset, length):
end = offset + length
if any(
path == tracked_path
and offset < tracked_end and end > tracked_begin
for tracked_path, tracked_begin, tracked_end
in tracked):
transferred.append(length)
def track_ranges(ranges, parallelism):
for path, offset, length in ranges:
track(path, offset, length)
return read_ranges(ranges, parallelism)
def track_file_range(path, offset, length):
track(path, offset, length)
return read_file_range(path, offset, length)
with patch.object(
file_io,
"read_ranges_coalesced",
side_effect=track_ranges), patch.object(
file_io,
"read_file_range",
side_effect=track_file_range):
middle, = dataset.__getitems__([2])
self.assertEqual(
[2, 3, 128, 128], list(middle["camera"].shape))
self.assertEqual(
[3, 128, 128], list(middle["camera_b"].shape))
self.assertEqual("torch.float32", str(middle["camera"].dtype))
np.testing.assert_allclose(
[
float(middle["camera"][0].mean()) * 255,
float(middle["camera"][1].mean()) * 255,
],
expected_frame_values[2:4],
atol=1e-4,
)
self.assertEqual(
[False, False], middle["camera_is_pad"].tolist())
self.assertEqual(
1, len(dataset._video_collators[0]._decoders))
self.assertGreater(sum(transferred), 0)
# A short remuxed clip may be read in full. Compare with the
# original video rather than requiring sparse IO within it.
read_limit = (video_path.stat().st_size * len(selected)
if remux else
sum(d.length for d in selected))
self.assertLess(sum(transferred), read_limit)
from torch.utils.data import DataLoader
worker_indices = []
for batch in DataLoader(
dataset,
batch_size=2,
shuffle=False,
num_workers=2,
multiprocessing_context="spawn"):
worker_indices.extend(batch["index"].tolist())
self.assertEqual(list(range(5)), worker_indices)
worker_indices = []
for batch in dataset.to_dataloader(
batch_size=2,
shuffle=False,
num_workers=2,
multiprocessing_context="spawn"):
worker_indices.extend(batch["index"].tolist())
self.assertTrue(batch["camera"].is_shared())
self.assertEqual(list(range(5)), worker_indices)
finally:
dataset.close()
action_dataset = pmm.PaimonLeRobotDataset(
table,
delta_timestamps={"action": [0.0, 0.1]},
)
try:
item = action_dataset[0]
self.assertEqual([2], list(item["action"].shape))
np.testing.assert_allclose(
[0.0, 1.0], item["action"].tolist())
self.assertEqual(
[3, 128, 128], list(item["camera"].shape))
self.assertEqual(
[3, 128, 128], list(item["camera_b"].shape))
finally:
action_dataset.close()
finally:
shutil.rmtree(temp_dir, ignore_errors=True)
def test_video_ordinals_follow_source_timestamps(self):
info = {
"fps": 10,
"features": {
"episode_index": {"dtype": "int64", "shape": [1]},
"frame_index": {"dtype": "int64", "shape": [1]},
"timestamp": {"dtype": "float32", "shape": [1]},
"camera": {
"dtype": "video",
"shape": [8, 10, 3],
"video_info": {"video.fps": 10.0},
},
},
}
rows = pa.table({
"episode_index": pa.array([0, 0, 1, 1], type=pa.int64()),
"frame_index": pa.array([0, 1, 0, 1], type=pa.int64()),
"timestamp": pa.array(
[0.0, 0.1, 0.0, 0.1], type=pa.float32()),
})
class Dataset:
root = Path("/")
def __init__(self, rows):
self.rows = rows
def read_batch(self, begin, end):
return self.rows.slice(begin, end - begin)
def video_sample_timestamps(self, unused_uri):
return [0.0, 0.1, 0.2, 0.3, 0.5, 0.6]
schema = _schema_from_info(info)
episodes = [
{
"episode_index": 0,
"length": 2,
"dataset_from_index": 0,
"dataset_to_index": 2,
"videos/camera/chunk_index": 0,
"videos/camera/file_index": 0,
"videos/camera/from_timestamp": 0.5,
"videos/camera/to_timestamp": 0.7,
},
{
"episode_index": 1,
"length": 2,
"dataset_from_index": 2,
"dataset_to_index": 4,
"videos/camera/chunk_index": 0,
"videos/camera/file_index": 0,
"videos/camera/from_timestamp": 0.1,
"videos/camera/to_timestamp": 0.3,
},
]
video_sources = {}
with patch(
"pypaimon.multimodal.lerobot.loader._video_source",
return_value=("file:/video.mp4", 10),
):
results = [
_read_batch(
Dataset(rows), info, begin, begin + 2, schema,
episode=episode, video_sources=video_sources,
)
for begin, episode in zip((0, 2), episodes)
]
self.assertEqual(
[4, 5, 1, 2],
[
pmm.VideoFrameDescriptor.deserialize(value.as_py()).frame_index
for result in results
for value in result["camera"]
],
)
missing = dict(episodes[0])
missing.update({
"videos/camera/from_timestamp": 0.8,
"videos/camera/to_timestamp": 1.0,
})
with patch(
"pypaimon.multimodal.lerobot.loader._video_source",
return_value=("file:/video.mp4", 10),
), self.assertRaisesRegex(ValueError, "has no frame"):
_read_batch(
Dataset(rows), info, 0, 2, schema,
episode=missing, video_sources={})
for timestamp_range in (
(-0.1, 0.1), (0.0, float("nan")), (0.0, 0.3)):
invalid_episode = dict(episodes[0])
invalid_episode.update({
"videos/camera/from_timestamp": timestamp_range[0],
"videos/camera/to_timestamp": timestamp_range[1],
})
with self.subTest(timestamp_range=timestamp_range), \
self.assertRaisesRegex(ValueError, "timestamp|duration"):
_read_batch(
Dataset(rows), info, 0, 2, schema,
episode=invalid_episode, video_sources={})
def test_video_sample_timestamps_skip_discard_packets(self):
class Container:
streams = SimpleNamespace(video=[SimpleNamespace(
time_base=Fraction(1, 10))])
def __enter__(self):
return self
def __exit__(self, unused_type, unused_value, unused_traceback):
pass
def demux(self, unused_stream):
return [
SimpleNamespace(
pts=-1, time_base=Fraction(1, 10),
is_discard=True),
SimpleNamespace(
pts=0, time_base=Fraction(1, 10),
is_discard=False),
SimpleNamespace(
pts=1, time_base=Fraction(1, 10),
is_discard=False),
]
fake_av = SimpleNamespace(open=lambda unused_source: Container())
with patch.dict(sys.modules, {"av": fake_av}):
timestamps = _video_sample_timestamps(
SimpleNamespace(), "file:/video.mp4")
self.assertEqual([0.0, 0.1], list(timestamps))
def test_video_frame_matching_quantizes_float32_timestamp(self):
frame_index = 61441
timestamps = array(
"d", (index / 30 for index in range(frame_index + 1)))
frame_timestamp = pa.scalar(
frame_index / 30, type=pa.float32()).as_py()
self.assertEqual(
frame_index,
_video_frame_ordinal(
timestamps, frame_timestamp, 0.0, pa.float32(), "camera"),
)
class _RemoteLeRobotFileIO:
def __init__(self, local_root, remote_root):
self.local_root = Path(local_root)
self.remote_root = remote_root.rstrip("/")
self.opened_paths = []
self.close_count = 0
def _local_path(self, remote_path):
prefix = self.remote_root + "/"
if remote_path == self.remote_root:
return self.local_root
if not remote_path.startswith(prefix):
raise FileNotFoundError(remote_path)
return self.local_root / remote_path[len(prefix):]
def _status(self, local_path):
relative = local_path.relative_to(self.local_root).as_posix()
remote_path = self.remote_root
if relative != ".":
remote_path += "/" + relative
native_path = remote_path.split("://", 1)[1]
file_type = pafs.FileType.Directory if local_path.is_dir() \
else pafs.FileType.File
size = local_path.stat().st_size \
if file_type == pafs.FileType.File else None
return pafs.FileInfo(native_path, file_type, size=size)
def get_file_status(self, remote_path):
local_path = self._local_path(remote_path)
if not local_path.exists():
raise FileNotFoundError(remote_path)
return self._status(local_path)
def list_status(self, remote_path):
return [self._status(path) for path in sorted(
self._local_path(remote_path).iterdir())]
def new_input_stream(self, remote_path):
self.opened_paths.append(remote_path)
return self._local_path(remote_path).open("rb")
def close(self):
self.close_count += 1
class _FailingCloseDataset:
def __init__(self, dataset):
self._dataset = dataset
def __getattr__(self, name):
return getattr(self._dataset, name)
def __len__(self):
return len(self._dataset)
def close(self):
raise RuntimeError("close failed")
@unittest.skipUnless(
sys.version_info >= (3, 10) and LeRobotDataset is not None,
"LeRobot requires Python 3.10+ and the lerobot extra",
)
class LeRobotImportTest(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.source_dir = Path(tempfile.mkdtemp(prefix="pypaimon_lerobot_source_"))
cls.image_source = cls.source_dir / "images"
cls._create_image_dataset(cls.image_source)
@classmethod
def tearDownClass(cls):
shutil.rmtree(cls.source_dir, ignore_errors=True)
def setUp(self):
self.temp_dir = Path(tempfile.mkdtemp(prefix="pypaimon_lerobot_test_"))
self.connection = pmm.connect(options={
"warehouse": str(self.temp_dir / "warehouse"),
})
def tearDown(self):
shutil.rmtree(self.temp_dir, ignore_errors=True)
@staticmethod
def _create_image_dataset(root):
dataset = LeRobotDataset.create(
repo_id="pypaimon/local-image-test",
root=root,
fps=10,
use_videos=False,
image_writer_processes=0,
image_writer_threads=0,
features={
"observation.state": {
"dtype": "float32",
"shape": (3,),
"names": ["x", "y", "z"],
},
"observation.matrix": {
"dtype": "float32",
"shape": (2, 2),
"names": None,
},
"action": {
"dtype": "float32",
"shape": (2,),
"names": ["x", "y"],
},
"reward": {
"dtype": "float32",
"shape": (1,),
"names": None,
},
"observation.image": {
"dtype": "image",
"shape": (8, 10, 3),
"names": ["height", "width", "channels"],
},
},
)
for episode_index, length in enumerate((2, 3)):
for frame_index in range(length):
value = episode_index * 80 + frame_index * 10
dataset.add_frame({
"observation.state": np.array(
[episode_index, frame_index, episode_index + frame_index],
dtype=np.float32,
),
"observation.matrix": np.array(
[[episode_index, frame_index], [frame_index, episode_index]],
dtype=np.float32,
),
"action": np.array(
[frame_index, -frame_index], dtype=np.float32),
"reward": np.array([float(frame_index == length - 1)],
dtype=np.float32),
"observation.image": np.full(
(8, 10, 3), value, dtype=np.uint8),
"task": "pick" if episode_index == 0 else "place",
})
dataset.save_episode()
dataset.finalize()
def test_hub_metadata_accepts_installed_lerobot_info(self):
from lerobot.datasets.lerobot_dataset import LeRobotDatasetMetadata
from pypaimon.multimodal.lerobot.source import _load_hub_info
metadata = LeRobotDatasetMetadata(
repo_id="pypaimon/local-image-test", root=self.image_source)
with patch("lerobot.datasets.lerobot_dataset.LeRobotDatasetMetadata",
return_value=metadata):
info = _load_hub_info(_LeRobotSource(
path="pypaimon/local-image-test", root=None,
repo_id="pypaimon/local-image-test"))
self.assertIsInstance(info, dict)
self.assertEqual(info["total_frames"], 5)
self.assertEqual(info["total_episodes"], 2)
self.assertEqual(info["features"]["observation.state"]["dtype"], "float32")
def test_custom_reader_accepts_installed_lerobot_metadata(self):
from lerobot.datasets.lerobot_dataset import LeRobotDatasetMetadata
metadata = LeRobotDatasetMetadata(
repo_id="pypaimon/local-image-test", root=self.image_source)
reader = _ManualDatasetReader(metadata)
try:
self.assertEqual(reader.meta.info["total_frames"], 5)
self.assertEqual(reader.meta.info["total_episodes"], 2)
self.assertEqual(
reader.meta.features["observation.state"]["shape"], (3,))
finally:
reader.close()
def test_import_accepts_integral_float_fps(self):
source = self.temp_dir / "float-fps-source"
shutil.copytree(self.image_source, source)
info_path = source / "meta" / "info.json"
info = json.loads(info_path.read_text())
info["fps"] = float(info["fps"])
info_path.write_text(json.dumps(info))
for name, location in (("local_fps", source), ("uri_fps", source.as_uri())):
with self.subTest(source=name):
self.connection.load_from_lerobot(name, location)
self.assertEqual(info, _catalog_metadata(self.connection, name + "_info"))
data = self.connection.get_table(name + "_frames").scan().to_arrow()
self.assertEqual(info["total_frames"], data.num_rows)
dataset = pmm.PaimonLeRobotDataset(self.connection.get_table(name + "_frames"))
self.assertEqual(info["fps"], dataset.meta.fps)
self.assertEqual(0, dataset[0]["frame_index"].item())
def test_local_import_preserves_extended_info(self):
source = self.temp_dir / "extended-source"
shutil.copytree(self.image_source, source)
info_path = source / "meta" / "info.json"
info = json.loads(info_path.read_text())
info["vendor_extension"] = {"labels": ["pick"], "enabled": True}
info_path.write_text(json.dumps(info))
self.connection.load_from_lerobot("extended_info", source)
self.assertEqual(
info, _catalog_metadata(self.connection, "extended_info_info"))
def test_hub_import_preserves_extended_info(self):
from lerobot.datasets.lerobot_dataset import LeRobotDatasetMetadata
source = self.temp_dir / "hub-source"
shutil.copytree(self.image_source, source)
info_path = source / "meta" / "info.json"
info = json.loads(info_path.read_text())
info["vendor_extension"] = {"labels": ["pick"], "enabled": True}
info_path.write_text(json.dumps(info))
repo_id = "pypaimon/hub-image-test"
metadata = LeRobotDatasetMetadata(repo_id=repo_id, root=source)
dataset = LeRobotDataset(repo_id=repo_id, root=source)
with patch("lerobot.datasets.lerobot_dataset.LeRobotDatasetMetadata",
return_value=metadata), patch(
"pypaimon.multimodal.lerobot.source._open_dataset",
return_value=dataset):
self.connection.load_from_lerobot("hub_info", repo_id)
self.assertEqual(
info, _catalog_metadata(self.connection, "hub_info_info"))
def test_table_dataset_pickle_preserves_episode_metadata_and_reads(self):
import torch
self.connection.load_from_lerobot("worker_pickle", self.image_source)
snapshots = self.connection.create_lerobot_tag(
"worker_pickle_frames", "training")
table = self.connection.get_table("worker_pickle_frames").copy({
"read.video.max-open-decoders": "2"})
dataset = pmm.PaimonLeRobotDataset(
table, tag_name="training", return_uint8=True)
restored = pickle.loads(pickle.dumps(dataset))
for reader in (dataset.reader, restored.reader):
self.assertEqual(snapshots["frames"], reader._snapshot_id)
self.assertEqual(
2, reader._read_table.options.read_video_max_open_decoders())
self.assertEqual(dataset.meta.episodes[:], restored.meta.episodes[:])
self.assertEqual(
dataset.meta.episodes._fingerprint,
restored.meta.episodes._fingerprint,
)
for index in (0, 2, 4):
original = dataset[index]
reread = restored[index]
self.assertEqual(original.keys(), reread.keys())
for key in original:
if torch.is_tensor(original[key]):
self.assertTrue(torch.equal(original[key], reread[key]))
else:
self.assertEqual(original[key], reread[key])
def test_training_reads_previously_imported_vector_features(self):
import torch
with patch("pypaimon.multimodal.lerobot.api._schema_from_info",
side_effect=lambda info: _schema_from_info(info, legacy_vectors=True)):
self.connection.load_from_lerobot(
"legacy_vectors", self.image_source, batch_size=2)
table = self.connection.get_table("legacy_vectors_frames")
self.assertEqual("VECTOR<FLOAT, 3> NOT NULL", str(next(
field.type for field in table.raw_table.fields
if field.name == "observation.state")))
dataset = pmm.PaimonLeRobotDataset(table)
try:
row = dataset[0]
self.assertEqual((3,), tuple(row["observation.state"].shape))
self.assertEqual((2, 2), tuple(row["observation.matrix"].shape))
self.assertTrue(torch.equal(row["action"], torch.tensor([0.0, 0.0])))
finally:
dataset.close()
def test_import_infers_schema_and_preserves_episodes(self):
result = self.connection.load_from_lerobot(
"robot_data", self.image_source, batch_size=2)
self.assertIsNone(result)
table = self.connection.get_table("robot_data_frames")
schema = table.raw_table.fields
types = {field.name: str(field.type) for field in schema}
self.assertEqual("ARRAY<FLOAT> NOT NULL", types["observation.state"])
self.assertEqual(
"ARRAY<ARRAY<FLOAT>> NOT NULL",
types["observation.matrix"],
)
self.assertEqual("ARRAY<FLOAT> NOT NULL", types["action"])
self.assertEqual("FLOAT NOT NULL", types["timestamp"])
self.assertEqual("BIGINT NOT NULL", types["episode_index"])
self.assertEqual("BLOB NOT NULL", types["observation.image"])
self.assertNotIn("dataset_id", types)
self.assertNotIn("task", types)
rows = table.scan().select([
"episode_index",
"frame_index",
"timestamp",
"index",
"task_index",
"observation.state",
"observation.matrix",
"action",
"reward",
]).to_arrow().sort_by("index").to_pylist()
self.assertEqual([0, 0, 1, 1, 1], [row["episode_index"] for row in rows])
self.assertEqual([0, 1, 0, 1, 2], [row["frame_index"] for row in rows])
self.assertEqual([0, 1, 2, 3, 4], [row["index"] for row in rows])
self.assertEqual([0, 0, 1, 1, 1],
[row["task_index"] for row in rows])
self.assertEqual([1.0, -1.0], rows[1]["action"])
self.assertEqual([[1.0, 2.0], [2.0, 1.0]],
rows[4]["observation.matrix"])
self.assertAlmostEqual(0.2, rows[4]["timestamp"], places=6)
self.assertEqual(1.0, rows[4]["reward"])
self.assertEqual(
json.loads((self.image_source / "meta" / "info.json").read_text()),
_catalog_metadata(self.connection, "robot_data_info"),
)
self.assertEqual(
json.loads((self.image_source / "meta" / "stats.json").read_text()),
_catalog_metadata(self.connection, "robot_data_stats"),
)
dataset_rows = _catalog_rows(self.connection, "robot_data_datasets")
self.assertEqual(1, len(dataset_rows))
self.assertEqual(
json.loads((self.image_source / "meta" / "info.json").read_text()),
json.loads(dataset_rows[0]["info"]),
)
self.assertEqual([], table.raw_table.tag_manager().list_tags())
episodes = _catalog_rows(self.connection, "robot_data_episodes")
episode_fields = {
field.name for field in self.connection.catalog.get_table(
self.connection._identifier(
"robot_data_episodes")).fields
}
source_episode_schema = pq.read_schema(next(
(self.image_source / "meta" / "episodes").rglob("*.parquet")))
self.assertTrue(_target_schema(
self.connection.catalog.get_table(self.connection._identifier(
"robot_data_episodes"))
).equals(source_episode_schema, check_metadata=False))
self.assertEqual([(0, 0, 2), (1, 2, 5)], [
(row["episode_index"], row["dataset_from_index"],
row["dataset_to_index"])
for row in episodes
])
self.assertEqual([["pick"], ["place"]], [
row["tasks"] for row in episodes])
self.assertTrue(any(
name.startswith("stats/") for name in episode_fields))
tasks = _catalog_rows(self.connection, "robot_data_tasks")
self.assertEqual([(0, "pick"), (1, "place")], [
(row["task_index"], row["task"]) for row in tasks
])
self.assertEqual(tasks, dataset_rows[0]["tasks"])
self.assertEqual(
2,
table.raw_table.snapshot_manager().get_latest_snapshot().id,
)
scalar, blobs = table.scan().select([
"index", "observation.image"]
).read_blobs()
imported = dict(zip(
scalar.column("index").to_pylist(), blobs["observation.image"]))
source = LeRobotDataset(
repo_id="pypaimon/local-image-test",
root=self.image_source,
).hf_dataset.with_format("arrow")[:]
expected = source.column("observation.image").to_pylist()
self.assertEqual(
[value["bytes"] for value in expected],
[imported[index] for index in range(5)],
)
with self.assertRaisesRegex(ValueError, "already exists"):
self.connection.load_from_lerobot(
"robot_data", self.image_source, batch_size=4)
self.assertEqual(5, table.scan().to_arrow().num_rows)
def test_episode_tasks_are_validated_incrementally(self):
from pypaimon.multimodal.lerobot import loader
with patch.object(
loader,
"_validate_episode_tasks",
wraps=loader._validate_episode_tasks) as validate:
self.connection.load_from_lerobot(
"incremental_tasks", self.image_source, batch_size=1)
self.assertEqual([0, 1], [
call.args[0] for call in validate.call_args_list
])
def test_episode_source_shards_share_paimon_files(self):
source = self.temp_dir / "episode_shards"
shutil.copytree(self.image_source, source)
episode_path = next(
(source / "meta" / "episodes").rglob("*.parquet"))
episodes = pq.read_table(episode_path)
episode_path.unlink()
for index in range(episodes.num_rows):
pq.write_table(
episodes.slice(index, 1),
episode_path.parent / ("part-%d.parquet" % index),
)
self.connection.load_from_lerobot("sharded_episodes", source)
table = self.connection.catalog.get_table(
self.connection._identifier("sharded_episodes_episodes"))
files = {
file.file_name
for split in table.new_read_builder().new_scan().plan().splits()
for file in split.files
}
self.assertEqual(1, len(files))
def test_import_arrow_tasks_and_subtasks(self):
source = self.temp_dir / "arrow_tasks"
shutil.copytree(self.image_source, source)
info_path = source / "meta" / "info.json"
info = json.loads(info_path.read_text())
info["features"]["subtask_index"] = {"dtype": "int64", "shape": [1], "names": None}
info_path.write_text(json.dumps(info))
for path in (source / "data").rglob("*.parquet"):
data = pq.read_table(path)
pq.write_table(data.append_column(
"subtask_index", pa.array([0] * data.num_rows, type=pa.int64())), path)
task_path = source / "meta" / "tasks.parquet"
tasks = pq.read_table(task_path).to_pandas()
expected_tasks = tasks.index.tolist()
pq.write_table(pa.table({"task_index": tasks["task_index"].tolist(),
"task": expected_tasks}), task_path)
pq.write_table(pa.table({"subtask_index": [0], "subtask": ["reach"]}),
source / "meta" / "subtasks.parquet")
for name, location in (("local_tasks", source), ("uri_tasks", source.as_uri())):
with self.subTest(source=name):
self.connection.load_from_lerobot(name, location, tag_name="train")
dataset = pmm.PaimonLeRobotDataset(
self.connection.get_table(name + "_frames"), tag_name="train")
self.assertEqual(expected_tasks, dataset.meta.tasks.index.tolist())
self.assertEqual(["reach"], dataset.meta.subtasks.index.tolist())
row = dataset[0]
self.assertEqual(expected_tasks[int(row["task_index"])], row["task"])
self.assertEqual("reach", row["subtask"])
self.assertEqual(info["total_frames"], len(dataset))
def test_import_empty_subtask_vocabulary(self):
import pandas as pd
source = self.temp_dir / "empty_subtasks"
shutil.copytree(self.image_source, source)
info_path = source / "meta/info.json"
info = json.loads(info_path.read_text())
info["features"]["subtask_index"] = {
"dtype": "int64", "shape": [1], "names": None}
info_path.write_text(json.dumps(info))
for path in (source / "data").rglob("*.parquet"):
data = pq.read_table(path)
pq.write_table(data.append_column(
"subtask_index", pa.nulls(data.num_rows, type=pa.int64())), path)
vocabulary = pa.Table.from_pandas(pd.DataFrame(
{"subtask_index": [0]},
index=pd.Index(["reach"], name="instruction"))).slice(0, 0)
pq.write_table(vocabulary, source / "meta/subtasks.parquet")
self.connection.load_from_lerobot("empty_subtasks", source, tag_name="train")
self.assertEqual([], _catalog_rows(self.connection, "empty_subtasks_subtasks"))
self.assertEqual(1, self.connection.catalog.get_tag(
self.connection._identifier("empty_subtasks_datasets"), "train").snapshot.id)
dataset = pmm.PaimonLeRobotDataset(
self.connection.get_table("empty_subtasks_frames"), tag_name="train")
self.assertIsNone(dataset[0]["subtask"])
self.assertIsNone(dataset[0]["subtask_index"])
batch = next(iter(dataset.to_dataloader(batch_size=5, num_workers=0)))
self.assertEqual([None] * 5, batch["subtask"])
self.assertEqual([-1] * 5, batch["subtask_index"].tolist())
self.assertFalse(batch["subtask_index_is_valid"].any())
def test_import_publishes_optional_subtasks(self):
import pandas as pd
source = self.temp_dir / "with_subtasks"
shutil.copytree(self.image_source, source)
info_path = source / "meta" / "info.json"
info = json.loads(info_path.read_text())
info["features"]["subtask_index"] = {
"dtype": "int64",
"shape": [1],
"names": None,
}
info_path.write_text(json.dumps(info))
next_subtask = 0
for path in sorted((source / "data").rglob("*.parquet")):
data = pq.read_table(path)
values = [
(next_subtask + index) % 2
for index in range(data.num_rows)
]
next_subtask += data.num_rows
if next_subtask == 5:
values[-1] = None
pq.write_table(data.append_column(
"subtask_index",
pa.array(values, type=pa.int64()),
), path)
subtasks = pa.Table.from_pandas(pd.DataFrame(
{"subtask_index": [0, 1]},
index=pd.Index(["reach", "grasp"], name="instruction"),
))
pq.write_table(subtasks, source / "meta" / "subtasks.parquet")
result = self.connection.load_from_lerobot(
"with_subtasks", source, tag_name="training")
frames = self.connection.get_table("with_subtasks_frames")
self.assertNotIn("subtask", [
field.name for field in frames.raw_table.fields
])
self.assertEqual([0, 1, 0, 1, None], frames.scan().select([
"index", "subtask_index"
]).to_arrow().sort_by("index").column("subtask_index").to_pylist())
self.assertEqual(
[{"subtask_index": 0, "subtask": "reach"},
{"subtask_index": 1, "subtask": "grasp"}],
_catalog_rows(self.connection, "with_subtasks_subtasks"),
)
self.assertIsNone(result)
dataset = pmm.PaimonLeRobotDataset(frames, tag_name="training")
self.assertEqual(["reach", "grasp"], list(
dataset.meta.subtasks.index))
self.assertEqual("reach", dataset[0]["subtask"])
self.assertIsNone(dataset[4]["subtask_index"])
self.assertIsNone(dataset[4]["subtask"])
self.assertEqual(["grasp", None], [
row["subtask"] for row in dataset.__getitems__([3, 4])
])
batch = next(iter(dataset.to_dataloader(
batch_size=5, shuffle=False, num_workers=0)))
self.assertEqual([0, 1, 2, 3, 4], batch["index"].tolist())
self.assertEqual([0, 1, 0, 1, -1], batch["subtask_index"].tolist())
self.assertEqual([True, True, True, True, False],
batch["subtask_index_is_valid"].tolist())
self.assertEqual(["reach", "grasp", "reach", "grasp", None],
batch["subtask"])
windowed = pmm.PaimonLeRobotDataset(
frames, tag_name="training",
delta_timestamps={"subtask_index": [-0.1, 0.0]})
self.assertEqual([1, None], [
None if value is None else int(value)
for value in windowed[4]["subtask_index"]
])
padded = pmm.PaimonLeRobotDataset(
frames, tag_name="training",
delta_timestamps={"subtask_index": [-0.1, 0.0, 0.2]})
samples = padded.__getitems__([2, 2])
self.assertEqual([True, False, False],
samples[0]["subtask_index_is_pad"].tolist())
for sample in samples:
self.assertEqual([0, 0, None], [
None if value is None else int(value)
for value in sample["subtask_index"]
])
samples[0]["subtask_index"][0].fill_(-100)
self.assertEqual(0, int(samples[0]["subtask_index"][1]))
self.assertEqual([0, 0, None], [
None if value is None else int(value)
for value in samples[1]["subtask_index"]
])
self.assertEqual(
1,
self.connection.catalog.get_tag(
self.connection._identifier("with_subtasks_datasets"),
"training",
).snapshot.id,
)
def test_import_preserves_quoted_database_name(self):
self.connection.catalog.create_database(
"db.name", ignore_if_exists=False)
self.connection.load_from_lerobot(
"`db.name`.robot", self.image_source)
table_names = self.connection.catalog.list_tables("db.name")
self.assertEqual([
"robot_datasets",
"robot_episodes",
"robot_frames",
], sorted(table_names))
def test_import_reuses_validated_episode_metadata(self):
from pypaimon.multimodal.lerobot import api
source = self.temp_dir / "stable_episodes"
shutil.copytree(self.image_source, source)
episode_path = next((source / "meta" / "episodes").rglob("*.parquet"))
original_write = api._write_dataset
def write_then_replace(*args, **kwargs):
snapshot_id = original_write(*args, **kwargs)
episodes = pq.read_table(episode_path)
tasks = episodes.column("tasks").to_pylist()
tasks[0] = ["place"]
pq.write_table(episodes.set_column(
episodes.schema.get_field_index("tasks"),
"tasks",
pa.array(tasks, type=episodes.schema.field("tasks").type),
), episode_path)
return snapshot_id
with patch.object(
api, "_write_dataset", side_effect=write_then_replace):
self.connection.load_from_lerobot("stable_episodes", source)
published = _catalog_rows(
self.connection, "stable_episodes_episodes")
self.assertEqual(["pick"], published[0]["tasks"])
def test_frame_controls_must_match_published_episode_metadata(self):
cases = [
("index", 99),
("episode_index", 0),
("frame_index", 1),
("timestamp", 0.2),
("task_index", 0),
]
for column, value in cases:
with self.subTest(column=column):
source = self.temp_dir / ("corrupt_" + column)
shutil.copytree(self.image_source, source)
path = next((source / "data").rglob("*.parquet"))
data = pq.read_table(path)
values = data.column(column).to_pylist()
values[2] = value
index = data.schema.get_field_index(column)
data = data.set_column(
index,
column,
pa.array(values, type=data.schema.field(index).type),
)
pq.write_table(data, path)
table_name = "corrupt_" + column
with self.assertRaisesRegex(
ValueError, "has %s" % column):
self.connection.load_from_lerobot(table_name, source)
self.connection.get_table(table_name + "_frames")
self.assertEqual([], _catalog_rows(
self.connection, table_name + "_info"))
def test_task_text_remains_in_published_task_mapping(self):
source = self.temp_dir / "reordered_tasks"
shutil.copytree(self.image_source, source)
path = source / "meta" / "tasks.parquet"
tasks = pq.read_table(path)
pq.write_table(tasks.take(pa.array([1, 0])), path)
self.connection.load_from_lerobot("reordered_tasks", source)
table = self.connection.get_table("reordered_tasks_frames")
self.assertNotIn("task", [
field.name for field in table.raw_table.fields
])
frames = table.scan().select([
"index", "task_index"
]).to_arrow().sort_by("index").to_pylist()
task_rows = _catalog_rows(
self.connection, "reordered_tasks_tasks")
task_name = (
"task" if "task" in task_rows[0] else "__index_level_0__")
published = {
row["task_index"]: row[task_name] for row in task_rows
}
self.assertEqual({0: "pick", 1: "place"}, published)
self.assertTrue(all(
row["task_index"] in published for row in frames))
def test_episode_tasks_must_exactly_match_frame_tasks(self):
source = self.temp_dir / "extra_episode_task"
shutil.copytree(self.image_source, source)
path = next((source / "meta" / "episodes").rglob("*.parquet"))
episodes = pq.read_table(path)
tasks = episodes.column("tasks").to_pylist()
tasks[0] = ["pick", "place"]
index = episodes.schema.get_field_index("tasks")
episodes = episodes.set_column(
index,
"tasks",
pa.array(tasks, type=episodes.schema.field(index).type),
)
pq.write_table(episodes, path)
with self.assertRaisesRegex(
ValueError, "declares task indices"):
self.connection.load_from_lerobot(
"extra_episode_task", source)
self.connection.get_table("extra_episode_task_frames")
self.assertEqual([], _catalog_rows(
self.connection, "extra_episode_task_info"))
def test_nonempty_dataset_cannot_publish_without_tasks(self):
source = self.temp_dir / "missing_tasks"
shutil.copytree(self.image_source, source)
info_path = source / "meta" / "info.json"
info = json.loads(info_path.read_text())
info["total_tasks"] = 0
info_path.write_text(json.dumps(info))
episode_path = next(
(source / "meta" / "episodes").rglob("*.parquet"))
episodes = pq.read_table(episode_path)
index = episodes.schema.get_field_index("tasks")
episodes = episodes.set_column(
index,
"tasks",
pa.array(
[[] for _ in range(episodes.num_rows)],
type=episodes.schema.field(index).type,
),
)
pq.write_table(episodes, episode_path)
with self.assertRaisesRegex(ValueError, "task_index"):
self.connection.load_from_lerobot("missing_tasks", source)
self.connection.get_table("missing_tasks_frames")
self.assertEqual([], _catalog_rows(
self.connection, "missing_tasks_info"))
def test_paimon_dataset_reads_lazy_batches_with_lerobot_metadata(self):
import torch
from torch.utils.data import DataLoader
result = self.connection.load_from_lerobot(
"training_data", self.image_source, batch_size=2,
tag_name="training")
self.assertIsNone(result)
table = self.connection.get_table("training_data_frames")
dataset = pmm.PaimonLeRobotDataset(
table,
tag_name="training",
delta_timestamps={"action": [-0.1, 0.0, 0.1]},
blob_parallelism=3,
)
self.assertEqual("training", dataset.tag_name)
self.assertEqual("training", dataset.meta.revision)
self.assertEqual(5, len(dataset))
self.assertEqual(2, dataset.num_episodes)
self.assertIsNotNone(dataset.meta.stats)
self.assertTrue(all(
isinstance(feature["shape"], tuple)
for feature in dataset.features.values()
))
self.assertEqual(["pick", "place"], list(dataset.meta.tasks.index))
episodes = dataset.meta.episodes
self.assertEqual(2, len(episodes))
self.assertEqual([0, 2], list(episodes["dataset_from_index"]))
self.assertEqual([2, 5], list(episodes["dataset_to_index"]))
self.assertFalse(any(
name.startswith("stats/")
for name in episodes.column_names))
self.assertFalse(hasattr(dataset, "index_mapping"))
frame_indexes = _catalog_rows(
self.connection, "training_data_frames$table_indexes")
self.assertEqual(
{"btree", "index"},
{frame_indexes[0]["index_type"],
frame_indexes[0]["index_field_name"]},
)
from pypaimon.multimodal.blob_read import fetch_blob_bodies
from pypaimon.read.table_scan import TableScan
original_plan = TableScan.plan
scan_calls = []
def counted_plan(scan):
scan_calls.append(scan)
return original_plan(scan)
with patch.object(TableScan, "plan", new=counted_plan), patch.object(
dataset.reader, "_read_rows",
wraps=dataset.reader._read_rows) as read, patch(
"pypaimon.multimodal.blob_read.fetch_blob_bodies",
wraps=fetch_blob_bodies) as fetch:
last, first = dataset.__getitems__([4, 0])
self.assertEqual(0, len(scan_calls))
self.assertEqual(1, read.call_count)
self.assertEqual([0, 1, 3, 4], read.call_args.args[0])
self.assertEqual(1, fetch.call_count)
self.assertEqual(3, fetch.call_args.args[3])
self.assertEqual("place", last["task"])
self.assertEqual([3, 8, 10], list(last["observation.image"].shape))
self.assertAlmostEqual(
100.0 / 255.0,
float(last["observation.image"].mean()),
places=5,
)
self.assertEqual(
[[1.0, -1.0], [2.0, -2.0], [2.0, -2.0]],
last["action"].tolist(),
)
self.assertEqual([False, False, True],
last["action_is_pad"].tolist())
self.assertEqual([True, False, False],
first["action_is_pad"].tolist())
restored_reader = pickle.loads(pickle.dumps(dataset.reader))
try:
# Both execution backends preserve indexed lookup after transport.
self.assertEqual(
[2], restored_reader.read_indices([2], ["index"])["index"].to_pylist())
finally:
restored_reader.close()
worker_indices = []
for batch in DataLoader(
dataset,
batch_size=2,
shuffle=False,
num_workers=2,
multiprocessing_context="spawn"):
worker_indices.extend(batch["index"].tolist())
self.assertEqual(list(range(5)), worker_indices)
worker_batches = list(dataset.to_dataloader(
batch_size=2,
shuffle=False,
num_workers=1,
multiprocessing_context="spawn",
worker_init_fn=_configure_uint8_transform_worker,
))
self.assertEqual("torch.uint8", str(
worker_batches[0]["observation.image"].dtype))
self.assertEqual(
1.0,
float(worker_batches[0]["observation.image"][0].float().mean()),
)
self.assertEqual(
101.0,
float(worker_batches[-1]["observation.image"][-1].float().mean()),
)
uint8_dataset = pmm.PaimonLeRobotDataset(
table,
tag_name="training",
return_uint8=True,
)
uint8_sample = uint8_dataset[4]
uint8_batch = uint8_dataset.__getitems__([4, 0])
uint8_image = uint8_sample["observation.image"]
self.assertEqual("torch.uint8", str(uint8_image.dtype))
self.assertEqual([3, 8, 10], list(uint8_image.shape))
self.assertEqual(100.0, float(uint8_image.float().mean()))
self.assertTrue(torch.equal(
uint8_image, uint8_batch[0]["observation.image"]))
torch.testing.assert_close(
last["observation.image"], uint8_image.float().div(255))
reordered = pmm.PaimonLeRobotDataset(
table,
tag_name="training",
episodes=[1, 0],
)
self.assertEqual([1, 0], reordered.episodes)
self.assertEqual(5, len(reordered))
self.assertEqual(0, int(reordered[0]["episode_index"]))
self.assertEqual(1, int(reordered[-1]["episode_index"]))
table.add(pa.Table.from_pylist([{
"index": 999,
"episode_index": 99,
"frame_index": 0,
"timestamp": 0.0,
"task_index": 0,
"observation.state": [0.0, 0.0, 0.0],
"observation.matrix": [[0.0, 0.0], [0.0, 0.0]],
"action": [0.0, 0.0],
"reward": 0.0,
"observation.image": _image_bytes(
np.zeros((8, 10, 3), dtype=np.uint8), self.temp_dir),
}], schema=_target_schema(table.raw_table)))
self.assertEqual(6, table.scan().to_arrow().num_rows)
episode = pmm.PaimonLeRobotDataset(
table,
tag_name="training",
episodes=[1],
)
self.assertEqual(3, len(episode))
self.assertEqual(1, episode.num_episodes)
self.assertEqual(2, int(episode[0]["index"]))
def test_paimon_dataset_rejects_unavailable_frame_index(self):
self.connection.load_from_lerobot(
"missing_frame_index", self.image_source)
# Exercise the Python locator's failures explicitly; Native lookup
# does not open this scanner or invoke its internal hooks.
dataset = pmm.PaimonLeRobotDataset(
self.connection.get_table("missing_frame_index_frames").copy({
"read.native.enabled": "false",
"scan.native-plan.enabled": "false",
}))
with patch.object(
dataset._frame_locator, "_index_scanner",
return_value=None):
with self.assertRaisesRegex(
RuntimeError, "requires a readable global index"):
dataset[0]
with patch.object(
dataset._frame_locator, "_index_scanner",
side_effect=OSError("index unavailable")):
with self.assertRaisesRegex(
RuntimeError, "Failed to open the Paimon global index"):
dataset[0]
scanner = Mock()
scanner.scan_with_coverage.side_effect = OSError("query failed")
with patch.object(
dataset._frame_locator, "_index_scanner",
return_value=scanner):
with self.assertRaisesRegex(
RuntimeError, "Failed to query the Paimon global index"):
dataset[0]
def test_paimon_dataset_rejects_query_authorization(self):
self.connection.load_from_lerobot(
"authorized_frames", self.image_source)
table = self.connection.get_table("authorized_frames_frames")
auth_results = [
TableQueryAuthResult([json.dumps({
"kind": "LEAF",
"transform": {
"name": "FIELD_REF",
"fieldRef": {"name": "episode_index"},
},
"function": "EQUAL",
"literals": [0],
})], None),
TableQueryAuthResult(
None, {"observation.state": json.dumps({"name": "NULL"})}),
]
for auth in auth_results:
with self.subTest(auth=auth.__dict__):
def query_auth(unused_options, identifier):
if identifier == table.raw_table.identifier:
return lambda unused_projection: auth
return None
with patch.object(
table.raw_table.catalog_environment,
"table_query_auth",
side_effect=query_auth):
with self.assertRaisesRegex(
ValueError, "query authorization"):
pmm.PaimonLeRobotDataset(table)
def test_oss_source_streams_parquet_and_preserves_episodes(self):
source = "oss://source-bucket/robot-images"
source_file_io = _RemoteLeRobotFileIO(self.image_source, source)
with patch(
"pypaimon.multimodal.lerobot.source._SourceFileIO",
return_value=source_file_io):
result = self.connection.load_from_lerobot(
"oss_images",
source,
batch_size=2,
)
self.assertIsNone(result)
table = self.connection.get_table("oss_images_frames")
rows = table.scan().select([
"episode_index", "frame_index", "index", "task_index"
]).to_arrow().sort_by("index").to_pylist()
self.assertEqual([0, 0, 1, 1, 1], [
row["episode_index"] for row in rows
])
self.assertEqual([0, 1, 0, 1, 2], [
row["frame_index"] for row in rows
])
self.assertEqual([0, 0, 1, 1, 1], [
row["task_index"] for row in rows])
self.assertTrue(any(
path.endswith("meta/stats.json")
for path in source_file_io.opened_paths
))
self.assertEqual(1, len([
path for path in source_file_io.opened_paths
if "/data/" in path and path.endswith(".parquet")
]))
def test_remote_tasks_use_the_pandas_index_column(self):
local_source = self.temp_dir / "custom_task_index"
shutil.copytree(self.image_source, local_source)
path = local_source / "meta" / "tasks.parquet"
tasks = pq.read_table(path).to_pandas().rename_axis("instruction")
pq.write_table(pa.Table.from_pandas(tasks), path)
source = "oss://source-bucket/custom-task-index"
source_file_io = _RemoteLeRobotFileIO(local_source, source)
with patch(
"pypaimon.multimodal.lerobot.source._SourceFileIO",
return_value=source_file_io):
self.connection.load_from_lerobot(
"custom_task_index", source)
rows = _catalog_rows(
self.connection, "custom_task_index_tasks")
self.assertEqual(
[(0, "pick"), (1, "place")],
[(row["task_index"], row["task"]) for row in rows],
)
def test_empty_oss_source_does_not_require_episode_directory(self):
local_source = self.temp_dir / "empty_remote"
(local_source / "meta").mkdir(parents=True)
(local_source / "meta" / "info.json").write_text(json.dumps({
"codebase_version": "v3.0",
"total_frames": 0,
"total_episodes": 0,
"total_tasks": 0,
"fps": 30,
"features": {
"index": {"dtype": "int64", "shape": [1]},
},
}))
source = "oss://source-bucket/empty-robot"
source_file_io = _RemoteLeRobotFileIO(local_source, source)
with patch(
"pypaimon.multimodal.lerobot.source._SourceFileIO",
return_value=source_file_io):
with self.assertRaisesRegex(ValueError, "non-empty"):
self.connection.load_from_lerobot("empty_oss", source)
def test_tag_falls_back_for_catalogs_without_tag_api(self):
with patch.object(
self.connection.catalog,
"create_tag",
side_effect=NotImplementedError):
result = self.connection.load_from_lerobot(
"tag_fallback", self.image_source, tag_name="training")
tag = "training"
table = self.connection.get_table("tag_fallback_frames")
self.assertIsNone(result)
self.assertEqual(
table.raw_table.snapshot_manager().get_latest_snapshot().id,
table.raw_table.tag_manager().get(tag).id,
)
def test_tag_response_loss_is_reconciled(self):
create_tag = self.connection.catalog.create_tag
lost = [False]
def create_then_lose_response(*args, **kwargs):
result = create_tag(*args, **kwargs)
if not lost[0]:
lost[0] = True
raise TimeoutError("lost tag response")
return result
with patch.object(
self.connection.catalog,
"create_tag",
side_effect=create_then_lose_response):
result = self.connection.load_from_lerobot(
"tag_response_loss", self.image_source, tag_name="training")
self.assertTrue(lost[0])
self.assertIsNone(result)
self.assertEqual(2, self.connection.catalog.get_tag(
self.connection._identifier("tag_response_loss_frames"),
"training").snapshot.id)
def test_tag_failure_leaves_imported_data(self):
with patch(
"pypaimon.multimodal.lerobot.metadata._create_tag",
side_effect=RuntimeError("tag failed")):
with self.assertRaisesRegex(RuntimeError, "tag failed"):
self.connection.load_from_lerobot(
"failed_publish", self.image_source, tag_name="training")
self.assertEqual(5, self.connection.get_table(
"failed_publish_frames").scan().to_arrow().num_rows)
self.assertEqual("v3.0", _catalog_metadata(
self.connection, "failed_publish_info")["codebase_version"])
def test_existing_companion_is_rejected(self):
self.connection.load_from_lerobot(
"other_group", self.image_source)
stale = self.connection._identifier("stale_datasets")
self.connection.catalog.rename_table(
self.connection._identifier("other_group_datasets"), stale)
with self.assertRaisesRegex(ValueError, "already exists"):
self.connection.load_from_lerobot(
"stale", self.image_source)
self.connection.get_table("stale_frames")
self.connection.catalog.get_table(stale)
def test_invalid_target_options_do_not_leave_table(self):
with self.assertRaisesRegex(ValueError, "data-evolution.enabled"):
self.connection.load_from_lerobot(
"invalid_options",
self.image_source,
options={"data-evolution.enabled": "false"},
)
with self.assertRaises(TableNotExistException):
self.connection.catalog.get_table(
self.connection._identifier("invalid_options_frames"))
result = self.connection.load_from_lerobot(
"invalid_options", self.image_source)
self.assertIsNone(result)
def test_target_open_failure_leaves_created_table(self):
original_get = self.connection.get_table
failed = [False]
def fail_once(name):
if not failed[0]:
failed[0] = True
raise RuntimeError("get failed")
return original_get(name)
with patch.object(
self.connection, "get_table", side_effect=fail_once):
with self.assertRaisesRegex(RuntimeError, "get failed"):
self.connection.load_from_lerobot(
"failed_open", self.image_source)
self.connection.get_table("failed_open_frames")
def test_dataset_close_failure_does_not_override_success(self):
from pypaimon.multimodal.lerobot import api
original_open = api._open_resolved_dataset
def open_with_failing_close(*args, **kwargs):
return _FailingCloseDataset(original_open(*args, **kwargs))
with self.assertLogs(
"pypaimon.multimodal.lerobot.source", level="WARNING"):
with patch.object(
api,
"_open_resolved_dataset",
side_effect=open_with_failing_close):
result = self.connection.load_from_lerobot(
"close_failure", self.image_source)
self.assertIsNone(result)
self.assertEqual("v3.0", _catalog_metadata(
self.connection, "close_failure_info")["codebase_version"])
def test_source_close_failure_does_not_override_success(self):
source = "oss://source-bucket/robot-images"
source_file_io = _RemoteLeRobotFileIO(self.image_source, source)
source_file_io.close = Mock(side_effect=RuntimeError("close failed"))
with self.assertLogs(
"pypaimon.multimodal.lerobot.source", level="WARNING"):
with patch(
"pypaimon.multimodal.lerobot.source._SourceFileIO",
return_value=source_file_io):
result = self.connection.load_from_lerobot(
"source_close_failure", source)
self.assertIsNone(result)
self.assertEqual("v3.0", _catalog_metadata(
self.connection, "source_close_failure_info")["codebase_version"])
def test_existing_target_is_rejected(self):
info = json.loads((self.image_source / "meta" / "info.json").read_text())
schema = _schema_from_info(info)
table = self.connection.create_table("existing_frames", schema=schema)
with self.assertRaisesRegex(ValueError, "already exists"):
self.connection.load_from_lerobot(
"existing", self.image_source)
self.assertIsNone(
table.raw_table.snapshot_manager().get_latest_snapshot())
def test_concurrent_import_cannot_claim_the_same_target(self):
from pypaimon.multimodal.lerobot import api
original_prepare = api._prepare_metadata_tables
root_created = threading.Event()
release = threading.Event()
def prepare_then_wait(*args, **kwargs):
root_created.set()
release.wait(10)
return original_prepare(*args, **kwargs)
with patch.object(
api,
"_prepare_metadata_tables",
side_effect=prepare_then_wait):
with ThreadPoolExecutor(max_workers=1) as executor:
future = executor.submit(
self.connection.load_from_lerobot,
"concurrent",
self.image_source,
)
try:
self.assertTrue(root_created.wait(10))
with self.assertRaisesRegex(
ValueError, "already exists"):
self.connection.load_from_lerobot(
"concurrent", self.image_source)
finally:
release.set()
result = future.result(timeout=30)
self.assertIsNone(result)
self.assertEqual(
5,
self.connection.get_table(
"concurrent_frames").scan().to_arrow().num_rows,
)
def test_concurrent_append_rejects_initial_import(self):
from pypaimon.multimodal.lerobot import api
original_write = api._write_dataset
def append_then_write(
table,
dataset,
info,
source,
source_schema,
batch_size,
metadata,
video_fields=()):
table.add(_read_batch(
dataset,
info,
0,
1,
source_schema,
))
return original_write(
table,
dataset,
info,
source,
source_schema,
batch_size,
metadata,
video_fields,
)
with patch.object(
api, "_write_dataset", side_effect=append_then_write):
with self.assertRaisesRegex(RuntimeError, "concurrent writes"):
self.connection.load_from_lerobot(
"concurrent_append", self.image_source)
self.connection.get_table("concurrent_append_frames")
self.assertEqual([], _catalog_rows(
self.connection, "concurrent_append_info"))
if __name__ == "__main__":
unittest.main()