Rate this Page
★ ★ ★ ★ ★

Source code for torchrl.data.replay_buffers.storages.ensemble

# Copyright (c) Meta Platforms, Inc. and affiliates.
#
# This source code is licensed under the MIT license found in the
# LICENSE file in the root directory of this source tree.
from __future__ import annotations

import textwrap
from collections import OrderedDict
from typing import Any

import numpy as np
import torch

from torchrl.data.replay_buffers.checkpointers import StorageEnsembleCheckpointer

from .base import Storage


class StorageEnsemble(Storage):
    """An ensemble of storages.

    This class is designed to work with :class:`~torchrl.data.replay_buffers.replay_buffers.ReplayBufferEnsemble`.

    Args:
        storages (sequence of Storage): the storages to make the composite storage.

    Keyword Args:
        transforms (list of :class:`~torchrl.envs.Transform`, optional): a list of
            transforms of the same length as storages.

    .. warning::
      This class signatures for :meth:`get` does not match other storages, as
      it will return a tuple ``(buffer_id, samples)`` rather than just the samples.

    .. warning::
       This class does not support writing (similarly to :class:`~torchrl.data.replay_buffers.writers.WriterEnsemble`).
       To extend one of the replay buffers, simply index the parent
       :class:`~torchrl.data.ReplayBufferEnsemble` object.

    .. seealso:: :class:`~torchrl.trainers.algorithms.configs.data.StorageEnsembleConfig`

    """

    _default_checkpointer = StorageEnsembleCheckpointer

    def __init__(
        self,
        *storages: Storage,
        transforms: list[Transform] = None,  # noqa: F821
    ):
        self._rng_private = None
        self._storages = storages
        self._transforms = transforms
        super().__init__(max_size=sum(storage.max_size for storage in storages))
        if transforms is not None and len(transforms) != len(storages):
            raise TypeError(
                "transforms must have the same length as the storages provided."
            )

    @property
    def _rng(self):
        return self._rng_private

    @_rng.setter
    def _rng(self, value):
        self._rng_private = value
        for storage in self._storages:
            storage._rng = value

    def _share_memory_(self) -> None:
        for storage in self._storages:
            storage._share_memory_()

    def _fork_safe(self) -> bool:
        return all(storage._fork_safe() for storage in self._storages)

    def extend(self, value):
        raise RuntimeError

    def add(self, value):
        raise RuntimeError

    def get(self, item):
        # we return the buffer id too to be able to track the appropriate collate_fn
        buffer_ids = item.get("buffer_ids")
        index = item.get("index")
        results = []
        for buffer_id, sample in zip(buffer_ids, index):
            buffer_id = self._convert_id(buffer_id)
            results.append((buffer_id, self._get_storage(buffer_id).get(sample)))
        if self._transforms is not None:
            results = [
                (buffer_id, self._transforms[buffer_id](result))
                if self._transforms[buffer_id] is not None
                else (buffer_id, result)
                for buffer_id, result in results
            ]
        return results

    def _convert_id(self, sub):
        if isinstance(sub, torch.Tensor):
            sub = sub.item()
        return sub

    def _get_storage(self, sub):
        return self._storages[sub]

    def state_dict(self) -> dict[str, Any]:
        return OrderedDict(
            (str(index), storage.state_dict())
            for index, storage in enumerate(self._storages)
        )

    def load_state_dict(self, state_dict: dict[str, Any]) -> None:
        for index, storage in enumerate(self._storages):
            storage.load_state_dict(state_dict[str(index)])

    def _empty(self) -> None:
        for storage in self._storages:
            storage._empty()

    _INDEX_ERROR = "Expected an index of type torch.Tensor, range, np.ndarray, int, slice or ellipsis, got {} instead."

[docs] def as_dataset(self): raise NotImplementedError( "StorageEnsemble cannot be read as a flat torch dataset. Read its " "member storages instead." )
def __getitem__(self, index): if isinstance(index, tuple): if index[0] is Ellipsis: index = (slice(None), index[1:]) result = self[index[0]] if len(index) > 1: if result is self: # then index[0] is an ellipsis/slice(None) sample = [storage[index[1:]] for storage in self._storages] return sample if isinstance(result, StorageEnsemble): new_index = (slice(None), *index[1:]) return result[new_index] return result[index[1:]] return result if isinstance(index, slice) and index == slice(None): return self if isinstance(index, (list, range, np.ndarray)): index = torch.as_tensor(index) if isinstance(index, torch.Tensor): if index.ndim > 1: raise RuntimeError( f"Cannot index a {type(self)} with tensor indices that have more than one dimension." ) if index.is_floating_point(): raise TypeError( "A floating point index was received when an integer dtype was expected." ) if isinstance(index, int) or (not isinstance(index, slice) and len(index) == 0): try: index = int(index) except Exception: raise IndexError(self._INDEX_ERROR.format(type(index))) try: return self._storages[index] except IndexError: raise IndexError(self._INDEX_ERROR.format(type(index))) if isinstance(index, torch.Tensor): index = index.tolist() storages = [self._storages[i] for i in index] transforms = ( [self._transforms[i] for i in index] if self._transforms is not None else [None] * len(index) ) else: # slice storages = self._storages[index] transforms = ( self._transforms[index] if self._transforms is not None else [None] * len(storages) ) return StorageEnsemble(*storages, transforms=transforms) def __len__(self): return len(self._storages) def __repr__(self): storages = textwrap.indent(f"storages={self._storages}", " " * 4) transforms = textwrap.indent(f"transforms={self._transforms}", " " * 4) return f"StorageEnsemble(\n{storages}, \n{transforms})"