| |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| import logging |
|
|
| import pytest |
| import torch |
|
|
| pytest.importorskip("datasets", reason="datasets is required (install lerobot[dataset])") |
|
|
| from datasets import Dataset |
|
|
| from lerobot.datasets.io_utils import ( |
| hf_transform_to_torch, |
| ) |
| from lerobot.datasets.sampler import EpisodeAwareSampler |
|
|
|
|
| def calculate_episode_data_index(hf_dataset: Dataset) -> dict[str, torch.Tensor]: |
| """Calculate episode data index for testing. Returns {"from": Tensor, "to": Tensor}.""" |
| episode_data_index: dict[str, list[int]] = {"from": [], "to": []} |
| current_episode = None |
| if len(hf_dataset) == 0: |
| return {"from": torch.tensor([]), "to": torch.tensor([])} |
| for idx, episode_idx in enumerate(hf_dataset["episode_index"]): |
| if episode_idx != current_episode: |
| episode_data_index["from"].append(idx) |
| if current_episode is not None: |
| episode_data_index["to"].append(idx) |
| current_episode = episode_idx |
| episode_data_index["to"].append(idx + 1) |
| return {k: torch.tensor(v) for k, v in episode_data_index.items()} |
|
|
|
|
| def test_drop_n_first_frames(): |
| dataset = Dataset.from_dict( |
| { |
| "timestamp": [0.1, 0.2, 0.3, 0.4, 0.5, 0.6], |
| "index": [0, 1, 2, 3, 4, 5], |
| "episode_index": [0, 0, 1, 2, 2, 2], |
| }, |
| ) |
| dataset.set_transform(hf_transform_to_torch) |
| episode_data_index = calculate_episode_data_index(dataset) |
| sampler = EpisodeAwareSampler(episode_data_index["from"], episode_data_index["to"], drop_n_first_frames=1) |
| assert sampler.indices == [1, 4, 5] |
| assert len(sampler) == 3 |
| assert list(sampler) == [1, 4, 5] |
|
|
|
|
| def test_drop_n_last_frames(): |
| dataset = Dataset.from_dict( |
| { |
| "timestamp": [0.1, 0.2, 0.3, 0.4, 0.5, 0.6], |
| "index": [0, 1, 2, 3, 4, 5], |
| "episode_index": [0, 0, 1, 2, 2, 2], |
| }, |
| ) |
| dataset.set_transform(hf_transform_to_torch) |
| episode_data_index = calculate_episode_data_index(dataset) |
| sampler = EpisodeAwareSampler(episode_data_index["from"], episode_data_index["to"], drop_n_last_frames=1) |
| assert sampler.indices == [0, 3, 4] |
| assert len(sampler) == 3 |
| assert list(sampler) == [0, 3, 4] |
|
|
|
|
| def test_episode_indices_to_use(): |
| dataset = Dataset.from_dict( |
| { |
| "timestamp": [0.1, 0.2, 0.3, 0.4, 0.5, 0.6], |
| "index": [0, 1, 2, 3, 4, 5], |
| "episode_index": [0, 0, 1, 2, 2, 2], |
| }, |
| ) |
| dataset.set_transform(hf_transform_to_torch) |
| episode_data_index = calculate_episode_data_index(dataset) |
| sampler = EpisodeAwareSampler( |
| episode_data_index["from"], episode_data_index["to"], episode_indices_to_use=[0, 2] |
| ) |
| assert sampler.indices == [0, 1, 3, 4, 5] |
| assert len(sampler) == 5 |
| assert list(sampler) == [0, 1, 3, 4, 5] |
|
|
|
|
| def test_shuffle(): |
| dataset = Dataset.from_dict( |
| { |
| "timestamp": [0.1, 0.2, 0.3, 0.4, 0.5, 0.6], |
| "index": [0, 1, 2, 3, 4, 5], |
| "episode_index": [0, 0, 1, 2, 2, 2], |
| }, |
| ) |
| dataset.set_transform(hf_transform_to_torch) |
| episode_data_index = calculate_episode_data_index(dataset) |
| sampler = EpisodeAwareSampler(episode_data_index["from"], episode_data_index["to"], shuffle=False) |
| assert sampler.indices == [0, 1, 2, 3, 4, 5] |
| assert len(sampler) == 6 |
| assert list(sampler) == [0, 1, 2, 3, 4, 5] |
| sampler = EpisodeAwareSampler(episode_data_index["from"], episode_data_index["to"], shuffle=True) |
| assert sampler.indices == [0, 1, 2, 3, 4, 5] |
| assert len(sampler) == 6 |
| assert set(sampler) == {0, 1, 2, 3, 4, 5} |
|
|
|
|
| def test_shuffle_is_reproducible_across_instances(): |
| |
| |
| sampler_a = EpisodeAwareSampler([0], [6], shuffle=True, seed=42) |
| sampler_b = EpisodeAwareSampler([0], [6], shuffle=True, seed=42) |
| epoch_0 = list(sampler_a) |
| assert list(sampler_b) == epoch_0 |
| |
| sampler_c = EpisodeAwareSampler([0], [6], shuffle=True, seed=42) |
| torch.randperm(1000) |
| assert list(sampler_c) == epoch_0 |
|
|
|
|
| def test_negative_drop_first_frames_raises(): |
| with pytest.raises(ValueError, match="drop_n_first_frames must be >= 0"): |
| EpisodeAwareSampler([0], [10], drop_n_first_frames=-1) |
|
|
|
|
| def test_negative_drop_last_frames_raises(): |
| with pytest.raises(ValueError, match="drop_n_last_frames must be >= 0"): |
| EpisodeAwareSampler([0], [10], drop_n_last_frames=-1) |
|
|
|
|
| def test_all_episodes_dropped_raises(): |
| |
| with pytest.raises(ValueError, match="No valid frames remain"): |
| EpisodeAwareSampler([0, 1, 2], [1, 2, 3], drop_n_first_frames=1) |
|
|
|
|
| def test_partial_episode_drop_warns(caplog): |
| |
| with caplog.at_level(logging.WARNING, logger="lerobot.datasets.sampler"): |
| sampler = EpisodeAwareSampler([0, 1], [1, 6], drop_n_first_frames=1) |
| |
| assert sampler.indices == [2, 3, 4, 5] |
| assert "Episode 0" in caplog.text |
|
|
|
|
| |
|
|
| from lerobot.datasets.sampler import compute_sampler_state |
|
|
| EPISODE_BOUNDS = ([0, 2, 3], [2, 3, 6]) |
|
|
|
|
| @pytest.mark.parametrize("num_frames", [1, 2, 3, 37, 64, 100]) |
| def test_deterministic_sampler_shuffle_is_permutation(num_frames): |
| for seed in (0, 1, 1234): |
| sampler = EpisodeAwareSampler([0], [num_frames], shuffle=True, seed=seed) |
| assert sorted(sampler) == list(range(num_frames)) |
|
|
|
|
| def test_deterministic_sampler_epochs_reproduce_and_differ(): |
| sampler_a = EpisodeAwareSampler([0], [100], shuffle=True, seed=42) |
| sampler_b = EpisodeAwareSampler([0], [100], shuffle=True, seed=42) |
| epoch_0 = list(sampler_a) |
| assert list(sampler_b) == epoch_0 |
| epoch_1 = list(sampler_a) |
| assert epoch_1 != epoch_0 |
| assert sorted(epoch_1) == sorted(epoch_0) |
| sampler_a.set_epoch(0) |
| assert list(sampler_a) == epoch_0 |
| assert list(EpisodeAwareSampler([0], [100], shuffle=True, seed=7)) != epoch_0 |
|
|
|
|
| def test_deterministic_sampler_resume_mid_epoch(): |
| reference = EpisodeAwareSampler(*EPISODE_BOUNDS, shuffle=True, seed=42) |
| epoch_0 = list(reference) |
| epoch_1 = list(reference) |
| for start in (0, 1, 4, len(epoch_0)): |
| resumed = EpisodeAwareSampler(*EPISODE_BOUNDS, shuffle=True, seed=42) |
| resumed.load_state_dict({"epoch": 0, "start_index": start}) |
| assert list(resumed) == epoch_0[start:] |
| |
| assert list(resumed) == epoch_1 |
|
|
|
|
| def test_deterministic_sampler_construction_stores_only_boundaries(): |
| |
| |
| num_frames = 1_000_000 |
| sampler = EpisodeAwareSampler([0], [num_frames], shuffle=True, seed=0) |
| assert len(sampler) == num_frames |
| assert sampler._starts.shape == (1,) and sampler._cum_lengths.shape == (1,) |
|
|
|
|
| def test_deterministic_sampler_resume_is_exact_at_scale(): |
| |
| |
| num_frames = 100_000 |
| reference = EpisodeAwareSampler([0], [num_frames], shuffle=True, seed=0) |
| epoch_0 = list(reference) |
| assert sorted(epoch_0) == list(range(num_frames)) |
| start = num_frames - 5 |
| resumed = EpisodeAwareSampler([0], [num_frames], shuffle=True, seed=0) |
| resumed.load_state_dict({"epoch": 0, "start_index": start}) |
| assert list(resumed) == epoch_0[start:] |
|
|
|
|
| def test_compute_sampler_state(): |
| |
| assert compute_sampler_state(step=0, num_frames=100, batch_size=10, num_processes=2) == { |
| "epoch": 0, |
| "start_index": 0, |
| } |
| |
| assert compute_sampler_state(step=7, num_frames=100, batch_size=10, num_processes=2) == { |
| "epoch": 1, |
| "start_index": 40, |
| } |
| |
| assert compute_sampler_state(step=12, num_frames=95, batch_size=10, num_processes=2) == { |
| "epoch": 2, |
| "start_index": 40, |
| } |
| |
| assert compute_sampler_state(step=11, num_frames=105, batch_size=10, num_processes=2) == { |
| "epoch": 1, |
| "start_index": 100, |
| } |
|
|