robocandywrapper 0.2.16__tar.gz → 0.2.18__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. {robocandywrapper-0.2.16/robocandywrapper.egg-info → robocandywrapper-0.2.18}/PKG-INFO +1 -5
  2. {robocandywrapper-0.2.16 → robocandywrapper-0.2.18}/pyproject.toml +1 -11
  3. {robocandywrapper-0.2.16 → robocandywrapper-0.2.18}/robocandywrapper/__init__.py +1 -1
  4. {robocandywrapper-0.2.16 → robocandywrapper-0.2.18}/robocandywrapper/metadata_view.py +24 -13
  5. {robocandywrapper-0.2.16 → robocandywrapper-0.2.18}/robocandywrapper/wrapper.py +32 -1
  6. {robocandywrapper-0.2.16 → robocandywrapper-0.2.18/robocandywrapper.egg-info}/PKG-INFO +1 -5
  7. {robocandywrapper-0.2.16 → robocandywrapper-0.2.18}/robocandywrapper.egg-info/SOURCES.txt +2 -1
  8. {robocandywrapper-0.2.16 → robocandywrapper-0.2.18}/robocandywrapper.egg-info/requires.txt +0 -4
  9. {robocandywrapper-0.2.16 → robocandywrapper-0.2.18}/setup.py +1 -1
  10. robocandywrapper-0.2.18/tests/test_task_index_remapping.py +134 -0
  11. {robocandywrapper-0.2.16 → robocandywrapper-0.2.18}/LICENSE +0 -0
  12. {robocandywrapper-0.2.16 → robocandywrapper-0.2.18}/MANIFEST.in +0 -0
  13. {robocandywrapper-0.2.16 → robocandywrapper-0.2.18}/README.md +0 -0
  14. {robocandywrapper-0.2.16 → robocandywrapper-0.2.18}/robocandywrapper/constants.py +0 -0
  15. {robocandywrapper-0.2.16 → robocandywrapper-0.2.18}/robocandywrapper/dataformats/__init__.py +0 -0
  16. {robocandywrapper-0.2.16 → robocandywrapper-0.2.18}/robocandywrapper/dataformats/lerobot_21/__init__.py +0 -0
  17. {robocandywrapper-0.2.16 → robocandywrapper-0.2.18}/robocandywrapper/dataformats/lerobot_21/convert_v20_to_v21.py +0 -0
  18. {robocandywrapper-0.2.16 → robocandywrapper-0.2.18}/robocandywrapper/dataformats/lerobot_21/dataset.py +0 -0
  19. {robocandywrapper-0.2.16 → robocandywrapper-0.2.18}/robocandywrapper/dataformats/lerobot_21/utils.py +0 -0
  20. {robocandywrapper-0.2.16 → robocandywrapper-0.2.18}/robocandywrapper/factory.py +0 -0
  21. {robocandywrapper-0.2.16 → robocandywrapper-0.2.18}/robocandywrapper/plugin.py +0 -0
  22. {robocandywrapper-0.2.16 → robocandywrapper-0.2.18}/robocandywrapper/plugins/__init__.py +0 -0
  23. {robocandywrapper-0.2.16 → robocandywrapper-0.2.18}/robocandywrapper/plugins/affordance.py +0 -0
  24. {robocandywrapper-0.2.16 → robocandywrapper-0.2.18}/robocandywrapper/plugins/control_mode.py +0 -0
  25. {robocandywrapper-0.2.16 → robocandywrapper-0.2.18}/robocandywrapper/plugins/episode_outcome.py +0 -0
  26. {robocandywrapper-0.2.16 → robocandywrapper-0.2.18}/robocandywrapper/plugins/molmopoint.py +0 -0
  27. {robocandywrapper-0.2.16 → robocandywrapper-0.2.18}/robocandywrapper/plugins/subtask.py +0 -0
  28. {robocandywrapper-0.2.16 → robocandywrapper-0.2.18}/robocandywrapper/samplers/__init__.py +0 -0
  29. {robocandywrapper-0.2.16 → robocandywrapper-0.2.18}/robocandywrapper/samplers/config.py +0 -0
  30. {robocandywrapper-0.2.16 → robocandywrapper-0.2.18}/robocandywrapper/samplers/factory.py +0 -0
  31. {robocandywrapper-0.2.16 → robocandywrapper-0.2.18}/robocandywrapper/samplers/weighted.py +0 -0
  32. {robocandywrapper-0.2.16 → robocandywrapper-0.2.18}/robocandywrapper/utils.py +0 -0
  33. {robocandywrapper-0.2.16 → robocandywrapper-0.2.18}/robocandywrapper.egg-info/dependency_links.txt +0 -0
  34. {robocandywrapper-0.2.16 → robocandywrapper-0.2.18}/robocandywrapper.egg-info/top_level.txt +0 -0
  35. {robocandywrapper-0.2.16 → robocandywrapper-0.2.18}/setup.cfg +0 -0
  36. {robocandywrapper-0.2.16 → robocandywrapper-0.2.18}/tests/test_dataset_weights_integration.py +0 -0
  37. {robocandywrapper-0.2.16 → robocandywrapper-0.2.18}/tests/test_key_rename_stats.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: robocandywrapper
3
- Version: 0.2.16
3
+ Version: 0.2.18
4
4
  Summary: Sweet wrappers for extending and remixing LeRobot Datasets
5
5
  Author: RoboCandyWrapper Contributors
6
6
  License: MIT License
@@ -42,10 +42,6 @@ Requires-Dist: numpy>=1.20.0
42
42
  Requires-Dist: torch>=2.0.0
43
43
  Requires-Dist: lerobot<0.5,>=0.4
44
44
  Requires-Dist: pandas>=1.3.0
45
- Requires-Dist: rewact_tools
46
- Requires-Dist: motion-primitives
47
- Requires-Dist: lerobot-policy-diffusion-motion-primitives
48
- Requires-Dist: lerobot-policy-diffusion-pointing
49
45
  Provides-Extra: dev
50
46
  Requires-Dist: pytest>=7.0.0; extra == "dev"
51
47
  Requires-Dist: black>=22.0.0; extra == "dev"
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "robocandywrapper"
7
- version = "0.2.16"
7
+ version = "0.2.18"
8
8
  description = "Sweet wrappers for extending and remixing LeRobot Datasets"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10,<3.11"
@@ -27,10 +27,6 @@ dependencies = [
27
27
  "torch>=2.0.0",
28
28
  "lerobot>=0.4,<0.5",
29
29
  "pandas>=1.3.0",
30
- "rewact_tools",
31
- "motion-primitives",
32
- "lerobot-policy-diffusion-motion-primitives",
33
- "lerobot-policy-diffusion-pointing",
34
30
  ]
35
31
 
36
32
  [project.optional-dependencies]
@@ -56,12 +52,6 @@ override-dependencies = [
56
52
  "evdev>=0.0.0; sys_platform == 'never'",
57
53
  ]
58
54
 
59
- [tool.uv.sources]
60
- lerobot-policy-diffusion-motion-primitives = { path = "../../experimental/lerobot_policy_diffusion_motion_primitives", editable = true }
61
- lerobot-policy-diffusion-pointing = { path = "../../experimental/lerobot_policy_diffusion_pointing", editable = true }
62
- motion-primitives = { path = "../../experimental/motion_primitives", editable = true }
63
- rewact-tools = { path = "../rewACT/rewact_tools", editable = true }
64
-
65
55
  [tool.black]
66
56
  line-length = 100
67
57
  target-version = ['py310']
@@ -18,7 +18,7 @@ from robocandywrapper.constants import (
18
18
  MOTION_PRIMITIVE_PLUGIN_NAME,
19
19
  )
20
20
 
21
- __version__ = "0.2.16"
21
+ __version__ = "0.2.18"
22
22
 
23
23
  __all__ = [
24
24
  "DatasetPlugin",
@@ -8,21 +8,22 @@ import numpy as np
8
8
  import pandas as pd
9
9
 
10
10
 
11
- def _task_names_from_meta_tasks(meta_tasks) -> set[str]:
11
+ def _task_index_to_name(meta_tasks) -> dict[int, str]:
12
12
  """
13
- Extract task name strings from a dataset's meta.tasks in either format.
13
+ Normalize a dataset's task metadata to task_index -> task name.
14
14
 
15
15
  - LeRobot 2.1 (dict): tasks is dict[task_index, task_name].
16
16
  - LeRobot 3.0 (parquet/DataFrame): tasks is a pandas DataFrame where the
17
17
  *row index* is the task name (string) and the only column is "task_index"
18
18
  (integer). So: df.index = task names, df["task_index"] = indices.
19
19
 
20
- Returns a set of unique task name strings.
20
+ Some task tables instead include both "task_index" and "task" columns.
21
21
  """
22
22
  if meta_tasks is None:
23
- return set()
23
+ return {}
24
24
  if isinstance(meta_tasks, dict):
25
- return {str(v) for v in meta_tasks.values() if isinstance(v, str)}
25
+ return {int(task_index): str(name) for task_index, name in meta_tasks.items()}
26
+
26
27
  # DataFrame or object with to_pandas() (e.g. HuggingFace Dataset)
27
28
  df = None
28
29
  if hasattr(meta_tasks, "to_pandas"):
@@ -30,14 +31,24 @@ def _task_names_from_meta_tasks(meta_tasks) -> set[str]:
30
31
  elif isinstance(meta_tasks, pd.DataFrame):
31
32
  df = meta_tasks
32
33
  if df is None:
33
- return set()
34
- # 3.0 format: optional "task" column, else task names are the row index
35
- if "task" in df.columns:
36
- return set(df["task"].dropna().astype(str).unique())
34
+ return {}
35
+
37
36
  if "task_index" in df.columns:
38
- # Task names are the DataFrame index (row labels), not the column
39
- return set(df.index.astype(str))
40
- return set()
37
+ if "task" in df.columns:
38
+ return {
39
+ int(row["task_index"]): str(row["task"])
40
+ for _, row in df.iterrows()
41
+ }
42
+ return {
43
+ int(row["task_index"]): str(task_name)
44
+ for task_name, row in df.iterrows()
45
+ }
46
+ if "task" in df.columns:
47
+ return {
48
+ int(task_index): str(name)
49
+ for task_index, name in enumerate(df["task"])
50
+ }
51
+ return {}
41
52
 
42
53
 
43
54
  def aggregate_stats_weighted(
@@ -301,7 +312,7 @@ class WrappedRobotDatasetMetadataView:
301
312
  all_task_names: set[str] = set()
302
313
  for dataset in self._datasets:
303
314
  meta_tasks = getattr(dataset.meta, "tasks", None)
304
- all_task_names.update(_task_names_from_meta_tasks(meta_tasks))
315
+ all_task_names.update(_task_index_to_name(meta_tasks).values())
305
316
 
306
317
  sorted_names = sorted(all_task_names)
307
318
  self._tasks = {idx: name for idx, name in enumerate(sorted_names)}
@@ -9,7 +9,10 @@ from lerobot.configs.types import FeatureType, PolicyFeature
9
9
  import torch
10
10
 
11
11
  from robocandywrapper import DatasetPlugin, PluginConflictError, PluginInstance
12
- from robocandywrapper.metadata_view import WrappedRobotDatasetMetadataView
12
+ from robocandywrapper.metadata_view import (
13
+ WrappedRobotDatasetMetadataView,
14
+ _task_index_to_name,
15
+ )
13
16
 
14
17
 
15
18
  class WrappedRobotDataset(torch.utils.data.Dataset):
@@ -114,6 +117,15 @@ class WrappedRobotDataset(torch.utils.data.Dataset):
114
117
  dataset_weights=dataset_weights,
115
118
  dataset_renames=self._dataset_renames,
116
119
  )
120
+ self._task_index_remaps = [
121
+ {
122
+ inner_index: self._meta.task_to_task_index[task_name]
123
+ for inner_index, task_name in _task_index_to_name(
124
+ getattr(dataset.meta, "tasks", None)
125
+ ).items()
126
+ }
127
+ for dataset in self._datasets
128
+ ]
117
129
 
118
130
  # ** MATCHING LeRobot MULTI-DATASET API DESIGN **
119
131
 
@@ -627,6 +639,25 @@ class WrappedRobotDataset(torch.utils.data.Dataset):
627
639
  else:
628
640
  item = dataset[local_idx]
629
641
 
642
+ if "task_index" in item and self._task_index_remaps[dataset_idx]:
643
+ task_index = item["task_index"]
644
+ inner_task_index = (
645
+ int(task_index.item()) if isinstance(task_index, torch.Tensor) else int(task_index)
646
+ )
647
+ try:
648
+ unified_task_index = self._task_index_remaps[dataset_idx][inner_task_index]
649
+ except KeyError:
650
+ raise KeyError(
651
+ f"task_index {inner_task_index} from dataset {dataset.repo_id!r} "
652
+ "is missing from its task metadata"
653
+ ) from None
654
+
655
+ if isinstance(task_index, torch.Tensor):
656
+ item["task_index"] = task_index.new_tensor(unified_task_index)
657
+ else:
658
+ item["task_index"] = unified_task_index
659
+ item["task"] = self._meta.tasks[unified_task_index]
660
+
630
661
  episode_idx = item["episode_index"].item() if isinstance(item["episode_index"], torch.Tensor) else item["episode_index"]
631
662
 
632
663
  # Add dataset index
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: robocandywrapper
3
- Version: 0.2.16
3
+ Version: 0.2.18
4
4
  Summary: Sweet wrappers for extending and remixing LeRobot Datasets
5
5
  Author: RoboCandyWrapper Contributors
6
6
  License: MIT License
@@ -42,10 +42,6 @@ Requires-Dist: numpy>=1.20.0
42
42
  Requires-Dist: torch>=2.0.0
43
43
  Requires-Dist: lerobot<0.5,>=0.4
44
44
  Requires-Dist: pandas>=1.3.0
45
- Requires-Dist: rewact_tools
46
- Requires-Dist: motion-primitives
47
- Requires-Dist: lerobot-policy-diffusion-motion-primitives
48
- Requires-Dist: lerobot-policy-diffusion-pointing
49
45
  Provides-Extra: dev
50
46
  Requires-Dist: pytest>=7.0.0; extra == "dev"
51
47
  Requires-Dist: black>=22.0.0; extra == "dev"
@@ -31,4 +31,5 @@ robocandywrapper/samplers/config.py
31
31
  robocandywrapper/samplers/factory.py
32
32
  robocandywrapper/samplers/weighted.py
33
33
  tests/test_dataset_weights_integration.py
34
- tests/test_key_rename_stats.py
34
+ tests/test_key_rename_stats.py
35
+ tests/test_task_index_remapping.py
@@ -2,10 +2,6 @@ numpy>=1.20.0
2
2
  torch>=2.0.0
3
3
  lerobot<0.5,>=0.4
4
4
  pandas>=1.3.0
5
- rewact_tools
6
- motion-primitives
7
- lerobot-policy-diffusion-motion-primitives
8
- lerobot-policy-diffusion-pointing
9
5
 
10
6
  [dev]
11
7
  pytest>=7.0.0
@@ -9,7 +9,7 @@ long_description = readme_file.read_text(encoding="utf-8") if readme_file.exists
9
9
 
10
10
  setup(
11
11
  name="robocandywrapper",
12
- version="0.2.16",
12
+ version="0.2.18",
13
13
  description="Sweet wrappers for extending and remixing LeRobot Datasets",
14
14
  long_description=long_description,
15
15
  long_description_content_type="text/markdown",
@@ -0,0 +1,134 @@
1
+ import sys
2
+
3
+ import lerobot.datasets.utils as lerobot_dataset_utils
4
+ import pandas as pd
5
+ import pytest
6
+ import torch
7
+
8
+ # Current main imports these helpers from the newer module location. Alias the
9
+ # older location when running against the repository's declared LeRobot range.
10
+ sys.modules.setdefault("lerobot.datasets.feature_utils", lerobot_dataset_utils)
11
+ sys.modules.setdefault("lerobot.datasets.io_utils", lerobot_dataset_utils)
12
+
13
+ from robocandywrapper.wrapper import WrappedRobotDataset
14
+
15
+
16
+ class PandasTasks:
17
+ def __init__(self, index_to_task: dict[int, str]):
18
+ self._frame = pd.DataFrame(
19
+ {"task_index": list(index_to_task)},
20
+ index=pd.Index(index_to_task.values(), name="task"),
21
+ )
22
+
23
+ def to_pandas(self) -> pd.DataFrame:
24
+ return self._frame.copy()
25
+
26
+
27
+ class MockMetadata:
28
+ def __init__(self, tasks):
29
+ self.tasks = tasks
30
+ self.stats = {}
31
+ self.info = {"fps": 20, "video": False}
32
+ self.camera_keys = []
33
+ self.image_keys = []
34
+ self.video_keys = []
35
+ self.total_frames = 1
36
+ self.total_episodes = 1
37
+ self.episodes = {}
38
+
39
+ @property
40
+ def fps(self):
41
+ return self.info["fps"]
42
+
43
+ @property
44
+ def features(self):
45
+ return {
46
+ "episode_index": {"dtype": "int64", "shape": [1]},
47
+ "task_index": {"dtype": "int64", "shape": [1]},
48
+ }
49
+
50
+
51
+ class MockDataset:
52
+ def __init__(self, repo_id: str, tasks, task_index):
53
+ self.repo_id = repo_id
54
+ self.meta = MockMetadata(tasks)
55
+ self.features = self.meta.features
56
+ self.hf_features = self.features
57
+ self._task_index = task_index
58
+ self.episodes = None
59
+ self.num_episodes = 1
60
+
61
+ def __len__(self):
62
+ return 1
63
+
64
+ def __getitem__(self, index):
65
+ return {
66
+ "episode_index": torch.tensor(0),
67
+ "task_index": (
68
+ self._task_index.clone()
69
+ if isinstance(self._task_index, torch.Tensor)
70
+ else self._task_index
71
+ ),
72
+ }
73
+
74
+
75
+ def test_single_dataset_sample_uses_sorted_wrapper_task_index():
76
+ dataset = MockDataset(
77
+ "single",
78
+ PandasTasks({0: "z task", 1: "a task"}),
79
+ torch.tensor(0, dtype=torch.int64),
80
+ )
81
+
82
+ wrapped = WrappedRobotDataset(dataset)
83
+ item = wrapped[0]
84
+
85
+ assert wrapped.meta.tasks == {0: "a task", 1: "z task"}
86
+ assert torch.equal(item["task_index"], torch.tensor(1, dtype=torch.int64))
87
+ assert item["task"] == "z task"
88
+
89
+
90
+ def test_mixed_dataset_samples_remap_colliding_local_task_indices():
91
+ first = MockDataset("first", {0: "z task"}, 0)
92
+ second = MockDataset("second", PandasTasks({0: "a task"}), 0)
93
+
94
+ wrapped = WrappedRobotDataset([first, second])
95
+
96
+ assert wrapped.meta.tasks == {0: "a task", 1: "z task"}
97
+ assert wrapped[0]["task_index"] == 1
98
+ assert wrapped[0]["task"] == "z task"
99
+ assert wrapped[1]["task_index"] == 0
100
+ assert wrapped[1]["task"] == "a task"
101
+
102
+
103
+ def test_unknown_inner_task_index_has_contextual_error():
104
+ dataset = MockDataset("broken", {0: "known task"}, 9)
105
+ wrapped = WrappedRobotDataset(dataset)
106
+
107
+ with pytest.raises(KeyError, match=r"task_index 9.*broken"):
108
+ wrapped[0]
109
+
110
+
111
+ def test_real_busybox_sample_remaps_task_index_without_changing_task(monkeypatch):
112
+ from lerobot.datasets.lerobot_dataset import LeRobotDataset
113
+
114
+ inner = LeRobotDataset(
115
+ "villekuosmanen/busybox_multitask",
116
+ episodes=[12],
117
+ download_videos=False,
118
+ )
119
+ monkeypatch.setattr(inner, "_query_videos", lambda *args, **kwargs: {})
120
+
121
+ inner_item = inner[0]
122
+ inner_task_index = int(inner_item["task_index"])
123
+ inner_task = str(inner.meta.tasks.index[inner.meta.tasks["task_index"] == inner_task_index][0])
124
+
125
+ wrapped = WrappedRobotDataset(inner)
126
+ wrapped_item = wrapped[0]
127
+ wrapped_task_index = int(wrapped_item["task_index"])
128
+
129
+ assert inner_task_index == 4
130
+ assert inner_task == "Move the right slider to position 5"
131
+ assert wrapped.meta.tasks[4] == "Move the left slider to position 5"
132
+ assert wrapped_task_index == 9
133
+ assert wrapped.meta.tasks[wrapped_task_index] == inner_task
134
+ assert wrapped_item["task"] == inner_task