"""Collection settings and the collector's environment configuration.
The environment is the task's official configuration (``bigym.loco.make``)
with three collection-only changes: a longer success hold, an episode cap
that never binds, and event progress recorded alongside each step.
Everything else here configures the operator's session, not the env.
"""
from __future__ import annotations
import sys
import time
from dataclasses import dataclass
from pathlib import Path
from typing import Literal, Sequence
import tyro
from bigym.loco.config import EnvConfig
from bigym.loco.objref import path_name
from bigym.loco.tasks import task_config
# The collection cap in recorded steps when the task budget is shorter:
# 2 min at 50 Hz. Training budgets are derived from the demos afterwards
# (bigym.loco.tasks.BUDGET_RULE), so collection must never be cut short.
COLLECT_MIN_OUTER_STEPS = 6000
[docs]
@dataclass(frozen=True)
class CollectConfig:
"""One VR collection session (bigym-collect); each field is a flag."""
task: str
"""Task name, e.g. move_plate."""
out_dir: Path | None = None
"""Batch directory for the .npz episodes; None writes
./bigym_demos/<task>/<timestamp>. An existing batch is resumed."""
episodes: int = 60
"""Stop after this many saved demos (60 is the published batch size)."""
seed: int = 1
"""Reset seed of the first attempt; each attempt adds one."""
collect_success_hold_seconds: float = 3.0
"""How long the success predicate must hold before an attempt succeeds.
It is longer than the training hold, so every demo ends with a stay-still
tail, and the training view is cut back to the env's hold."""
episode_steps: int | None = None
"""Episode cap in recorded steps; None uses the task budget, but at
least 6000 steps (2 min at 50 Hz)."""
episode_seconds: float | None = None
"""Episode cap in seconds, instead of episode_steps."""
max_steps: int | None = None
"""Optional cap on steps per attempt; cannot extend the episode cap."""
keep_empty_session: bool = False
"""Keep the batch dir when no demo was saved."""
keep_black_rgb: bool = False
"""Save episodes whose rgb_obs is all zero instead of rejecting them."""
store_event_progress: bool = True
"""Record event_progress per step."""
store_fullbody: bool = True
"""Record full-body diagnostics (qpos, qvel, controller commands) per
step next to the training action."""
yaw_mode: Literal["base", "none"] = "base"
"""Right stick X: base turns the robot (controller wz), none ignores it."""
base_vx_scale: float = 0.35
"""Left stick Y to forward velocity (m/s)."""
base_vy_scale: float = 0.25
"""Left stick X to lateral velocity (m/s)."""
base_wz_scale: float = 0.5
"""Right stick X to turn rate (rad/s)."""
base_z_scale: float = 0.004
"""Right stick Y to height-command change per step (m)."""
height_cmd_max: float = 0.80
"""Cap on the integrated height command (m); above ~0.8 the controller
locks the knees and stops stepping. 0 disables the cap."""
pitch_rate: float = 0.8
"""Torso pitch change per second at full stick in pitch mode (rad/s)."""
stick_deadzone: float = 0.15
"""Thumbstick deadzone."""
base_cmd_slew: float | None = 0.7
"""Max change per second of the velocity commands; None maps the stick
directly. Recorded actions carry the slewed commands."""
recenter_height_offset: float = 0.0
"""Extra z offset when aligning the headset to the robot head camera."""
vr_space_mode: Literal["follow_head", "fixed"] = "follow_head"
"""follow_head keeps the view attached to the robot head as it walks;
fixed keeps the recentered world frame."""
settle_view: Literal["curtain", "live"] = "curtain"
"""Headset view during the reset warmup: a still dark frame, or the
moving scene for debugging."""
hud: Literal["minimal", "full", "off"] = "minimal"
"""In-headset stats overlay."""
resolution: Literal["lq", "mq", "hq"] = "lq"
"""Headset render resolution."""
mujoco_gl: Literal["glfw", "egl", "osmesa"] = "glfw"
"""MUJOCO_GL, set before MuJoCo is imported."""
openxr_log_level: Literal["trace", "debug", "info", "warn", "error"] = "error"
"""OpenXR runtime log level."""
spectator: Literal["none", "mujoco", "viser", "both"] = "none"
"""Desktop views of the session: a MuJoCo window, a viser web page, or
both. A MuJoCo window shares the headset stream's GPU and can stutter it."""
spectator_hz: float = 15.0
"""viser update rate."""
spectator_port: int = 8080
"""viser HTTP port."""
spectator_gpu: int | None = None
"""EGL device for the viser camera renders (multi-GPU machines)."""
frame_spike_ms: float = 20.0
"""Frames whose work exceeds this are logged to frame_timing.jsonl;
0 disables timing."""
operator: str | None = None
"""Who is collecting (default: $BIGYM_OPERATOR, then the login name)."""
export_lerobot: bool = False
"""After the session, write the training view and export it as a
LeRobot v3 dataset to <training view>_lerobot."""
task_text: str | None = None
"""Language instruction for the LeRobot export; None looks the task up
in bigym/loco/demos/task_instructions.yaml."""
def parse_cli(argv: Sequence[str] | None = None) -> CollectConfig:
"""Parse ``bigym-collect`` flags into a :class:`CollectConfig`."""
args = list(sys.argv[1:] if argv is None else argv)
return tyro.cli(CollectConfig, args=args, prog="bigym-collect")
def collection_env_config(config: CollectConfig) -> tuple[EnvConfig, EnvConfig]:
"""The env a session records in, and the configuration it trains for.
Returns:
``(collection, training)``: training is the task's official
configuration; collection differs from it only in the success hold,
the episode cap and event progress.
"""
training = task_config(config.task)
dsr = int(training.demo_down_sample_rate)
outer_hz = 500.0 / dsr
if config.episode_steps is not None and config.episode_seconds is not None:
raise ValueError("pass only one of episode_steps and episode_seconds")
if config.episode_steps is not None:
steps = int(config.episode_steps)
elif config.episode_seconds is not None:
steps = int(round(float(config.episode_seconds) * outer_hz))
else:
steps = max(int(training.episode_length or 0) // dsr, COLLECT_MIN_OUTER_STEPS)
if steps <= 0:
raise ValueError(f"episode steps must be positive, got {steps}")
collection = training.override(
episode_length=steps * dsr,
success_hold_seconds=float(config.collect_success_hold_seconds),
event_progress_enabled=bool(config.store_event_progress),
)
return collection, training
def default_out_dir(task: str) -> Path:
"""``./bigym_demos/<path_name(task)>/<timestamp>``."""
return Path.cwd() / "bigym_demos" / path_name(task) / time.strftime("%Y%m%d_%H%M%S")