Source code for simulusion.training.tasks.common

"""Pieces several tasks share: where offline batches come from."""

from __future__ import annotations

from dataclasses import dataclass
from pathlib import Path
from typing import Any, Iterator, List, Optional, Sequence, Tuple

import numpy as np
import torch
from torch.utils.data import DataLoader

from simulusion.data.episode import Episode
from simulusion.data.folder import DatasetMeta, EpisodeFolder
from simulusion.data.sequence import SequenceDataset, collate_sequences


[docs] @dataclass class SequenceDataConfig: """Where an offline task reads its episodes, and how it cuts them. Attributes: path: An episode folder, or for ``source: lerobot`` a local LeRobot dataset folder. source: ``"folder"`` (Simulusion episodes, videos, ``.npy``) or ``"lerobot"``. camera: The view to train on, for datasets with several. sequence_length: Frames per training window; the task's natural length (its model's context) when ``None``. stride: Frames between window starts. image_size: Resize frames to this square size; the model's size when ``None``. batch_size: Windows per batch (per process under data parallelism). num_workers: ``DataLoader`` worker processes. val_fraction: Fraction of episodes held out for evaluation; 0 for none. val_batches: Batches scored per evaluation. episodes: Use only these episode indices. preload: Decode every episode into memory up front. seed: Seed for the train / validation split. """ path: Optional[str] = None source: str = "folder" camera: Optional[str] = None sequence_length: Optional[int] = None stride: int = 1 image_size: Optional[int] = None batch_size: int = 16 num_workers: int = 0 val_fraction: float = 0.0 val_batches: int = 8 episodes: Optional[List[int]] = None preload: bool = False seed: int = 0
class _Subset(Sequence[Episode]): def __init__(self, base: Any, indices: List[int]) -> None: self.base, self.indices = base, indices self.lengths = [base.lengths[i] for i in indices] def __len__(self) -> int: return len(self.indices) def __getitem__(self, i: int) -> Episode: # type: ignore[override] return self.base[self.indices[i]]
[docs] def open_episodes(config: SequenceDataConfig) -> Tuple[Any, Optional[DatasetMeta]]: """The episodes ``config`` points at, and their metadata if known.""" if not config.path: raise ValueError( "This task trains on a dataset: set data.path to an episode folder " "(or a LeRobot dataset with data.source=lerobot)." ) if config.source == "folder": folder = EpisodeFolder(config.path, episodes=config.episodes) return folder, folder.meta if config.source == "lerobot": from simulusion.data.lerobot import LeRobotEpisodes dataset = LeRobotEpisodes( config.path, camera=config.camera, episodes=config.episodes, ) return dataset, dataset.meta raise ValueError( f"Unknown data.source '{config.source}'; use 'folder' or 'lerobot'." )
[docs] def sequence_loaders( config: SequenceDataConfig, trainer: Any, *, sequence_length: int, image_size: Optional[int | Tuple[int, int]], require_actions: bool = False, ) -> Tuple[Iterator[dict], Optional[DataLoader], Optional[DatasetMeta]]: """An endless iterator of training batches, a validation loader, and metadata.""" episodes, meta = open_episodes(config) camera = config.camera if config.source == "folder" else None train_eps: Any = episodes val_eps: Any = None if config.val_fraction > 0: order = np.random.default_rng(config.seed).permutation(len(episodes)) n_val = min( max(1, int(round(len(episodes) * config.val_fraction))), len(episodes) - 1 ) if n_val < 1: raise ValueError("val_fraction needs at least two episodes.") val_eps = _Subset(episodes, sorted(order[:n_val].tolist())) train_eps = _Subset(episodes, sorted(order[n_val:].tolist())) def windows(eps: Any) -> SequenceDataset: return SequenceDataset( eps, config.sequence_length or sequence_length, stride=config.stride, image_size=config.image_size or image_size, main_camera=camera, require_actions=require_actions, preload=config.preload, ) generator = torch.Generator().manual_seed(config.seed) train = DataLoader( windows(train_eps), batch_size=config.batch_size, shuffle=True, drop_last=True, num_workers=config.num_workers, collate_fn=collate_sequences, generator=generator, persistent_workers=config.num_workers > 0, ) if len(train) == 0: raise ValueError( f"Only {len(train.dataset)} training windows for batch_size=" # type: ignore[arg-type] f"{config.batch_size}; lower data.batch_size or add data." ) val = None if val_eps is not None: val = DataLoader( windows(val_eps), batch_size=config.batch_size, shuffle=False, num_workers=config.num_workers, collate_fn=collate_sequences, ) val = trainer.prepare_dataloader(val) return trainer.infinite(trainer.prepare_dataloader(train)), val, meta
#: Arrays a ring replay buffer (Dreamer's, IRIS's) may hold, row-aligned. _REPLAY_ARRAYS = ("observations", "actions", "rewards", "terminals", "terminated")
[docs] def save_replay(buffer: Any, path: Path) -> None: """Write the filled part of a ring replay buffer, plus its write position.""" n = buffer.size if buffer.full else buffer.idx arrays = { name: getattr(buffer, name)[:n] for name in _REPLAY_ARRAYS if hasattr(buffer, name) } state = np.array([buffer.idx, int(buffer.full), buffer.steps, buffer.episodes]) np.savez(path, state=state, **arrays)
[docs] def load_replay(buffer: Any, path: Path) -> None: """Restore what :func:`save_replay` wrote into a buffer of the same size.""" with np.load(path, allow_pickle=False) as data: n = data["observations"].shape[0] if n > buffer.size: raise ValueError( f"The saved replay holds {n} transitions; this buffer holds {buffer.size}." ) for name in _REPLAY_ARRAYS: if name in data.files and hasattr(buffer, name): getattr(buffer, name)[:n] = data[name] idx, full, steps, episodes = (int(v) for v in data["state"]) buffer.idx, buffer.full, buffer.steps, buffer.episodes = ( idx, bool(full), steps, episodes, )
[docs] def mean_metrics(rows: List[dict]) -> dict: keys = set().union(*rows) if rows else set() return {k: float(np.mean([r[k] for r in rows if k in r])) for k in sorted(keys)}
__all__ = [ "SequenceDataConfig", "load_replay", "mean_metrics", "open_episodes", "save_replay", "sequence_loaders", ]