Abha2001's picture
Initial upload of BFN-hybrid + DDPM PushT-xarm policies
752c314 verified
Raw
History Blame Contribute Delete
8.1 kB
"""Base class for all robotics policies.
This module defines the abstract base class `BasePolicy`, which standardizes
the interface for environment interaction (`act`), model inference (`forward`),
training (`compute_loss`), and device/data management.
"""
from __future__ import annotations
from abc import ABC, abstractmethod
from typing import Any, Dict, List, Mapping, Optional, Tuple, Union
import numpy as np
import torch
import torch.nn as nn
from utils.bfn_utils import str_to_torch_dtype
__all__ = ["BasePolicy"]
class BasePolicy(nn.Module, ABC):
"""Abstract base class for robotics policies.
Provides shared functionality for:
1. Device and dtype management.
2. Data conversion (Numpy <-> Torch).
3. Automatic batch dimension handling.
4. Action clipping and normalization hooks.
Subclasses must implement:
- `forward(obs, ...)`: The core PyTorch inference logic.
- `compute_loss(batch)`: The training logic (optional, raises NotImplementedError by default).
"""
def __init__(
self,
action_space: Any,
*,
device: str = "cpu",
dtype: str = "float32",
clip_actions: bool = True,
normalizer: Optional[Any] = None,
):
"""Initializes the BasePolicy.
Args:
action_space: The Gym action space (used for clipping).
device: Default device to place tensors on ('cpu', 'cuda').
dtype: Default dtype for tensor creation ('float32', 'float16').
clip_actions: Whether to clamp output actions to the action_space bounds.
normalizer: Optional normalization module (e.g., LinearNormalizer).
"""
super().__init__()
self.action_space = action_space
self._device = torch.device(device)
self._dtype = str_to_torch_dtype(dtype)
self.clip_actions = clip_actions
self.normalizer = normalizer
# --- Properties ---
@property
def device(self) -> torch.device:
"""Returns the current device of the policy.
Infers device from the first parameter if available (robust to .to() calls),
otherwise falls back to the initialization value.
"""
try:
return next(self.parameters()).device
except StopIteration:
return self._device
@property
def dtype(self) -> torch.dtype:
"""Returns the current dtype of the policy."""
try:
return next(self.parameters()).dtype
except StopIteration:
return self._dtype
# --- Public Interface ---
def set_normalizer(self, normalizer: Any) -> None:
"""Updates the normalizer used by the policy."""
self.normalizer = normalizer
@abstractmethod
def forward(
self,
obs: Union[torch.Tensor, Dict[str, torch.Tensor]],
*,
deterministic: bool = False,
**kwargs: Any,
) -> torch.Tensor:
"""Core inference method returning a batch of actions.
Args:
obs: Observations with leading batch dimension [B, ...].
deterministic: Whether to sample deterministically (policy dependent).
**kwargs: Additional arguments (e.g., conditioning info).
Returns:
Action tensor of shape [B, ActionDim].
"""
raise NotImplementedError
def compute_loss(self, batch: Any) -> torch.Tensor:
"""Computes training loss for the policy.
Args:
batch: A batch of data (usually containing 'obs', 'action').
Returns:
Scalar loss tensor.
Raises:
NotImplementedError: If the policy does not support internal training logic.
"""
raise NotImplementedError(
f"compute_loss is not implemented for {self.__class__.__name__}."
)
@torch.inference_mode()
def act(
self,
obs: Any,
*,
deterministic: bool = False,
return_torch: bool = False,
**kwargs: Any,
) -> Union[np.ndarray, torch.Tensor]:
"""Convenience wrapper for environment interaction.
Handles:
1. conversion of numpy obs -> torch tensors.
2. adding batch dimension if missing.
3. inference via `forward`.
4. clipping actions.
5. conversion of torch output -> numpy action (optional).
Args:
obs: Observation from the environment (Numpy array, Dict, or Tensor).
deterministic: Whether to use deterministic mode.
return_torch: If True, returns a Tensor on device; else returns Numpy array.
**kwargs: Passed to `forward`.
Returns:
Action (Numpy array or Tensor).
"""
# 1. Convert to Tensor
obs_t = self._to_tensor(obs)
# 2. Auto-Batching
# We assume environment interaction usually provides a single unbatched observation
obs_t, batch_added = self._maybe_add_batch_dim(obs_t)
# 3. Inference
action = self.forward(obs_t, deterministic=deterministic, **kwargs)
# 4. Clipping
action = self._clip_actions(action)
# 5. Return
if return_torch:
return action
# Remove batch dim if we added it
if batch_added:
action = action.squeeze(0)
return action.detach().cpu().numpy()
# --- Internal Helpers ---
def _to_tensor(self, data: Any) -> Any:
"""Recursively converts input data to tensors on the correct device/dtype."""
if isinstance(data, torch.Tensor):
return data.to(device=self.device, dtype=self.dtype)
if isinstance(data, Mapping):
return {k: self._to_tensor(v) for k, v in data.items()}
if isinstance(data, (list, tuple)):
return type(data)(self._to_tensor(v) for v in data)
# Fallback for numpy arrays / scalars
return torch.as_tensor(data, device=self.device, dtype=self.dtype)
def _maybe_add_batch_dim(self, obs: Any) -> Tuple[Any, bool]:
"""Adds a leading batch dimension if the input appears to be unbatched.
Note: This uses a heuristic. If the input is a Tensor, we assume it is
unbatched if it matches the observation space shape (not implemented here generic enough)
OR we rely on the caller context (usually `act` is single-step).
Here, we unconditionally unsqueeze dim 0 for `act` convenience.
"""
batch_added = False
if isinstance(obs, torch.Tensor):
# Heuristic: We assume `act` is called with single observations.
# For robust batch detection, one would check obs_space.shape.
# Here we simply unsqueeze to ensure [1, ...] shape.
obs = obs.unsqueeze(0)
batch_added = True
elif isinstance(obs, Mapping):
# Handle Dict inputs (e.g. {'image': ..., 'state': ...})
# Only unsqueeze tensors.
new_obs = {}
for k, v in obs.items():
if isinstance(v, torch.Tensor):
new_obs[k] = v.unsqueeze(0)
batch_added = True # Mark true if ANY tensor was unsqueezed
else:
new_obs[k] = v
obs = new_obs
return obs, batch_added
def _clip_actions(self, action: torch.Tensor) -> torch.Tensor:
"""Clips actions to the environment bounds if `clip_actions` is True."""
if not self.clip_actions:
return action
# Check if action space has bounds
if not hasattr(self.action_space, "low") or not hasattr(
self.action_space, "high"
):
return action
# Create tensor bounds on the fly (caching could be an optimization)
low = torch.as_tensor(
self.action_space.low, device=action.device, dtype=action.dtype
)
high = torch.as_tensor(
self.action_space.high, device=action.device, dtype=action.dtype
)
return torch.clamp(action, low, high)