Source code for so101_mvbench.evaluation.multienv_obs_utils
"""Multi-env eval helpers — per-env obs slicing + per-env tracker init.
Role in the data flow:
- Data flow 3 (per-env inference): :func:`slice_visual_obs` fixes **Risk K-1** —
the upstream ``sim_obs_to_policy_processor`` internally indexes ``visual[...][0]``,
i.e. without slicing ALL envs would see env-0's image.
- Data flow 5 (per-env tracking): :func:`init_trackers` instantiates the existing
``LiftCubeEvalTracker`` N 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``).
"""
from __future__ import annotations
from typing import Any
import torch
from so101_mvbench.evaluation.eval_tracker import LiftCubeEvalTracker
__version__ = "0.1.0"
[docs]
def slice_visual_obs(visual_obs: dict[str, Any], env_idx: int) -> dict[str, Any]:
"""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 upstream ``sim_obs_to_policy_processor`` internally grabs ``visual[...][0]``;
to feed environment ``env_idx`` we slice each camera tensor to
``[env_idx:env_idx+1]`` (the length-1 batch dim is preserved) — the downstream
``[0]`` then selects exactly ``env_idx``'s image instead of always env 0.
Args:
visual_obs: Dict camera-key -> tensor ``(num_envs, ...)``.
env_idx: Environment index to extract.
Returns:
Dict camera-key -> tensor ``(1, ...)`` (view/slice, not a copy).
"""
return {key: cam[env_idx : env_idx + 1] for key, cam in visual_obs.items()}
[docs]
def init_trackers(
n_envs: int,
cube: Any,
robot: Any,
jaw_body_idx: int,
home_pose_rad: list[float],
*,
home_pose_tol_rad: float | None = None,
plateau_steps: dict[str, int] | None = None,
) -> list[LiftCubeEvalTracker]:
"""Build one ``LiftCubeEvalTracker`` per environment (ADR-003).
Mirrors the single-env construction in ``eval_session.setup_episode`` but indexes
each Isaac tensor per environment ``[i]`` instead of ``[0]``.
Args:
n_envs: Number of parallel environments.
cube: Isaac RigidObject (``cube.data.root_link_pos_w`` shape ``(N, 3+)``).
robot: Isaac Articulation (``robot.data.body_link_pos_w`` shape ``(N, B, 3)``).
jaw_body_idx: Body index of the gripper jaw.
home_pose_rad: Target home pose (>=5 arm joints).
home_pose_tol_rad: Per-joint home tolerance (None -> tracker default).
plateau_steps: Per-stage plateau early-termination budgets.
Returns:
List of ``n_envs`` independent trackers, each seeded from its env's initial
cube height + gripper->cube distance.
"""
trackers: list[LiftCubeEvalTracker] = []
for i in range(n_envs):
cube_xyz = cube.data.root_link_pos_w[i, :3]
gripper_xyz = robot.data.body_link_pos_w[i, jaw_body_idx, :3]
initial_dist = torch.norm(gripper_xyz - cube_xyz).item()
trackers.append(
LiftCubeEvalTracker(
cube_xyz[2].item(),
initial_dist,
home_pose_rad,
home_pose_tol_rad=home_pose_tol_rad,
plateau_steps=plateau_steps,
)
)
return trackers