Source code for embodichain.lab.sim.scene.expansion.contracts

# ----------------------------------------------------------------------------
# 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.
# ----------------------------------------------------------------------------

"""Immutable scene proposals, independent of task and execution lifecycles."""

from __future__ import annotations

from dataclasses import dataclass
import hashlib
import json

__all__ = ["ScenePoseChange", "SceneVariant"]


from .._validation import _identifier, _nonnegative_integer, _rigid_pose


[docs] @dataclass(frozen=True) class ScenePoseChange: """A proposed pose for one existing physical entity. Args: entity_uid: Stable physical entity UID, independent of environment slots. arena_pose: Homogeneous 4x4 transform from the entity frame into its local Z-up arena frame. Inputs are copied into immutable tuples. .. attention:: An arena pose excludes replication offsets. The host converts it to backend coordinates when applying the candidate. This contract does not establish support, collision freedom, or manipulation feasibility. """ entity_uid: str arena_pose: tuple[tuple[float, ...], ...] def __post_init__(self) -> None: _identifier(self.entity_uid, "entity_uid") object.__setattr__(self, "arena_pose", _rigid_pose(self.arena_pose))
[docs] @dataclass(frozen=True) class SceneVariant: """An immutable scene proposal with reproducible proposal identity. Args: parent_scene_id: Content/revision identity of the original scene. seed: Nonnegative proposal-generation seed. ordinal: Nonnegative position in the candidate source. pose_changes: Unique existing entities and their proposed arena poses. metadata: Unique string key/value provenance entries. Entries and pose changes are sorted so equivalent input ordering has one identity. .. attention:: ``variant_id`` identifies a proposal, not a settled initial state or a successful execution. Hosts record those identities and evidence separately. Metadata describes provenance, not executable operations. Since ``ordinal`` participates in the hash, consuming the same layout at a different source position changes ``variant_id``. File-backed consumers should retain the original layout identity and proposal ordinal in metadata for provenance across selection orders; this ID is not a geometry-only deduplication key. """ parent_scene_id: str seed: int ordinal: int pose_changes: tuple[ScenePoseChange, ...] metadata: tuple[tuple[str, str], ...] = () def __post_init__(self) -> None: _identifier(self.parent_scene_id, "parent_scene_id") _nonnegative_integer(self.seed, "seed") _nonnegative_integer(self.ordinal, "ordinal") try: changes = tuple(self.pose_changes) except TypeError as exc: raise ValueError( "pose_changes must contain ScenePoseChange values" ) from exc if any(not isinstance(change, ScenePoseChange) for change in changes): raise ValueError("pose_changes must contain ScenePoseChange values") if len({change.entity_uid for change in changes}) != len(changes): raise ValueError("pose_changes must contain unique entity UIDs") object.__setattr__( self, "pose_changes", tuple(sorted(changes, key=lambda change: change.entity_uid)), ) try: entries = tuple(self.metadata) if any(isinstance(entry, str) for entry in entries): raise ValueError("metadata must contain string key/value pairs") metadata = tuple(tuple(entry) for entry in entries) except TypeError as exc: raise ValueError("metadata must contain string key/value pairs") from exc if any(len(entry) != 2 for entry in metadata): raise ValueError("metadata must contain string key/value pairs") for key, value in metadata: _identifier(key, "metadata key") if not isinstance(value, str): raise ValueError("metadata values must be strings") if len({key for key, _ in metadata}) != len(metadata): raise ValueError("metadata must contain unique keys") object.__setattr__(self, "metadata", tuple(sorted(metadata))) @property def variant_id(self) -> str: """Return a stable identity including geometry, source, seed, and ordinal. Returns: SHA-256 identity independent of Python hash randomization and temporary physical environment assignments. """ payload = { "schema": "scene-variant/v1", "parent_scene_id": self.parent_scene_id, "seed": self.seed, "ordinal": self.ordinal, "pose_changes": tuple( (change.entity_uid, change.arena_pose) for change in self.pose_changes ), "metadata": self.metadata, } encoded = json.dumps( payload, sort_keys=True, separators=(",", ":"), allow_nan=False ).encode("utf-8") return "scene-variant:" + hashlib.sha256(encoded).hexdigest()