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

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

"""Bounded fixed candidates for scene expansion hosts."""

from __future__ import annotations

from collections.abc import Iterable, Iterator
from itertools import islice

from .cfg import SceneExpansionCfg
from .contracts import ScenePoseChange, SceneVariant, _identifier

__all__ = ["FixedSceneCandidateSource"]


[docs] class FixedSceneCandidateSource: """Snapshot a bounded prefix of authored proposals for repeatable iteration. Args: cfg: Proposal budget and allowed physical entity UIDs. parent_scene_id: Identity of the scene the proposals modify. candidates: Iterable of pose-change iterables. At most ``cfg.max_candidates`` entries are consumed; later entries are deliberately untouched. An empty pose-change iterable describes an explicit nominal proposal. metadata: Immutable string key/value provenance added to each proposal. The source owns no simulator, retries, or execution state. Configuration is revalidated at this boundary and no mutable caller configuration is retained. Every fresh iteration returns the same immutable proposals. """
[docs] def __init__( self, cfg: SceneExpansionCfg, parent_scene_id: str, candidates: Iterable[Iterable[ScenePoseChange]], *, metadata: tuple[tuple[str, str], ...] = (), ) -> None: if not isinstance(cfg, SceneExpansionCfg): raise TypeError("cfg must be a SceneExpansionCfg") cfg.validate() validated = cfg.copy() _identifier(parent_scene_id, "parent_scene_id") # Validate source metadata even if no candidates are available. source_metadata = SceneVariant( parent_scene_id, validated.seed, 0, (), metadata ).metadata permitted = set(validated.movable_entity_ids) variants = [] for ordinal, changes in enumerate(islice(candidates, validated.max_candidates)): variant = SceneVariant( parent_scene_id, validated.seed, ordinal, tuple(changes), source_metadata, ) unsupported = { change.entity_uid for change in variant.pose_changes } - permitted if unsupported: raise ValueError( f"candidate changes entities not declared movable: {sorted(unsupported)}" ) variants.append(variant) self._variants = tuple(variants)
@property def variants(self) -> tuple[SceneVariant, ...]: """Return the bounded, immutable snapshot of candidate proposals. Returns: Candidates in source order, with consecutive zero-based ordinals. """ return self._variants def __iter__(self) -> Iterator[SceneVariant]: """Return a fresh iterator over the candidate snapshot. Returns: Iterator independent of previous iterations or caller mutations. """ return iter(self._variants) def __len__(self) -> int: """Return the number of snapshotted candidates. Returns: Candidate count, bounded by the source configuration. """ return len(self._variants)