Source code for embodichain.lab.sim.scene.operations
# ----------------------------------------------------------------------------
# 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.
# ----------------------------------------------------------------------------
"""Preflighted deterministic arena-local scene changes."""
from __future__ import annotations
from collections.abc import Mapping
from dataclasses import dataclass
from types import MappingProxyType
import torch
from .query import SceneView, _backend_tensor
from ._validation import _rigid_pose
__all__ = ["SceneChangeSet", "SceneChangeResult", "apply_changes"]
[docs]
@dataclass(frozen=True)
class SceneChangeSet:
"""Owned rigid-body pose changes with an explicit movable-entity allowlist.
Args:
poses: UID to proper 4x4 entity-to-arena transforms, in meters and Z-up.
movable_uids: Entities the calling policy permits to move.
"""
poses: Mapping[str, tuple[tuple[float, ...], ...]]
movable_uids: tuple[str, ...]
def __post_init__(self) -> None:
permissions = tuple(self.movable_uids)
if any(
type(uid) is not str or not uid or uid != uid.strip() for uid in permissions
) or len(set(permissions)) != len(permissions):
raise ValueError("movable_uids must contain unique nonempty strings")
owned = {}
for uid, pose in self.poses.items():
if uid not in permissions:
raise ValueError(f"Scene change lacks movable permission for {uid!r}")
owned[uid] = _rigid_pose(pose)
object.__setattr__(self, "poses", MappingProxyType(owned))
object.__setattr__(self, "movable_uids", permissions)
[docs]
@dataclass(frozen=True)
class SceneChangeResult:
"""Changed entities and host-owned caches that need invalidation."""
changed_uids: tuple[str, ...]
refresh_required: tuple[str, ...] = ("geometry", "planning", "observations")
def _preflight_changes(
view: SceneView, changes: SceneChangeSet
) -> dict[str, tuple[object, torch.Tensor]]:
view.check_topology()
if not isinstance(changes, SceneChangeSet):
raise TypeError("changes must be a SceneChangeSet")
entities = {uid: (kind, entity) for kind, uid, entity in view.entities()}
selected = {}
for uid in changes.poses:
pair = entities.get(uid)
if pair is None or pair[0] != "rigid_object":
raise ValueError(f"Pose changes require an existing rigid object: {uid!r}")
entity = pair[1]
if any(
not callable(getattr(entity, method, None))
for method in ("clear_dynamics", "set_local_pose")
):
raise ValueError(f"Entity {uid!r} does not support pose changes")
selected[uid] = (
entity,
_backend_tensor(changes.poses[uid], device=view.sim.device),
)
return selected
[docs]
def apply_changes(view: SceneView, changes: SceneChangeSet) -> SceneChangeResult:
"""Apply allowed poses and clear dynamics, without advancing physics.
Args:
view: Fixed-topology B=1 scene selection.
changes: Fully specified arena-local poses and movable permissions.
Returns:
Changed UIDs and required host cache invalidations; render state is synced.
Raises:
ValueError: For unsupported changes before any entity is modified.
RuntimeError: For failed native writes; earlier writes remain applied.
"""
entities = _preflight_changes(view, changes)
try:
for entity, pose in entities.values():
entity.clear_dynamics(env_ids=list(view.env_ids))
entity.set_local_pose(pose, env_ids=list(view.env_ids))
if entities:
view.sync_render_state()
except Exception as exc:
raise RuntimeError(
"Scene change failed during native writes; earlier writes may remain applied"
) from exc
return SceneChangeResult(tuple(changes.poses))