from __future__ import annotations from dataclasses import dataclass, field from typing import Any, Protocol from dovla_cil.tasks.schema import SceneSpec, TaskSpec Observation = dict[str, Any] @dataclass(frozen=True) class SimState: task_id: str scene_id: str | None seed: int | None symbolic_state: dict[str, Any] metadata: dict[str, Any] = field(default_factory=dict) class ActionChunk: """Semantic simulator action chunk. Examples: - `ActionChunk.single("move_to", object="red_mug")` - `ActionChunk([{"command": "grasp", "object": "red_mug"}])` """ def __init__( self, actions: dict[str, Any] | list[dict[str, Any]] | tuple[dict[str, Any], ...] | None = None, **single_action: Any, ) -> None: if actions is None: if not single_action: raise ValueError("ActionChunk requires at least one action") actions = single_action if isinstance(actions, dict): action_list = [actions] else: action_list = list(actions) if not action_list: raise ValueError("ActionChunk requires at least one action") normalized: list[dict[str, Any]] = [] for action in action_list: if not isinstance(action, dict): raise TypeError("Each semantic action must be a dict") if not _action_command(action): raise ValueError("Each semantic action requires 'command', 'type', 'op', or 'name'") normalized.append(dict(action)) self.actions = tuple(normalized) @classmethod def single(cls, command: str, **params: Any) -> ActionChunk: return cls({"command": command, **params}) def to_dict(self) -> dict[str, Any]: return {"actions": [dict(action) for action in self.actions]} def __iter__(self): return iter(self.actions) def __eq__(self, other: Any) -> bool: return isinstance(other, ActionChunk) and self.actions == other.actions def __repr__(self) -> str: return f"ActionChunk(actions={self.actions!r})" @dataclass(frozen=True) class RolloutResult: observation: Observation reward: float done: bool info: dict[str, Any] = field(default_factory=dict) trajectory: list[dict[str, Any]] = field(default_factory=list) contacts: list[dict[str, Any]] = field(default_factory=list) before_state: dict[str, Any] = field(default_factory=dict) after_state: dict[str, Any] = field(default_factory=dict) @property def next_observation(self) -> Observation: """Compatibility alias for the earlier scaffold.""" return self.observation SimulatorTransition = RolloutResult class SimulatorBackend(Protocol): name: str def seed(self, seed: int) -> None: """Set deterministic simulator seed.""" def reset_task(self, task: TaskSpec, scene: SceneSpec | None = None) -> SimState: """Reset simulator to a task/scene and return the initial symbolic state.""" def serialize_state(self) -> bytes: """Serialize the exact simulator state.""" def restore_state(self, state_blob: bytes) -> None: """Restore an exact serialized simulator state.""" def render_observation(self) -> Observation: """Render or expose the current observation.""" def get_symbolic_state(self) -> dict[str, Any]: """Return a symbolic state dictionary.""" def execute_action_chunk(self, action: ActionChunk) -> RolloutResult: """Execute an action chunk and return rollout metadata.""" def close(self) -> None: """Release resources.""" def _action_command(action: dict[str, Any]) -> str: return str( action.get("command") or action.get("type") or action.get("op") or action.get("name") or "" )