Source code for so101_mvbench.utils.episodes_spec
# SPDX-License-Identifier: MIT
"""Parse CLI ``--episodes`` range-syntax into a sorted ``list[int]``.
Supported forms:
- ``'0'`` single ID
- ``'0,1,5'`` comma-list
- ``'0-10'`` inclusive range (lo ≤ hi)
- ``'0,5-10,15'`` mixed comma + range
- ``'all'`` auto-discover from ``<dataset_root>/trajectories/episode_*.npy``
The function is intentionally pure (no Isaac Sim deps) so it can be unit-tested
in isolation. Shared across console-scripts that operate on a subset of episodes
(``synthetic_multicam_recorder``, future tools).
"""
from __future__ import annotations
from pathlib import Path
__all__ = ["parse_episodes_spec"]
[docs]
def parse_episodes_spec(spec: str, dataset_root: Path) -> list[int]:
"""Parse a ``--episodes`` spec string into a sorted list of integer IDs.
Args:
spec: One of: ``'all'``, ``'<n>'``, ``'<n>,<m>'``, ``'<lo>-<hi>'``, or
any comma-separated mix of singles + inclusive ranges. Whitespace
around tokens is ignored. Case-insensitive for ``'all'``.
dataset_root: Source LeRobot dataset directory. Used only when
``spec == 'all'`` — globs ``<dataset_root>/trajectories/episode_*.npy``.
Returns:
Sorted list of unique integer episode IDs. Duplicates within the
spec are deduplicated.
Raises:
ValueError: If ``spec`` is empty/whitespace, malformed, or contains a
reversed range (``hi < lo``). Also if ``spec == 'all'`` but the
``trajectories/`` directory has no matching files.
"""
if spec is None:
raise ValueError("episodes spec is None")
spec = spec.strip()
if not spec:
raise ValueError("empty episodes spec")
if spec.lower() == "all":
traj_dir = dataset_root / "trajectories"
ids = sorted(
int(p.stem.split("_")[-1])
for p in traj_dir.glob("episode_*.npy")
)
if not ids:
raise ValueError(
f"--episodes='all' but no trajectories/episode_*.npy in {traj_dir}"
)
return ids
result: set[int] = set()
for part in spec.split(","):
part = part.strip()
if not part:
# Tolerate leading/trailing/duplicate commas (",0", "0,,5", "0,")
continue
if "-" in part:
try:
lo_s, hi_s = part.split("-", 1)
lo = int(lo_s)
hi = int(hi_s)
except ValueError as e:
raise ValueError(
f"malformed range token {part!r} (expected '<lo>-<hi>')"
) from e
if hi < lo:
raise ValueError(
f"reversed range {part!r}: hi ({hi}) < lo ({lo})"
)
result.update(range(lo, hi + 1))
else:
try:
result.add(int(part))
except ValueError as e:
raise ValueError(
f"malformed integer token {part!r}"
) from e
if not result:
raise ValueError(f"empty episodes spec after parsing: {spec!r}")
return sorted(result)