Source code for embodichain.learning.rl.utils.helper

# ----------------------------------------------------------------------------
# Copyright (c) 2021-2026 DexForce Technology Co., Ltd.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ----------------------------------------------------------------------------

from __future__ import annotations

from collections.abc import Mapping, Sequence
from typing import Any

import torch
from tensordict import TensorDict

__all__ = [
    "dict_to_tensordict",
    "flatten_dict_observation",
    "flatten_observation_groups",
]


[docs] def flatten_dict_observation(obs: TensorDict) -> torch.Tensor: """Flatten a hierarchical observation TensorDict into a 2D tensor. Args: obs: Observation TensorDict with batch dimension `[num_envs]`. Returns: Flattened observation tensor of shape `[num_envs, obs_dim]`. """ obs_list: list[torch.Tensor] = [] def _collect_tensors(data: TensorDict) -> None: for key in sorted(data.keys()): value = data[key] if isinstance(value, TensorDict): _collect_tensors(value) elif isinstance(value, torch.Tensor): obs_list.append(value.flatten(start_dim=1)) _collect_tensors(obs) if not obs_list: raise ValueError("No tensors found in observation TensorDict.") return torch.cat(obs_list, dim=-1)
[docs] def flatten_observation_groups( obs: TensorDict, groups: Sequence[str] | None, ) -> torch.Tensor: """Flatten selected observation groups in the configured order. A group name selects one top-level TensorDict key. Dot-separated names may select nested keys, for example ``"robot.qpos"``. When ``groups`` is ``None``, every tensor is flattened using :func:`flatten_dict_observation` for compatibility with existing tasks. Args: obs: Observation TensorDict with batch dimension ``[num_envs]``. groups: Observation group names consumed by one model. Returns: Concatenated observation tensor with shape ``[num_envs, obs_dim]``. Raises: ValueError: If ``groups`` is empty or a selected group has no tensors. KeyError: If a configured group is absent from ``obs``. """ if groups is None: return flatten_dict_observation(obs) if len(groups) == 0: raise ValueError("Observation groups cannot be empty.") flattened: list[torch.Tensor] = [] for group in groups: value: TensorDict | torch.Tensor = obs for key in group.split("."): if not isinstance(value, TensorDict) or key not in value.keys(): raise KeyError( f"Observation group '{group}' was not found. " f"Available top-level groups: {list(obs.keys())}." ) value = value[key] if isinstance(value, TensorDict): flattened.append(flatten_dict_observation(value)) elif isinstance(value, torch.Tensor): flattened.append(value.flatten(start_dim=1)) else: raise TypeError( f"Observation group '{group}' must contain tensors, got " f"{type(value)!r}." ) return torch.cat(flattened, dim=-1)
[docs] def dict_to_tensordict( obs_dict: torch.Tensor | TensorDict | Mapping[str, Any], device: torch.device | str, ) -> TensorDict: """Convert an environment observation mapping into a TensorDict. Args: obs_dict: Tensor or mapping returned by ``reset()`` or ``step()``. device: Target device for the resulting TensorDict. Returns: Observation TensorDict moved onto the target device. """ if isinstance(obs_dict, TensorDict): return obs_dict.to(device) if isinstance(obs_dict, torch.Tensor): tensor = obs_dict.to(device) batch_size = [tensor.shape[0]] if tensor.ndim > 1 else [] return TensorDict({"obs": tensor}, batch_size=batch_size, device=device) if not isinstance(obs_dict, Mapping): raise TypeError( f"Expected tensor, observation mapping, or TensorDict, " f"got {type(obs_dict)!r}." ) return TensorDict.from_dict(dict(obs_dict), device=device)