API reference#
Hand-curated automodule listing for so101_mvbench. Heavy dependencies
(Isaac Sim, Isaac Lab, torch, LeRobot) are mocked at documentation build time,
so signatures render without a simulator installed.
Note
Every module of the package is listed below except these, and the omissions
are deliberate rather than an oversight. tests/unit/test_docs_api_coverage.py
holds the same list and fails if a new module appears in neither place.
recording.teleop_recorder,recording.multicam_replay,evaluation.async_eval,audit.determinism,audit.cube_replayandtools.drift_configsare command modules. The first five boot Isaac Sim at import, and all six are documented as commands in Command overview.tasks.lift_cube_env_cfgis the Isaac-bound scene configuration, not an importable API.assets.so101_whitedeclares one articulation-config constant and has no module docstring, soautomodulewould render an empty section. Read the file itself, which carries its provenance in a comment header.
Recording (stage 1)#
RecordingSession — Context Manager for LeRobotDataset lifecycle.
Wraps LeRobotDataset + VideoEncodingManager to guarantee finalize()
runs on exit (Ctrl+C, exception, or normal completion). Based on
the pattern from lerobot/scripts/lerobot_record.py.
Usage:
with RecordingSession(repo_id, root, fps, features) as session:
session.add_frame(frame_dict)
session.save_episode()
# finalize() called automatically — parquet footer written
The atexit hook provides an additional safety net in case __exit__
is bypassed (e.g. os._exit or SIGKILL).
- class so101_mvbench.recording.recording_session.RecordingSession(repo_id, root, fps, features, task_name='')[source]#
Bases:
objectContext Manager for LeRobotDataset with streaming_encoding.
Handles dataset creation/resume, VideoEncodingManager lifecycle, and guaranteed finalize() on exit.
- Parameters:
EpisodeController — State Machine for gamepad recording episodes.
Manages S/R key transitions, BinSpawner advance/retry logic, BinMap
persistence, and scene state capture. Adapted from the TeleopFSM in
leisaac_experiments/teleop/fsm.py.
States:
IDLE — waiting for S (start recording)
RECORDING — S pressed, frames being collected via add_frame()
Transitions:
IDLE → S → RECORDING
RECORDING → S → save + IDLE
RECORDING → R → discard + reset + IDLE
IDLE → R → advance/retry + reset + IDLE
- class so101_mvbench.recording.episode_controller.EpisodeController(session, env, spawner=None, bin_map=None, binmap_path=None, scene_state_fn=None, apply_spawn_fn=None, action_gen=None, repo_root='', seed=42, object_key='cube', start_episode=0)[source]#
Bases:
objectS/R Key State Machine with BinSpawner + BinMap integration.
The controller owns the episode lifecycle logic that was previously scattered across boolean flags in policy_recorder.py. The main loop only needs to call
process_keys()andadd_frame()each tick.- Parameters:
session – RecordingSession instance for save/discard/add_frame.
env – Isaac Lab environment (for env.reset + cube teleport).
spawner – BinSpawner instance, or None if bin spawning is off.
bin_map – BinMap instance, or None.
binmap_path – Path to binmap.json, or None.
scene_state_fn – Callable(env, episode, repo_root, seed, object_key) for saving scene state after reset.
apply_spawn_fn – Callable(env, spawner) for cube teleport after reset.
action_gen – Action generator (has .reset() method).
repo_root (
str) – Dataset root path (for scene_state save).seed (
int) – Random seed.object_key (
str) – Scene object key (e.g. “cube”).start_episode (int)
- __init__(session, env, spawner=None, bin_map=None, binmap_path=None, scene_state_fn=None, apply_spawn_fn=None, action_gen=None, repo_root='', seed=42, object_key='cube', start_episode=0)[source]#
- on_auto_episode_end()[source]#
Handle auto-record episode completion (episode_length_steps reached).
State stays
RECORDINGso the next auto-episode starts capturing frames immediately — this is the auto_record contract.- Return type:
- class so101_mvbench.recording.episode_controller.EpisodeState(*values)[source]#
Bases:
IntEnumRecording state machine states.
Gamepad-driven action generator for human teleoperation.
Wraps gamepad_utils.JointGamepad in absolute mode to produce action dicts
compatible with LeRobotSO101Interface.real_to_sim_obs_processor().
Same interface as SineWaveActionGenerator: call get_action() each frame
to get a dict of joint positions in LeRobot raw units.
The neutral (start) position is computed at runtime from the simulation’s
init_state using get_raw_actions_from_radians(), ensuring perfect alignment
with whatever USD model is loaded.
- class so101_mvbench.recording.gamepad_action_generator.GamepadActionGenerator(coord_iface, initial_obs_rad, config_path=None)[source]#
Bases:
objectGamepad action generator that outputs LeRobot raw-unit joint positions.
- Parameters:
coord_iface –
LeRobotSO101Interfaceinstance (provides coordinate transforms and joint range info).initial_obs_rad (
Tensor) – Initial joint positions fromenv.reset()observation (policy obs tensor, in radians, shape[6]).config_path (
str|Path|None) – Path to gamepad JSON config. If None, uses the defaultso101_lerobot_teleop.jsonshipped withgamepad_utils.
HomeMacro — leisaac-style PD-hold Auto-Pilot to default_joint_pos.
Y-button triggers a smooth 1.5s return to the neutral pose while preserving the current gripper state. Produces BOTH the sim-space action (zero-offset tensor for env.step) and the raw-unit action for parquet logging, so observation.state and action remain consistent during the macro.
Defensive Level B:
Shape sanity checks in __init__ and trigger()
NaN guards on every physics read (current_pos, default_pos, final_pos)
try/except around coord_iface calls (graceful error + abort)
the exception handler in step() sets _steps_remaining=0 on failure
Logs success only below 8.6 deg and warns only above 17 deg; in between it stays quiet, so a normal run does not bury the log in noise
The init log prints every configuration parameter
Sync strategy:
Normal completion (N ticks expired): action_gen.reset() → gamepad to neutral
Cancel mid-glide (S/R key): _sync_gamepad_to_current_pos() → gamepad to actual sim state, preventing snap-back on next advance() call
- class so101_mvbench.recording.home_macro.HomeMacro(env, action_gen, coord_iface, action_shape, device, hold_steps=45)[source]#
Bases:
objectY-button PD-hold auto-pilot to default_joint_pos.
- Parameters:
env – Isaac Lab environment (for scene[“robot”].data access).
action_gen – GamepadActionGenerator instance (exposes _gamepad + reset).
coord_iface – LeRobotSO101Interface (provides get_raw_actions_from_radians and .device).
action_shape (tuple[int, …]) – Shape of the action tensor — must be (num_envs, n_joints).
device (str | torch.device) – Target device for sim-space action tensor (typically env.device).
hold_steps (int) – Number of env.step() ticks to hold the macro (default 45).
- Raises:
ValueError – If hold_steps <= 0 or action_shape is malformed.
AttributeError – If coord_iface lacks get_raw_actions_from_radians.
Recording manifest — documents provenance of recorded datasets.
Writes a JSON manifest alongside each recording that captures: - Input files (trajectories, scene states) - Recording settings (seed, determinism, neutral scene) - Script version - Timestamps
This enables traceability: given any _ego_base/ or _external_*/ directory, the manifest tells you exactly how it was produced.
- so101_mvbench.recording.recording_manifest.write_manifest(output_dir, *, script_name, script_version, trajectory_dir, episode_indices, scene_state_path, seed, camera_pose_ids=None, camera_radius_m=None, num_envs=1, pose_subdirs=None, extra=None)[source]#
Write a recording_manifest.json to the output directory.
Scans the output directory for generated files (videos, parquet) and includes them in the manifest for full traceability.
- Parameters:
output_dir (
Path) – Directory where the manifest is saved (e.g. _ego_base/).script_name (
str) – Name of the recording script.script_version (
str) – Script VERSION string.trajectory_dir (
str) – Path to the trajectory npy directory.episode_indices (
list[int] |None) – List of episode indices replayed, or None for all.scene_state_path (
str|None) – Path to scene_state.json, or None.seed (
int) – Random seed used.camera_pose_ids (
list[str] |None) – List of camera pose IDs (external only).num_envs (
int) – Number of parallel environments.pose_subdirs (
list[str] |None) – Optional list of per-pose subdir names to restrict the file scan (e.g.["_external_az000_el45", ...]). Required for batch recorders writing the manifest one level above the per-pose dataset dirs — without it the scan picks up stale archival data co-located under output_dir.extra (
dict|None) – Additional key-value pairs to include.camera_radius_m (float | None)
- Return type:
- Returns:
Path to the written manifest file.
Assembly (stage 2)#
Extract per-episode action trajectories from a LeRobot v3.0 dataset.
Reads parquet files and saves each episode’s action sequence as a
standalone .npy file. This decouples the trajectory replay from the
full LeRobot import chain (torch, lerobot, etc.).
- Usage:
- python trajectory_extractor.py
–dataset_root datasets/so101_gamepad_teleop –output_dir datasets/trajectories/so101_gamepad_teleop
- so101_mvbench.recording.trajectory_extractor.extract_actions_by_episode(parquet_path)[source]#
Read a chunked parquet file and split actions by
episode_index.LeRobot v3 concatenates multiple episodes into one parquet file; the
episode_indexcolumn identifies which rows belong to which episode.
- so101_mvbench.recording.trajectory_extractor.extract_actions_from_parquet(parquet_path)[source]#
Read a single parquet file and return the action column as ndarray.
- so101_mvbench.recording.trajectory_extractor.extract_dataset(dataset_root, output_dir)[source]#
Extract all episodes from a LeRobot v3.0 dataset to .npy files.
- Parameters:
- Return type:
- Returns:
Metadata dict written to
trajectories_meta.json.- Raises:
FileNotFoundError – If dataset_root or info.json does not exist.
ValueError – If no parquet files are found.
Merge multiple single-bin LeRobot datasets into one multi-bin training dataset.
- Usage:
- python -m so101_mvbench.assembly.bin_fusion
–datasets_root /path/to/datasets –target /path/to/merged_pilot –bins bin_c1_r1 bin_c2_r1
- class so101_mvbench.assembly.bin_fusion.BinSource(bin_id, path, episode_count=0, frame_count=0, global_episode_offset=0)[source]#
Bases:
object- Parameters:
- class so101_mvbench.assembly.bin_fusion.FusionConfig(target, datasets_root, bins=<factory>, exclude=<factory>, exclude_where=<factory>, strict=False, dry_run=False)[source]#
Bases:
object- Parameters:
- so101_mvbench.assembly.bin_fusion.apply_episode_filters(episodes, exclude_where)[source]#
Return episode indices that survive all exclude-where filters.
- so101_mvbench.assembly.bin_fusion.discover_bins(datasets_root)[source]#
Find all bin_c*_r* directories under datasets_root.
- so101_mvbench.assembly.bin_fusion.fuse_bins(config)[source]#
Main fusion logic.
- Return type:
- Parameters:
config (FusionConfig)
- so101_mvbench.assembly.bin_fusion.load_binmap_episodes(binmap_path)[source]#
Load binmap.json and return only success=true entries.
- so101_mvbench.assembly.bin_fusion.matches_filter(episode, key, value)[source]#
Check if an episode’s property matches a filter value.
- so101_mvbench.assembly.bin_fusion.parse_exclude_where(values)[source]#
Parse ‘key=value’ strings into (key, value) tuples.
- so101_mvbench.assembly.bin_fusion.resolve_selection(available, explicit_bins, exclude)[source]#
Apply bin-level selection and exclusion.
- so101_mvbench.assembly.bin_fusion.validate_post_merge(target, expected_episodes, sources)[source]#
Run post-merge checks including video content verification.
- so101_mvbench.assembly.bin_fusion.validate_pre_merge(sources, strict)[source]#
Run pre-merge checks. Returns list of warnings.
- so101_mvbench.assembly.bin_fusion.validate_video_content(target, sources)[source]#
Verify merged videos by comparing mid-episode frames against source bins.
LeRobot v3.0 concatenates all episode videos into one MP4 per camera-key per chunk. Episode boundaries are resolved via timestamp indices in the
episodes/*.parquetmetadata. Ref: https://huggingface.co/docs/lerobot/en/using_dataset_toolsFor each source bin, picks 3 sample episodes (first, middle, last) and compares the mid-frame of each episode in the merged video against the same frame in the source video. This catches wrong ordering, corrupted concatenation, or shifted frame indices.
Training (stage 3)#
ACT trainer wrapper for camera-permutation experiments.
Expands –cameras wrist front into the LeRobot –policy.input_features JSON, then invokes lerobot.scripts.lerobot_train with the rest of the args.
Reduces a typical Phase-1 baseline command from ~10 lines (multi-line bash with embedded JSON) to a single line:
- train_act_camera_perm
–cameras wrist front –dataset bin_c1_r1__wrist_front_sideLR_top –steps 30000 –batch_size 8 –output_dir outputs/2cam_wrist_front
Cameras are looked up from _CAMERA_SHAPES (default: 480×640 RGB for all five workspace cameras: wrist, front, side_l, side_r, top).
Output dir contains LeRobot’s standard checkpoints/, train.log, etc.
- so101_mvbench.training.train_act_camera_perm.build_input_features(cameras)[source]#
Build the LeRobot policy.input_features dict from a list of cameras.
- Parameters:
cameras (
list[str]) – list of camera names (e.g. [“wrist”, “front”]). Each must exist as a key in _CAMERA_SHAPES.- Return type:
- Returns:
A dict like:
{ "observation.images.wrist": {"type": "VISUAL", "shape": [3,480,640]}, "observation.images.front": {"type": "VISUAL", "shape": [3,480,640]}, "observation.state": {"type": "STATE", "shape": [6]}, }
eval_checkpoint_selector.py — Cherry-Shen-Style Best-Checkpoint-Finder.
Evaluates all checkpoints/NNNNNN/pretrained_model/ directories of a training run against a holdout episode set (eval_loss on the holdout subset) and writes the best step + per-step results to <run_dir>/best_checkpoint.json (plus eval_vs_train_loss.png for visual inspection).
Console-script entry: eval_checkpoint_selector (after pip install -e .).
- Usage:
- eval_checkpoint_selector –run_dir outputs/2cam_wrist_front
–dataset_root datasets/liftcube_binned/bin_c1_r1__wrist_front_sideLR_top
- Reads:
<run_dir>/checkpoints/NNNNNN/pretrained_model/ — all checkpoints <run_dir>/holdout_episodes.json — held-out episode indices <run_dir>/train.log (optional) — for train-loss overlay
- Writes:
<run_dir>/best_checkpoint.json — {best_step, best_eval_loss, all_results} <run_dir>/eval_vs_train_loss.png — overlay plot (skipped with –no-plot)
Design decisions (verified against Cherry Shen’s so101_bench/scripts/train/train.py and LeRobot 0.5.1 source, 2026-04-19):
policy.train() + deterministic seed: Cherry’s original uses eval(), but LeRobot 0.5.1 modeling_act.py:149 has a bug — the KLD-loss block lacks an and self.training guard. In eval mode mu_hat/log_sigma_x2_hat are None → TypeError in line 155. We use policy.train() (VAE encoder active) combined with torch.manual_seed(seed) before each checkpoint eval to make the VAE reparameterization-sampling deterministic. torch.no_grad() still prevents weight updates.
Preprocessor applied explicitly: LeRobot 0.5.1 moved normalization out of the policy’s forward() into a separate NormalizerProcessorStep (loaded via make_pre_post_processors). Without this, loss values are computed on unnormalized inputs — orders of magnitude larger than train-time loss, and not comparable. See lerobot_train.py:414 batch = preprocessor(batch).
Empty holdout guard: Cherry’s train.py:472 has if eval_dataset is not None. We mirror that: empty holdout list raises ValueError rather than silently producing a zero-frame dataset. LeRobot 0.5.1 LeRobotDataset(episodes=[]) loads ZERO frames (pa_ds.field.isin([]) = empty filter), not “all” — subsequent ds[0] access would raise IndexError. The guard catches the logical error early with an actionable message.
- class so101_mvbench.training.checkpoint_selector.RunLayout(layout, holdout_json, ckpt_dir, best_json_out, plot_png_out, train_log)[source]#
Bases:
objectResolved file paths the selector operates on.
For design03: holdout/ckpt/train_log live in the TRAINING capsule; best_json/plot live in the SELECTION capsule (=write target). For legacy/in-place mode: read AND write paths share a common run_dir.
- Parameters:
- so101_mvbench.training.checkpoint_selector.create_selection_capsule(training_run, dataset_root, selector_args)[source]#
Create a fresh selection capsule under selections/<job>__<train_ts>/<sel_ts>/.
Writes input sidecars (training_run symlink, holdout copy, dataset symlink, selector_args.json) + README.md + processing/ skeleton, then returns the capsule path along with a RunLayout pointing reads at the training capsule and writes at this new selection capsule.
- Return type:
- Parameters:
- Reads from training capsule:
input/holdout_episodes.json (copied verbatim — selection capsule must be self-contained per schema)
output/checkpoints/ (read-only, never moved)
processing/train.log (auto-discovered for plot overlay)
- so101_mvbench.training.checkpoint_selector.detect_run_layout(run_dir)[source]#
Detect whether run_dir is a Capsule (input/proc/output) or Legacy run.
- Capsule (run_train.sh v3.0.0+, in-place selector mode):
run_dir/ ├── input/holdout_episodes.json ├── processing/train.log └── output/{checkpoints/, best_checkpoint.json, eval_vs_train_loss.png}
- Legacy (run_train.sh v2.0.0):
run_dir/ ├── holdout_episodes.json ├── checkpoints/{NNNNNN/, last/pretrained_model/train.log} ├── best_checkpoint.json └── eval_vs_train_loss.png
Decision rule: if run_dir/input/ AND run_dir/output/ both exist, it’s a Capsule. Otherwise it’s Legacy. Mismatch (e.g. only input/ exists) is an error.
- so101_mvbench.training.checkpoint_selector.eval_policy_on_dataset(policy, preprocessor, dataset, device, batch_size=32)[source]#
Compute mean forward-loss on a dataset without gradient computation.
policy.train() keeps VAE encoder active (LeRobot 0.5.1 bug: eval-mode forward() crashes on KLD-block with None mu_hat, see modeling_act.py:149)
preprocessor(batch) applied to normalize inputs (LeRobot 0.5.1 API)
torch.no_grad() prevents weight updates
Seed is set BEFORE this function is called, to make VAE sampling deterministic
- so101_mvbench.training.checkpoint_selector.evaluate_all_checkpoints(layout, dataset_root, batch_size, device, seed=42)[source]#
Evaluate every checkpoint in layout.ckpt_dir against the holdout set.
- so101_mvbench.training.checkpoint_selector.parse_train_loss_from_log(log_path)[source]#
Parse LeRobot training log for per-K-step average train-loss.
LeRobot logs lines like:
INFO 2026-04-22 14:21:49 ot_train.py:439 step:30K smpl:241K ep:490 epch:20.41 loss:0.052 ...
The step is K-rounded (floor division by 1000). Each K-bucket contains ~log_freq log entries (e.g. 10 entries per K at log_freq=100). We aggregate loss per bucket via mean so the returned list has one (step, mean_loss) tuple per 1000-step window.
Returns tuples of (step_absolute=K*1000, mean_loss), sorted by step. Unreadable / non-matching log is tolerated — returns [] in that case.
Evaluation (stages 4 and 5)#
Task progress tracker for LiftCube evaluation.
Tracks 4 monotonic progress stages, grasp attempts, and early termination conditions. Designed to be testable without Isaac Sim — accepts raw floats, no torch/Isaac Lab dependency.
- Stages (v2.0.0 — post-HomeMacro recording era):
0_miss — Gripper never reached the cube (score=0.0) 1_reach — Gripper within REACH_DIST of cube (score=0.33) 2_lift — Cube peak z > initial_z + LIFT_DELTA_REL (score=0.66); “cube has left the table”, very permissive, peak-based. 3_home — Arm has reached the home pose AND cube still held, sustained for HOME_HOLD_STEPS (score=1.0, final success).
- Breaking-change vs v1.0.0:
Stage “2_pick” merged into “2_lift” (same semantic: cube off the table).
LIFT_HEIGHT (absolute 12cm) removed — replaced by relative LIFT_DELTA_REL. Motivation: after training with the Y-button Home-Macro, the cube ends up at an arbitrary z-height in the home pose, not at a fixed 12cm.
HOLD_STEPS/SMOOTH_WINDOW (2s sustained at 12cm) replaced by composite home-reached criterion: arm_at_home ∧ cube_not_dropped, sustained HOME_HOLD_STEPS. The cube MUST still be in the gripper when the arm reaches home — otherwise it is a failed task, not a success.
- Early Termination:
ET-1 no_approach — dist_3d not reduced by 50% after 10s (stage<1 guard) ET-2 cube_lost — cube_z dropped below table level (ET_CUBE_LOST_DELTA)
References
Cherry Shen (so101_bench): 5 progress stages for pick-and-place.
DEXTRAH (NVlabs): 2s sustained success timeout.
home_macro._HOME_REACHED_THRESHOLD_RAD: source of truth for REACH_HOME_RAD (8.6°), kept in sync manually.
- class so101_mvbench.evaluation.eval_tracker.LiftCubeEvalTracker(initial_cube_z, initial_dist_3d, home_pose_rad, home_pose_tol_rad=None, plateau_steps=None)[source]#
Bases:
objectPer-episode eval tracker. Call update() once per sim step.
- Parameters:
- __init__(initial_cube_z, initial_dist_3d, home_pose_rad, home_pose_tol_rad=None, plateau_steps=None)[source]#
- Parameters:
initial_cube_z (
float) – Cube z-coordinate at episode start (m).initial_dist_3d (
float) – Initial 3D gripper-to-cube distance (m).home_pose_rad (
list[float]) – Target home pose for arm joints 0..4 (rad). Gripper (joint 5) is not checked — Y-macro holds it.home_pose_tol_rad (
float|None) – Per-joint max error for arm_at_home detection (rad). If None, falls back to REACH_HOME_RAD (0.15 = 8.6°). Authoritative source: evaluation/config/eval.json target_end_position_tolerance_deg.plateau_steps (
dict[str,int] |None) – Per-stage max-stage-stuck ET threshold dict (e.g. {“1_reach”: 600, “2_lift”: 600}). Falls back to ET_PLATEAU_STEPS_DEFAULT if None. Authoritative source: evaluation/config/eval.json plateau_steps.
- property should_terminate: bool#
True when the episode should stop — either early failure or success.
Early-termination on success is a speed optimization: once Stage 3 (arm_at_home ∧ cube_held, 0.5s sustained) is reached, the remaining frames carry no signal. Stopping saves ~60-70% of eval wall-time for successful episodes. Fail-episodes still run to EPISODE_LENGTH_S to give ET-1 (no_approach @ step 600) its full detection window.
- update(cube_z, dist_3d, step, arm_joint_pos=None)[source]#
Feed one sim step.
- Parameters:
cube_z (
float) – Current cube z-coordinate (m).dist_3d (
float) – Current 3D gripper-to-cube distance (m).step (
int) – Sim step index (monotonic).arm_joint_pos (
list[float] |None) – Current joint positions in rad (>=5-dim). Required for Stage-3 home-reached detection. If None, Stage 3 cannot be reached.
- Return type:
Loader for the shipped evaluation config (evaluation/config/eval.json).
Lives in its own module, free of any Isaac import, so it can be unit-tested and
reused. It used to be a private function inside async_eval, which launches
Isaac Sim at import time – the reason its old test had to be parked.
The JSON stores degrees because humans wrote it; consumers work in radians, so the conversion happens here, once.
- so101_mvbench.evaluation.eval_config.load_eval_config(path=None)[source]#
Read the eval config and convert its angles to radians.
- Parameters:
path (
Path|None) – Override for tests. Defaults to the shipped package resource.- Return type:
- Returns:
Dict with
target_end_position_rad(list, one entry per joint),target_end_position_tolerance_radandplateau_steps.- Raises:
FileNotFoundError – If the config file does not exist.
KeyError – If a required key is missing.
SceneStateLoader — apply per-episode scene_state JSONs to Isaac Sim (single-env).
Rewrite of design00 scene_loader.py without the except Exception: return {}
silent-continue anti-pattern. All failures raise with clear messages.
Responsibility#
Load
episode_NNNNNN_scene_state.jsonfiles from a meta-dir into memoryFor each episode: write USD attributes into Isaac Sim (lights, yaws, colors, cube)
Not our job#
Validate JSON schema / ranges / NaN →
startup.pydid that alreadyOwn the list of keys →
shared.scene_state.SceneStateis SOTMulti-env broadcasting → eval is single-env,
prims[0]is the only prim
Version 1.1.0
- exception so101_mvbench.evaluation.scene_loader.SceneApplyError[source]#
Bases:
RuntimeErrorRaised when applying scene_state to Isaac Sim fails. NOT silent.
- class so101_mvbench.evaluation.scene_loader.SceneStateLoader(scene_state_dir, binmap_path, device)[source]#
Bases:
objectIterator over scene_state.json files + Isaac Sim USD applicator.
- Parameters:
scene_state_dir (Path)
binmap_path (Path)
device (str)
- apply(env, episode_idx, *, cube_noise_m=0.0, cube_rot_noise_deg=0.0, visual_override=None)[source]#
Apply scene_state for
episode_idxto Isaac Sim.- Parameters:
visual_override (
VisualConfig|None) – If None (IID), visual config is built from scene_state.json (reproduces training scene exactly). If a VisualConfig is passed (OOD), it overrides all visual properties for controlled variation testing.episode_idx (int)
cube_noise_m (float)
cube_rot_noise_deg (float)
- Return type:
Returns dict with
"scene_state"(the loaded JSON) and"placed_cube"(actually placed coordinates from scene_builder).
- state_for_episode(episode_idx)[source]#
Return scene_state dict for episode index (0-based).
Wraps with modulo when episode_idx >= len(states), so OOD grids with more episodes than scene_states reuse visual properties cyclically.
SimLeRobotSO101Interface — Sim-only subclass of upstream LeRobotSO101Interface.
Policy inference for Isaac-Sim eval must use the EXACT SAME chain as on-robot inference to keep Sim/Hardware parity. Upstream already provides that chain in LeRobotSO101Interface at third_party/lerobot_so101_teleop/…/lerobot_interface.py.
Upstream’s init_device() calls make_robot_from_config() which instantiates a SO101FollowerConfig. That drags in scservo_sdk (Feetech Motor SDK) — NOT a dependency of this package. So we override ONLY init_device() to skip the hardware-connection and build a _RobotStub that provides the four downstream attributes make_policy() and predict_action() need:
self.robot.observation_features (used in make_policy) self.robot.action_features (used in make_policy) self.robot.name (used for DummyDatasetMeta) self.robot.robot_type (used in predict_action per-step)
All OTHER methods (make_policy, sim_obs_to_policy_processor, predict_action, prediction_to_sim_processor) are inherited UNCHANGED from upstream.
Rename-map env → policy is auto-detected from policy’s config.json input_features. Supports three layouts:
wrist + N external cameras (e.g. wrist+front, wrist+front+top)
N external cameras without wrist (e.g. front+top, side_l+side_r)
wrist-only (1-cam)
Convention: env ‘ego’ → policy ‘wrist’; env ‘external’, ‘external_2’, … → non-wrist cameras sorted alphabetically. Falls back to legacy POLICY_RENAME_MAPS only if config.json read fails.
Version 1.2.0 (multi-external + no-wrist rename_map auto-detect)
- class so101_mvbench.evaluation.policy_wrapper.SimLeRobotSO101Interface(device, port, id, cameras, fps, kind='leader', rename_map=None)[source]#
Bases:
LeRobotSO101InterfaceSim-only subclass — skips Feetech hardware init.
Inherits make_policy / predict_action / sim_obs_to_policy_processor / prediction_to_sim_processor from upstream UNCHANGED. Only init_device is overridden to bypass scservo_sdk.
- init_device(visualize=False)[source]#
Build a _RobotStub with just the 4 attributes downstream methods need.
No self.robot = make_robot_from_config(self.cfg). No scservo_sdk. No rerun init (we run headless or in the eval GUI viewport separately).
Feature-dict format MUST match the real SO101Follower (see
lerobot/robots/so_follower/so_follower.py:66-81):motor features: dict[str, type] -> {"joint_name.pos": float, ...} camera features: dict[str, tuple] -> {"cam_name": (H, W, 3), ...}
- Parameters:
visualize (bool)
- make_policy(name_or_path, *, temporal_ensemble_coeff=None, force_disable_te=False)[source]#
Override upstream make_policy to accept temporal_ensemble_coeff override.
Body mirrors upstream LeRobotSO101Interface.make_policy 1:1 (same calls, same order) with ONE addition: if
temporal_ensemble_coeffis set (orforce_disable_te=True), the value is written ontopolicy_configBEFORElerobot.policies.factory.make_policybuilds the model. This is necessary because ACT wires up itsTemporalEnsemblerinside__init__— post-hoc config edits do NOT take effect (the ensembler dataclass is bound at init-time).- TE-semantics:
force_disable_te=True→ policy_config.temporal_ensemble_coeff = Nonetemporal_ensemble_coeff=X→ policy_config.temporal_ensemble_coeff = Xneither → keep checkpoint’s config default
- so101_mvbench.evaluation.policy_wrapper.rename_map_for_policy(policy_path)[source]#
Resolve env→policy camera rename-map for the given policy checkpoint.
- Strategy:
Auto-detect from policy config.json input_features (NVIDIA-style layout)
Fallback: lookup hardcoded POLICY_RENAME_MAPS by JOB_NAME (legacy capsule layout: …/train/<JOB_NAME>/<ts>/checkpoints/<step>/pretrained_model)
Multi-env eval helpers — per-env obs slicing + per-env tracker init.
Role in the data flow:
Data flow 3 (per-env inference):
slice_visual_obs()fixes Risk K-1 — the upstreamsim_obs_to_policy_processorinternally indexesvisual[...][0], i.e. without slicing ALL envs would see env-0’s image.Data flow 5 (per-env tracking):
init_trackers()instantiates the existingLiftCubeEvalTrackerN times (ADR-003 — no new vectorized tracker), each seeded from its own initial pose.
GPU-free: no Isaac Lab imports; unit-testable with CPU tensors / mocks
(tests/test_multienv_obs_utils.py).
- so101_mvbench.evaluation.multienv_obs_utils.init_trackers(n_envs, cube, robot, jaw_body_idx, home_pose_rad, *, home_pose_tol_rad=None, plateau_steps=None)[source]#
Build one
LiftCubeEvalTrackerper environment (ADR-003).Mirrors the single-env construction in
eval_session.setup_episodebut indexes each Isaac tensor per environment[i]instead of[0].- Parameters:
n_envs (
int) – Number of parallel environments.cube (
Any) – Isaac RigidObject (cube.data.root_link_pos_wshape(N, 3+)).robot (
Any) – Isaac Articulation (robot.data.body_link_pos_wshape(N, B, 3)).jaw_body_idx (
int) – Body index of the gripper jaw.home_pose_rad (
list[float]) – Target home pose (>=5 arm joints).home_pose_tol_rad (
float|None) – Per-joint home tolerance (None -> tracker default).plateau_steps (
dict[str,int] |None) – Per-stage plateau early-termination budgets.
- Return type:
- Returns:
List of
n_envsindependent trackers, each seeded from its env’s initial cube height + gripper->cube distance.
- so101_mvbench.evaluation.multienv_obs_utils.slice_visual_obs(visual_obs, env_idx)[source]#
Per-env view of the visual obs dict (Risk-K-1 fix).
obs["visual"]maps camera-key -> tensor of shape(num_envs, H, W, C). The upstreamsim_obs_to_policy_processorinternally grabsvisual[...][0]; to feed environmentenv_idxwe slice each camera tensor to[env_idx:env_idx+1](the length-1 batch dim is preserved) — the downstream[0]then selects exactlyenv_idx’s image instead of always env 0.
Image augmentations at the sim->policy boundary (camera-importance + general).
augment_image_frame() is the single dispatch point where the TiledCamera frame
handed from Isaac Sim to the ACT policy can be transformed by arbitrary image-
processing algorithms. Pure numpy/OpenCV on ONE uint8 HWC RGB frame -> a NEW frame
of the same shape; importable WITHOUT an Isaac Sim boot (CPU-testable). A new algo
= one function + one _AUGMENTATIONS entry; no eval-loop change.
- Families:
degradations (camera-importance study, strength-graded): blackout, gauss, blur transforms (general image processing): invert, contrast, canny … vieles mehr — just register another function.
- so101_mvbench.evaluation.augmentation.augment_image_frame(img, kind, strength=0.0, rng=None)[source]#
Apply ONE image augmentation at the sim->policy boundary (registry dispatch).
- Parameters:
img (np.ndarray) – uint8 HWC RGB frame
(H, W, C)— the native obs frame (CLAUDE.md §4).kind (str) – registry key (see
AUGMENTATION_KINDS); selects the algorithm.strength (float) – per-kind knob — gauss=sigma(0-1) · blur=odd kernel · contrast=factor · canny=upper threshold; ignored for blackout/invert.
rng (np.random.Generator | None) –
np.random.Generatorfor stochastic kinds (gauss) — reproducibility.
- Return type:
np.ndarray
- Returns:
A NEW contiguous uint8 HWC RGB array, same shape as
img(input untouched, no channel swap — preserves the obs->policy->video format contract).
- so101_mvbench.evaluation.augmentation.check_strength(kind, strength)[source]#
Raise if kind was asked for without a strength that means anything.
async_eval calls this before it builds the environment, so a mislabelled sweep dies in about ten seconds rather than after an hour of evaluation. It cannot fire earlier than that: AppLauncher boots Isaac at import time, well before main() sees the parsed flags.
CSV + JSON results writer for evaluation runs.
Writes per-episode results to CSV (Pandas-ready) and an aggregated summary to JSON. Handles multi-run statistics (mean ± std).
- class so101_mvbench.evaluation.results_writer.ResultsWriter(output_dir, config)[source]#
Bases:
objectWrites evaluation results to CSV + JSON.
Async MP4 video recorder for evaluation episodes.
Captures camera frames at a fixed interval and encodes them to MP4 in a background thread (ThreadPoolExecutor). Frames are resized before encoding to reduce file size.
- class so101_mvbench.evaluation.video_recorder.VideoRecorder(base_dir, policy_cams=None, fps=10, interval=15, resize=(320, 240))[source]#
Bases:
objectAsync MP4 recorder for camera streams the policy consumes.
By default (
policy_cams=None) captures bothcamera_egoandcamera_external— for backwards compatibility with debug workflows. Passingpolicy_cams=list(rename_map.keys())filters to only the cameras the policy actually consumes, so 1-cam policies don’t emit a misleadingexternal.mp4that was never fed to the model.- Parameters:
- capture(obs, step)[source]#
Grab camera frames if step matches interval.
Cameras not in
self._policy_camsare skipped — avoids emitting MP4s for cameras the policy never consumed.
eval_report: standalone post-analysis (pandas + matplotlib).
Reads eval_results_random_*.csv + eval_summary.json + quality_gate.json from an eval output directory and produces:
stage_outcome.png — the 4 task stages, split by termination reason attempts_histogram.png — histogram of grasp attempts per episode cube_position_analysis.png — cube-xy scatter colored by success/stage cube_rotation_analysis.png — cube-yaw histogram split by success episode_timeline.png — per-episode outcome Gantt chart timing_and_distance.png — time_to_reach/lift/home + min dist scatter report.txt — plain-text summary report.md — markdown-formatted briefing
Standalone: no Jupyter / nbconvert dependency, no simulator needed.
- Usage:
eval_report –dir <eval_output> # via pyproject [project.scripts] entry-point
Metrics doc: see METRICS.md next to this file.
Version 1.1.0 — added cube_position/rotation/timeline/timing plots.
eval_aggregate_report — cross-run statistical analysis of multiple eval-runs.
Reads N eval-output directories (each containing eval_summary.json + eval_results_random_*.csv from a single run_eval.sh execution) and produces a cross-run aggregation report:
aggregate_report.md — markdown summary (mean ± std) aggregate_report.txt — plain-text summary plots/cross_run_success_rate.png — boxplot of success_rate per run plots/per_episode_success.png — fraction-success per episode (1-indexed) plots/cross_run_attempts.png — distribution of num_attempts per run plots/cross_run_stage_distribution.png — stacked bar of stages per run
Designed for thesis-grade statistics: mean, std, min/max, per-episode flakiness.
Standalone — no Jupyter / nbconvert dependency, no simulator needed.
- Usage:
eval_aggregate_report –run_dirs <dir1> <dir2> … –output_dir <agg_dir>
- Each <dir_N> must contain:
eval_summary.json
eval_results_random_*.csv
Version 1.0.0
Shared matplotlib conventions for the plots this package writes.
One rule so far, but it applies to every figure: the legend belongs BELOW the x-axis. An outside-right legend eats roughly a quarter of the width for a few short entries, and an in-plot one covers data – in the episode timeline it literally hid a bar. Vertical space is the cheap direction.
- so101_mvbench.evaluation.plot_style.legend_below(ax, handles=None, fontsize=9, max_ncol=4, y=None)[source]#
Place
ax’s legend as a horizontal row underneath the x-axis.handlesoverrides the artists collected from the axes, for figures that build proxy handles. Does nothing when there is nothing to label.yis an axes-fraction offset. Left at None it is derived from a constant PHYSICAL gap, because the same fraction means different distances on a tall square plot and on a short wide one: at a fixed -0.09 the legend landed on top of the x-axis label of the wide rotation and timing figures.- Return type:
Audit tools#
Read per-episode CSVs from cube_replay, generate 3-panel divergence plot per episode + a global summary.csv.
CPU-only — no simulator needed (numpy + matplotlib + scipy).
- so101_mvbench.audit.cube_plot.pairwise_max(pos_rel, quat)[source]#
Per-frame maxima over all env-pairs of position & rotation divergence.
Let P = { (i, j) : 0 ≤ i < j < E } be the set of unordered env-pairs (size n_pairs = E·(E−1)/2). For each frame t we compute:
max_norm[t] = max_{(i,j)∈P} ‖ p_i(t) − p_j(t) ‖₂ [meters] max_dx[t] = max_{(i,j)∈P} | x_i(t) − x_j(t) | [meters] max_dy[t] = max_{(i,j)∈P} | y_i(t) − y_j(t) | max_dz[t] = max_{(i,j)∈P} | z_i(t) − z_j(t) | max_quat_deg[t] = max_{(i,j)∈P} 2·arccos( |q_i(t) · q_j(t)| ) · 180/π- where:
p_e(t) = (x_e, y_e, z_e)(t) ∈ ℝ³ is the cube position in env e at frame t (in m, env_origin-offset subtracted, common local frame)
q_e(t) ∈ ℝ⁴ is the unit quaternion in XYZW order (Isaac Lab post-PR-4437) of the cube in env e
the absolute value of the dot product handles the q ≡ −q sign ambiguity, making the angular distance sign-invariant
2·arccos(|q·q'|)is the geodesic rotation distance on SO(3) in radians; multiplied by 180/π → degrees.
The per-axis components (dx, dy, dz) are useful for diagnostics: they reveal WHERE the divergence sits (lateral vs vertical), which the Euclidean norm alone hides.
Shapes: pos_rel (E, T, 3), quat (E, T, 4). Returns: (max_norm, max_dx, max_dy, max_dz, max_quat_deg), each shape (T,).
- Parameters:
pos_rel (numpy.ndarray)
quat (numpy.ndarray)
- so101_mvbench.audit.cube_plot.per_env_euler_to_ref(quat, ref=0)[source]#
Signed Euler decomposition of the relative rotation R_ref⁻¹·R_e per env.
For each env e (e ≠ ref) at each frame t:
R_e(t) = Rotation.from_quat(q_e(t)) # quat already XYZW, scipy convention matches R_ref(t) = Rotation.from_quat(q_ref(t)) R_rel(t) = R_ref(t)⁻¹ · R_e(t) # relative rotation (roll, pitch, yaw)[e, t] = R_rel(t).as_euler(“xyz”, degrees=True)
- where:
q_e is the unit quaternion in XYZW order (Isaac Lab convention since PR #4437, matches scipy.spatial.transform.Rotation “scalar-last”)
“xyz” denotes the intrinsic Tait-Bryan convention (X, then Y′, then Z″)
Output values are SIGNED degrees in (−180°, +180°] per axis
CRITICAL: this is NOT euler(R_e) − euler(R_ref), which would be wrong under angle wrapping and gimbal lock. The correct way is to first compose the relative rotation R_rel = R_ref⁻¹·R_e, then decompose that ONE rotation.
Quaternion sign ambiguity (q ≡ −q): scipy.spatial.transform.Rotation handles this internally — Rotation.from_quat(q) and Rotation.from_quat(−q) yield identical rotation objects.
Shapes: quat (E, T, 4). Returns euler (E, T, 3) signed degrees [roll, pitch, yaw]. The slice [ref] stays zero (R_rel is identity for the reference env itself).
- Parameters:
quat (numpy.ndarray)
ref (int)
- so101_mvbench.audit.cube_plot.per_env_to_ref_metrics(pos_rel, quat, ref=0)[source]#
Per-env distance and rotation-angle relative to a reference env.
For each env e at each frame t:
dist[e, t] = ‖ p_e(t) − p_ref(t) ‖₂ [meters] ang_deg[e, t] = 2·arccos( |q_e(t) · q_ref(t)| ) [degrees]
- where:
p_e(t) ∈ ℝ³ is the cube position in env e at frame t (env_origin-offset already subtracted so envs are comparable in a common local frame)
q_e(t) ∈ ℝ⁴ is the unit quaternion in XYZW order (Isaac Lab convention since PR #4437)
q_e · q_ref denotes the component-wise dot product
the absolute value handles the quaternion sign ambiguity: q and −q represent the same rotation, so the distance must be sign-invariant
The factor 2 in
2·arccos(|q·q'|)is the geodesic angle on SO(3); for unit quaternions,arccos(|q·q'|)is HALF the rotation angle between R(q) and R(q’).Shapes: pos_rel (E, T, 3), quat (E, T, 4). Returns (dist, ang_deg) of shape (E, T).
- Parameters:
pos_rel (numpy.ndarray)
quat (numpy.ndarray)
ref (int)
Dataset tools#
List all gym-registered Isaac Lab environments from this package.
- Usage:
list_envs
Filters to envs whose ID starts with “Lerobot-” or “LiftCube-” (this package’s namespace), so upstream Isaac Lab task IDs are not shown. Output is sorted alphabetically by task ID for stable, reviewer-reproducible diffs.
Adapted from NVIDIA Sim-to-Real-SO-101-Workshop scripts/list_envs.py.
- so101_mvbench.tools.list_envs.main()[source]#
Print this package’s gym-registered envs as a plain table.
Uses
os._exit(0)instead ofsimulation_app.close()because Isaac Sim 6 hangs onclose()(see.claude/memory/reference_isaac_sim6_shutdown.md). Without the hard exit the printed table never flushes before the hang.- Return type:
validate_bin — Objective per-bin validation for AP-7 gate decisions.
Checks a single recorded bin for: - Parquet readability + episode count consistency with binmap.json - Video file count (2 cameras × n_success episodes) - Scene-state JSON presence for each episode - Cube position inside expected bin rectangle (within half_extent_m tolerance)
Exit code: 0 if all checks pass, 1 on first failure.
Usage:
python validate_bin.py --bin_id 1
python validate_bin.py --bin_id 13 --dataset_root /custom/path
- class so101_mvbench.tools.validate_bin.BinLayout(col, row, n_cols, n_rows, bin_size_m, origin_x, origin_y, robot_yaw_rad)[source]#
Bases:
objectCompute expected bin center + bounds in world frame.
- Parameters:
- __init__(col, row, n_cols, n_rows, bin_size_m, origin_x, origin_y, robot_yaw_rad)#
- so101_mvbench.tools.validate_bin.check_binmap(binmap_path)[source]#
Load binmap.json and return (n_success, success_episode_indices).
- so101_mvbench.tools.validate_bin.check_cube_positions(success_eps, layout, tolerance_m)[source]#
Verify cube (x, y) for each success episode is inside the bin rectangle.
- so101_mvbench.tools.validate_bin.check_parquet(parquet_path, expected_count)[source]#
Load parquet, verify distinct episode_index count. Returns actual count.
- so101_mvbench.tools.validate_bin.check_scene_states(meta_root, n_success)[source]#
Count scene_state JSON files. Should be ≥ n_success.
- so101_mvbench.tools.validate_bin.check_videos(videos_root, n_success, min_size_bytes=50000)[source]#
Count .mp4 files under videos/. Should be 2 * n_success (2 cameras).
Delete specific episodes from a LeRobot v3 dataset.
Removes episode data from parquet, videos, scene_states, and binmap. Re-indexes remaining episodes so indices are contiguous.
- Usage:
# List all episodes python delete_episodes.py –dataset_root /path/to/bin_c0_r0 –list
# Delete episodes 1 and 3 python delete_episodes.py –dataset_root /path/to/bin_c0_r0 –delete 1,3
# Dry run (show what would be deleted) python delete_episodes.py –dataset_root /path/to/bin_c0_r0 –delete 1,3 –dry_run
- so101_mvbench.tools.delete_episodes.delete_episodes(root, delete_indices, dry_run=False)[source]#
Delete specified episodes and re-index remaining.
- so101_mvbench.tools.delete_episodes.list_episodes(root)[source]#
Print all episodes with frame counts.
Pipeline utilities for LeRobot v3.0 dataset lifecycle management.
Provides centralized dataset validation, cleanup, and resume handling used by all pipeline scripts (recorder, visualizer, trainer, inference).
- class so101_mvbench.utils.dataset_manager.DatasetManager[source]#
Bases:
objectManages LeRobot v3.0 dataset lifecycle – validation, cleanup, resume.
- static cleanup_corrupt_tail(dataset_path)[source]#
Remove corrupt trailing parquet file(s) from aborted recording.
When Ctrl+C kills the recorder mid-write, the async_episode_processor leaves a corrupt .parquet file. This method detects and removes it, then updates info.json to match the remaining valid data.
- static handle_existing_dataset(dataset_path, auto=False)[source]#
Check for existing dataset and prompt user for action.
Automatically cleans up corrupt trailing files before presenting the menu. In auto mode or non-interactive sessions, makes a safe default choice without prompting.
- static validate_dataset(dataset_path)[source]#
Check dataset integrity and return a status dict.
- Parameters:
dataset_path (
Path) – Root path of the LeRobot dataset.- Returns:
exists (bool), valid (bool), total_episodes (int), total_frames (int), valid_files (int), corrupt_files (list[str])
- Return type:
Dict with keys
Parse CLI --episodes range-syntax into a sorted list[int].
Supported forms:
'0'single ID'0,1,5'comma-list'0-10'inclusive range (lo ≤ hi)'0,5-10,15'mixed comma + range'all'auto-discover from<dataset_root>/trajectories/episode_*.npy
The function is intentionally pure (no Isaac Sim deps) so it can be unit-tested
in isolation. Shared across console-scripts that operate on a subset of episodes
(synthetic_multicam_recorder, future tools).
- so101_mvbench.utils.episodes_spec.parse_episodes_spec(spec, dataset_root)[source]#
Parse a
--episodesspec string into a sorted list of integer IDs.- Parameters:
spec (
str) – One of:'all','<n>','<n>,<m>','<lo>-<hi>', or any comma-separated mix of singles + inclusive ranges. Whitespace around tokens is ignored. Case-insensitive for'all'.dataset_root (
Path) – Source LeRobot dataset directory. Used only whenspec == 'all'— globs<dataset_root>/trajectories/episode_*.npy.
- Return type:
- Returns:
Sorted list of unique integer episode IDs. Duplicates within the spec are deduplicated.
- Raises:
ValueError – If
specis empty/whitespace, malformed, or contains a reversed range (hi < lo). Also ifspec == 'all'but thetrajectories/directory has no matching files.
Scene and geometry#
Shared SO-101 joint constants and coordinate transforms.
Single source of truth for the scene geometry the whole pipeline shares: the workspace-frame anchors, the rigid shift between scenes, the joint metadata, and the raw-degrees to sim-radians transform pair.
Deliberately free of any Isaac Sim / AppLauncher import, so that recording,
replay and evaluation can read these constants at module level without
booting a simulator. Joint metadata mirrors the LeRobot interface
(SO101_USD_MAPPING, SO101_JOINT_ORDER).
- so101_mvbench.utils.so101_transforms.ARM_BASE_OFFSET: tuple[float, float, float] = (-0.2, 0.0, 0.7463)#
Rigid translation applied to the ENTIRE coupled configuration when moving from the recording scene into the cylindrical room.
- so101_mvbench.utils.so101_transforms.BASE_SCENE_ORIGIN_W: tuple[float, float, float] = (0.0, 0.0, 0.8663)#
{W} origin of the scene
LiftCubeSceneCfgis authored in.Since the cylroom migration the base scene IS the cylindrical room, so scene overrides (RIG_SHIFT) are computed against this origin: for the shipped cylroom configs the rig shift is zero and the base scene spawns as authored. Legacy datasets recorded in the retired floor-level table scene still declare (or default to)
HEMISPHERE_ORIGIN_Was their SOURCE origin – that constant stays for data interpretation, this one describes the code’s scene.
- so101_mvbench.utils.so101_transforms.CUBE_SPAWN_Z_IN_W: float = -0.06999999999999999#
Cube spawn height expressed in the workspace frame {W}. Scene-independent: the spawn height in any scene’s world frame is its {W} origin z plus this.
- so101_mvbench.utils.so101_transforms.FLOOR_CUBE_SPAWN_Z: float = 0.05#
Cube spawn height recorded in
scene_state.object_init_pos_m.z, expressed in the recording scene’s floor frame.
- so101_mvbench.utils.so101_transforms.HEMISPHERE_ORIGIN_W: tuple[float, float, float] = (0.2, 0.0, 0.12)#
{W} origin of the retired floor-level table scene, in world frame {0}.
Cameras, cube and bin grid are all anchored in the workspace frame {W}; this constant is one scene’s {W} origin expressed in {0} (⁰ξ_W per Corke 2023, §2.1.2). External cameras orbit that point and aim at it.
The scene it describes is no longer built: the package’s base scene is the cylindrical room (
BASE_SCENE_ORIGIN_W). The constant stays because it is the frame that recorded data without a frame declaration lives in, so it remains the source origin when replaying or evaluating such a dataset.
- so101_mvbench.utils.so101_transforms.PHASE3_FLOOR_Z: float = 0.0237#
pad origin 0.0257 minus half-thickness 0.002). Verified empirically in a settled scene: arm-base world z = 0.0, pad z = 0.0257, cube z = 0.0477. The arm ORIGIN sits ~2.4 cm BELOW this surface – its foot plate contacts the floor, not the origin. Do NOT use the table surface as the arm origin; that over-lifts the rig by 2.4 cm and leaves pad and cube floating.
- Type:
Floor surface the recorded rig sat on (= action-pad bottom
- so101_mvbench.utils.so101_transforms.ROBOT_TABLE_POS: tuple[float, float, float] = (-0.2, 0.0, 0.7463)#
Arm base world position on the pedestal table. Identical to ARM_BASE_OFFSET because the arm base sits at the world origin in the recording scene.
- so101_mvbench.utils.so101_transforms.SO101_ACTION_FEATURES: dict[str, type] = {'elbow_flex.pos': <class 'float'>, 'gripper.pos': <class 'float'>, 'shoulder_lift.pos': <class 'float'>, 'shoulder_pan.pos': <class 'float'>, 'wrist_flex.pos': <class 'float'>, 'wrist_roll.pos': <class 'float'>}#
Action feature schema — 6 joint positions in raw degrees.
- so101_mvbench.utils.so101_transforms.SO101_JOINT_ORDER: list[str] = ['shoulder_pan.pos', 'shoulder_lift.pos', 'elbow_flex.pos', 'wrist_flex.pos', 'wrist_roll.pos', 'gripper.pos']#
Joint names in LeRobot observation/action order (with .pos suffix).
- so101_mvbench.utils.so101_transforms.SO101_OBSERVATION_FEATURES: dict[str, type] = {'elbow_flex.pos': <class 'float'>, 'gripper.pos': <class 'float'>, 'shoulder_lift.pos': <class 'float'>, 'shoulder_pan.pos': <class 'float'>, 'wrist_flex.pos': <class 'float'>, 'wrist_roll.pos': <class 'float'>}#
Static observation feature schema (without cameras).
- so101_mvbench.utils.so101_transforms.SO101_USD_MAPPING: dict[str, dict[str, int]] = {'elbow_flex': {'joint_max': 90, 'joint_min': -100}, 'gripper': {'joint_max': 100, 'joint_min': -10}, 'shoulder_lift': {'joint_max': 100, 'joint_min': -100}, 'shoulder_pan': {'joint_max': 110, 'joint_min': -110}, 'wrist_flex': {'joint_max': 95, 'joint_min': -95}, 'wrist_roll': {'joint_max': 160, 'joint_min': -160}}#
USD joint limits in degrees — defines the mapping between normalized [0, 1] space and physical joint angles.
- so101_mvbench.utils.so101_transforms.TABLE_HEMISPHERE_ORIGIN: tuple[float, float, float] = (0.0, 0.0, 0.8663)#
the recording-scene origin shifted by the same rigid delta as everything else. This is the workspace frame {W} origin of the cylindrical-room scene, expressed in world frame {0}.
- Type:
Camera-hemisphere origin in the cylindrical room
- so101_mvbench.utils.so101_transforms.TABLE_TOP_Z: float = 0.77#
Top cylinder centre 0.745 plus half-height 0.025.
- Type:
Tabletop SURFACE height in the cylindrical room
- so101_mvbench.utils.so101_transforms.WARMUP_STEPS: int = 60#
Physics warmup after env.reset() — 2 seconds at FPS=30.
- so101_mvbench.utils.so101_transforms.WORLD_DELTA_Z: float = 0.7463#
The one rigid z-shift that maps the recorded floor onto the table surface.
- so101_mvbench.utils.so101_transforms.build_joint_tensors(device)[source]#
Pre-compute joint min/max tensors for coordinate transforms.
- Parameters:
device (str | torch.device) – Torch device (e.g.
"cuda:0"ortorch.device("cpu")).- Return type:
tuple[torch.Tensor, torch.Tensor]
- Returns:
Tuple of
(joint_mins, joint_maxs)tensors, each shape(6,), containing USD joint limits in degrees.
- so101_mvbench.utils.so101_transforms.raw_degrees_to_sim_radians(raw_values, joint_mins, joint_maxs)[source]#
Convert robot raw degrees to Isaac Sim joint position radians.
Transform chain: raw_degrees → normalized [0,1] → mapped degrees → radians.
- Parameters:
raw_values (
Tensor) – Raw action values, shape(6,). Joints 0-4: range [-100, 100]. Gripper (5): range [0, 100].joint_mins (
Tensor) – USD joint minimums in degrees, shape(6,).joint_maxs (
Tensor) – USD joint maximums in degrees, shape(6,).
- Return type:
Tensor- Returns:
Joint positions in radians, shape
(6,).
- so101_mvbench.utils.so101_transforms.rigid_shift_from_recording(workspace_origin_w, source_origin_w=(0.2, 0.0, 0.12))[source]#
Translation that carries a recorded rig from its source scene into a target scene.
A scene declares its workspace-frame {W} origin (
SceneConfig. workspace_origin_m); recorded data declares the origin of the scene it was captured in (binmap.json/scene_stateworkspace_origin_m, defaulting to the floor-level table scene atHEMISPHERE_ORIGIN_Wfor datasets that predate the field). The rigid shift for the ENTIRE coupled rig – arm base, cube, action pad, camera hemisphere – is simply the difference between the two origins. This is the pose-graph property that keeps recorded joint trajectories valid across scenes: the rig moves as one, so the arm-to-cube geometry never changes.Table-scene recording replayed in the cylindrical room: returns exactly
ARM_BASE_OFFSET. Same scene on both sides: returns (0, 0, 0).
- so101_mvbench.utils.so101_transforms.sim_radians_to_raw_degrees(sim_obs, joint_mins, joint_maxs)[source]#
Convert Isaac Sim joint positions (radians) to robot raw degrees.
Inverse of
raw_degrees_to_sim_radians().- Parameters:
sim_obs (
Tensor) – Joint positions in radians, shape(6,).joint_mins (
Tensor) – USD joint minimums in degrees, shape(6,).joint_maxs (
Tensor) – USD joint maximums in degrees, shape(6,).
- Return type:
Tensor- Returns:
- Raw degree values, shape
(6,). Joints 0-4: range [-100, 100]. Gripper (5): range [0, 100].
- Raw degree values, shape
SceneConfig — unified scene declaration schema.
Single source of truth for scene structure: cameras, objects, robot,
visuals, and the scene’s workspace origin (workspace_origin_m).
Consumed by teleop_recorder, multicam_replay, and async_eval
via SceneConfig.from_json; the shipped configs live in
tasks/scene_configs/.
- … and 5 more (see plan-file “9 fragmentierte Configs” inventory)
Schema versioning: major version (1.x) must match SCHEMA_VERSION;
minor bumps are forward-compatible for new optional fields with defaults,
but they may also tighten validation on existing fields (e.g. a previously
optional field becoming required for one type variant). The major bump
is reserved for changes that rename or remove fields or alter the semantics
of existing values.
Version history:
1.0Initial schema.
1.1CameraSpec.radius_mis required for external entries and forbidden on ego entries.1.2CameraSpec.pose_idis removed. External cameras declare their hemisphere position via three explicit floats (azimuth_deg,elevation_deg,radius_m). Pre-1.2 configs must be migrated by splittingpose_idinto the two angle fields.
- class so101_mvbench.utils.scene_config.CameraSpec(type, azimuth_deg=None, elevation_deg=None, radius_m=None, offset_pos_m=None, offset_rot_deg=None, resolution_hw=(480, 640), focal_length=5.0, horizontal_aperture=10.0, up_world=None)[source]#
Bases:
objectOne camera in the scene.
ego— gripper-mounted (useoffset_pos_m+offset_rot_degrelative to the gripper joint).
external— hemisphere-mounted around the workspace origin. Positionis declared by three explicit floats:
azimuth_deg,elevation_deg,radius_m. All three are REQUIRED. The world-frame(x, y, z)is computed from them byutils/camera_grid.spherical_to_cartesian.
Schema 1.2 removed the string-form
pose_id(e.g."az000_el45"): carrying both numbers and a string-encoding of two of them invited parsing, key mangling, and registry mutation downstream.- Parameters:
- __init__(type, azimuth_deg=None, elevation_deg=None, radius_m=None, offset_pos_m=None, offset_rot_deg=None, resolution_hw=(480, 640), focal_length=5.0, horizontal_aperture=10.0, up_world=None)#
- class so101_mvbench.utils.scene_config.ObjectSpec(type, init_pos_m=None, init_rpy_rad=(0.0, 0.0, 0.0), size_m=None, color_rgb=(0.8, 0.8, 0.8), mass_kg=0.05, usd_path=None)[source]#
Bases:
objectOne scene object: rigid cube or static USD asset.
init_pos_mis expressed in the workspace frame {W}, not in world coordinates: add the scene’sworkspace_origin_mto get the world pose. Declaring it {W}-relative is what makes an object placement portable between scenes (see the frame contract).- Parameters:
- __init__(type, init_pos_m=None, init_rpy_rad=(0.0, 0.0, 0.0), size_m=None, color_rgb=(0.8, 0.8, 0.8), mass_kg=0.05, usd_path=None)#
- class so101_mvbench.utils.scene_config.RobotSpec(type='so101_white', init_pos_m=(0.0, 0.0, 0.0), init_rot_xyzw=(0.0, 0.0, 0.7071, 0.7071), default_joint_pos_rad=<factory>, home_joint_pos_rad=<factory>, color_rgb=(1.0, 1.0, 1.0))[source]#
Bases:
objectRobot config: type, initial pose, default + home joint positions.
Joint dicts use Isaac-USD names (
Rotation,Pitch,Elbow,Wrist_Pitch,Wrist_Roll,Jaw) for consistency withLiftCubeEnvCfg.actions.joint_positions.joint_names.- Parameters:
- __init__(type='so101_white', init_pos_m=(0.0, 0.0, 0.0), init_rot_xyzw=(0.0, 0.0, 0.7071, 0.7071), default_joint_pos_rad=<factory>, home_joint_pos_rad=<factory>, color_rgb=(1.0, 1.0, 1.0))#
- class so101_mvbench.utils.scene_config.SceneConfig(schema_version='1.2', workspace_origin_m=(0.0, 0.0, 0.8663), cameras=<factory>, objects=<factory>, robot=<factory>, visual=<factory>)[source]#
Bases:
objectSingle source of truth for scene declaration.
- Parameters:
schema_version (str)
cameras (dict[str, CameraSpec])
objects (dict[str, ObjectSpec])
robot (RobotSpec)
visual (VisualSpec)
- __init__(schema_version='1.2', workspace_origin_m=(0.0, 0.0, 0.8663), cameras=<factory>, objects=<factory>, robot=<factory>, visual=<factory>)#
- Parameters:
schema_version (str)
cameras (dict[str, CameraSpec])
objects (dict[str, ObjectSpec])
robot (RobotSpec)
visual (VisualSpec)
- Return type:
None
- classmethod from_json(path)[source]#
Load SceneConfig from a JSON file.
Hard-fails when the JSON’s
schema_versionmajor component does not matchSCHEMA_VERSION. JSON list-fields are converted back to tuples for type-consistency with downstream Isaac Lab APIs.- Return type:
- Parameters:
-
workspace_origin_m:
tuple[float,float,float] = (0.0, 0.0, 0.8663)# {W} origin in world coordinates. Defaults to the scene the package ships (the cylindrical room). A config that omits it therefore declares the current base scene, not a retired one. The legacy fallback for recorded data without a frame declaration is a separate rule and lives in the consumers (see
so101_transforms.rigid_shift_from_recording).
- class so101_mvbench.utils.scene_config.VisualSpec(room_light_exposure=1.0, env_light_exposure=1.0, sky_dome_yaw_rad=0.0, action_pad_yaw_rad=0.0, dome_light_y_mirror=True)[source]#
Bases:
objectVisual defaults: lighting, sky dome orientation, action pad.
- Parameters:
Scene State — capture, save, and load exact scene conditions per episode.
Stores all randomizable parameters so that trajectory replays can reproduce identical scenes across camera permutations. Uses self-documenting dict format with named keys (not positional arrays).
- Conventions:
Orientation: Euler RPY (rad), not quaternions
Unit suffixes: _m for meters, _rad for radians
Dict keys match joint/axis names for unambiguous access
- class so101_mvbench.utils.scene_state.SceneState(episode_index, seed, task, object_key, object_init_pos_m=<factory>, object_init_rpy_rad=<factory>, action_pad_yaw_rad=0.0, room_light_exposure=0.0, env_light_exposure=0.0, sky_dome_yaw_rad=0.0, robot_color_rgb=<factory>, object_color_rgb=<factory>, object_texture='', table_texture='', table_form='', robot_default_joint_pos_rad=<factory>, look_at_m=<factory>, workspace_origin_m=None)[source]#
Bases:
objectExact scene conditions for one episode.
Saved as JSON after env.reset(), before the first action. The replayer loads this to reproduce identical conditions.
All numeric fields use physical unit suffixes (_m, _rad). Vector quantities use dicts with named keys for self-documentation.
- Parameters:
episode_index (
int) – Episode number within the recording session.seed (
int) – Random seed used for env.reset().task (
str) – Isaac Lab task name (e.g. “LiftCube-Sim”).object_key (
str) – Scene key of the tracked object (e.g. “cube”, “yellow_ring”).object_init_pos_m (
dict[str,float]) – Object position after reset {“x”, “y”, “z”}.object_init_rpy_rad (
dict[str,float]) – Object orientation after reset {“roll”, “pitch”, “yaw”}.action_pad_yaw_rad (
float) – Action pad yaw angle after reset.room_light_exposure (
float) – Room light exposure value after reset.env_light_exposure (
float) – Environment light exposure value after reset.sky_dome_yaw_rad (
float) – Sky dome yaw angle after reset.robot_color_rgb (
dict[str,float]) – Robot diffuse color {“r”, “g”, “b”} in [0, 1].object_color_rgb (
dict[str,float]) – Object (cube) diffuse color {“r”, “g”, “b”} in [0, 1].object_texture (
str) – Object (cube) texture path or “” if none applied.table_texture (
str) – Action pad texture path or “” if none.table_form (
str) – Action pad USD asset name (e.g. “action-pad-round.usda”).robot_default_joint_pos_rad (
dict[str,float]) – Robot default joint positions {“Rotation”, …}.look_at_m (
dict[str,float]) – Camera look-at point {“x”, “y”, “z”}.
- __init__(episode_index, seed, task, object_key, object_init_pos_m=<factory>, object_init_rpy_rad=<factory>, action_pad_yaw_rad=0.0, room_light_exposure=0.0, env_light_exposure=0.0, sky_dome_yaw_rad=0.0, robot_color_rgb=<factory>, object_color_rgb=<factory>, object_texture='', table_texture='', table_form='', robot_default_joint_pos_rad=<factory>, look_at_m=<factory>, workspace_origin_m=None)#
- classmethod from_env(env, episode_index, seed, look_at, object_key='cube', workspace_origin_m=None)[source]#
Capture scene state from a live Isaac Sim environment.
Must be called after env.reset() and before the first env.step().
- Parameters:
- Return type:
- Returns:
SceneState with all captured values.
- get_joint_pos_list()[source]#
Return joint positions in SO-ARM101 order.
Order: Rotation, Pitch, Elbow, Wrist_Flex, Wrist_Roll, Gripper.
- classmethod load(path)[source]#
Load scene state from JSON file.
- Parameters:
path (
Path) – Input file path.- Return type:
- Returns:
SceneState instance.
- so101_mvbench.utils.scene_state.rpy_to_quat_xyzw(roll, pitch, yaw)[source]#
Convert RPY Euler angles (ZYX aerospace convention) to XYZW quaternion.
Inverse of
_quat_to_rpy. Result matches Isaac Lab’s post-PR-#4437 XYZW convention used bywrite_root_pose_to_simandinit_state.rot.
Shared scene builder — single source of truth for scene setup across all stages.
Used by: gamepad_recorder, ego_recorder, external_batch_recorder, eval_policy. Guarantees identical visual scene for the same inputs, regardless of which stage calls it.
- class so101_mvbench.utils.scene_builder.VisualConfig(action_pad_yaw_rad=3.141592653589793, room_light_exposure=-1.0, env_light_exposure=1.0, sky_dome_yaw_rad=3.141592653589793, robot_color_rgb=(0.95, 0.95, 0.95), cube_color_rgb=(0.8, 0.2, 0.2), dome_light_y_mirror=True)[source]#
Bases:
objectVisual scene properties applied after env.reset().
Default values match
_ZERO_SPATIAL/_ZERO_VISUALfrommdp/__init__.py— deterministic ID-replay baseline.Immutable (frozen). Create OOD variants via
dataclasses.replace().- Parameters:
- __init__(action_pad_yaw_rad=3.141592653589793, room_light_exposure=-1.0, env_light_exposure=1.0, sky_dome_yaw_rad=3.141592653589793, robot_color_rgb=(0.95, 0.95, 0.95), cube_color_rgb=(0.8, 0.2, 0.2), dome_light_y_mirror=True)#
- so101_mvbench.utils.scene_builder.apply_scene(env, episode_idx, binmap, *, visual=VisualConfig(action_pad_yaw_rad=3.141592653589793, room_light_exposure=-1.0, env_light_exposure=1.0, sky_dome_yaw_rad=3.141592653589793, robot_color_rgb=(0.95, 0.95, 0.95), cube_color_rgb=(0.8, 0.2, 0.2), dome_light_y_mirror=True), cube_noise_m=0.0, cube_rot_noise_deg=0.0)[source]#
Set up the complete scene — robot joints, cube pose, visual properties.
Call AFTER
env.reset()and BEFORE warmup steps. Multi-Env safe: applies to ALL environment instances.- Parameters:
env – Gym-wrapped Isaac Lab environment.
episode_idx (
int) – Episode index for BinMap lookup.binmap –
BinMapinstance with per-episode cube configs.visual (
VisualConfig) – Visual scene configuration. UseID_VISUAL_CONFIG(default) for in-distribution replay, or a customVisualConfigfor OOD.cube_noise_m (
float) – Additive uniform noise on cube x/y (for OOD eval).cube_rot_noise_deg (
float) – Additive uniform noise on cube yaw (for OOD eval).
- Returns:
{"cube_x_m": float, "cube_y_m": float, "cube_yaw_rad": float}.- Return type:
Dict with actually placed cube coordinates
- so101_mvbench.utils.scene_builder.load_visual_config(path)[source]#
Load a VisualConfig from a JSON file.
Only fields present in the JSON are overridden — missing fields keep their ID defaults. This allows OOD configs that vary a single parameter.
Example JSON (vary only cube color):
{"cube_color_rgb": [0.2, 0.8, 0.2]}
- Return type:
- Parameters:
- so101_mvbench.utils.scene_builder.resolve_all_prim_paths(env, asset_name)[source]#
Resolve USD prim paths for ALL envs (Multi-Env safe).
Handles AssetBaseCfg/RigidObjectCfg/ArticulationCfg (.cfg.prim_path) and XformPrimView (._prim_path) which has no .cfg attribute. Returns every match for the
{ENV_REGEX_NS}pattern.
Deterministic bin-based stratified cube spawner for recording pipeline.
Divides the robot workspace into an n_cols × n_rows grid of bins. Within each bin, a 3×3 sub-grid of deterministic positions is generated (corners, edge midpoints, center). At each position, 3 fixed yaw rotations are applied (0°, 22.5°, 45°). Episodes are scheduled block-wise: all episodes for one bin complete before advancing to the next.
Adapted from leisaac_experiments.scene.sampling with simplifications:
- No ObjectRegistry dependency — half_extent_m passed directly.
- No load_from_hdf5() — resume exclusively via BinMap JSON sidecar.
- Deterministic sub-grid positions instead of least-visited-first random.
- Pre-computed schedule with cursor-based traversal.
Designed to be Isaac-Sim-free: no isaaclab imports at module level.
Integration with Isaac Lab happens via post-reset teleport in
policy_recorder._apply_spawn_config().
BinMap / BinEpisodeRecord provide offline-persistable episode→bin tracking: each recording session saves a JSON sidecar (<dataset>_binmap.json) that maps every episode index to its bin coordinates, pose, and success flag.
- class so101_mvbench.utils.bin_spawner.BinEpisodeRecord(episode_idx, bin_col, bin_row, x_m, y_m, yaw_rad, success)[source]#
Bases:
objectSingle episode entry in the BinMap index.
- Parameters:
- episode_idx#
Dataset episode index.
- bin_col#
Grid column of the spawned bin.
- bin_row#
Grid row of the spawned bin.
- x_m#
World-frame spawn x-coordinate (meters).
- y_m#
World-frame spawn y-coordinate (meters).
- yaw_rad#
Spawn yaw angle (radians).
- success#
True if the episode was marked successful (N key).
- class so101_mvbench.utils.bin_spawner.BinMap(n_cols, n_rows, object_key, episodes=<factory>, ood_bins=<factory>, workspace_origin_m=None)[source]#
Bases:
objectOffline-persistable index mapping episode indices to bin coordinates.
Saved as a JSON sidecar file (<dataset>_binmap.json) alongside the dataset. Enables:
Resume after crash: reconstruct BinSpawner cursor from the count of successful episodes.
Offline analysis: inspect per-bin coverage, identify under-sampled bins, filter episodes by bin.
- Parameters:
- n_cols#
Number of grid columns.
- n_rows#
Number of grid rows.
- object_key#
Active object key (matches BinSpawner.object_key).
- episodes#
Ordered list of episode records (append-only).
- ood_bins#
Set of (col, row) tuples excluded from sampling.
- __init__(n_cols, n_rows, object_key, episodes=<factory>, ood_bins=<factory>, workspace_origin_m=None)#
- get_episode_config(episode_idx)[source]#
Lookup SpawnConfig for a specific dataset episode index.
- Parameters:
episode_idx (
int) – Dataset episode index (matches BinEpisodeRecord.episode_idx).- Return type:
- Returns:
SpawnConfig reconstructed from the BinMap entry, or None if the episode_idx is not present. If multiple records share the same episode_idx (retries before success), returns the last occurrence.
- classmethod load(path)[source]#
Load BinMap from a JSON sidecar file.
- Parameters:
path (
Path) – Path to an existing_binmap.jsonfile.- Return type:
- Returns:
Reconstructed BinMap instance.
- Raises:
FileNotFoundError – If path does not exist.
- log_episode(episode_idx, cfg, success)[source]#
Append an episode record to the index.
- Parameters:
episode_idx (
int) – Dataset episode index.cfg (
SpawnConfig) – SpawnConfig used for this episode.success (
bool) – True if the episode was accepted (N key).
- Return type:
- render_ascii(active_col=-1, active_row=-1)[source]#
Render bin coverage as ASCII grid matching the robot’s view.
The grid is rotated 90° CW from local frame to match what the operator sees looking at the robot from behind (robot_yaw=90°): - Columns run left→right = World +Y → -Y - Rows run top→bottom = World +X (far) → -X (near robot)
- Cell format:
[ ]— unvisited (no successful episode)[ N]— visited (N = number of successful episodes)[ *]— active (currently selected bin)[OOD]— out-of-distribution (excluded from sampling)
-
workspace_origin_m:
tuple[float,float,float] |None= None# Workspace-frame {W} origin of the scene the episodes were RECORDED in, expressed in that scene’s world frame. Declares which frame the episode x_m/y_m coordinates live in, so a replay into a different scene can derive its rigid shift.
Noneon datasets recorded before this field existed, which means the floor-level table scene (HEMISPHERE_ORIGIN_W).
- class so101_mvbench.utils.bin_spawner.BinSpawner(workspace, n_cols, n_rows, half_extent_m, object_key='cube', robot_yaw_rad=0.0, ood_bins=frozenset({}), selected_bin=None)[source]#
Bases:
objectDeterministic bin-based stratified cube spawner.
Pre-computes a schedule of all episodes at construction time:
For each trainable bin (block-wise): generate 3×3 sub-grid positions.
For each position: apply the 3 fixed yaw rotations in
ROTATIONS_RAD(0°, 22.5°, 45°).Episodes are traversed via cursor: N advances, R retries.
All returned coordinates are in the world frame:
world = workspace.origin + rotate(local, robot_yaw)- Parameters:
workspace (
WorkspaceBounds) – Physical bounds of the spawn region.n_cols (
int) – Number of grid columns.n_rows (
int) – Number of grid rows.half_extent_m (
float) – Object half-extent for safe_bounds margin.object_key (
str) – Object key stored in SpawnConfig (default “cube”).robot_yaw_rad (
float) – Robot base yaw angle in radians for rotating local workspace offsets into world frame. Default 0.0.ood_bins (
frozenset[tuple[int,int]]) – Set of (col, row) tuples to exclude from sampling.
- __init__(workspace, n_cols, n_rows, half_extent_m, object_key='cube', robot_yaw_rad=0.0, ood_bins=frozenset({}), selected_bin=None)[source]#
- mark_retry()[source]#
Mark current episode for retry (cursor stays).
Called on R-key (retry) — next env.reset() will repeat the same position and rotation.
- Return type:
- mark_success()[source]#
Mark current episode as success and advance cursor.
Called on N-key (success) — next env.reset() will pick the next episode in the schedule.
- Return type:
- next_config()[source]#
Return the current spawn config, advancing if flagged.
Called by _apply_spawn_config() after each env.reset(). Respects the _advance flag set by mark_success() / mark_retry().
- Return type:
- Returns:
SpawnConfig for the current episode.
- Raises:
StopIteration – If all episodes have been recorded.
- restore_from_binmap(bin_map)[source]#
Restore cursor position from a persisted BinMap.
Sets cursor to the number of successful episodes, which corresponds to the next unrecorded position in the schedule.
- Parameters:
bin_map (
BinMap) – BinMap loaded from a JSON sidecar file.- Raises:
ValueError – If bin_map grid dimensions differ from this spawner.
- Return type:
- class so101_mvbench.utils.bin_spawner.BinState(*values)[source]#
Bases:
EnumVisual state of a single bin cell for display purposes.
- class so101_mvbench.utils.bin_spawner.SpawnConfig(object_key, x_m, y_m, yaw_rad, bin_col, bin_row)[source]#
Bases:
objectConcrete spawn configuration for a single episode.
All coordinates are in the world frame (origin offset applied).
- object_key#
Registry key of the spawned object (e.g. “cube”).
- x_m#
World-frame x position (meters).
- y_m#
World-frame y position (meters).
- yaw_rad#
Object yaw rotation (radians).
- bin_col#
Grid column index of the containing bin.
- bin_row#
Grid row index of the containing bin.
- __init__(object_key, x_m, y_m, yaw_rad, bin_col, bin_row)#
- class so101_mvbench.utils.bin_spawner.WorkspaceBounds(x_min_m, x_max_m, y_min_m, y_max_m, origin_x=0.0, origin_y=0.0, yaw_min_rad=-3.141592653589793, yaw_max_rad=3.141592653589793)[source]#
Bases:
objectAxis-aligned bounding box of the robot’s reachable workspace.
Min/max values are LOCAL — relative to (origin_x, origin_y). BinSpawner converts to world coordinates as: world = origin + local.
This allows the same workspace shape to be re-used across different table/arm positions simply by changing origin_x/origin_y.
- Parameters:
- x_min_m#
Minimum x bound relative to origin (meters).
- x_max_m#
Maximum x bound relative to origin (meters).
- y_min_m#
Minimum y bound relative to origin (meters).
- y_max_m#
Maximum y bound relative to origin (meters).
- origin_x#
World-frame x coordinate of the workspace origin (meters).
- origin_y#
World-frame y coordinate of the workspace origin (meters).
- yaw_min_rad#
Minimum yaw angle (radians). Default: -π.
- yaw_max_rad#
Maximum yaw angle (radians). Default: +π.
- __init__(x_min_m, x_max_m, y_min_m, y_max_m, origin_x=0.0, origin_y=0.0, yaw_min_rad=-3.141592653589793, yaw_max_rad=3.141592653589793)#
- classmethod from_center(center_x, center_y, n_cols, n_rows, bin_size, **kwargs)[source]#
Create bounds from grid center, dimensions, and bin size.
Computes x/y extents as center ± (n × bin_size) / 2. Any extra keyword arguments (origin_x, origin_y, yaw_*) are forwarded to the constructor.
- Parameters:
center_x (
float) – Grid center x in local frame (meters).center_y (
float) – Grid center y in local frame (meters).n_cols (
int) – Number of grid columns.n_rows (
int) – Number of grid rows.bin_size (
float) – Size of each bin in meters.**kwargs – Forwarded to WorkspaceBounds (e.g. origin_x, origin_y).
- Return type:
- Returns:
WorkspaceBounds with computed x/y limits.
- safe_bounds(half_extent_m)[source]#
Return inset bounds keeping an object fully within workspace.
Shrinks x/y bounds by half_extent_m on each side. Origin and yaw are preserved unchanged.
- Parameters:
half_extent_m (
float) – Object half-extent in meters.- Return type:
- Returns:
New WorkspaceBounds with inset x/y limits.
- Raises:
ValueError – If margin makes bounds degenerate (max <= min).
- so101_mvbench.utils.bin_spawner.apply_cube_pose(env, x_m, y_m, yaw_rad, *, env_origins=None, cube_key='cube')[source]#
Teleport cube to (x, y) with yaw rotation + zero velocity.
Single source of truth for cube spawn, shared by the recorder (
teleop_recorder), the replay (multicam_replay), and the eval scene loader (scene_loader).Quaternion is written to
root_state[:, 3:7]in XYZW order (Isaac Lab post-PR #4437 convention, confirmed in base_rigid_object.py:173 docstring). Pure Z-rotation by yaw_rad: (qx=0, qy=0, qz=sin(yaw/2), qw=cos(yaw/2)).Velocity is zeroed explicitly (root_state[:, 7:] = 0.0) — fixes the “cube retains momentum between episodes” bug where
write_root_pose_to_simwas used (which leaves velocity untouched).Z-position stays at
default_root_state[:, 2](cube spawn height from ArticulationCfg.init_state.pos.z). Matches original recording behavior.- Parameters:
env – Gymnasium-wrapped Isaac Lab environment.
x_m (
float) – Target cube X position in meters (world-frame or env-local).y_m (
float) – Target cube Y position in meters.yaw_rad (
float) – Cube yaw rotation in radians (around Z-axis).env_origins – Per-env world-frame origin tensor of shape (num_envs, 3), required for multi-env (cube gets env_origins + x/y as world pos). Pass None for single-env (default).
cube_key (
str) – Scene key for the cube asset (default “cube”).
- Return type:
- so101_mvbench.utils.bin_spawner.parse_ood_bins(ood_str)[source]#
Parse OOD bins from a semicolon-separated CLI string.
Format:
"col,row;col,row;..."Empty string returns empty frozenset.
Camera pose grid for the camera permutation experiment.
Defines a hemisphere grid of external camera poses in workspace frame {W}, centered at the origin (0, 0, 0). The caller must provide the transform ⁰ξ_W (workspace origin in world frame) to place cameras in Isaac Sim.
- Frame convention (Corke, 2023, §2.1.2):
{0} World frame — Isaac Sim origin {W} Workspace frame — centered on the manipulation target {C} Camera frame — position on the hemisphere in {W}
⁰p_camera = ⁰ξ_W ⊕ ᵂp_camera
No Isaac Sim dependency at module load (torch pulled lazily inside the
setter functions). Re-exports HEMISPHERE_ORIGIN_W from
so101_transforms as single source of truth (was
DEFAULT_WORKSPACE_ORIGIN_W until 2026-05-12 — renamed to reflect
that this is the geometric origin of the camera hemisphere).
- Usage:
python camera_grid.py –list # print all 24 poses (workspace frame) python camera_grid.py –visualize # 3D scatter plot around origin
- so101_mvbench.utils.camera_grid.COARSE_GRID: list[CameraPose] = [CameraPose(id='az000_el20', azimuth_deg=0, elevation_deg=20, radius_m=0.4, x=0.3758770483143634, y=0.0, z=0.1368080573302675), CameraPose(id='az045_el20', azimuth_deg=45, elevation_deg=20, radius_m=0.4, x=0.26578520975546993, y=0.2657852097554699, z=0.1368080573302675), CameraPose(id='az090_el20', azimuth_deg=90, elevation_deg=20, radius_m=0.4, x=2.301583120455701e-17, y=0.3758770483143634, z=0.1368080573302675), CameraPose(id='az135_el20', azimuth_deg=135, elevation_deg=20, radius_m=0.4, x=-0.2657852097554699, y=0.26578520975546993, z=0.1368080573302675), CameraPose(id='az180_el20', azimuth_deg=180, elevation_deg=20, radius_m=0.4, x=-0.3758770483143634, y=4.603166240911402e-17, z=0.1368080573302675), CameraPose(id='az225_el20', azimuth_deg=225, elevation_deg=20, radius_m=0.4, x=-0.26578520975547, y=-0.2657852097554699, z=0.1368080573302675), CameraPose(id='az270_el20', azimuth_deg=270, elevation_deg=20, radius_m=0.4, x=-6.904749361367102e-17, y=-0.3758770483143634, z=0.1368080573302675), CameraPose(id='az315_el20', azimuth_deg=315, elevation_deg=20, radius_m=0.4, x=0.2657852097554699, y=-0.26578520975547, z=0.1368080573302675), CameraPose(id='az000_el45', azimuth_deg=0, elevation_deg=45, radius_m=0.4, x=0.28284271247461906, y=0.0, z=0.282842712474619), CameraPose(id='az045_el45', azimuth_deg=45, elevation_deg=45, radius_m=0.4, x=0.20000000000000004, y=0.2, z=0.282842712474619), CameraPose(id='az090_el45', azimuth_deg=90, elevation_deg=45, radius_m=0.4, x=1.731912112470987e-17, y=0.28284271247461906, z=0.282842712474619), CameraPose(id='az135_el45', azimuth_deg=135, elevation_deg=45, radius_m=0.4, x=-0.2, y=0.20000000000000004, z=0.282842712474619), CameraPose(id='az180_el45', azimuth_deg=180, elevation_deg=45, radius_m=0.4, x=-0.28284271247461906, y=3.463824224941974e-17, z=0.282842712474619), CameraPose(id='az225_el45', azimuth_deg=225, elevation_deg=45, radius_m=0.4, x=-0.2000000000000001, y=-0.2, z=0.282842712474619), CameraPose(id='az270_el45', azimuth_deg=270, elevation_deg=45, radius_m=0.4, x=-5.1957363374129606e-17, y=-0.28284271247461906, z=0.282842712474619), CameraPose(id='az315_el45', azimuth_deg=315, elevation_deg=45, radius_m=0.4, x=0.19999999999999998, y=-0.2000000000000001, z=0.282842712474619), CameraPose(id='az000_el70', azimuth_deg=0, elevation_deg=70, radius_m=0.4, x=0.13680805733026755, y=0.0, z=0.37587704831436336), CameraPose(id='az045_el70', azimuth_deg=45, elevation_deg=70, radius_m=0.4, x=0.09673790505919015, y=0.09673790505919014, z=0.37587704831436336), CameraPose(id='az090_el70', azimuth_deg=90, elevation_deg=70, radius_m=0.4, x=8.377077475353988e-18, y=0.13680805733026755, z=0.37587704831436336), CameraPose(id='az135_el70', azimuth_deg=135, elevation_deg=70, radius_m=0.4, x=-0.09673790505919014, y=0.09673790505919015, z=0.37587704831436336), CameraPose(id='az180_el70', azimuth_deg=180, elevation_deg=70, radius_m=0.4, x=-0.13680805733026755, y=1.6754154950707976e-17, z=0.37587704831436336), CameraPose(id='az225_el70', azimuth_deg=225, elevation_deg=70, radius_m=0.4, x=-0.09673790505919017, y=-0.09673790505919014, z=0.37587704831436336), CameraPose(id='az270_el70', azimuth_deg=270, elevation_deg=70, radius_m=0.4, x=-2.513123242606196e-17, y=-0.13680805733026755, z=0.37587704831436336), CameraPose(id='az315_el70', azimuth_deg=315, elevation_deg=70, radius_m=0.4, x=0.09673790505919012, y=-0.09673790505919017, z=0.37587704831436336)]#
Default 24-pose hemisphere grid (8 azimuth × 3 elevation).
- class so101_mvbench.utils.camera_grid.CameraPose(id, azimuth_deg, elevation_deg, radius_m, x, y, z)[source]#
Bases:
objectA single camera position on the hemisphere.
- Parameters:
- id#
Unique identifier, e.g.
az045_el45.
- azimuth_deg#
Azimuth angle in degrees (0 = +X, 90 = +Y).
- elevation_deg#
Elevation angle in degrees above horizontal.
- radius_m#
Distance from look-at target in meters.
- x#
World-frame X coordinate in meters.
- y#
World-frame Y coordinate in meters.
- z#
World-frame Z coordinate in meters.
- so101_mvbench.utils.camera_grid.DEFAULT_AZIMUTHS_DEG: tuple[float, ...] = (0, 45, 90, 135, 180, 225, 270, 315)#
Azimuth angles for the coarse grid (degrees, 0 = +X axis).
- so101_mvbench.utils.camera_grid.DEFAULT_ELEVATIONS_DEG: tuple[float, ...] = (20, 45, 70)#
Elevation angles for the coarse grid (degrees above horizontal).
- so101_mvbench.utils.camera_grid.DEFAULT_RADIUS_M: float = 0.4#
Distance from look-at target to each camera position in meters.
- so101_mvbench.utils.camera_grid.WORKSPACE_CENTER: tuple[float, float, float] = (0.0, 0.0, 0.0)#
Default center for hemisphere grid — workspace frame {W} origin.
- so101_mvbench.utils.camera_grid.generate_hemisphere_grid(azimuths_deg=(0, 45, 90, 135, 180, 225, 270, 315), elevations_deg=(20, 45, 70), radius_m=0.4, center=(0.0, 0.0, 0.0))[source]#
Generate a list of camera poses on a hemisphere.
- Parameters:
- Return type:
- Returns:
List of
CameraPoseobjects, sorted by (elevation, azimuth).
- so101_mvbench.utils.camera_grid.get_pose_by_id(pose_id)[source]#
Look up a camera pose by its ID.
- so101_mvbench.utils.camera_grid.get_pose_world_position(pose, hemisphere_origin=(0.2, 0.0, 0.12))[source]#
Compute world-frame position of a hemisphere pose.
Pure math, no Isaac Sim. Returns
(pose.x + wx, pose.y + wy, pose.z + wz)wherepose.{x,y,z}is the {W}-frame offset and(wx, wy, wz)is the look-at target in world frame {0} (≡ ⁰ξ_W).Used by matplotlib visualizers (no sim) and any caller that needs the world-frame eye position without applying it to a camera asset.
- Parameters:
pose (
CameraPose) – Hemisphere camera pose (in workspace frame {W}).hemisphere_origin (
tuple[float,float,float]) – ⁰ξ_W transform (world frame) — point all cameras aim at. Coincides with workspace frame {W} origin.
- Return type:
- Returns:
Tuple
(x, y, z)in world frame, meters.
- so101_mvbench.utils.camera_grid.make_extra_poses(radius_m=0.4)[source]#
Construct all non-grid singleton poses at radius_m.
Returns the top-down singularity and the el=-5° near-ground poses, scaled to the requested radius. Callers that regenerate the hemisphere grid at runtime (e.g.
external_batch_recorder --radius) merge this into their lookup table alongsidegenerate_hemisphere_grid()so every documented pose-id resolves.- Return type:
- Parameters:
radius_m (float)
- so101_mvbench.utils.camera_grid.parse_elevations_from_pose_ids(pose_ids)[source]#
Extract the unique elevation angles (deg) from a list of pose IDs.
Pose IDs follow
az{azimuth:03d}_el{elevation:02d}(negatives with a leading dash, e.g.az000_el-05). Used by callers that regenerate the hemisphere grid at runtime so they don’t need a separate CLI flag for elevations — the IDs are self-describing.
- so101_mvbench.utils.camera_grid.resolve_poses(pose_ids, radius_m=0.4)[source]#
Resolve a list of pose-IDs to
CameraPoseobjects at radius_m.Elevations are auto-derived from the pose-IDs themselves (
parse_elevations_from_pose_ids()). The hemisphere grid is regenerated at runtime so any well-formed combination of azimuth/ elevation resolves consistently — including the singleton extras (top-down at el=90°, near-ground at el=-5°).This is the recommended API for callers that take a user-supplied list of pose-IDs (recorders, batch tools). For ad-hoc lookups of a single pose at DEFAULT_RADIUS_M, use
get_pose_by_id().- Parameters:
- Return type:
- Returns:
Camera poses in the same order as pose_ids.
- Raises:
ValueError – If any pose ID cannot be resolved at radius_m.
- so101_mvbench.utils.camera_grid.set_external_camera_pose(env, pose, hemisphere_origin=(0.2, 0.0, 0.12), camera_name='camera_external', up_world=None)[source]#
Set a SINGLE pose, broadcast to ALL envs via env_origins.
Use for single-pose runs (hemisphere_screenshot, trajectory_replayer, eval/camera_override). All envs render the SAME camera position relative to their own env_origin — Isaac Lab’s broadcast semantics on
set_world_poses_from_view((1,3), (1,3))against an N-env scene.- Parameters:
env – Gym-wrapped Isaac Lab env (must have
unwrapped.scene[camera_name]).pose (
CameraPose|str) – CameraPose object or pose ID string (e.g."az000_el45").hemisphere_origin (
tuple[float,float,float]) – ⁰ξ_W transform (world frame) — point all cameras aim at. Coincides with workspace frame {W} origin.camera_name (
str) – Scene asset key for the camera (default"camera_external").up_world (
tuple[float,float,float] |None) – Optional FIXED world up-vector. When given, the camera roll is derived from this constant up instead of the stage up-axis, so a camera swept along a meridian tilts continuously with no roll jump at the pole (used by the camera-drift ablation). DefaultNonekeeps the originalset_world_poses_from_viewbehaviour (backward compat).
- Returns:
Eye tensor of shape
(num_envs, 3)— for logging/debug.- Raises:
KeyError –
camera_namenot inenv.unwrapped.scene.ValueError –
poseis a string but not a known pose ID.
- so101_mvbench.utils.camera_grid.set_external_camera_poses(env, poses, hemisphere_origin=(0.2, 0.0, 0.12), camera_name='camera_external')[source]#
Set a DIFFERENT pose per env. Asserts
len(poses) == num_envs.Use for per-env permutation (external_batch_recorder runs N cameras simultaneously, env_i renders pose_i).
- Parameters:
env – Gym-wrapped Isaac Lab env (must have
unwrapped.scene[camera_name]).poses (
Sequence[CameraPose|str]) – List of CameraPose objects or pose ID strings. Length MUST equalenv.unwrapped.scene.num_envs.hemisphere_origin (
tuple[float,float,float]) – ⁰ξ_W transform (world frame) — point all cameras aim at. Coincides with workspace frame {W} origin.camera_name (
str) – Scene asset key for the camera (default"camera_external").
- Returns:
Eye tensor of shape
(num_envs, 3)— for logging/debug.- Raises:
KeyError –
camera_namenot inenv.unwrapped.scene.ValueError –
len(poses) != num_envsor unknown pose ID string.
- so101_mvbench.utils.camera_grid.spherical_to_cartesian(azimuth_deg, elevation_deg, radius_m, center)[source]#
Convert spherical coordinates to Cartesian world coordinates.
- Parameters:
- Return type:
- Returns:
(x, y, z) camera position in meters.
Event-reset terms for LiftCube tasks — visual / spatial randomization.
All functions here have the standard Isaac Lab reset-term signature
func(env, env_ids, **params) and are wired into a LiftCubeEventCfg
via EventTermCfg(func=randomize_X, mode=”reset”, params={…}).
Multi-Env-safe prim resolver: resolve_all_prim_paths from utils.scene_builder.
- so101_mvbench.mdp.resets.randomize_cube_color(env, env_ids, color_list, asset_cfg=None)[source]#
Randomize cube color from a discrete palette.
Shader attribute:
inputs:diffuseColor(UsdPreviewSurface from CuboidCfg). Applies to ALL env instances (Multi-Env safe).
- so101_mvbench.mdp.resets.randomize_light_exposure(env, env_ids, exposure_range, asset_cfg=None)[source]#
Randomize light exposure via USD attribute (inputs:exposure).
Applies to ALL env instances (Multi-Env safe).
- so101_mvbench.mdp.resets.randomize_robot_color(env, env_ids, color_list)[source]#
Randomize robot mesh color from a discrete palette.
Shader attribute:
inputs:diffuse_color_constant(PreviewSurface). Path/Looks/material_a_3d_printed/Shaderis specific to the SO-ARM101 USD. Applies to ALL env instances (Multi-Env safe).
- so101_mvbench.mdp.resets.randomize_static_asset_yaw(env, env_ids, yaw_range, asset_cfg=None)[source]#
Randomize asset yaw (Z-rotation) via USD orient XformOp.
Sets only the orient op, preserving translate. Uses Gf.Quatd (WXYZ) directly to avoid XYZW/WXYZ confusion between isaaclab and isaacsim APIs.
Applies to ALL env instances (Multi-Env safe).
Core#
Runtime settings: where user data lives.
Replaces the thesis workspace’s ws_utils.get_workspace_root(). A public user
has no “workspace” – they have a data directory they choose. Everything the
package ships (USD assets, scene configs, eval.json) is resolved relative to
__file__ instead and never goes through here.
Usage:
from so101_mvbench.settings import SETTINGS
root = SETTINGS.require_datasets_root() # fails loud with the env var name
out = SETTINGS.outputs_root / "my_run"
Override per process:
SO101_BENCH_DATA_ROOT=/mnt/data so101-train ...
Override in a test:
Settings.from_env(data_root=tmp_path)
- class so101_mvbench.settings.Settings(data_root, datasets_root, outputs_root, log_file)[source]#
Bases:
objectWhere user data lives. Immutable; build a new one to change it.
- __init__(data_root, datasets_root, outputs_root, log_file)#
- classmethod from_env(**overrides)[source]#
Build from environment variables, with keyword overrides taking priority.
Logging with millisecond timestamps and an optional file mirror.
Why this exists rather than bare logging.basicConfig: under Isaac Lab, the
Kit engine redirects stdout, so print() and console handlers can silently
go nowhere. The file sink is therefore the primary channel, exactly as in the
thesis pipeline this package was extracted from.
Entry points call configure_root_logger() once; library modules only ever
call get_logger().
Set SO101_BENCH_LOG_FILE to mirror everything into a file.
- so101_mvbench.logging.configure_root_logger(level=20)[source]#
Configure the root logger so third-party loggers inherit the handlers.