Source code for unisim.scene
from __future__ import annotations
from dataclasses import dataclass, field
from pathlib import Path
from typing import TYPE_CHECKING
import numpy as np
from unisim.terrain.generator import TerrainGeneratorCfg
if TYPE_CHECKING:
from unisim.backend.base import SimBackend
[docs]
def resolve_scene_fragment_path(fragment_file: str, model_file: Path) -> Path:
"""Resolve a ``SceneCfg.fragment_files`` entry against the scene model file.
Single resolution rule shared by the MuJoCo and Motrix scene
materializers: absolute paths pass through; relative paths that exist
resolve against the CWD; anything else resolves relative to the model
file's directory.
"""
path = Path(fragment_file)
if path.is_absolute():
return path
if path.is_file():
return path.resolve()
return (model_file.parent / path).resolve()
[docs]
@dataclass
class TerrainSceneCfg:
"""Backend-agnostic terrain slot declaration for a scene."""
generator: TerrainGeneratorCfg | None = None
hfield_name: str = "terrain_hfield"
geom_name: str | None = None
@dataclass
class SceneCfg:
"""Scene source and optional cold-path composition configuration."""
model_file: str
fragment_files: list[str] = field(default_factory=list)
terrain: TerrainSceneCfg | None = None
entities: dict[str, object] = field(default_factory=dict)
"""Logical entity partitions materialized by the base-owned manager facade."""
# Optional render-only model override. When set, offline playback/video
# export renders this XML instead of ``model_file`` while physics keeps
# using ``model_file``. Used to give the renderer a visual twin of the
# scene (e.g. a per-env replicable obstacle) without touching the trained
# collision model. ``None`` => render with ``model_file`` (unchanged).
visual_model_file: str | None = None
default_keyframe_name: str | None = None
"""Optional named keyframe used as the Manager-Based default state."""
[docs]
def resolve_scene_default_qpos(cfg: SceneCfg, backend: SimBackend) -> np.ndarray | None:
"""Resolve one named default-qpos snapshot without changing the qpos0 path."""
keyframe_name = cfg.default_keyframe_name
if keyframe_name is not None and not isinstance(keyframe_name, str):
raise TypeError(
"SceneCfg default_keyframe_name must be a non-empty string or None, "
f"got {type(keyframe_name).__name__}"
)
if keyframe_name == "":
raise ValueError("SceneCfg default_keyframe_name must be a non-empty string or None")
if keyframe_name is None:
return None
capability = f"default keyframe {keyframe_name!r} qpos"
try:
value = backend.get_keyframe_qpos(keyframe_name)
except (AttributeError, NotImplementedError) as exc:
raise NotImplementedError(
f"Manager scene default keyframe {keyframe_name!r} is unavailable on "
f"backend '{backend.backend_type}': {exc}"
) from exc
except (KeyError, ValueError) as exc:
raise ValueError(
f"Manager scene could not resolve default keyframe {keyframe_name!r} on "
f"backend '{backend.backend_type}': {exc}"
) from exc
if not isinstance(value, np.ndarray):
raise TypeError(
f"Manager scene {capability} on backend '{backend.backend_type}' must return "
f"np.ndarray, got {type(value).__name__}"
)
if value.ndim != 1:
raise ValueError(
f"Manager scene {capability} on backend '{backend.backend_type}' returned shape "
f"{value.shape}; expected 1-D"
)
if not np.issubdtype(value.dtype, np.floating):
raise TypeError(
f"Manager scene {capability} on backend '{backend.backend_type}' must be "
f"floating, got {value.dtype}"
)
if not np.isfinite(value).all():
raise ValueError(
f"Manager scene {capability} on backend '{backend.backend_type}' returned NaN or Inf"
)
resolved = np.array(value, copy=True)
resolved.setflags(write=False)
return resolved