Skip to content

Commit a396ca6

Browse files
committed
refactor: build replay envs from scene configs
1 parent e83535b commit a396ca6

2 files changed

Lines changed: 144 additions & 17 deletions

File tree

python/rcs/sim_state_replay.py

Lines changed: 83 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
from __future__ import annotations
22

3+
import copy
4+
import importlib
35
import time
46
from dataclasses import dataclass
57
from pathlib import Path
@@ -12,6 +14,7 @@
1214
import typer
1315
from PIL import Image
1416
from rcs.envs.base import ControlMode
17+
from rcs.envs.scenes import SimEnvCreator
1518
from rcs.envs.sim import SimStateObservationWrapper
1619

1720
import rcs # noqa: F401
@@ -20,13 +23,17 @@
2023

2124
DATASET_ARGUMENT = typer.Argument(..., exists=True, file_okay=False, dir_okay=True)
2225

23-
ENV_ID_OPTION = typer.Option(help="Gymnasium env id used for replay.")
26+
SCENE_CONFIG_OPTION = typer.Option(
27+
help="Scene creator class name or import path used to build the replay env, for example EmptyWorldFR3."
28+
)
2429
TRAJECTORY_UUID_OPTION = typer.Option(help="UUID of the recorded trajectory to replay.")
25-
CAMERA_OPTION = typer.Option("--camera", help="Camera names to enable on the replay env.")
26-
RESOLUTION_OPTION = typer.Option(help="Replay camera resolution as WIDTH HEIGHT.")
27-
FRAME_RATE_OPTION = typer.Option(help="Replay camera frame rate.")
28-
RENDER_MODE_OPTION = typer.Option(help="Gym render mode for the replay env.")
29-
CONTROL_MODE_OPTION = typer.Option(help="Control mode name for env creation.")
30+
CAMERA_OPTION = typer.Option("--camera", help="Optional camera names to enable on the replay env.")
31+
RESOLUTION_OPTION = typer.Option(help="Optional replay camera resolution as WIDTH HEIGHT.")
32+
FRAME_RATE_OPTION = typer.Option(help="Optional replay camera frame rate override.")
33+
RENDER_MODE_OPTION = typer.Option(
34+
help="Replay viewer mode. Use 'human' to open the GUI, anything else keeps it headless."
35+
)
36+
CONTROL_MODE_OPTION = typer.Option(help="Optional control mode name override for env creation.")
3037
SLEEP_OPTION = typer.Option(help="Optional delay between restored states.")
3138
OUTPUT_DIR_OPTION = typer.Option(help="Optional directory for re-rendered RGB frames.")
3239
PREFER_DUCKDB_OPTION = typer.Option(help="Use duckdb for parquet loading when it is available.")
@@ -202,30 +209,89 @@ def replay_trajectory(
202209
time.sleep(sleep_s)
203210

204211

212+
def resolve_scene_creator(scene_config: str) -> SimEnvCreator[Any]:
213+
module_name, _, class_name = scene_config.rpartition(".")
214+
if not module_name:
215+
module_name = "rcs.envs.configs"
216+
class_name = scene_config
217+
module = importlib.import_module(module_name)
218+
scene_creator_cls = getattr(module, class_name, None)
219+
if scene_creator_cls is None:
220+
msg = f"Could not find scene creator '{class_name}' in module '{module_name}'."
221+
raise ValueError(msg)
222+
scene_creator = scene_creator_cls()
223+
if not isinstance(scene_creator, SimEnvCreator):
224+
msg = f"{module_name}.{class_name} is not an RCS SimEnvCreator."
225+
raise TypeError(msg)
226+
return scene_creator
227+
228+
229+
def create_replay_env(
230+
scene_config: str,
231+
*,
232+
camera: list[str] | None = None,
233+
resolution: tuple[int, int] | None = None,
234+
frame_rate: int | None = None,
235+
render_mode: str = "human",
236+
control_mode: str | None = None,
237+
) -> gym.Env:
238+
scene_creator = resolve_scene_creator(scene_config)
239+
cfg = scene_creator.config()
240+
241+
if control_mode is not None:
242+
cfg.control_mode = ControlMode[control_mode]
243+
cfg.sim_cfg.async_control = False
244+
cfg.sim_cfg.realtime = False
245+
cfg.headless = render_mode != "human"
246+
247+
if camera is not None:
248+
if cfg.camera_cfgs is None:
249+
msg = f"Scene config '{scene_config}' does not define any cameras to replay."
250+
raise ValueError(msg)
251+
missing_cameras = [name for name in camera if name not in cfg.camera_cfgs]
252+
if missing_cameras:
253+
available_cameras = sorted(cfg.camera_cfgs)
254+
msg = (
255+
f"Unknown replay cameras {missing_cameras} for scene config '{scene_config}'. "
256+
f"Available cameras are {available_cameras}."
257+
)
258+
raise ValueError(msg)
259+
cfg.camera_cfgs = {name: copy.deepcopy(cfg.camera_cfgs[name]) for name in camera}
260+
elif cfg.camera_cfgs is not None:
261+
cfg.camera_cfgs = {name: copy.deepcopy(camera_cfg) for name, camera_cfg in cfg.camera_cfgs.items()}
262+
263+
if cfg.camera_cfgs is not None:
264+
for camera_cfg in cfg.camera_cfgs.values():
265+
if resolution is not None:
266+
camera_cfg.resolution_width, camera_cfg.resolution_height = resolution
267+
if frame_rate is not None:
268+
camera_cfg.frame_rate = frame_rate
269+
270+
return scene_creator.create_env(cfg)
271+
272+
205273
@app.command()
206274
def replay(
207275
dataset: Annotated[Path, DATASET_ARGUMENT],
208-
env_id: Annotated[str, ENV_ID_OPTION] = "rcs/FR3SimplePickUpSim-v0",
276+
scene_config: Annotated[str, SCENE_CONFIG_OPTION] = "EmptyWorldFR3",
209277
trajectory_uuid: Annotated[str | None, TRAJECTORY_UUID_OPTION] = None,
210278
camera: Annotated[list[str] | None, CAMERA_OPTION] = None,
211-
resolution: Annotated[tuple[int, int], RESOLUTION_OPTION] = (256, 256),
212-
frame_rate: Annotated[int, FRAME_RATE_OPTION] = 0,
279+
resolution: Annotated[tuple[int, int] | None, RESOLUTION_OPTION] = None,
280+
frame_rate: Annotated[int | None, FRAME_RATE_OPTION] = None,
213281
render_mode: Annotated[str, RENDER_MODE_OPTION] = "human",
214-
control_mode: Annotated[str, CONTROL_MODE_OPTION] = ControlMode.CARTESIAN_TRPY.name,
282+
control_mode: Annotated[str | None, CONTROL_MODE_OPTION] = None,
215283
sleep_s: Annotated[float, SLEEP_OPTION] = 0.0,
216284
output_dir: Annotated[Path | None, OUTPUT_DIR_OPTION] = None,
217285
prefer_duckdb: Annotated[bool, PREFER_DUCKDB_OPTION] = True,
218286
):
219-
if camera is None:
220-
camera = []
221287
resolved_uuid = resolve_trajectory_uuid(dataset, trajectory_uuid, prefer_duckdb=prefer_duckdb)
222-
env = gym.make(
223-
env_id,
224-
render_mode=render_mode,
225-
control_mode=ControlMode[control_mode],
288+
env = create_replay_env(
289+
scene_config,
290+
camera=camera,
226291
resolution=resolution,
227292
frame_rate=frame_rate,
228-
cam_list=camera,
293+
render_mode=render_mode,
294+
control_mode=control_mode,
229295
)
230296
try:
231297
recorded_steps = load_trajectory(dataset, resolved_uuid, prefer_duckdb=prefer_duckdb)

python/tests/test_sim_state_record_replay.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,25 @@
22

33
from dataclasses import dataclass
44
from pathlib import Path
5+
from types import SimpleNamespace
56

67
import gymnasium as gym
78
import mujoco as mj
89
import numpy as np
910
import pyarrow.dataset as ds
1011
from rcs._core.common import RobotPlatform
1112
from rcs.camera.interface import CameraFrame, DataFrame, Frame, FrameSet
13+
from rcs.envs.base import ControlMode
14+
from rcs.envs.configs import EmptyWorldFR3
1215
from rcs.envs.sim import SimStateObservationWrapper
1316
from rcs.envs.storage_wrapper import StorageWrapper
1417
from rcs.sim.sim import Sim
1518
from rcs.sim_state_replay import (
1619
RecordedSimStep,
20+
create_replay_env,
1721
load_trajectory,
1822
replay_trajectory,
23+
resolve_scene_creator,
1924
restore_sim_step,
2025
)
2126

@@ -124,6 +129,62 @@ def reset(self, *, seed: int | None = None, options: dict | None = None):
124129
return {}, {}
125130

126131

132+
def test_resolve_scene_creator_defaults_to_rcs_env_configs():
133+
assert isinstance(resolve_scene_creator("EmptyWorldFR3"), EmptyWorldFR3)
134+
135+
136+
@dataclass
137+
class DummyCameraConfig:
138+
resolution_width: int
139+
resolution_height: int
140+
frame_rate: int
141+
142+
143+
class DummySceneCreator:
144+
def __init__(self):
145+
self.created_cfg = None
146+
147+
def config(self):
148+
return SimpleNamespace(
149+
control_mode=ControlMode.CARTESIAN_TQuat,
150+
sim_cfg=SimpleNamespace(async_control=True, realtime=True),
151+
headless=False,
152+
camera_cfgs={
153+
"bird_eye": DummyCameraConfig(1280, 720, 30),
154+
"wrist": DummyCameraConfig(640, 480, 15),
155+
},
156+
)
157+
158+
def create_env(self, cfg):
159+
self.created_cfg = cfg
160+
return "dummy-env"
161+
162+
163+
def test_create_replay_env_uses_scene_config_and_overrides_camera_settings(monkeypatch):
164+
scene_creator = DummySceneCreator()
165+
monkeypatch.setattr("rcs.sim_state_replay.resolve_scene_creator", lambda _scene_config: scene_creator)
166+
167+
env = create_replay_env(
168+
"EmptyWorldFR3",
169+
camera=["wrist"],
170+
resolution=(320, 240),
171+
frame_rate=5,
172+
render_mode="rgb_array",
173+
control_mode=ControlMode.CARTESIAN_TRPY.name,
174+
)
175+
176+
assert env == "dummy-env"
177+
assert scene_creator.created_cfg is not None
178+
assert scene_creator.created_cfg.control_mode == ControlMode.CARTESIAN_TRPY
179+
assert scene_creator.created_cfg.sim_cfg.async_control is False
180+
assert scene_creator.created_cfg.sim_cfg.realtime is False
181+
assert scene_creator.created_cfg.headless is True
182+
assert sorted(scene_creator.created_cfg.camera_cfgs) == ["wrist"]
183+
assert scene_creator.created_cfg.camera_cfgs["wrist"].resolution_width == 320
184+
assert scene_creator.created_cfg.camera_cfgs["wrist"].resolution_height == 240
185+
assert scene_creator.created_cfg.camera_cfgs["wrist"].frame_rate == 5
186+
187+
127188
def test_replay_trajectory_syncs_gui_without_stepping():
128189
spy_sim = SpySim()
129190
env = SpyReplayEnv(spy_sim)

0 commit comments

Comments
 (0)