Cookbook: Add A Simulator Adapter

Use an installed extension package when a benchmark has a live environment that can be stepped in closed loop. No OpenWAM source edit is required.

Implement The Contract

Implement SimulatorBackend from open_wam.sdk.simulator. The adapter owns environment construction and lifecycle, observation extraction, source-action conversion, stepping, success detection, rendering, and close.

Keep dependency-heavy simulator imports inside the factory so importing the extension hook remains lightweight.

from open_wam.sdk.simulator import (
    SimulatorFactoryContext,
    register_simulator_adapter,
)


def build_adapter(context: SimulatorFactoryContext):
    from acme_sim import Environment

    from .adapter import AcmeAdapter

    environment = Environment(
        asset_root=context.local_paths.get("simulators.acme_root"),
        endpoint=context.options.get("endpoint", "local"),
    )
    return AcmeAdapter(environment)


def register_open_wam() -> None:
    register_simulator_adapter("acme", build_adapter)

The backend should expose reset with EpisodeSpec, return SimulatorObservation, and return ControlTransition from each step. done means terminal; success independently records task success. A timeout is not a success. Both types, plus ControlCommand, are available from the simulator SDK.

SimulatorObservation.state is already in the configured model-ready state encoding, with shape [state_dim] and finite values. Convert native robot state in your backend. Policies with proprio conditioning require a state at every consumed observation; missing rows and wrong-width vectors raise errors. No-proprio policies may omit state. Short startup histories repeat the known initial state along time, never pad missing state features with zeros.

materialize_control(source_action, data_config=...) converts a dataset-source action into ControlCommand(action=..., source_action=...). The first field is passed to step; the second records that executed command in dataset-source coordinates. Apply clipping or discrete gripper conversion before returning the pair. Do not normalize or map dimensions here: pipeline.action_adapter owns those conversions in both directions.

The shared controller reconciles actual images, actions, and proprio before replanning. --action-commit-mode first_frame executes one complete model-frame action group; full_chunk executes the whole prediction. Camera/action timing must agree with the encoder stride. It is an error to supply a partial frame and relabel it as a complete one. The result retains a typed termination with the last transition and its info, distinguishing task success, environment failure and the control limit. JSON summaries report the reason; application-specific info stays on the typed result so arbitrary simulator objects are not implicitly serialized. achieved_action_hz is executed controls divided by live_wall_time_s, including live replanning waits, pacing, stepping and rendering. wall_time_s additionally includes model startup and planner teardown, not environment reset. Unused planner errors and drain duration are separate fields and cannot change task success.

Run It

openwam-sim-rollout \
  --extension acme_open_wam \
  --benchmark acme \
  --sim-option endpoint=localhost:5000 \
  --cfg experiment.yaml \
  --checkpoint checkpoint_step_1000

Use --sim-option KEY=VALUE for application-owned construction values. Put machine paths in configs/local_paths.yaml; the factory receives the resolved mapping through context.local_paths.

Validation

Test the factory and backend against fake environments in ordinary CI. Add one closed-loop smoke that checks reset, action conversion, step, success, render, and close. Real simulator checks belong in self-hosted, scheduled, or label-gated jobs that retain result JSON and videos.

Only built-in integrations maintained by this repository belong under src/open_wam/integrations/. A third-party benchmark should remain an out-of-tree extension.