Source code for synora.envs.dmc

from __future__ import annotations

from typing import Any

from synora.envs._actions import clip_box_action
from synora.envs._contract import finalize_step_info
from synora.utils.gym_compat import gym
import numpy as np


[docs] class DeepMindControlEnv: """Gym-style adapter for DeepMind Control Suite tasks. The wrapper exposes DMC observations and actions through Gym spaces and adds a rendered RGB image to each observation dict so image-based world model pipelines can train consistently across backends. Features: - Parses domain-task names (e.g., "cheetah-run" -> domain="cheetah", task="run") - Automatically handles special cases like "cup" -> "ball_in_cup" - Renders RGB images at configurable resolution - Returns observations as dict with both state vectors and images Args: name (str): Environment name in format "domain-task" (e.g., "cheetah-run"). seed (int): Random seed for environment initialization. size (tuple): Target image size as (height, width) (default: (64, 64)). camera (int, optional): Camera ID for rendering. Defaults to 0 for most domains, 2 for quadruped. Attributes: observation_space (gym.spaces.Dict): Dict space with state keys and "image". action_space (gym.spaces.Box): Continuous action space from DMC spec. Example: >>> env = DeepMindControlEnv("cheetah-run", seed=0, size=(64, 64)) >>> obs = env.reset() >>> print(obs.keys()) # dict_keys(['position', 'velocity', 'image']) """ def __init__( self, name: str, seed: int, size: tuple[int, int] = (64, 64), camera: int | None = None, ) -> None: if "-" not in name: raise ValueError( f"DeepMind Control task names use 'domain-task' form, e.g. " f"'cartpole-balance' or 'walker-walk'; got {name!r}." ) domain, task = name.split("-", 1) if domain == "cup": # Only domain with multiple words. domain = "ball_in_cup" self._name = name self._domain = domain self._task = task self._seed = int(seed) self._size = (int(size[0]), int(size[1])) if camera is None: camera = dict(quadruped=2).get(domain, 0) self._camera = camera self._env = self._make_env(self._seed) spaces: dict[str, gym.spaces.Space[Any]] = {} for key, value in self._env.observation_spec().items(): spaces[key] = gym.spaces.Box(-np.inf, np.inf, value.shape, dtype=np.float32) spaces["image"] = gym.spaces.Box(0, 255, (3,) + self._size, dtype=np.uint8) self._observation_space = gym.spaces.Dict(spaces) spec = self._env.action_spec() self._action_space = gym.spaces.Box( spec.minimum, spec.maximum, dtype=np.float32 ) self._seed_spaces(self._seed) def _make_env(self, seed: int) -> Any: try: from dm_control import suite except ModuleNotFoundError as exc: raise ModuleNotFoundError( "The DeepMind Control backend requires the 'dm_control' package, " "which is not installed. Install it with:\n\n" " pip install synora[dmc] # Python <= 3.12\n" " python -m synora.install_dmc # any version, incl. 3.13\n\n" "On CPython 3.13 the extra alone is not enough: dm-control pins " "`labmaze`, which has no 3.13 wheel and builds with Bazel. The " "installer above uses the pure-Python `labmaze-new` instead.\n\n" "or pick a backend that is already available (for example a " "Gymnasium task via env_backend='gym', such as 'Pendulum-v1')." ) from exc return suite.load(self._domain, self._task, task_kwargs={"random": int(seed)}) def _seed_spaces(self, seed: int | None) -> None: if seed is None: return for space in (self._action_space, self._observation_space): if hasattr(space, "seed"): try: space.seed(seed) except Exception: pass @property def observation_space(self) -> gym.spaces.Dict: return self._observation_space @property def action_space(self) -> gym.spaces.Box: return self._action_space
[docs] def step(self, action: np.ndarray) -> tuple[dict, float, bool, dict]: clipped = clip_box_action(action, self.action_space.low, self.action_space.high) time_step = self._env.step(clipped) obs = dict(time_step.observation) obs["image"] = self.render().transpose(2, 0, 1).copy() reward = time_step.reward or 0 done = time_step.last() vector_observation = np.concatenate( [ np.asarray(value, dtype=np.float32).reshape(-1) for value in time_step.observation.values() ], axis=0, ) info = finalize_step_info( { "discount": np.array(time_step.discount, np.float32), "action": clipped.copy(), "executed_action": clipped.copy(), "vector_observation": vector_observation, }, done=done, terminated=done, truncated=False, ) return obs, float(reward), done, info
[docs] def reset(self, seed: int | None = None) -> dict: if seed is not None: self._seed = int(seed) self._env = self._make_env(self._seed) self._seed_spaces(self._seed) time_step = self._env.reset() obs = dict(time_step.observation) obs["image"] = self.render().transpose(2, 0, 1).copy() return obs
[docs] def render(self, *args: Any, **kwargs: Any) -> np.ndarray: if kwargs.get("mode", "rgb_array") != "rgb_array": raise ValueError("Only render mode 'rgb_array' is supported.") return self._env.physics.render(*self._size, camera_id=self._camera)
[docs] def make_dmc_env( env_id: str, seed: int = 0, size: tuple[int, int] = (64, 64), camera: int | None = None, ) -> DeepMindControlEnv: """Create a DeepMind Control Suite task, e.g. ``make_dmc_env("walker-walk")``.""" return DeepMindControlEnv(env_id, seed=seed, size=size, camera=camera)