From d7ac1aefd115f7f1e0b851444be9b90850a4b9b3 Mon Sep 17 00:00:00 2001 From: Codex Date: Mon, 22 Jun 2026 15:34:34 +0800 Subject: [PATCH 01/30] test: add labutopia poc metric tests --- tests/labutopia_poc/test_labutopia_metrics.py | 315 ++++++++++++++++++ 1 file changed, 315 insertions(+) create mode 100644 tests/labutopia_poc/test_labutopia_metrics.py diff --git a/tests/labutopia_poc/test_labutopia_metrics.py b/tests/labutopia_poc/test_labutopia_metrics.py new file mode 100644 index 0000000..04ee14b --- /dev/null +++ b/tests/labutopia_poc/test_labutopia_metrics.py @@ -0,0 +1,315 @@ +import numpy as np +import importlib +import sys +import types + + +class _StubGeometry: + def __init__(self, *args, **kwargs): + pass + + def buffer(self, *args, **kwargs): + return self + + def contains(self, *args, **kwargs): + return False + + +class _StubCollisionManager: + def add_object(self, *args, **kwargs): + pass + + def in_collision_internal(self, *args, **kwargs): + return False + + +def _ensure_module(name): + module = sys.modules.setdefault(name, types.ModuleType(name)) + if "." in name: + parent_name, child_name = name.rsplit(".", 1) + parent = _ensure_module(parent_name) + if not hasattr(parent, child_name): + setattr(parent, child_name, module) + return module + + +def _set_missing_attr(module, name, value): + if not hasattr(module, name): + setattr(module, name, value) + + +def _install_open3d_stub(): + open3d = _ensure_module("open3d") + geometry = _ensure_module("open3d.geometry") + utility = _ensure_module("open3d.utility") + io = _ensure_module("open3d.io") + _set_missing_attr(open3d, "geometry", geometry) + _set_missing_attr(open3d, "utility", utility) + _set_missing_attr(open3d, "io", io) + _set_missing_attr(geometry, "TriangleMesh", _StubGeometry) + _set_missing_attr(geometry, "PointCloud", _StubGeometry) + _set_missing_attr(geometry, "LineSet", _StubGeometry) + _set_missing_attr(geometry, "AxisAlignedBoundingBox", _StubGeometry) + _set_missing_attr(geometry, "KDTreeSearchParamHybrid", _StubGeometry) + _set_missing_attr(utility, "Vector3dVector", lambda value=None: value) + _set_missing_attr(utility, "Vector2iVector", lambda value=None: value) + _set_missing_attr(io, "read_triangle_mesh", lambda *args, **kwargs: _StubGeometry()) + + +def _install_shapely_stub(): + shapely = _ensure_module("shapely") + shapely_geometry = _ensure_module("shapely.geometry") + shapely_affinity = _ensure_module("shapely.affinity") + shapely_base = _ensure_module("shapely.geometry.base") + shapely_vectorized = _ensure_module("shapely.vectorized") + _set_missing_attr(shapely, "geometry", shapely_geometry) + _set_missing_attr(shapely, "affinity", shapely_affinity) + _set_missing_attr(shapely, "vectorized", shapely_vectorized) + _set_missing_attr(shapely_geometry, "Point", _StubGeometry) + _set_missing_attr(shapely_geometry, "Polygon", _StubGeometry) + _set_missing_attr(shapely_geometry, "MultiPolygon", _StubGeometry) + _set_missing_attr(shapely_base, "BaseGeometry", _StubGeometry) + _set_missing_attr( + shapely_vectorized, "contains", lambda *args, **kwargs: np.array([]) + ) + _set_missing_attr( + shapely_affinity, "translate", lambda polygon, *args, **kwargs: polygon + ) + _set_missing_attr( + shapely_affinity, "rotate", lambda polygon, *args, **kwargs: polygon + ) + + +def _install_omni_stub(): + core = _ensure_module("omni.isaac.core") + articulations = _ensure_module("omni.isaac.core.articulations") + materials = _ensure_module("omni.isaac.core.materials") + objects = _ensure_module("omni.isaac.core.objects") + omni_pbr = _ensure_module("omni.isaac.core.materials.omni_pbr") + prims = _ensure_module("omni.isaac.core.prims") + robots = _ensure_module("omni.isaac.core.robots.robot") + sensor = _ensure_module("omni.isaac.sensor") + semantics_utils = _ensure_module("omni.isaac.core.utils.semantics") + prim_utils = _ensure_module("omni.isaac.core.utils.prims") + stage_utils = _ensure_module("omni.isaac.core.utils.stage") + types_utils = _ensure_module("omni.isaac.core.utils.types") + usd = _ensure_module("omni.usd") + _set_missing_attr(core, "World", _StubGeometry) + _set_missing_attr(articulations, "Articulation", _StubGeometry) + _set_missing_attr(materials, "PhysicsMaterial", _StubGeometry) + _set_missing_attr(objects, "VisualCuboid", _StubGeometry) + _set_missing_attr(omni_pbr, "OmniPBR", _StubGeometry) + _set_missing_attr(prims, "GeometryPrim", _StubGeometry) + _set_missing_attr(prims, "RigidPrim", _StubGeometry) + _set_missing_attr(prims, "XFormPrim", _StubGeometry) + _set_missing_attr(robots, "Robot", _StubGeometry) + _set_missing_attr(sensor, "Camera", _StubGeometry) + _set_missing_attr(types_utils, "JointsState", _StubGeometry) + _set_missing_attr(usd, "get_context", lambda *args, **kwargs: _StubGeometry()) + _set_missing_attr( + semantics_utils, "add_update_semantics", lambda *args, **kwargs: None + ) + for name in ( + "create_prim", + "delete_prim", + "get_prim_at_path", + "get_prim_parent", + "get_prim_path", + "is_prim_path_valid", + ): + _set_missing_attr(prim_utils, name, lambda *args, **kwargs: None) + _set_missing_attr( + stage_utils, "add_reference_to_stage", lambda *args, **kwargs: None + ) + _set_missing_attr(stage_utils, "get_current_stage", lambda *args, **kwargs: None) + + +def _install_concave_hull_stub(): + concave_hull = _ensure_module("concave_hull") + _set_missing_attr( + concave_hull, "concave_hull", lambda points, *args, **kwargs: points + ) + + +def _install_trimesh_stub(): + trimesh = _ensure_module("trimesh") + collision = _ensure_module("trimesh.collision") + _set_missing_attr(trimesh, "Trimesh", _StubGeometry) + _set_missing_attr(trimesh, "collision", collision) + _set_missing_attr(collision, "CollisionManager", _StubCollisionManager) + + +def _install_missing_pxr_symbol_stub(import_error): + message = str(import_error) + prefix = "cannot import name '" + suffix = "' from 'pxr'" + if not message.startswith(prefix) or suffix not in message: + return False + missing_symbol = message[len(prefix) :].split("'", 1)[0] + pxr = _ensure_module("pxr") + _set_missing_attr(pxr, missing_symbol, _ensure_module(f"pxr.{missing_symbol}")) + return True + + +def _install_stub_for_missing_module(missing_name): + installers = { + "concave_hull": _install_concave_hull_stub, + "omni": _install_omni_stub, + "open3d": _install_open3d_stub, + "shapely": _install_shapely_stub, + "trimesh": _install_trimesh_stub, + } + for prefix, installer in installers.items(): + if missing_name == prefix or missing_name.startswith(f"{prefix}."): + installer() + return True + return False + + +def _import_metric_extensions(max_attempts=12): + last_error = None + for _ in range(max_attempts): + try: + return importlib.import_module("genmanip.extensions.metrics") + except ModuleNotFoundError as exc: + last_error = exc + if exc.name is None or not _install_stub_for_missing_module(exc.name): + raise + sys.modules.pop("genmanip.extensions.metrics", None) + except ImportError as exc: + last_error = exc + if not _install_missing_pxr_symbol_stub(exc): + raise + sys.modules.pop("genmanip.extensions.metrics", None) + raise last_error + + +_import_metric_extensions() +from genmanip.core.metrics.utils import MetricFactory + + +class FakeObject: + def __init__(self, position): + self.position = np.array(position) + + def get_world_pose(self): + return self.position, np.array([1.0, 0.0, 0.0, 0.0]) + + +class FakeScene: + def __init__(self, object_list=None, articulation_list=None): + self.object_list = object_list or {} + self.articulation_list = articulation_list or {} + + +def step_metric(metric, scene, times=1): + for _ in range(times): + metric.update(scene) + return metric.status + + +def test_object_height_delta_uses_initial_pose_and_strict_threshold(): + obj = FakeObject([0.0, 0.0, 0.5]) + scene = FakeScene({"obj_conical_bottle02": obj}) + metric = MetricFactory.build( + "manip/labutopia/object_height_delta", + skip_steps=1, + succ_cnts=0, + sub_goal_setting={ + "obj_uid": "obj_conical_bottle02", + "axis": "z", + "min_delta": 0.125, + }, + ) + + assert step_metric(metric, scene) is False + obj.position[2] = 0.625 + assert step_metric(metric, scene) is False + obj.position[2] = 0.75 + assert step_metric(metric, scene) is True + + +def test_object_height_delta_requires_consecutive_success_frames(): + obj = FakeObject([0.0, 0.0, 0.5]) + scene = FakeScene({"obj_conical_bottle02": obj}) + metric = MetricFactory.build( + "manip/labutopia/object_height_delta", + skip_steps=1, + succ_cnts=2, + sub_goal_setting={ + "obj_uid": "obj_conical_bottle02", + "axis": "z", + "min_delta": 0.125, + }, + ) + + assert step_metric(metric, scene) is False + obj.position[2] = 0.75 + assert step_metric(metric, scene) is False + obj.position[2] = 0.5 + assert step_metric(metric, scene) is False + obj.position[2] = 0.75 + assert step_metric(metric, scene) is False + assert step_metric(metric, scene) is False + assert step_metric(metric, scene) is True + + +def test_object_at_target_uses_radial_xy_and_initial_z(): + initial_z = 0.5 + obj = FakeObject([0.25, 0.25, initial_z]) + target = FakeObject([0.5, 0.5, 0.0]) + scene = FakeScene( + { + "obj_beaker2": obj, + "obj_target_plat": target, + } + ) + metric = MetricFactory.build( + "manip/labutopia/object_at_target", + skip_steps=1, + succ_cnts=0, + sub_goal_setting={ + "obj_uid": "obj_beaker2", + "target_uid": "obj_target_plat", + "xy_radius": 0.25, + "z_tolerance": 0.125, + }, + ) + + assert step_metric(metric, scene) is False + obj.position = np.array([0.6875, 0.6875, initial_z]) + assert step_metric(metric, scene) is False + obj.position = np.array([0.75, 0.5, initial_z]) + assert step_metric(metric, scene) is False + obj.position = np.array([0.5, 0.5, 0.625]) + assert step_metric(metric, scene) is False + obj.position = np.array([0.625, 0.5, initial_z]) + assert step_metric(metric, scene) is True + obj.position = np.array([0.5, 0.5, initial_z + 0.25]) + assert step_metric(metric, scene) is False + + +def test_handle_displacement_uses_initial_pose_and_distance_threshold(): + obj = FakeObject([0.0, 0.0, 0.0]) + scene = FakeScene({"obj_DryingBox_01_handle": obj}) + metric = MetricFactory.build( + "manip/labutopia/handle_displacement", + skip_steps=1, + succ_cnts=0, + sub_goal_setting={ + "obj_uid": "obj_DryingBox_01_handle", + "min_distance": 0.125, + }, + ) + + assert step_metric(metric, scene) is False + obj.position = np.array([0.0625, 0.0625, 0.0]) + assert step_metric(metric, scene) is False + obj.position = np.array([0.09375, 0.0625, 0.0]) + assert step_metric(metric, scene) is False + obj.position = np.array([0.125, 0.0, 0.0]) + assert step_metric(metric, scene) is False + obj.position = np.array([0.09375, 0.09375, 0.0]) + assert step_metric(metric, scene) is True + obj.position = np.array([0.25, 0.0, 0.0]) + assert step_metric(metric, scene) is True From f4114c46b4f587ccfd7e892a66783df0b3d0f70a Mon Sep 17 00:00:00 2001 From: Codex Date: Mon, 22 Jun 2026 16:03:51 +0800 Subject: [PATCH 02/30] feat: add labutopia poc metrics --- .../extensions/metrics/labutopia/__init__.py | 1 + .../metrics/labutopia/object_metrics.py | 156 ++++++++++++++++++ 2 files changed, 157 insertions(+) create mode 100644 genmanip/extensions/metrics/labutopia/__init__.py create mode 100644 genmanip/extensions/metrics/labutopia/object_metrics.py diff --git a/genmanip/extensions/metrics/labutopia/__init__.py b/genmanip/extensions/metrics/labutopia/__init__.py new file mode 100644 index 0000000..bad8ccb --- /dev/null +++ b/genmanip/extensions/metrics/labutopia/__init__.py @@ -0,0 +1 @@ +"""LabUtopia metric extensions.""" diff --git a/genmanip/extensions/metrics/labutopia/object_metrics.py b/genmanip/extensions/metrics/labutopia/object_metrics.py new file mode 100644 index 0000000..4f5b0fa --- /dev/null +++ b/genmanip/extensions/metrics/labutopia/object_metrics.py @@ -0,0 +1,156 @@ +from typing import Any, Literal + +import numpy as np +from pydantic import BaseModel, Field, field_validator + +from genmanip.core.metrics.base import BaseMetric +from genmanip.core.metrics.utils import MetricFactory + + +AxisName = Literal["x", "y", "z"] + + +def _position(scene, obj_uid: str) -> np.ndarray: + if obj_uid not in scene.object_list: + raise KeyError(f"Object '{obj_uid}' not found in scene.object_list") + pose = scene.object_list[obj_uid].get_world_pose() + return np.asarray(pose[0], dtype=float) + + +class ObjectHeightDeltaConfig(BaseModel): + obj_uid: str = Field(..., description="UID of the object to track") + axis: AxisName = Field(default="z", description="Axis to measure") + min_delta: float = Field(..., description="Minimum positive displacement") + + @field_validator("min_delta") + @classmethod + def validate_min_delta(cls, value): + if value <= 0: + raise ValueError("min_delta must be positive") + return value + + +class ObjectAtTargetConfig(BaseModel): + obj_uid: str = Field(..., description="UID of the object to track") + target_uid: str = Field(..., description="UID of the target object") + xy_radius: float = Field(..., description="Maximum radial XY distance") + z_tolerance: float = Field(..., description="Maximum distance from initial z") + + @field_validator("xy_radius", "z_tolerance") + @classmethod + def validate_positive_threshold(cls, value): + if value <= 0: + raise ValueError("thresholds must be positive") + return value + + +class HandleDisplacementConfig(BaseModel): + obj_uid: str = Field(..., description="UID of the handle object to track") + min_distance: float = Field(..., description="Minimum positive displacement") + + @field_validator("min_distance") + @classmethod + def validate_min_distance(cls, value): + if value <= 0: + raise ValueError("min_distance must be positive") + return value + + +@MetricFactory.register("manip/labutopia/object_height_delta") +class ObjectHeightDelta(BaseMetric): + def __init__( + self, + skip_steps: int = 1, + succ_cnts: int = 0, + sub_goal_setting: dict[str, Any] = {}, + **kwargs, + ): + super().__init__(skip_steps, succ_cnts, sub_goal_setting, **kwargs) + self.setting = ObjectHeightDeltaConfig(**sub_goal_setting) + self._initial_position = None + + def check_status(self, scene) -> bool: + current = _position(scene, self.setting.obj_uid) + if self._initial_position is None: + self._initial_position = current.copy() + + axis_idx = {"x": 0, "y": 1, "z": 2}[self.setting.axis] + delta = current[axis_idx] - self._initial_position[axis_idx] + return bool(delta > self.setting.min_delta) + + def get_info(self): + return { + "setting": self.setting.model_dump(), + "initial_position": ( + None + if self._initial_position is None + else self._initial_position.tolist() + ), + } + + +@MetricFactory.register("manip/labutopia/object_at_target") +class ObjectAtTarget(BaseMetric): + def __init__( + self, + skip_steps: int = 1, + succ_cnts: int = 0, + sub_goal_setting: dict[str, Any] = {}, + **kwargs, + ): + super().__init__(skip_steps, succ_cnts, sub_goal_setting, **kwargs) + self.setting = ObjectAtTargetConfig(**sub_goal_setting) + self._initial_z = None + + def check_status(self, scene) -> bool: + current = _position(scene, self.setting.obj_uid) + target = _position(scene, self.setting.target_uid) + if self._initial_z is None: + self._initial_z = float(current[2]) + + xy_dist = np.linalg.norm(current[:2] - target[:2]) + z_dist = abs(current[2] - self._initial_z) + return bool( + xy_dist < self.setting.xy_radius + and z_dist < self.setting.z_tolerance + ) + + def get_info(self): + return { + "setting": self.setting.model_dump(), + "initial_z": self._initial_z, + } + + +@MetricFactory.register("manip/labutopia/handle_displacement") +class HandleDisplacement(BaseMetric): + def __init__( + self, + skip_steps: int = 1, + succ_cnts: int = 0, + sub_goal_setting: dict[str, Any] = {}, + **kwargs, + ): + super().__init__(skip_steps, succ_cnts, sub_goal_setting, **kwargs) + self.setting = HandleDisplacementConfig(**sub_goal_setting) + self._initial_position = None + + def check_status(self, scene) -> bool: + current = _position(scene, self.setting.obj_uid) + if self._initial_position is None: + self._initial_position = current.copy() + + return bool( + np.linalg.norm(current - self._initial_position) + > self.setting.min_distance + ) + + def get_info(self): + return { + "setting": self.setting.model_dump(), + "initial_position": ( + None + if self._initial_position is None + else self._initial_position.tolist() + ), + } From 60c04368cda669fea7e197a1de68ea3932f6c6b4 Mon Sep 17 00:00:00 2001 From: Codex Date: Mon, 22 Jun 2026 16:13:04 +0800 Subject: [PATCH 03/30] feat: add labutopia asset overlay builder --- .../labutopia_poc/build_asset_overlay.py | 177 ++++++++++++++++++ .../labutopia_poc/test_build_asset_overlay.py | 57 ++++++ 2 files changed, 234 insertions(+) create mode 100644 standalone_tools/labutopia_poc/build_asset_overlay.py create mode 100644 tests/labutopia_poc/test_build_asset_overlay.py diff --git a/standalone_tools/labutopia_poc/build_asset_overlay.py b/standalone_tools/labutopia_poc/build_asset_overlay.py new file mode 100644 index 0000000..9638111 --- /dev/null +++ b/standalone_tools/labutopia_poc/build_asset_overlay.py @@ -0,0 +1,177 @@ +"""Build the LabUtopia level-1 proof-of-concept asset overlay.""" + +from __future__ import annotations + +import argparse +import hashlib +import json +import shutil +from pathlib import Path + + +DEFAULT_LABUTOPIA_ROOT = Path("/cpfs/shared/simulation/zhuzihou/dev/LabUtopia") +DEFAULT_OVERLAY_ROOT = Path( + "/cpfs/shared/simulation/zhuzihou/dev/_datasets/" + "EBench-Assets-Overlay/labutopia_level1_poc/assets" +) +SOURCE_SCENE_RELATIVE = Path("assets/chemistry_lab/lab_001/lab_001.usd") +SOURCE_DIR_RELATIVE = SOURCE_SCENE_RELATIVE.parent +OVERLAY_SCENE_RELATIVE = Path("scene_usds/labutopia/level1_poc/lab_001") +USD_NAME = "scene_usds/labutopia/level1_poc/lab_001/scene" +MANIFEST_RELATIVE = Path("manifests/labutopia_level1_poc.json") + +PRIM_RENAME_MAP = { + "conical_bottle02": "obj_conical_bottle02", + "beaker2": "obj_beaker2", + "target_plat": "obj_target_plat", + "DryingBox_01_handle": "obj_DryingBox_01_handle", +} +TASK_PRIMS = { + "level1_pick": { + "object_uid": "obj_conical_bottle02", + "prim_path": "/World/_scene/obj_conical_bottle02", + }, + "level1_place": { + "object_uid": "obj_beaker2", + "target_uid": "obj_target_plat", + "object_prim_path": "/World/_scene/obj_beaker2", + "target_prim_path": "/World/_scene/obj_target_plat", + }, + "level1_open_door": { + "object_uid": "obj_DryingBox_01_handle", + "prim_path": "/World/_scene/obj_DryingBox_01_handle", + }, +} +REQUIRED_GENMANIP_OBJECT_UIDS = [ + "obj_conical_bottle02", + "obj_beaker2", + "obj_target_plat", + "obj_DryingBox_01_handle", +] +TABLE_UID = "table" + + +def _sha256(path: Path) -> str: + digest = hashlib.sha256() + with path.open("rb") as file: + for chunk in iter(lambda: file.read(1024 * 1024), b""): + digest.update(chunk) + return digest.hexdigest() + + +def _write_scene_wrapper(path: Path) -> None: + path.write_text( + """#usda 1.0 +( + defaultPrim = "World" +) + +def Xform "World" +{ + def Xform "_scene" ( + prepend payload = @scene.usd@ + ) + { + } +} +""", + encoding="utf-8", + ) + + +def _copied_files(overlay_root: Path, paths: list[Path]) -> list[dict[str, object]]: + entries = [] + for path in sorted(paths): + entries.append( + { + "relative_path": path.relative_to(overlay_root).as_posix(), + "bytes": path.stat().st_size, + "sha256": _sha256(path), + } + ) + return entries + + +def build_asset_overlay( + labutopia_root: str | Path = DEFAULT_LABUTOPIA_ROOT, + overlay_root: str | Path = DEFAULT_OVERLAY_ROOT, +) -> dict[str, object]: + labutopia_root = Path(labutopia_root) + overlay_root = Path(overlay_root) + source_dir = labutopia_root / SOURCE_DIR_RELATIVE + source_scene = labutopia_root / SOURCE_SCENE_RELATIVE + if not source_scene.is_file(): + raise FileNotFoundError(source_scene) + + overlay_scene_dir = overlay_root / OVERLAY_SCENE_RELATIVE + if overlay_scene_dir.exists(): + shutil.rmtree(overlay_scene_dir) + overlay_scene_dir.parent.mkdir(parents=True, exist_ok=True) + shutil.copytree(source_dir, overlay_scene_dir) + + scene_usd = overlay_scene_dir / "scene.usd" + shutil.copy2(source_scene, scene_usd) + scene_usda = overlay_scene_dir / "scene.usda" + _write_scene_wrapper(scene_usda) + + copied_paths = [ + path + for path in overlay_scene_dir.rglob("*") + if path.is_file() and path.name != "scene.usda" + ] + manifest = { + "source_repo": str(labutopia_root), + "source_scene": str(source_scene), + "overlay_root": str(overlay_root), + "usd_name": USD_NAME, + "task_prims": TASK_PRIMS, + "prim_rename_map": PRIM_RENAME_MAP, + "table_uid": TABLE_UID, + "required_genmanip_object_uids": REQUIRED_GENMANIP_OBJECT_UIDS, + "copied_files": _copied_files(overlay_root, copied_paths), + "notes": [ + "This overlay does not rewrite the LabUtopia USD stage.", + "The obj_* names are the GenManip-facing contract; verify source prims before relying on automated rename assumptions.", + ], + } + + manifest_path = overlay_root / MANIFEST_RELATIVE + manifest_path.parent.mkdir(parents=True, exist_ok=True) + manifest_path.write_text( + json.dumps(manifest, indent=2, sort_keys=True) + "\n", + encoding="utf-8", + ) + return { + "overlay_scene_dir": str(overlay_scene_dir), + "scene_usd": str(scene_usd), + "scene_usda": str(scene_usda), + "manifest": str(manifest_path), + } + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser( + description="Build the LabUtopia level-1 proof-of-concept asset overlay." + ) + parser.add_argument( + "--labutopia-root", + type=Path, + default=DEFAULT_LABUTOPIA_ROOT, + help=f"LabUtopia checkout root. Default: {DEFAULT_LABUTOPIA_ROOT}", + ) + parser.add_argument( + "--overlay-root", + type=Path, + default=DEFAULT_OVERLAY_ROOT, + help=f"Overlay asset root. Default: {DEFAULT_OVERLAY_ROOT}", + ) + return parser.parse_args() + + +def main() -> None: + outputs = build_asset_overlay(**vars(parse_args())) + print(json.dumps(outputs, indent=2, sort_keys=True)) + + +if __name__ == "__main__": + main() diff --git a/tests/labutopia_poc/test_build_asset_overlay.py b/tests/labutopia_poc/test_build_asset_overlay.py new file mode 100644 index 0000000..32570c2 --- /dev/null +++ b/tests/labutopia_poc/test_build_asset_overlay.py @@ -0,0 +1,57 @@ +import hashlib +import json + +from standalone_tools.labutopia_poc.build_asset_overlay import build_asset_overlay + + +def test_build_asset_overlay_writes_scene_wrapper_manifest_and_cleans_reruns( + tmp_path, +): + labutopia_root = tmp_path / "LabUtopia" + source_dir = labutopia_root / "assets" / "chemistry_lab" / "lab_001" + source_dir.mkdir(parents=True) + scene_bytes = b"#usda 1.0\n\ndef Xform \"World\" {}\n" + (source_dir / "lab_001.usd").write_bytes(scene_bytes) + (source_dir / "SubUSDs").mkdir() + (source_dir / "SubUSDs" / "prop.usd").write_text("prop", encoding="utf-8") + + overlay_root = tmp_path / "overlay" / "assets" + build_asset_overlay(labutopia_root=labutopia_root, overlay_root=overlay_root) + + scene_dir = ( + overlay_root / "scene_usds" / "labutopia" / "level1_poc" / "lab_001" + ) + assert (scene_dir / "scene.usd").exists() + assert (scene_dir / "scene.usda").exists() + assert '@scene.usd@' in (scene_dir / "scene.usda").read_text( + encoding="utf-8" + ) + + manifest_path = overlay_root / "manifests" / "labutopia_level1_poc.json" + manifest = json.loads(manifest_path.read_text(encoding="utf-8")) + assert ( + manifest["usd_name"] + == "scene_usds/labutopia/level1_poc/lab_001/scene" + ) + assert set(manifest["task_prims"]) == { + "level1_pick", + "level1_place", + "level1_open_door", + } + assert manifest["prim_rename_map"]["conical_bottle02"] == "obj_conical_bottle02" + assert "obj_conical_bottle02" in manifest["required_genmanip_object_uids"] + + copied_scene = next( + item + for item in manifest["copied_files"] + if item["relative_path"] + == "scene_usds/labutopia/level1_poc/lab_001/scene.usd" + ) + assert copied_scene["bytes"] == len(scene_bytes) + assert copied_scene["sha256"] == hashlib.sha256(scene_bytes).hexdigest() + + stale_path = scene_dir / "stale.txt" + stale_path.write_text("remove me", encoding="utf-8") + build_asset_overlay(labutopia_root=labutopia_root, overlay_root=overlay_root) + + assert not stale_path.exists() From d322f4da3e5d6dc01731dfb4daf5766fa6402fad Mon Sep 17 00:00:00 2001 From: Codex Date: Mon, 22 Jun 2026 16:17:40 +0800 Subject: [PATCH 04/30] fix: guard labutopia overlay source recursion --- .../labutopia_poc/build_asset_overlay.py | 16 ++++++++++ .../labutopia_poc/test_build_asset_overlay.py | 29 +++++++++++++++++++ 2 files changed, 45 insertions(+) diff --git a/standalone_tools/labutopia_poc/build_asset_overlay.py b/standalone_tools/labutopia_poc/build_asset_overlay.py index 9638111..0aeb2d3 100644 --- a/standalone_tools/labutopia_poc/build_asset_overlay.py +++ b/standalone_tools/labutopia_poc/build_asset_overlay.py @@ -92,6 +92,21 @@ def _copied_files(overlay_root: Path, paths: list[Path]) -> list[dict[str, objec return entries +def _reject_overlay_scene_inside_source( + source_dir: Path, overlay_scene_dir: Path +) -> None: + resolved_source_dir = source_dir.resolve() + resolved_overlay_scene_dir = overlay_scene_dir.resolve() + try: + resolved_overlay_scene_dir.relative_to(resolved_source_dir) + except ValueError: + return + raise ValueError( + "Overlay scene directory must not be inside the LabUtopia source scene " + f"directory: {resolved_overlay_scene_dir} is within {resolved_source_dir}" + ) + + def build_asset_overlay( labutopia_root: str | Path = DEFAULT_LABUTOPIA_ROOT, overlay_root: str | Path = DEFAULT_OVERLAY_ROOT, @@ -104,6 +119,7 @@ def build_asset_overlay( raise FileNotFoundError(source_scene) overlay_scene_dir = overlay_root / OVERLAY_SCENE_RELATIVE + _reject_overlay_scene_inside_source(source_dir, overlay_scene_dir) if overlay_scene_dir.exists(): shutil.rmtree(overlay_scene_dir) overlay_scene_dir.parent.mkdir(parents=True, exist_ok=True) diff --git a/tests/labutopia_poc/test_build_asset_overlay.py b/tests/labutopia_poc/test_build_asset_overlay.py index 32570c2..2289a13 100644 --- a/tests/labutopia_poc/test_build_asset_overlay.py +++ b/tests/labutopia_poc/test_build_asset_overlay.py @@ -1,6 +1,9 @@ import hashlib import json +import pytest + +import standalone_tools.labutopia_poc.build_asset_overlay as build_overlay from standalone_tools.labutopia_poc.build_asset_overlay import build_asset_overlay @@ -55,3 +58,29 @@ def test_build_asset_overlay_writes_scene_wrapper_manifest_and_cleans_reruns( build_asset_overlay(labutopia_root=labutopia_root, overlay_root=overlay_root) assert not stale_path.exists() + + +def test_build_asset_overlay_rejects_overlay_scene_inside_source_dir( + tmp_path, monkeypatch +): + labutopia_root = tmp_path / "LabUtopia" + source_dir = labutopia_root / "assets" / "chemistry_lab" / "lab_001" + source_dir.mkdir(parents=True) + (source_dir / "lab_001.usd").write_text("#usda 1.0\n", encoding="utf-8") + + def fail_if_copytree_runs(source, destination): + raise AssertionError(f"copytree should not run for {source} -> {destination}") + + monkeypatch.setattr(build_overlay.shutil, "copytree", fail_if_copytree_runs) + + with pytest.raises(ValueError, match="inside the LabUtopia source scene directory"): + build_asset_overlay(labutopia_root=labutopia_root, overlay_root=source_dir) + + assert not ( + source_dir + / "scene_usds" + / "labutopia" + / "level1_poc" + / "lab_001" + / "scene_usds" + ).exists() From 434ed3f96c6825bcd8525ad4fded9d9dfff5a67e Mon Sep 17 00:00:00 2001 From: Codex Date: Mon, 22 Jun 2026 16:21:06 +0800 Subject: [PATCH 05/30] fix: align labutopia overlay manifest contract --- .../labutopia_poc/build_asset_overlay.py | 38 +++++++++---------- .../labutopia_poc/test_build_asset_overlay.py | 35 ++++++++++++++--- 2 files changed, 47 insertions(+), 26 deletions(-) diff --git a/standalone_tools/labutopia_poc/build_asset_overlay.py b/standalone_tools/labutopia_poc/build_asset_overlay.py index 0aeb2d3..cafffe0 100644 --- a/standalone_tools/labutopia_poc/build_asset_overlay.py +++ b/standalone_tools/labutopia_poc/build_asset_overlay.py @@ -21,32 +21,29 @@ MANIFEST_RELATIVE = Path("manifests/labutopia_level1_poc.json") PRIM_RENAME_MAP = { - "conical_bottle02": "obj_conical_bottle02", - "beaker2": "obj_beaker2", - "target_plat": "obj_target_plat", - "DryingBox_01_handle": "obj_DryingBox_01_handle", + "/World/conical_bottle02": "obj_conical_bottle02", + "/World/beaker2": "obj_beaker2", + "/World/target_plat": "obj_target_plat", + "/World/DryingBox_01": "obj_DryingBox_01", + "/World/DryingBox_01/handle": "obj_DryingBox_01_handle", + "/World/table": "obj_table", } TASK_PRIMS = { - "level1_pick": { - "object_uid": "obj_conical_bottle02", - "prim_path": "/World/_scene/obj_conical_bottle02", - }, - "level1_place": { - "object_uid": "obj_beaker2", - "target_uid": "obj_target_plat", - "object_prim_path": "/World/_scene/obj_beaker2", - "target_prim_path": "/World/_scene/obj_target_plat", - }, - "level1_open_door": { - "object_uid": "obj_DryingBox_01_handle", - "prim_path": "/World/_scene/obj_DryingBox_01_handle", - }, + "level1_pick": ["/World/conical_bottle02"], + "level1_place": ["/World/beaker2", "/World/target_plat"], + "level1_open_door": [ + "/World/DryingBox_01", + "/World/DryingBox_01/handle", + "/World/DryingBox_01/RevoluteJoint", + ], } REQUIRED_GENMANIP_OBJECT_UIDS = [ "obj_conical_bottle02", "obj_beaker2", "obj_target_plat", + "obj_DryingBox_01", "obj_DryingBox_01_handle", + "obj_table", ] TABLE_UID = "table" @@ -146,8 +143,9 @@ def build_asset_overlay( "required_genmanip_object_uids": REQUIRED_GENMANIP_OBJECT_UIDS, "copied_files": _copied_files(overlay_root, copied_paths), "notes": [ - "This overlay does not rewrite the LabUtopia USD stage.", - "The obj_* names are the GenManip-facing contract; verify source prims before relying on automated rename assumptions.", + "scene.usda payloads the raw LabUtopia scene under /World/_scene.", + "The builder does not rewrite source prim names.", + "GenManip runtime discovery still needs a rename/wrapper pass before smoke if raw prim names are retained.", ], } diff --git a/tests/labutopia_poc/test_build_asset_overlay.py b/tests/labutopia_poc/test_build_asset_overlay.py index 2289a13..a64fe92 100644 --- a/tests/labutopia_poc/test_build_asset_overlay.py +++ b/tests/labutopia_poc/test_build_asset_overlay.py @@ -36,13 +36,36 @@ def test_build_asset_overlay_writes_scene_wrapper_manifest_and_cleans_reruns( manifest["usd_name"] == "scene_usds/labutopia/level1_poc/lab_001/scene" ) - assert set(manifest["task_prims"]) == { - "level1_pick", - "level1_place", - "level1_open_door", + assert manifest["task_prims"] == { + "level1_pick": ["/World/conical_bottle02"], + "level1_place": ["/World/beaker2", "/World/target_plat"], + "level1_open_door": [ + "/World/DryingBox_01", + "/World/DryingBox_01/handle", + "/World/DryingBox_01/RevoluteJoint", + ], } - assert manifest["prim_rename_map"]["conical_bottle02"] == "obj_conical_bottle02" - assert "obj_conical_bottle02" in manifest["required_genmanip_object_uids"] + assert manifest["prim_rename_map"] == { + "/World/conical_bottle02": "obj_conical_bottle02", + "/World/beaker2": "obj_beaker2", + "/World/target_plat": "obj_target_plat", + "/World/DryingBox_01": "obj_DryingBox_01", + "/World/DryingBox_01/handle": "obj_DryingBox_01_handle", + "/World/table": "obj_table", + } + assert manifest["required_genmanip_object_uids"] == [ + "obj_conical_bottle02", + "obj_beaker2", + "obj_target_plat", + "obj_DryingBox_01", + "obj_DryingBox_01_handle", + "obj_table", + ] + assert manifest["notes"] == [ + "scene.usda payloads the raw LabUtopia scene under /World/_scene.", + "The builder does not rewrite source prim names.", + "GenManip runtime discovery still needs a rename/wrapper pass before smoke if raw prim names are retained.", + ] copied_scene = next( item From 5b118ba341ba65923a3564a43c3aa2a00966ea44 Mon Sep 17 00:00:00 2001 From: Codex Date: Mon, 22 Jun 2026 16:29:17 +0800 Subject: [PATCH 06/30] feat: add labutopia poc task package --- configs/cameras/labutopia_franka_poc.yml | 16 +++ .../common/assets_manifest.json | 21 ++++ .../common/task_semantics.yml | 99 +++++++++++++++++++ .../franka_poc/franka_poc.json | 5 + .../franka_poc/level1_open_door.yml | 45 +++++++++ .../franka_poc/level1_pick.yml | 46 +++++++++ .../franka_poc/level1_place.yml | 47 +++++++++ .../labutopia_lab_poc/labutopia_lab_poc.json | 4 + .../lift2_candidate/level1_open_door.yml | 45 +++++++++ .../lift2_candidate/level1_pick.yml | 46 +++++++++ .../lift2_candidate/level1_place.yml | 47 +++++++++ .../lift2_candidate/lift2_candidate.json | 5 + 12 files changed, 426 insertions(+) create mode 100644 configs/cameras/labutopia_franka_poc.yml create mode 100644 configs/tasks/ebench/labutopia_lab_poc/common/assets_manifest.json create mode 100644 configs/tasks/ebench/labutopia_lab_poc/common/task_semantics.yml create mode 100644 configs/tasks/ebench/labutopia_lab_poc/franka_poc/franka_poc.json create mode 100644 configs/tasks/ebench/labutopia_lab_poc/franka_poc/level1_open_door.yml create mode 100644 configs/tasks/ebench/labutopia_lab_poc/franka_poc/level1_pick.yml create mode 100644 configs/tasks/ebench/labutopia_lab_poc/franka_poc/level1_place.yml create mode 100644 configs/tasks/ebench/labutopia_lab_poc/labutopia_lab_poc.json create mode 100644 configs/tasks/ebench/labutopia_lab_poc/lift2_candidate/level1_open_door.yml create mode 100644 configs/tasks/ebench/labutopia_lab_poc/lift2_candidate/level1_pick.yml create mode 100644 configs/tasks/ebench/labutopia_lab_poc/lift2_candidate/level1_place.yml create mode 100644 configs/tasks/ebench/labutopia_lab_poc/lift2_candidate/lift2_candidate.json diff --git a/configs/cameras/labutopia_franka_poc.yml b/configs/cameras/labutopia_franka_poc.yml new file mode 100644 index 0000000..fd2971f --- /dev/null +++ b/configs/cameras/labutopia_franka_poc.yml @@ -0,0 +1,16 @@ +camera1: + name: camera_1 + exists: false + prim_path: /LabUtopiaCamera1 + position: [2.0, 0.0, 2.0] + orientation: [0.61237, 0.35355, 0.35355, 0.61237] + resolution: [256, 256] + image_type: rgb +camera2: + name: camera_2 + exists: false + prim_path: /LabUtopiaCamera2 + position: [0.1, 0.0, 2.5] + orientation: [0.70711, 0.0, 0.0, -0.70711] + resolution: [256, 256] + image_type: rgb diff --git a/configs/tasks/ebench/labutopia_lab_poc/common/assets_manifest.json b/configs/tasks/ebench/labutopia_lab_poc/common/assets_manifest.json new file mode 100644 index 0000000..f69de36 --- /dev/null +++ b/configs/tasks/ebench/labutopia_lab_poc/common/assets_manifest.json @@ -0,0 +1,21 @@ +{ + "overlay_root": "/cpfs/shared/simulation/zhuzihou/dev/_datasets/EBench-Assets-Overlay/labutopia_level1_poc/assets", + "runtime_usd_name": "scene_usds/labutopia/level1_poc/lab_001/scene", + "generated_manifest": "/cpfs/shared/simulation/zhuzihou/dev/_datasets/EBench-Assets-Overlay/labutopia_level1_poc/assets/manifests/labutopia_level1_poc.json", + "required_object_uids": [ + "obj_conical_bottle02", + "obj_beaker2", + "obj_target_plat", + "obj_DryingBox_01", + "obj_DryingBox_01_handle", + "obj_table" + ], + "table_uid": "table", + "env_vars": { + "MDL_SYSTEM_PATH": "/isaac-sim/materials/:{ASSETS_DIR}/scene_usds/labutopia/level1_poc/lab_001/SubUSDs/materials:{ASSETS_DIR}/miscs/mdl/labutopia/mdl" + }, + "runtime_notes": [ + "The current overlay does not rewrite raw LabUtopia prim names.", + "Smoke must verify or perform a rename wrapper before runtime if GenManip object discovery requires the generated obj_* UIDs." + ] +} diff --git a/configs/tasks/ebench/labutopia_lab_poc/common/task_semantics.yml b/configs/tasks/ebench/labutopia_lab_poc/common/task_semantics.yml new file mode 100644 index 0000000..b081616 --- /dev/null +++ b/configs/tasks/ebench/labutopia_lab_poc/common/task_semantics.yml @@ -0,0 +1,99 @@ +level1_pick: + source_config: /cpfs/shared/simulation/zhuzihou/dev/LabUtopia/config/level1_pick.yaml + source_task_name: Level1_pick + source_task_type: pick + source_controller_type: pick + source_usd_path: assets/chemistry_lab/lab_001/lab_001.usd + instruction: Pick up the conical bottle from the table. + max_steps: 800 + source_objects: + - path: /World/conical_bottle02 + role: object_to_pick + position_range: + x: [0.21, 0.36] + y: [-0.08, 0.08] + z: [0.80, 0.80] + genmanip_objects: + object: obj_conical_bottle02 + table: obj_table + metric: + type: manip/labutopia/object_height_delta + sub_goal_setting: + obj_uid: obj_conical_bottle02 + axis: z + min_delta: 0.10 + skip_steps: 1 + succ_cnts: 59 +level1_place: + source_config: /cpfs/shared/simulation/zhuzihou/dev/LabUtopia/config/level1_place.yaml + source_task_name: Level1_place + source_task_type: place + source_controller_type: place + source_usd_path: assets/chemistry_lab/lab_001/lab_001.usd + instruction: Pick up the beaker and place it on the target platform. + max_steps: 2000 + source_objects: + - path: /World/beaker2 + role: object_to_place + position_range: + x: [0.19, 0.34] + y: [0.10, 0.25] + z: [0.84, 0.84] + - path: /World/target_plat + role: target_platform + position_range: + x: [0.18, 0.33] + y: [-0.32, -0.17] + z: [0.776, 0.776] + genmanip_objects: + object: obj_beaker2 + target: obj_target_plat + table: obj_table + metric: + type: manip/labutopia/object_at_target + sub_goal_setting: + obj_uid: obj_beaker2 + target_uid: obj_target_plat + xy_radius: 0.05 + z_tolerance: 0.05 + skip_steps: 1 + succ_cnts: 59 +level1_open_door: + source_config: /cpfs/shared/simulation/zhuzihou/dev/LabUtopia/config/level1_open_door.yaml + source_task_name: Level1_open + source_task_type: openclose + source_controller_type: open + source_usd_path: assets/chemistry_lab/lab_001/lab_001.usd + instruction: Open the door of the drying box. + max_steps: 1000 + source_objects: + - path: /World/DryingBox_01 + role: articulated_drying_box + position_range: + x: [0.70, 0.80] + y: [0.05, 0.15] + z: [0.78, 0.78] + - path: /World/DryingBox_01/handle + role: door_handle + - path: /World/DryingBox_01/RevoluteJoint + role: preferred_door_joint + genmanip_objects: + articulated_object: obj_DryingBox_01 + handle: obj_DryingBox_01_handle + table: obj_table + metrics: + preferred: + type: manip/labutopia/joint_angle_delta + sub_goal_setting: + obj_uid: obj_DryingBox_01 + joint_name: RevoluteJoint + min_delta_rad: 0.35 + skip_steps: 1 + succ_cnts: 59 + fallback: + type: manip/labutopia/handle_displacement + sub_goal_setting: + obj_uid: obj_DryingBox_01_handle + min_distance: 0.12 + skip_steps: 1 + succ_cnts: 59 diff --git a/configs/tasks/ebench/labutopia_lab_poc/franka_poc/franka_poc.json b/configs/tasks/ebench/labutopia_lab_poc/franka_poc/franka_poc.json new file mode 100644 index 0000000..ea4b707 --- /dev/null +++ b/configs/tasks/ebench/labutopia_lab_poc/franka_poc/franka_poc.json @@ -0,0 +1,5 @@ +[ + "ebench/labutopia_lab_poc/franka_poc/level1_pick.yml", + "ebench/labutopia_lab_poc/franka_poc/level1_place.yml", + "ebench/labutopia_lab_poc/franka_poc/level1_open_door.yml" +] diff --git a/configs/tasks/ebench/labutopia_lab_poc/franka_poc/level1_open_door.yml b/configs/tasks/ebench/labutopia_lab_poc/franka_poc/level1_open_door.yml new file mode 100644 index 0000000..e8b4e2f --- /dev/null +++ b/configs/tasks/ebench/labutopia_lab_poc/franka_poc/level1_open_door.yml @@ -0,0 +1,45 @@ +evaluation_configs: + - task_name: ebench/labutopia_lab_poc/franka_poc/level1_open_door + instruction: Open the door of the drying box. + usd_name: scene_usds/labutopia/level1_poc/lab_001/scene + table_uid: table + mode: manual + num_test: 1 + num_steps: 1000 + physics_dt: 0.0166666667 + rendering_dt: 0.0166666667 + robots: + - type: manip/franka/panda_hand + position: [-0.4, 0.0, 0.71] + domain_randomization: + cameras: + config_path: configs/cameras/labutopia_franka_poc.yml + type: fixed + random_environment: + has_wall: false + hdr: false + robot_base_position: false + robot_eepose: false + table_texture: false + table_type: false + wall_texture: false + rewrite_instruction: false + generation_config: + action_path: + mode: manual + robot: 0 + goal: + - - - type: manip/labutopia/handle_displacement + obj_uid: obj_DryingBox_01_handle + min_distance: 0.12 + skip_steps: 1 + succ_cnts: 59 + mode: manual + planner: curobo + object_config: {} + preprocess_config: [] + layout_config: + ignored_objects: [] + type: null + env_vars: + MDL_SYSTEM_PATH: "/isaac-sim/materials/:{ASSETS_DIR}/scene_usds/labutopia/level1_poc/lab_001/SubUSDs/materials:{ASSETS_DIR}/miscs/mdl/labutopia/mdl" diff --git a/configs/tasks/ebench/labutopia_lab_poc/franka_poc/level1_pick.yml b/configs/tasks/ebench/labutopia_lab_poc/franka_poc/level1_pick.yml new file mode 100644 index 0000000..0a7f092 --- /dev/null +++ b/configs/tasks/ebench/labutopia_lab_poc/franka_poc/level1_pick.yml @@ -0,0 +1,46 @@ +evaluation_configs: + - task_name: ebench/labutopia_lab_poc/franka_poc/level1_pick + instruction: Pick up the conical bottle from the table. + usd_name: scene_usds/labutopia/level1_poc/lab_001/scene + table_uid: table + mode: manual + num_test: 1 + num_steps: 800 + physics_dt: 0.0166666667 + rendering_dt: 0.0166666667 + robots: + - type: manip/franka/panda_hand + position: [-0.4, 0.0, 0.71] + domain_randomization: + cameras: + config_path: configs/cameras/labutopia_franka_poc.yml + type: fixed + random_environment: + has_wall: false + hdr: false + robot_base_position: false + robot_eepose: false + table_texture: false + table_type: false + wall_texture: false + rewrite_instruction: false + generation_config: + action_path: + mode: manual + robot: 0 + goal: + - - - type: manip/labutopia/object_height_delta + obj_uid: obj_conical_bottle02 + axis: z + min_delta: 0.10 + skip_steps: 1 + succ_cnts: 59 + mode: manual + planner: curobo + object_config: {} + preprocess_config: [] + layout_config: + ignored_objects: [] + type: null + env_vars: + MDL_SYSTEM_PATH: "/isaac-sim/materials/:{ASSETS_DIR}/scene_usds/labutopia/level1_poc/lab_001/SubUSDs/materials:{ASSETS_DIR}/miscs/mdl/labutopia/mdl" diff --git a/configs/tasks/ebench/labutopia_lab_poc/franka_poc/level1_place.yml b/configs/tasks/ebench/labutopia_lab_poc/franka_poc/level1_place.yml new file mode 100644 index 0000000..c48bfa8 --- /dev/null +++ b/configs/tasks/ebench/labutopia_lab_poc/franka_poc/level1_place.yml @@ -0,0 +1,47 @@ +evaluation_configs: + - task_name: ebench/labutopia_lab_poc/franka_poc/level1_place + instruction: Pick up the beaker and place it on the target platform. + usd_name: scene_usds/labutopia/level1_poc/lab_001/scene + table_uid: table + mode: manual + num_test: 1 + num_steps: 2000 + physics_dt: 0.0166666667 + rendering_dt: 0.0166666667 + robots: + - type: manip/franka/panda_hand + position: [-0.4, 0.0, 0.71] + domain_randomization: + cameras: + config_path: configs/cameras/labutopia_franka_poc.yml + type: fixed + random_environment: + has_wall: false + hdr: false + robot_base_position: false + robot_eepose: false + table_texture: false + table_type: false + wall_texture: false + rewrite_instruction: false + generation_config: + action_path: + mode: manual + robot: 0 + goal: + - - - type: manip/labutopia/object_at_target + obj_uid: obj_beaker2 + target_uid: obj_target_plat + xy_radius: 0.05 + z_tolerance: 0.05 + skip_steps: 1 + succ_cnts: 59 + mode: manual + planner: curobo + object_config: {} + preprocess_config: [] + layout_config: + ignored_objects: [] + type: null + env_vars: + MDL_SYSTEM_PATH: "/isaac-sim/materials/:{ASSETS_DIR}/scene_usds/labutopia/level1_poc/lab_001/SubUSDs/materials:{ASSETS_DIR}/miscs/mdl/labutopia/mdl" diff --git a/configs/tasks/ebench/labutopia_lab_poc/labutopia_lab_poc.json b/configs/tasks/ebench/labutopia_lab_poc/labutopia_lab_poc.json new file mode 100644 index 0000000..1d67505 --- /dev/null +++ b/configs/tasks/ebench/labutopia_lab_poc/labutopia_lab_poc.json @@ -0,0 +1,4 @@ +[ + "ebench/labutopia_lab_poc/franka_poc/franka_poc.json", + "ebench/labutopia_lab_poc/lift2_candidate/lift2_candidate.json" +] diff --git a/configs/tasks/ebench/labutopia_lab_poc/lift2_candidate/level1_open_door.yml b/configs/tasks/ebench/labutopia_lab_poc/lift2_candidate/level1_open_door.yml new file mode 100644 index 0000000..4d91820 --- /dev/null +++ b/configs/tasks/ebench/labutopia_lab_poc/lift2_candidate/level1_open_door.yml @@ -0,0 +1,45 @@ +evaluation_configs: + - task_name: ebench/labutopia_lab_poc/lift2_candidate/level1_open_door + instruction: Open the door of the drying box. + usd_name: scene_usds/labutopia/level1_poc/lab_001/scene + table_uid: table + mode: manual + num_test: 1 + num_steps: 1000 + physics_dt: 0.0166666667 + rendering_dt: 0.0166666667 + robots: + - type: manip/lift2/R5a + position: [0.0, 0.0, 0.0] + domain_randomization: + cameras: + config_path: configs/cameras/fixed_camera_lift2_simbox.yml + type: fixed + random_environment: + has_wall: false + hdr: false + robot_base_position: false + robot_eepose: false + table_texture: false + table_type: false + wall_texture: false + rewrite_instruction: false + generation_config: + action_path: + mode: manual + robot: 0 + goal: + - - - type: manip/labutopia/handle_displacement + obj_uid: obj_DryingBox_01_handle + min_distance: 0.12 + skip_steps: 1 + succ_cnts: 59 + mode: manual + planner: curobo + object_config: {} + preprocess_config: [] + layout_config: + ignored_objects: [] + type: null + env_vars: + MDL_SYSTEM_PATH: "/isaac-sim/materials/:{ASSETS_DIR}/scene_usds/labutopia/level1_poc/lab_001/SubUSDs/materials:{ASSETS_DIR}/miscs/mdl/labutopia/mdl" diff --git a/configs/tasks/ebench/labutopia_lab_poc/lift2_candidate/level1_pick.yml b/configs/tasks/ebench/labutopia_lab_poc/lift2_candidate/level1_pick.yml new file mode 100644 index 0000000..0d9f3ec --- /dev/null +++ b/configs/tasks/ebench/labutopia_lab_poc/lift2_candidate/level1_pick.yml @@ -0,0 +1,46 @@ +evaluation_configs: + - task_name: ebench/labutopia_lab_poc/lift2_candidate/level1_pick + instruction: Pick up the conical bottle from the table. + usd_name: scene_usds/labutopia/level1_poc/lab_001/scene + table_uid: table + mode: manual + num_test: 1 + num_steps: 800 + physics_dt: 0.0166666667 + rendering_dt: 0.0166666667 + robots: + - type: manip/lift2/R5a + position: [0.0, 0.0, 0.0] + domain_randomization: + cameras: + config_path: configs/cameras/fixed_camera_lift2_simbox.yml + type: fixed + random_environment: + has_wall: false + hdr: false + robot_base_position: false + robot_eepose: false + table_texture: false + table_type: false + wall_texture: false + rewrite_instruction: false + generation_config: + action_path: + mode: manual + robot: 0 + goal: + - - - type: manip/labutopia/object_height_delta + obj_uid: obj_conical_bottle02 + axis: z + min_delta: 0.10 + skip_steps: 1 + succ_cnts: 59 + mode: manual + planner: curobo + object_config: {} + preprocess_config: [] + layout_config: + ignored_objects: [] + type: null + env_vars: + MDL_SYSTEM_PATH: "/isaac-sim/materials/:{ASSETS_DIR}/scene_usds/labutopia/level1_poc/lab_001/SubUSDs/materials:{ASSETS_DIR}/miscs/mdl/labutopia/mdl" diff --git a/configs/tasks/ebench/labutopia_lab_poc/lift2_candidate/level1_place.yml b/configs/tasks/ebench/labutopia_lab_poc/lift2_candidate/level1_place.yml new file mode 100644 index 0000000..6446264 --- /dev/null +++ b/configs/tasks/ebench/labutopia_lab_poc/lift2_candidate/level1_place.yml @@ -0,0 +1,47 @@ +evaluation_configs: + - task_name: ebench/labutopia_lab_poc/lift2_candidate/level1_place + instruction: Pick up the beaker and place it on the target platform. + usd_name: scene_usds/labutopia/level1_poc/lab_001/scene + table_uid: table + mode: manual + num_test: 1 + num_steps: 2000 + physics_dt: 0.0166666667 + rendering_dt: 0.0166666667 + robots: + - type: manip/lift2/R5a + position: [0.0, 0.0, 0.0] + domain_randomization: + cameras: + config_path: configs/cameras/fixed_camera_lift2_simbox.yml + type: fixed + random_environment: + has_wall: false + hdr: false + robot_base_position: false + robot_eepose: false + table_texture: false + table_type: false + wall_texture: false + rewrite_instruction: false + generation_config: + action_path: + mode: manual + robot: 0 + goal: + - - - type: manip/labutopia/object_at_target + obj_uid: obj_beaker2 + target_uid: obj_target_plat + xy_radius: 0.05 + z_tolerance: 0.05 + skip_steps: 1 + succ_cnts: 59 + mode: manual + planner: curobo + object_config: {} + preprocess_config: [] + layout_config: + ignored_objects: [] + type: null + env_vars: + MDL_SYSTEM_PATH: "/isaac-sim/materials/:{ASSETS_DIR}/scene_usds/labutopia/level1_poc/lab_001/SubUSDs/materials:{ASSETS_DIR}/miscs/mdl/labutopia/mdl" diff --git a/configs/tasks/ebench/labutopia_lab_poc/lift2_candidate/lift2_candidate.json b/configs/tasks/ebench/labutopia_lab_poc/lift2_candidate/lift2_candidate.json new file mode 100644 index 0000000..37dcfe9 --- /dev/null +++ b/configs/tasks/ebench/labutopia_lab_poc/lift2_candidate/lift2_candidate.json @@ -0,0 +1,5 @@ +[ + "ebench/labutopia_lab_poc/lift2_candidate/level1_pick.yml", + "ebench/labutopia_lab_poc/lift2_candidate/level1_place.yml", + "ebench/labutopia_lab_poc/lift2_candidate/level1_open_door.yml" +] From 0b8a6c25ad67faafb56ef11cbb5d913c659c2da1 Mon Sep 17 00:00:00 2001 From: Codex Date: Mon, 22 Jun 2026 16:40:35 +0800 Subject: [PATCH 07/30] fix: align labutopia task runtime contract --- .../common/assets_manifest.json | 52 ++++- .../common/task_semantics.yml | 185 +++++++++--------- genmanip/core/metrics/metrics_manager.py | 35 +++- .../labutopia_poc/build_asset_overlay.py | 59 ++++-- .../labutopia_poc/test_build_asset_overlay.py | 79 +++++++- 5 files changed, 287 insertions(+), 123 deletions(-) diff --git a/configs/tasks/ebench/labutopia_lab_poc/common/assets_manifest.json b/configs/tasks/ebench/labutopia_lab_poc/common/assets_manifest.json index f69de36..08a55c1 100644 --- a/configs/tasks/ebench/labutopia_lab_poc/common/assets_manifest.json +++ b/configs/tasks/ebench/labutopia_lab_poc/common/assets_manifest.json @@ -2,6 +2,53 @@ "overlay_root": "/cpfs/shared/simulation/zhuzihou/dev/_datasets/EBench-Assets-Overlay/labutopia_level1_poc/assets", "runtime_usd_name": "scene_usds/labutopia/level1_poc/lab_001/scene", "generated_manifest": "/cpfs/shared/simulation/zhuzihou/dev/_datasets/EBench-Assets-Overlay/labutopia_level1_poc/assets/manifests/labutopia_level1_poc.json", + "scene_uid": "labutopia_level1_poc", + "source_prim_paths": [ + "/World/conical_bottle02", + "/World/beaker2", + "/World/target_plat", + "/World/DryingBox_01", + "/World/DryingBox_01/handle", + "/World/table" + ], + "source_task_prims": { + "level1_pick": [ + "/World/conical_bottle02" + ], + "level1_place": [ + "/World/beaker2", + "/World/target_plat" + ], + "level1_open_door": [ + "/World/DryingBox_01", + "/World/DryingBox_01/handle", + "/World/DryingBox_01/RevoluteJoint" + ] + }, + "source_to_runtime_object_key": { + "/World/conical_bottle02": "obj_conical_bottle02", + "/World/beaker2": "obj_beaker2", + "/World/target_plat": "obj_target_plat", + "/World/DryingBox_01": "obj_DryingBox_01", + "/World/DryingBox_01/handle": "obj_DryingBox_01_handle", + "/World/table": "table" + }, + "runtime_object_keys": [ + "obj_conical_bottle02", + "obj_beaker2", + "obj_target_plat", + "obj_DryingBox_01", + "obj_DryingBox_01_handle", + "table" + ], + "wrapper_prim_paths": { + "obj_conical_bottle02": "/World/labutopia_level1_poc/obj_obj_conical_bottle02", + "obj_beaker2": "/World/labutopia_level1_poc/obj_obj_beaker2", + "obj_target_plat": "/World/labutopia_level1_poc/obj_obj_target_plat", + "obj_DryingBox_01": "/World/labutopia_level1_poc/obj_obj_DryingBox_01", + "obj_DryingBox_01_handle": "/World/labutopia_level1_poc/obj_obj_DryingBox_01_handle", + "table": "/World/labutopia_level1_poc/obj_table" + }, "required_object_uids": [ "obj_conical_bottle02", "obj_beaker2", @@ -15,7 +62,8 @@ "MDL_SYSTEM_PATH": "/isaac-sim/materials/:{ASSETS_DIR}/scene_usds/labutopia/level1_poc/lab_001/SubUSDs/materials:{ASSETS_DIR}/miscs/mdl/labutopia/mdl" }, "runtime_notes": [ - "The current overlay does not rewrite raw LabUtopia prim names.", - "Smoke must verify or perform a rename wrapper before runtime if GenManip object discovery requires the generated obj_* UIDs." + "scene.usda exposes a single scene uid under /World for GenManip discovery.", + "Immediate obj_* wrapper prims payload the selected LabUtopia source prims.", + "Runtime object keys strip one leading obj_ from wrapper prim names." ] } diff --git a/configs/tasks/ebench/labutopia_lab_poc/common/task_semantics.yml b/configs/tasks/ebench/labutopia_lab_poc/common/task_semantics.yml index b081616..fa9a32b 100644 --- a/configs/tasks/ebench/labutopia_lab_poc/common/task_semantics.yml +++ b/configs/tasks/ebench/labutopia_lab_poc/common/task_semantics.yml @@ -1,99 +1,98 @@ -level1_pick: - source_config: /cpfs/shared/simulation/zhuzihou/dev/LabUtopia/config/level1_pick.yaml - source_task_name: Level1_pick - source_task_type: pick - source_controller_type: pick - source_usd_path: assets/chemistry_lab/lab_001/lab_001.usd - instruction: Pick up the conical bottle from the table. - max_steps: 800 - source_objects: - - path: /World/conical_bottle02 - role: object_to_pick - position_range: - x: [0.21, 0.36] - y: [-0.08, 0.08] - z: [0.80, 0.80] - genmanip_objects: - object: obj_conical_bottle02 - table: obj_table - metric: - type: manip/labutopia/object_height_delta - sub_goal_setting: - obj_uid: obj_conical_bottle02 - axis: z - min_delta: 0.10 - skip_steps: 1 - succ_cnts: 59 -level1_place: - source_config: /cpfs/shared/simulation/zhuzihou/dev/LabUtopia/config/level1_place.yaml - source_task_name: Level1_place - source_task_type: place - source_controller_type: place - source_usd_path: assets/chemistry_lab/lab_001/lab_001.usd - instruction: Pick up the beaker and place it on the target platform. - max_steps: 2000 - source_objects: - - path: /World/beaker2 - role: object_to_place - position_range: - x: [0.19, 0.34] - y: [0.10, 0.25] - z: [0.84, 0.84] - - path: /World/target_plat - role: target_platform - position_range: - x: [0.18, 0.33] - y: [-0.32, -0.17] - z: [0.776, 0.776] - genmanip_objects: - object: obj_beaker2 - target: obj_target_plat - table: obj_table - metric: - type: manip/labutopia/object_at_target - sub_goal_setting: - obj_uid: obj_beaker2 - target_uid: obj_target_plat - xy_radius: 0.05 - z_tolerance: 0.05 - skip_steps: 1 - succ_cnts: 59 -level1_open_door: - source_config: /cpfs/shared/simulation/zhuzihou/dev/LabUtopia/config/level1_open_door.yaml - source_task_name: Level1_open - source_task_type: openclose - source_controller_type: open - source_usd_path: assets/chemistry_lab/lab_001/lab_001.usd - instruction: Open the door of the drying box. - max_steps: 1000 - source_objects: - - path: /World/DryingBox_01 - role: articulated_drying_box - position_range: - x: [0.70, 0.80] - y: [0.05, 0.15] - z: [0.78, 0.78] - - path: /World/DryingBox_01/handle - role: door_handle - - path: /World/DryingBox_01/RevoluteJoint - role: preferred_door_joint - genmanip_objects: - articulated_object: obj_DryingBox_01 - handle: obj_DryingBox_01_handle - table: obj_table - metrics: - preferred: - type: manip/labutopia/joint_angle_delta +tasks: + level1_pick: + source_config: /cpfs/shared/simulation/zhuzihou/dev/LabUtopia/config/level1_pick.yaml + source_task_name: Level1_pick + source_task_type: pick + source_controller_type: pick + source_usd_path: assets/chemistry_lab/lab_001/lab_001.usd + instruction: Pick up the conical bottle from the table. + max_steps: 800 + source_objects: + - path: /World/conical_bottle02 + role: object_to_pick + position_range: + x: [0.21, 0.36] + y: [-0.08, 0.08] + z: [0.80, 0.80] + genmanip_objects: + object: obj_conical_bottle02 + table: obj_table + metric: + type: manip/labutopia/object_height_delta sub_goal_setting: - obj_uid: obj_DryingBox_01 - joint_name: RevoluteJoint - min_delta_rad: 0.35 + obj_uid: obj_conical_bottle02 + axis: z + min_delta: 0.10 skip_steps: 1 succ_cnts: 59 - fallback: - type: manip/labutopia/handle_displacement + level1_place: + source_config: /cpfs/shared/simulation/zhuzihou/dev/LabUtopia/config/level1_place.yaml + source_task_name: Level1_place + source_task_type: place + source_controller_type: place + source_usd_path: assets/chemistry_lab/lab_001/lab_001.usd + instruction: Pick up the beaker and place it on the target platform. + max_steps: 2000 + source_objects: + - path: /World/beaker2 + role: object_to_place + position_range: + x: [0.19, 0.34] + y: [0.10, 0.25] + z: [0.84, 0.84] + - path: /World/target_plat + role: target_platform + position_range: + x: [0.18, 0.33] + y: [-0.32, -0.17] + z: [0.776, 0.776] + genmanip_objects: + object: obj_beaker2 + target: obj_target_plat + table: obj_table + metric: + type: manip/labutopia/object_at_target sub_goal_setting: - obj_uid: obj_DryingBox_01_handle - min_distance: 0.12 + obj_uid: obj_beaker2 + target_uid: obj_target_plat + xy_radius: 0.05 + z_tolerance: 0.05 skip_steps: 1 succ_cnts: 59 + level1_open_door: + source_config: /cpfs/shared/simulation/zhuzihou/dev/LabUtopia/config/level1_open_door.yaml + source_task_name: Level1_open + source_task_type: openclose + source_controller_type: open + source_usd_path: assets/chemistry_lab/lab_001/lab_001.usd + instruction: Open the door of the drying box. + max_steps: 1000 + source_objects: + - path: /World/DryingBox_01 + role: articulated_drying_box + position_range: + x: [0.70, 0.80] + y: [0.05, 0.15] + z: [0.78, 0.78] + - path: /World/DryingBox_01/handle + role: door_handle + - path: /World/DryingBox_01/RevoluteJoint + role: preferred_door_joint + genmanip_objects: + articulated_object: obj_DryingBox_01 + handle: obj_DryingBox_01_handle + table: obj_table + metrics: + preferred: + type: manip/default/check_joint_angle + sub_goal_setting: + articulation_obj_uid: obj_DryingBox_01 + joint_name: RevoluteJoint + angle_deg_range: [30, 120] + fallback: + type: manip/labutopia/handle_displacement + sub_goal_setting: + obj_uid: obj_DryingBox_01_handle + min_distance: 0.12 + skip_steps: 1 + succ_cnts: 59 diff --git a/genmanip/core/metrics/metrics_manager.py b/genmanip/core/metrics/metrics_manager.py index dd655f4..c986c5e 100644 --- a/genmanip/core/metrics/metrics_manager.py +++ b/genmanip/core/metrics/metrics_manager.py @@ -5,12 +5,36 @@ Licensed under the MIT License. """ +import importlib.util +from pathlib import Path from concurrent.futures import ThreadPoolExecutor import time from genmanip.core.metrics.utils import MetricFactory +def _ensure_metric_registered(metric_type: str) -> None: + if metric_type in MetricFactory._registry: + return + if not metric_type.startswith("manip/labutopia/"): + return + + module_path = ( + Path(__file__).resolve().parents[2] + / "extensions" + / "metrics" + / "labutopia" + / "object_metrics.py" + ) + spec = importlib.util.spec_from_file_location( + "_genmanip_labutopia_object_metrics", module_path + ) + if spec is None or spec.loader is None: + return + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + + class MetricsManager: def __init__(self, goal_setting: list[list[dict]]): self.goal_setting = self.ensure_list_nesting(goal_setting, 3) @@ -50,10 +74,13 @@ def get_next_metric(self) -> list | None: next_metric_setting = self.goal_setting.pop(0) self.long_stride_idx += 1 - metrics = [ - [MetricFactory.build(cfg["type"], **cfg) for cfg in inner] - for inner in next_metric_setting - ] + metrics = [] + for inner in next_metric_setting: + collection_metrics = [] + for cfg in inner: + _ensure_metric_registered(cfg["type"]) + collection_metrics.append(MetricFactory.build(cfg["type"], **cfg)) + metrics.append(collection_metrics) self.print_metrics_info(metrics) return metrics diff --git a/standalone_tools/labutopia_poc/build_asset_overlay.py b/standalone_tools/labutopia_poc/build_asset_overlay.py index cafffe0..338c6a2 100644 --- a/standalone_tools/labutopia_poc/build_asset_overlay.py +++ b/standalone_tools/labutopia_poc/build_asset_overlay.py @@ -19,16 +19,17 @@ OVERLAY_SCENE_RELATIVE = Path("scene_usds/labutopia/level1_poc/lab_001") USD_NAME = "scene_usds/labutopia/level1_poc/lab_001/scene" MANIFEST_RELATIVE = Path("manifests/labutopia_level1_poc.json") +SCENE_UID = "labutopia_level1_poc" -PRIM_RENAME_MAP = { +SOURCE_TO_RUNTIME_OBJECT_KEY = { "/World/conical_bottle02": "obj_conical_bottle02", "/World/beaker2": "obj_beaker2", "/World/target_plat": "obj_target_plat", "/World/DryingBox_01": "obj_DryingBox_01", "/World/DryingBox_01/handle": "obj_DryingBox_01_handle", - "/World/table": "obj_table", + "/World/table": "table", } -TASK_PRIMS = { +SOURCE_TASK_PRIMS = { "level1_pick": ["/World/conical_bottle02"], "level1_place": ["/World/beaker2", "/World/target_plat"], "level1_open_door": [ @@ -48,6 +49,19 @@ TABLE_UID = "table" +def _wrapper_name(runtime_object_key: str) -> str: + if runtime_object_key == TABLE_UID: + return "obj_table" + return f"obj_{runtime_object_key}" + + +def _wrapper_prim_paths() -> dict[str, str]: + return { + runtime_key: f"/World/{SCENE_UID}/{_wrapper_name(runtime_key)}" + for runtime_key in SOURCE_TO_RUNTIME_OBJECT_KEY.values() + } + + def _sha256(path: Path) -> str: digest = hashlib.sha256() with path.open("rb") as file: @@ -57,7 +71,17 @@ def _sha256(path: Path) -> str: def _write_scene_wrapper(path: Path) -> None: - path.write_text( + wrapper_defs = [] + for source_path, runtime_key in SOURCE_TO_RUNTIME_OBJECT_KEY.items(): + wrapper_defs.append( + f""" def Xform "{_wrapper_name(runtime_key)}" ( + prepend payload = @scene.usd@<{source_path}> + ) + {{ + }}""" + ) + + scene_text = ( """#usda 1.0 ( defaultPrim = "World" @@ -65,13 +89,14 @@ def _write_scene_wrapper(path: Path) -> None: def Xform "World" { - def Xform "_scene" ( - prepend payload = @scene.usd@ +""" + + f' def Xform "{SCENE_UID}"\n' + + " {\n" + + "\n".join(wrapper_defs) + + "\n }\n}\n" ) - { - } -} -""", + path.write_text( + scene_text, encoding="utf-8", ) @@ -137,15 +162,19 @@ def build_asset_overlay( "source_scene": str(source_scene), "overlay_root": str(overlay_root), "usd_name": USD_NAME, - "task_prims": TASK_PRIMS, - "prim_rename_map": PRIM_RENAME_MAP, + "scene_uid": SCENE_UID, + "source_task_prims": SOURCE_TASK_PRIMS, + "source_prim_paths": list(SOURCE_TO_RUNTIME_OBJECT_KEY.keys()), + "source_to_runtime_object_key": SOURCE_TO_RUNTIME_OBJECT_KEY, + "runtime_object_keys": list(SOURCE_TO_RUNTIME_OBJECT_KEY.values()), + "wrapper_prim_paths": _wrapper_prim_paths(), "table_uid": TABLE_UID, "required_genmanip_object_uids": REQUIRED_GENMANIP_OBJECT_UIDS, "copied_files": _copied_files(overlay_root, copied_paths), "notes": [ - "scene.usda payloads the raw LabUtopia scene under /World/_scene.", - "The builder does not rewrite source prim names.", - "GenManip runtime discovery still needs a rename/wrapper pass before smoke if raw prim names are retained.", + "scene.usda exposes a single scene uid under /World for GenManip discovery.", + "Immediate obj_* wrapper prims payload the selected LabUtopia source prims.", + "Runtime object keys strip one leading obj_ from wrapper prim names.", ], } diff --git a/tests/labutopia_poc/test_build_asset_overlay.py b/tests/labutopia_poc/test_build_asset_overlay.py index a64fe92..4be477f 100644 --- a/tests/labutopia_poc/test_build_asset_overlay.py +++ b/tests/labutopia_poc/test_build_asset_overlay.py @@ -1,5 +1,7 @@ import hashlib import json +import subprocess +import sys import pytest @@ -26,9 +28,22 @@ def test_build_asset_overlay_writes_scene_wrapper_manifest_and_cleans_reruns( ) assert (scene_dir / "scene.usd").exists() assert (scene_dir / "scene.usda").exists() - assert '@scene.usd@' in (scene_dir / "scene.usda").read_text( - encoding="utf-8" - ) + scene_usda = (scene_dir / "scene.usda").read_text(encoding="utf-8") + assert 'def Xform "World"' in scene_usda + assert 'def Xform "labutopia_level1_poc"' in scene_usda + assert 'def Xform "_scene"' not in scene_usda + assert ( + 'def Xform "obj_obj_conical_bottle02" (\n' + ' prepend payload = @scene.usd@' + ) in scene_usda + assert ( + 'def Xform "obj_obj_DryingBox_01_handle" (\n' + ' prepend payload = @scene.usd@' + ) in scene_usda + assert ( + 'def Xform "obj_table" (\n' + ' prepend payload = @scene.usd@' + ) in scene_usda manifest_path = overlay_root / "manifests" / "labutopia_level1_poc.json" manifest = json.loads(manifest_path.read_text(encoding="utf-8")) @@ -36,7 +51,8 @@ def test_build_asset_overlay_writes_scene_wrapper_manifest_and_cleans_reruns( manifest["usd_name"] == "scene_usds/labutopia/level1_poc/lab_001/scene" ) - assert manifest["task_prims"] == { + assert manifest["scene_uid"] == "labutopia_level1_poc" + assert manifest["source_task_prims"] == { "level1_pick": ["/World/conical_bottle02"], "level1_place": ["/World/beaker2", "/World/target_plat"], "level1_open_door": [ @@ -45,13 +61,29 @@ def test_build_asset_overlay_writes_scene_wrapper_manifest_and_cleans_reruns( "/World/DryingBox_01/RevoluteJoint", ], } - assert manifest["prim_rename_map"] == { + assert manifest["source_to_runtime_object_key"] == { "/World/conical_bottle02": "obj_conical_bottle02", "/World/beaker2": "obj_beaker2", "/World/target_plat": "obj_target_plat", "/World/DryingBox_01": "obj_DryingBox_01", "/World/DryingBox_01/handle": "obj_DryingBox_01_handle", - "/World/table": "obj_table", + "/World/table": "table", + } + assert manifest["runtime_object_keys"] == [ + "obj_conical_bottle02", + "obj_beaker2", + "obj_target_plat", + "obj_DryingBox_01", + "obj_DryingBox_01_handle", + "table", + ] + assert manifest["wrapper_prim_paths"] == { + "obj_conical_bottle02": "/World/labutopia_level1_poc/obj_obj_conical_bottle02", + "obj_beaker2": "/World/labutopia_level1_poc/obj_obj_beaker2", + "obj_target_plat": "/World/labutopia_level1_poc/obj_obj_target_plat", + "obj_DryingBox_01": "/World/labutopia_level1_poc/obj_obj_DryingBox_01", + "obj_DryingBox_01_handle": "/World/labutopia_level1_poc/obj_obj_DryingBox_01_handle", + "table": "/World/labutopia_level1_poc/obj_table", } assert manifest["required_genmanip_object_uids"] == [ "obj_conical_bottle02", @@ -61,10 +93,11 @@ def test_build_asset_overlay_writes_scene_wrapper_manifest_and_cleans_reruns( "obj_DryingBox_01_handle", "obj_table", ] + assert manifest["table_uid"] == "table" assert manifest["notes"] == [ - "scene.usda payloads the raw LabUtopia scene under /World/_scene.", - "The builder does not rewrite source prim names.", - "GenManip runtime discovery still needs a rename/wrapper pass before smoke if raw prim names are retained.", + "scene.usda exposes a single scene uid under /World for GenManip discovery.", + "Immediate obj_* wrapper prims payload the selected LabUtopia source prims.", + "Runtime object keys strip one leading obj_ from wrapper prim names.", ] copied_scene = next( @@ -107,3 +140,31 @@ def fail_if_copytree_runs(source, destination): / "lab_001" / "scene_usds" ).exists() + + +def test_metrics_manager_lazily_registers_labutopia_metrics_without_omni(): + script = """ +import sys + +assert 'omni' not in sys.modules +from genmanip.core.metrics.metrics_manager import MetricsManager + +manager = MetricsManager([ + [[{ + 'type': 'manip/labutopia/object_height_delta', + 'sub_goal_setting': { + 'obj_uid': 'obj_conical_bottle02', + 'axis': 'z', + 'min_delta': 0.1, + }, + }]] +]) +metric = manager.cur_union_metric[0][0] +assert metric.__class__.__name__ == 'ObjectHeightDelta' +assert 'omni' not in sys.modules +""" + subprocess.run( + [sys.executable, "-c", script], + cwd=build_overlay.Path(__file__).resolve().parents[2], + check=True, + ) From e29036f08b5e3899274607cb684f4d79a2b12bb7 Mon Sep 17 00:00:00 2001 From: Codex Date: Mon, 22 Jun 2026 16:46:35 +0800 Subject: [PATCH 08/30] test: validate labutopia task package --- .../labutopia_poc/validate_task_package.py | 289 ++++++++++++++++++ .../test_validate_task_package.py | 13 + 2 files changed, 302 insertions(+) create mode 100644 standalone_tools/labutopia_poc/validate_task_package.py create mode 100644 tests/labutopia_poc/test_validate_task_package.py diff --git a/standalone_tools/labutopia_poc/validate_task_package.py b/standalone_tools/labutopia_poc/validate_task_package.py new file mode 100644 index 0000000..8fc3a3e --- /dev/null +++ b/standalone_tools/labutopia_poc/validate_task_package.py @@ -0,0 +1,289 @@ +#!/usr/bin/env python3 +"""Static validator for the LabUtopia EBench proof-of-concept task package.""" + +from __future__ import annotations + +import contextlib +import io +import json +import sys +from pathlib import Path +from typing import Any + +import yaml + + +ROOT = Path(__file__).resolve().parents[2] +TASK_ROOT = ROOT / "configs/tasks" +PACKAGE_ROOT = TASK_ROOT / "ebench/labutopia_lab_poc" +TASK_PREFIX = "ebench/labutopia_lab_poc/" +SCENE_UID = "labutopia_level1_poc" +RUNTIME_USD_NAME = "scene_usds/labutopia/level1_poc/lab_001/scene" +EXPECTED_TASKS = {"level1_pick", "level1_place", "level1_open_door"} +EXPECTED_WRAPPER_PRIM_PATHS = { + "obj_conical_bottle02": "/World/labutopia_level1_poc/obj_obj_conical_bottle02", + "obj_beaker2": "/World/labutopia_level1_poc/obj_obj_beaker2", + "obj_target_plat": "/World/labutopia_level1_poc/obj_obj_target_plat", + "obj_DryingBox_01": "/World/labutopia_level1_poc/obj_obj_DryingBox_01", + "obj_DryingBox_01_handle": "/World/labutopia_level1_poc/obj_obj_DryingBox_01_handle", + "table": "/World/labutopia_level1_poc/obj_table", +} +PROFILE_EXPECTATIONS = { + "franka_poc": { + "robot_type": "manip/franka/panda_hand", + "camera_config": "configs/cameras/labutopia_franka_poc.yml", + }, + "lift2_candidate": { + "robot_type": "manip/lift2/R5a", + "camera_config": "configs/cameras/fixed_camera_lift2_simbox.yml", + }, +} +ALLOWED_METRICS = { + "manip/labutopia/object_height_delta": { + "obj_uid", + "axis", + "min_delta", + "skip_steps", + "succ_cnts", + }, + "manip/labutopia/object_at_target": { + "obj_uid", + "target_uid", + "xy_radius", + "z_tolerance", + "skip_steps", + "succ_cnts", + }, + "manip/labutopia/handle_displacement": { + "obj_uid", + "min_distance", + "skip_steps", + "succ_cnts", + }, +} + + +def _load_json(path: Path) -> Any: + with path.open(encoding="utf-8") as handle: + return json.load(handle) + + +def _load_yaml(path: Path) -> Any: + with path.open(encoding="utf-8") as handle: + return yaml.safe_load(handle) + + +def _assert(condition: bool, message: str) -> None: + if not condition: + raise AssertionError(message) + + +def _task_path(relative_path: str, index_path: Path) -> Path: + path = TASK_ROOT / relative_path + _assert(path.exists(), f"{index_path}: indexed task path does not exist: {path}") + return path + + +def _load_index(path: Path) -> list[str]: + data = _load_json(path) + _assert(isinstance(data, list), f"{path}: expected JSON list index") + _assert( + all(isinstance(item, str) for item in data), + f"{path}: expected every index entry to be a string", + ) + return data + + +def _indexed_task_yaml_paths() -> list[Path]: + top_index = PACKAGE_ROOT / "labutopia_lab_poc.json" + profile_indexes = [_task_path(item, top_index) for item in _load_index(top_index)] + _assert( + {path.parent.name for path in profile_indexes} + == {"franka_poc", "lift2_candidate"}, + f"{top_index}: expected franka_poc and lift2_candidate profile indexes", + ) + + task_paths: list[Path] = [] + for index_path in profile_indexes: + for item in _load_index(index_path): + task_path = _task_path(item, index_path) + _assert(task_path.suffix in {".yml", ".yaml"}, f"{task_path}: expected YAML") + task_paths.append(task_path) + + _assert(len(task_paths) == 6, f"{top_index}: expected 6 task YAMLs") + return sorted(task_paths) + + +def _validate_assets_manifest() -> None: + path = PACKAGE_ROOT / "common/assets_manifest.json" + _assert(path.exists(), f"{path}: missing LabUtopia assets manifest") + manifest = _load_json(path) + + _assert( + manifest.get("scene_uid") == SCENE_UID, + f"{path}: scene_uid must be {SCENE_UID!r}", + ) + _assert( + manifest.get("runtime_usd_name") == RUNTIME_USD_NAME, + f"{path}: runtime_usd_name must be {RUNTIME_USD_NAME!r}", + ) + _assert( + manifest.get("wrapper_prim_paths") == EXPECTED_WRAPPER_PRIM_PATHS, + f"{path}: wrapper_prim_paths must preserve GenManip key stripping", + ) + + generated_manifest = manifest.get("generated_manifest") + _assert( + isinstance(generated_manifest, str) and generated_manifest, + f"{path}: generated_manifest must be a non-empty path", + ) + generated_path = Path(generated_manifest) + _assert( + generated_path.exists(), + f"{path}: generated manifest path does not exist: {generated_path}", + ) + generated = _load_json(generated_path) + for common_key, generated_key in { + "runtime_usd_name": "usd_name", + "scene_uid": "scene_uid", + "runtime_object_keys": "runtime_object_keys", + "wrapper_prim_paths": "wrapper_prim_paths", + "source_to_runtime_object_key": "source_to_runtime_object_key", + }.items(): + _assert( + manifest.get(common_key) == generated.get(generated_key), + f"{path}: {common_key} differs from {generated_path}:{generated_key}", + ) + + +def _validate_task_semantics() -> None: + path = PACKAGE_ROOT / "common/task_semantics.yml" + data = _load_yaml(path) + tasks = data.get("tasks") if isinstance(data, dict) else None + _assert(isinstance(tasks, dict), f"{path}: expected top-level tasks mapping") + _assert(set(tasks) == EXPECTED_TASKS, f"{path}: unexpected task keys: {set(tasks)}") + + open_door = tasks["level1_open_door"] + preferred = open_door.get("metrics", {}).get("preferred") + _assert(isinstance(preferred, dict), f"{path}: missing open_door preferred metric") + _assert( + preferred.get("type") == "manip/default/check_joint_angle", + f"{path}: open_door preferred metric must be manip/default/check_joint_angle", + ) + settings = preferred.get("sub_goal_setting") + _assert( + isinstance(settings, dict), + f"{path}: open_door preferred metric missing sub_goal_setting", + ) + for key in ("articulation_obj_uid", "joint_name", "angle_deg_range"): + _assert(key in settings, f"{path}: open_door preferred metric missing {key}") + + +def _walk_goal_dicts(value: Any, path: Path) -> list[dict[str, Any]]: + if isinstance(value, dict): + return [value] + if isinstance(value, list): + results: list[dict[str, Any]] = [] + for item in value: + results.extend(_walk_goal_dicts(item, path)) + return results + raise AssertionError(f"{path}: goal contains unsupported value {value!r}") + + +def _validate_metric(metric: dict[str, Any], path: Path) -> None: + _assert("sub_goal_setting" not in metric, f"{path}: runtime goal uses sub_goal_setting") + metric_type = metric.get("type") + _assert( + metric_type in ALLOWED_METRICS, + f"{path}: unsupported LabUtopia metric type {metric_type!r}", + ) + missing = ALLOWED_METRICS[metric_type] - set(metric) + _assert(not missing, f"{path}: metric {metric_type} missing top-level params {missing}") + + +def _validate_runtime_task(path: Path) -> None: + sys.path.insert(0, str(ROOT)) + from genmanip.core.scene.scene_config import SceneConfig + + data = _load_yaml(path) + configs = data.get("evaluation_configs") if isinstance(data, dict) else None + _assert(isinstance(configs, list), f"{path}: expected evaluation_configs list") + _assert(len(configs) == 1, f"{path}: expected exactly one evaluation_configs item") + cfg = configs[0] + _assert(isinstance(cfg, dict), f"{path}: evaluation config must be a mapping") + + SceneConfig(**cfg) + _assert(cfg.get("num_test") is not None, f"{path}: missing num_test") + task_name = cfg.get("task_name") + _assert( + isinstance(task_name, str) and task_name.startswith(TASK_PREFIX), + f"{path}: task_name must start with {TASK_PREFIX!r}", + ) + _assert(cfg.get("table_uid") == "table", f"{path}: table_uid must be 'table'") + + profile = path.parent.name + expected = PROFILE_EXPECTATIONS.get(profile) + _assert(expected is not None, f"{path}: unknown LabUtopia task profile {profile!r}") + robots = cfg.get("robots") + _assert(isinstance(robots, list) and robots, f"{path}: robots must be non-empty") + _assert( + robots[0].get("type") == expected["robot_type"], + f"{path}: {profile} robot type must be {expected['robot_type']!r}", + ) + camera = cfg.get("domain_randomization", {}).get("cameras", {}) + _assert( + camera.get("config_path") == expected["camera_config"], + f"{path}: {profile} camera config must be {expected['camera_config']!r}", + ) + + goals = cfg.get("generation_config", {}).get("goal") + _assert(goals is not None, f"{path}: missing generation_config.goal") + metrics = _walk_goal_dicts(goals, path) + _assert(metrics, f"{path}: generation_config.goal contains no metric dicts") + for metric in metrics: + _validate_metric(metric, path) + + +def _validate_metrics_manager_lazy_registration() -> None: + sys.path.insert(0, str(ROOT)) + from genmanip.core.metrics.metrics_manager import MetricsManager + + with contextlib.redirect_stdout(io.StringIO()): + manager = MetricsManager( + [ + [ + [ + { + "type": "manip/labutopia/object_height_delta", + "sub_goal_setting": { + "obj_uid": "obj_conical_bottle02", + "axis": "z", + "min_delta": 0.1, + }, + } + ] + ] + ] + ) + metric = manager.cur_union_metric[0][0] + _assert( + metric.__class__.__name__ == "ObjectHeightDelta", + "MetricsManager did not lazily register LabUtopia object_height_delta", + ) + + +def validate_task_package() -> None: + _validate_assets_manifest() + _validate_task_semantics() + for path in _indexed_task_yaml_paths(): + _validate_runtime_task(path) + _validate_metrics_manager_lazy_registration() + + +def main() -> None: + validate_task_package() + print("LabUtopia task package validation OK") + + +if __name__ == "__main__": + main() diff --git a/tests/labutopia_poc/test_validate_task_package.py b/tests/labutopia_poc/test_validate_task_package.py new file mode 100644 index 0000000..e9b4a13 --- /dev/null +++ b/tests/labutopia_poc/test_validate_task_package.py @@ -0,0 +1,13 @@ +import subprocess +import sys + + +def test_validate_task_package_cli_reports_success(): + result = subprocess.run( + [sys.executable, "standalone_tools/labutopia_poc/validate_task_package.py"], + check=True, + capture_output=True, + text=True, + ) + + assert "LabUtopia task package validation OK" in result.stdout From 29bb22e748195601f9deaeeb9fbcc359cb780ba2 Mon Sep 17 00:00:00 2001 From: Codex Date: Mon, 22 Jun 2026 16:54:53 +0800 Subject: [PATCH 09/30] fix: tighten labutopia package validator --- .../labutopia_poc/validate_task_package.py | 47 +++++++++++- .../test_validate_task_package.py | 74 +++++++++++++++++++ 2 files changed, 119 insertions(+), 2 deletions(-) diff --git a/standalone_tools/labutopia_poc/validate_task_package.py b/standalone_tools/labutopia_poc/validate_task_package.py index 8fc3a3e..4dc79c7 100644 --- a/standalone_tools/labutopia_poc/validate_task_package.py +++ b/standalone_tools/labutopia_poc/validate_task_package.py @@ -20,6 +20,18 @@ SCENE_UID = "labutopia_level1_poc" RUNTIME_USD_NAME = "scene_usds/labutopia/level1_poc/lab_001/scene" EXPECTED_TASKS = {"level1_pick", "level1_place", "level1_open_door"} +EXPECTED_TASK_ORDER = ["level1_pick", "level1_place", "level1_open_door"] +EXPECTED_TOP_INDEX_ENTRIES = [ + "ebench/labutopia_lab_poc/franka_poc/franka_poc.json", + "ebench/labutopia_lab_poc/lift2_candidate/lift2_candidate.json", +] +EXPECTED_PROFILE_INDEX_ENTRIES = { + profile: [ + f"ebench/labutopia_lab_poc/{profile}/{task}.yml" + for task in EXPECTED_TASK_ORDER + ] + for profile in ("franka_poc", "lift2_candidate") +} EXPECTED_WRAPPER_PRIM_PATHS = { "obj_conical_bottle02": "/World/labutopia_level1_poc/obj_obj_conical_bottle02", "obj_beaker2": "/World/labutopia_level1_poc/obj_obj_beaker2", @@ -96,7 +108,12 @@ def _load_index(path: Path) -> list[str]: def _indexed_task_yaml_paths() -> list[Path]: top_index = PACKAGE_ROOT / "labutopia_lab_poc.json" - profile_indexes = [_task_path(item, top_index) for item in _load_index(top_index)] + top_entries = _load_index(top_index) + _assert( + top_entries == EXPECTED_TOP_INDEX_ENTRIES, + f"{top_index}: expected profile indexes {EXPECTED_TOP_INDEX_ENTRIES!r}", + ) + profile_indexes = [_task_path(item, top_index) for item in top_entries] _assert( {path.parent.name for path in profile_indexes} == {"franka_poc", "lift2_candidate"}, @@ -105,7 +122,23 @@ def _indexed_task_yaml_paths() -> list[Path]: task_paths: list[Path] = [] for index_path in profile_indexes: - for item in _load_index(index_path): + profile = index_path.parent.name + expected_entries = EXPECTED_PROFILE_INDEX_ENTRIES[profile] + entries = _load_index(index_path) + _assert( + len(entries) == len(set(entries)) == 3, + f"{index_path}: expected 3 distinct task YAML entries", + ) + _assert( + entries == expected_entries, + f"{index_path}: expected task entries {expected_entries!r}", + ) + basenames = {Path(item).stem for item in entries} + _assert( + basenames == EXPECTED_TASKS, + f"{index_path}: expected task basenames {EXPECTED_TASKS!r}", + ) + for item in entries: task_path = _task_path(item, index_path) _assert(task_path.suffix in {".yml", ".yaml"}, f"{task_path}: expected YAML") task_paths.append(task_path) @@ -246,6 +279,12 @@ def _validate_runtime_task(path: Path) -> None: def _validate_metrics_manager_lazy_registration() -> None: sys.path.insert(0, str(ROOT)) + for module_name in list(sys.modules): + if module_name == "genmanip.extensions.metrics" or module_name.startswith( + "genmanip.extensions.metrics." + ): + del sys.modules[module_name] + from genmanip.core.metrics.metrics_manager import MetricsManager with contextlib.redirect_stdout(io.StringIO()): @@ -270,6 +309,10 @@ def _validate_metrics_manager_lazy_registration() -> None: metric.__class__.__name__ == "ObjectHeightDelta", "MetricsManager did not lazily register LabUtopia object_height_delta", ) + _assert( + "genmanip.extensions.metrics" not in sys.modules, + "MetricsManager imported the full genmanip.extensions.metrics package", + ) def validate_task_package() -> None: diff --git a/tests/labutopia_poc/test_validate_task_package.py b/tests/labutopia_poc/test_validate_task_package.py index e9b4a13..818ef19 100644 --- a/tests/labutopia_poc/test_validate_task_package.py +++ b/tests/labutopia_poc/test_validate_task_package.py @@ -1,5 +1,79 @@ +import json import subprocess import sys +import types + +import pytest + +from standalone_tools.labutopia_poc import validate_task_package + + +EXPECTED_TOP_INDEX = [ + "ebench/labutopia_lab_poc/franka_poc/franka_poc.json", + "ebench/labutopia_lab_poc/lift2_candidate/lift2_candidate.json", +] +EXPECTED_TASKS = ["level1_pick", "level1_place", "level1_open_door"] + + +def _write_json(path, data): + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(json.dumps(data), encoding="utf-8") + + +def _write_task_files(task_root): + for profile in ("franka_poc", "lift2_candidate"): + profile_root = task_root / "ebench/labutopia_lab_poc" / profile + profile_root.mkdir(parents=True, exist_ok=True) + for task_name in EXPECTED_TASKS: + (profile_root / f"{task_name}.yml").write_text("{}", encoding="utf-8") + + +def _write_valid_indexes(task_root): + package_root = task_root / "ebench/labutopia_lab_poc" + _write_json(package_root / "labutopia_lab_poc.json", EXPECTED_TOP_INDEX) + for profile in ("franka_poc", "lift2_candidate"): + _write_json( + package_root / profile / f"{profile}.json", + [ + f"ebench/labutopia_lab_poc/{profile}/{task}.yml" + for task in EXPECTED_TASKS + ], + ) + + +def test_indexed_task_yaml_paths_rejects_duplicate_profile_entries(tmp_path, monkeypatch): + task_root = tmp_path / "tasks" + _write_task_files(task_root) + _write_valid_indexes(task_root) + package_root = task_root / "ebench/labutopia_lab_poc" + _write_json( + package_root / "franka_poc/franka_poc.json", + [ + "ebench/labutopia_lab_poc/franka_poc/level1_pick.yml", + "ebench/labutopia_lab_poc/franka_poc/level1_pick.yml", + "ebench/labutopia_lab_poc/franka_poc/level1_place.yml", + ], + ) + monkeypatch.setattr(validate_task_package, "TASK_ROOT", task_root) + monkeypatch.setattr(validate_task_package, "PACKAGE_ROOT", package_root) + + with pytest.raises(AssertionError, match="franka_poc.json"): + validate_task_package._indexed_task_yaml_paths() + + +def test_metrics_manager_lazy_registration_does_not_keep_metrics_package_imported(): + sys.modules["genmanip.extensions.metrics"] = types.ModuleType( + "genmanip.extensions.metrics" + ) + try: + validate_task_package._validate_metrics_manager_lazy_registration() + assert "genmanip.extensions.metrics" not in sys.modules + finally: + for module_name in list(sys.modules): + if module_name == "genmanip.extensions.metrics" or module_name.startswith( + "genmanip.extensions.metrics." + ): + del sys.modules[module_name] def test_validate_task_package_cli_reports_success(): From 48589eaa5f3e9d5da03fec9f1562008493b4da2d Mon Sep 17 00:00:00 2001 From: Codex Date: Mon, 22 Jun 2026 17:04:10 +0800 Subject: [PATCH 10/30] fix: align labutopia runtime articulation contract --- .../franka_poc/level1_open_door.yml | 1 + .../franka_poc/level1_pick.yml | 1 + .../franka_poc/level1_place.yml | 1 + .../lift2_candidate/level1_open_door.yml | 1 + .../lift2_candidate/level1_pick.yml | 1 + .../lift2_candidate/level1_place.yml | 1 + .../labutopia_poc/validate_task_package.py | 21 +++++++++++++++++-- .../test_validate_task_package.py | 8 +++++++ 8 files changed, 33 insertions(+), 2 deletions(-) diff --git a/configs/tasks/ebench/labutopia_lab_poc/franka_poc/level1_open_door.yml b/configs/tasks/ebench/labutopia_lab_poc/franka_poc/level1_open_door.yml index e8b4e2f..9d2a0ae 100644 --- a/configs/tasks/ebench/labutopia_lab_poc/franka_poc/level1_open_door.yml +++ b/configs/tasks/ebench/labutopia_lab_poc/franka_poc/level1_open_door.yml @@ -28,6 +28,7 @@ evaluation_configs: action_path: mode: manual robot: 0 + articulation: [] goal: - - - type: manip/labutopia/handle_displacement obj_uid: obj_DryingBox_01_handle diff --git a/configs/tasks/ebench/labutopia_lab_poc/franka_poc/level1_pick.yml b/configs/tasks/ebench/labutopia_lab_poc/franka_poc/level1_pick.yml index 0a7f092..66d24a9 100644 --- a/configs/tasks/ebench/labutopia_lab_poc/franka_poc/level1_pick.yml +++ b/configs/tasks/ebench/labutopia_lab_poc/franka_poc/level1_pick.yml @@ -28,6 +28,7 @@ evaluation_configs: action_path: mode: manual robot: 0 + articulation: [] goal: - - - type: manip/labutopia/object_height_delta obj_uid: obj_conical_bottle02 diff --git a/configs/tasks/ebench/labutopia_lab_poc/franka_poc/level1_place.yml b/configs/tasks/ebench/labutopia_lab_poc/franka_poc/level1_place.yml index c48bfa8..bd7dbe1 100644 --- a/configs/tasks/ebench/labutopia_lab_poc/franka_poc/level1_place.yml +++ b/configs/tasks/ebench/labutopia_lab_poc/franka_poc/level1_place.yml @@ -28,6 +28,7 @@ evaluation_configs: action_path: mode: manual robot: 0 + articulation: [] goal: - - - type: manip/labutopia/object_at_target obj_uid: obj_beaker2 diff --git a/configs/tasks/ebench/labutopia_lab_poc/lift2_candidate/level1_open_door.yml b/configs/tasks/ebench/labutopia_lab_poc/lift2_candidate/level1_open_door.yml index 4d91820..aaa85df 100644 --- a/configs/tasks/ebench/labutopia_lab_poc/lift2_candidate/level1_open_door.yml +++ b/configs/tasks/ebench/labutopia_lab_poc/lift2_candidate/level1_open_door.yml @@ -28,6 +28,7 @@ evaluation_configs: action_path: mode: manual robot: 0 + articulation: [] goal: - - - type: manip/labutopia/handle_displacement obj_uid: obj_DryingBox_01_handle diff --git a/configs/tasks/ebench/labutopia_lab_poc/lift2_candidate/level1_pick.yml b/configs/tasks/ebench/labutopia_lab_poc/lift2_candidate/level1_pick.yml index 0d9f3ec..a90d1f9 100644 --- a/configs/tasks/ebench/labutopia_lab_poc/lift2_candidate/level1_pick.yml +++ b/configs/tasks/ebench/labutopia_lab_poc/lift2_candidate/level1_pick.yml @@ -28,6 +28,7 @@ evaluation_configs: action_path: mode: manual robot: 0 + articulation: [] goal: - - - type: manip/labutopia/object_height_delta obj_uid: obj_conical_bottle02 diff --git a/configs/tasks/ebench/labutopia_lab_poc/lift2_candidate/level1_place.yml b/configs/tasks/ebench/labutopia_lab_poc/lift2_candidate/level1_place.yml index 6446264..97dd1ea 100644 --- a/configs/tasks/ebench/labutopia_lab_poc/lift2_candidate/level1_place.yml +++ b/configs/tasks/ebench/labutopia_lab_poc/lift2_candidate/level1_place.yml @@ -28,6 +28,7 @@ evaluation_configs: action_path: mode: manual robot: 0 + articulation: [] goal: - - - type: manip/labutopia/object_at_target obj_uid: obj_beaker2 diff --git a/standalone_tools/labutopia_poc/validate_task_package.py b/standalone_tools/labutopia_poc/validate_task_package.py index 4dc79c7..cf9100c 100644 --- a/standalone_tools/labutopia_poc/validate_task_package.py +++ b/standalone_tools/labutopia_poc/validate_task_package.py @@ -4,6 +4,7 @@ from __future__ import annotations import contextlib +import copy import io import json import sys @@ -237,6 +238,7 @@ def _validate_metric(metric: dict[str, Any], path: Path) -> None: def _validate_runtime_task(path: Path) -> None: sys.path.insert(0, str(ROOT)) from genmanip.core.scene.scene_config import SceneConfig + from genmanip.utils.standalone.version_utils import process_archived_config data = _load_yaml(path) configs = data.get("evaluation_configs") if isinstance(data, dict) else None @@ -245,7 +247,8 @@ def _validate_runtime_task(path: Path) -> None: cfg = configs[0] _assert(isinstance(cfg, dict), f"{path}: evaluation config must be a mapping") - SceneConfig(**cfg) + runtime_cfg = process_archived_config(copy.deepcopy(cfg)) + SceneConfig(**runtime_cfg) _assert(cfg.get("num_test") is not None, f"{path}: missing num_test") task_name = cfg.get("task_name") _assert( @@ -269,7 +272,21 @@ def _validate_runtime_task(path: Path) -> None: f"{path}: {profile} camera config must be {expected['camera_config']!r}", ) - goals = cfg.get("generation_config", {}).get("goal") + generation_config = cfg.get("generation_config") + _assert( + isinstance(generation_config, dict), + f"{path}: generation_config must be a mapping", + ) + _assert( + "articulation" in generation_config, + f"{path}: generation_config.articulation is required by runtime config upgrade", + ) + _assert( + isinstance(generation_config["articulation"], (list, dict)), + f"{path}: generation_config.articulation must be a list or mapping", + ) + + goals = generation_config.get("goal") _assert(goals is not None, f"{path}: missing generation_config.goal") metrics = _walk_goal_dicts(goals, path) _assert(metrics, f"{path}: generation_config.goal contains no metric dicts") diff --git a/tests/labutopia_poc/test_validate_task_package.py b/tests/labutopia_poc/test_validate_task_package.py index 818ef19..f6f0c55 100644 --- a/tests/labutopia_poc/test_validate_task_package.py +++ b/tests/labutopia_poc/test_validate_task_package.py @@ -85,3 +85,11 @@ def test_validate_task_package_cli_reports_success(): ) assert "LabUtopia task package validation OK" in result.stdout + + +def test_labutopia_tasks_define_runtime_articulation_contract(): + for path in validate_task_package._indexed_task_yaml_paths(): + data = validate_task_package._load_yaml(path) + cfg = data["evaluation_configs"][0] + + assert "articulation" in cfg["generation_config"], str(path) From 33438a7e6c5e33aaa522b360c0524870f3262f10 Mon Sep 17 00:00:00 2001 From: Codex Date: Mon, 22 Jun 2026 22:49:46 +0800 Subject: [PATCH 11/30] Add LabUtopia EBench report page --- .github/workflows/deploy-labutopia-report.yml | 49 ++ configs/cameras/labutopia_franka_poc.yml | 10 + docs/.nojekyll | 1 + docs/index.html | 62 ++ docs/labutopia_lab_poc/franka_smoke.md | 107 +++ ...pia-ebench-lift2-baseline-lane-planning.md | 73 ++ ...26-06-22-labutopia-ebench-weekly-report.md | 188 +++++ .../index.html | 713 ++++++++++++++++++ ...22-ebench-labutopia-lift2-baseline-lane.md | 526 +++++++++++++ .../plans/2026-06-22-ebench-labutopia-poc.md | 53 ++ genmanip/core/evaluator/env.py | 12 +- genmanip/core/evaluator/episode_result.py | 37 + genmanip/core/evaluator/isaac_worker.py | 36 + genmanip/core/evaluator/isaac_worker_pool.py | 55 +- genmanip/core/evaluator/labutopia_assets.py | 77 ++ genmanip/core/evaluator/labutopia_layout.py | 106 +++ genmanip/core/evaluator/progress_manager.py | 19 + .../labutopia_poc/validate_task_package.py | 36 + .../test_episode_result_completion.py | 60 ++ .../test_isaac_worker_asyncio.py | 41 + .../test_labutopia_assets_override.py | 61 ++ .../test_labutopia_layout_fallback.py | 110 +++ .../test_validate_task_package.py | 52 ++ 23 files changed, 2476 insertions(+), 8 deletions(-) create mode 100644 .github/workflows/deploy-labutopia-report.yml create mode 100644 docs/.nojekyll create mode 100644 docs/index.html create mode 100644 docs/labutopia_lab_poc/franka_smoke.md create mode 100644 docs/records/2026-06-22-labutopia-ebench-lift2-baseline-lane-planning.md create mode 100644 docs/records/2026-06-22-labutopia-ebench-weekly-report.md create mode 100644 docs/records/evidence/2026-06-22-labutopia-ebench-weekly-report/index.html create mode 100644 docs/superpowers/plans/2026-06-22-ebench-labutopia-lift2-baseline-lane.md create mode 100644 docs/superpowers/plans/2026-06-22-ebench-labutopia-poc.md create mode 100644 genmanip/core/evaluator/episode_result.py create mode 100644 genmanip/core/evaluator/labutopia_assets.py create mode 100644 genmanip/core/evaluator/labutopia_layout.py create mode 100644 tests/labutopia_poc/test_episode_result_completion.py create mode 100644 tests/labutopia_poc/test_isaac_worker_asyncio.py create mode 100644 tests/labutopia_poc/test_labutopia_assets_override.py create mode 100644 tests/labutopia_poc/test_labutopia_layout_fallback.py diff --git a/.github/workflows/deploy-labutopia-report.yml b/.github/workflows/deploy-labutopia-report.yml new file mode 100644 index 0000000..97f6700 --- /dev/null +++ b/.github/workflows/deploy-labutopia-report.yml @@ -0,0 +1,49 @@ +name: Deploy LabUtopia Report + +# GenManip LabUtopia reports are zero-build static HTML under docs/. +# This mirrors the EOS learning-site deployment style: upload docs/ as the Pages artifact. + +on: + workflow_dispatch: + push: + branches: + - labutopia-ebench-poc + - main + paths: + - "docs/**" + - ".github/workflows/deploy-labutopia-report.yml" + +permissions: + contents: read + +concurrency: + group: labutopia-report-pages + cancel-in-progress: false + +jobs: + deploy: + runs-on: ubuntu-latest + permissions: + pages: write + id-token: write + contents: read + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Configure Pages + uses: actions/configure-pages@v5 + with: + enablement: true + + - name: Upload docs/ as Pages artifact + uses: actions/upload-pages-artifact@v3 + with: + path: docs + + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/configs/cameras/labutopia_franka_poc.yml b/configs/cameras/labutopia_franka_poc.yml index fd2971f..f7e9ae9 100644 --- a/configs/cameras/labutopia_franka_poc.yml +++ b/configs/cameras/labutopia_franka_poc.yml @@ -6,6 +6,11 @@ camera1: orientation: [0.61237, 0.35355, 0.35355, 0.61237] resolution: [256, 256] image_type: rgb + with_distance: false + with_semantic: false + with_bbox2d: false + with_bbox3d: false + with_motion_vector: false camera2: name: camera_2 exists: false @@ -14,3 +19,8 @@ camera2: orientation: [0.70711, 0.0, 0.0, -0.70711] resolution: [256, 256] image_type: rgb + with_distance: false + with_semantic: false + with_bbox2d: false + with_bbox3d: false + with_motion_vector: false diff --git a/docs/.nojekyll b/docs/.nojekyll new file mode 100644 index 0000000..9698116 --- /dev/null +++ b/docs/.nojekyll @@ -0,0 +1 @@ +# Disable Jekyll processing for the static docs artifact. diff --git a/docs/index.html b/docs/index.html new file mode 100644 index 0000000..f8976e7 --- /dev/null +++ b/docs/index.html @@ -0,0 +1,62 @@ + + + + + + + GenManip LabUtopia Reports + + + +
+

GenManip LabUtopia Reports

+

Zero-build static report surface for LabUtopia x EBench integration evidence.

+
+ LabUtopia x EBench 接入周报 · 2026-06-22 +

Franka POC end-to-end smoke has completed; Lift2 official baseline readiness remains behind P0-P4 evidence gates.

+
+ 3/3 episodes complete + 23 passed, 1 skipped + Franka POC only + GitHub Pages ready +
+
+
+ + diff --git a/docs/labutopia_lab_poc/franka_smoke.md b/docs/labutopia_lab_poc/franka_smoke.md new file mode 100644 index 0000000..0f391b5 --- /dev/null +++ b/docs/labutopia_lab_poc/franka_smoke.md @@ -0,0 +1,107 @@ +# LabUtopia Franka POC Smoke + +Run date: 2026-06-22 + +## Goal + +Verify that the LabUtopia Franka POC can run through GenManip/EBench client-server lifecycle: submit, reset, step, record results, finish all tasks, and shut down without interfering with the separate EOS service. + +## Runtime + +- Conda env: `/cpfs/shared/simulation/zhuzihou/dev/conda-managed/envs/embodied-eval-os-sim-isaacsim41-genmanip-py310` +- Server port: `18088` +- EOS/other-engineer port: `8087`, not modified +- Run ID: `labutopia_franka_smoke_clean8_20260622_100208` +- Ray tmp dir: `/tmp/gm8877` + +## Result Matrix + +| Task | Reset | Step | Metric | Evidence | +| --- | --- | --- | --- | --- | +| `level1_pick` | PASS | PASS | PASS | reset completed in 87.73s, score persisted as 0.0 | +| `level1_place` | PASS | PASS | PASS | reset completed in 40.33s, score persisted as 0.0 | +| `level1_open_door` | PASS | PASS | PASS | reset completed in 41.31s, score persisted as 0.0 | + +Final status: PASS. The client exited normally and the final evaluation result included all three tasks. The 0.0 scores are expected because this smoke uses default client actions, not a solving policy. + +## Commands + +Server: + +```bash +PY=/cpfs/shared/simulation/zhuzihou/dev/conda-managed/envs/embodied-eval-os-sim-isaacsim41-genmanip-py310/bin/python +ENV=/cpfs/shared/simulation/zhuzihou/dev/conda-managed/envs/embodied-eval-os-sim-isaacsim41-genmanip-py310 +CUROBO_SRC=/cpfs/shared/simulation/mamengchen/curobo-wbc-backup/src +CUDA11_LIB=/isaac-sim/exts/omni.isaac.ml_archive/pip_prebundle/nvidia/cuda_runtime/lib +RUN_ID=labutopia_franka_smoke_$(date -u +%Y%m%d_%H%M%S) +export ACCEPT_EULA=Y OMNI_KIT_ACCEPT_EULA=YES PYTHONNOUSERSITE=1 +export RAY_ADDRESS=local RAY_USAGE_STATS_ENABLED=0 RAY_TMPDIR=/tmp/gm8877 +export PYTHONPATH="$CUROBO_SRC:/root/.config/superpowers/worktrees/GenManip/labutopia-ebench-poc" +export LD_LIBRARY_PATH="$CUDA11_LIB:$ENV/lib/python3.10/site-packages/torch/lib:${LD_LIBRARY_PATH:-}" +$PY ray_eval_server.py --host 127.0.0.1 --port 18088 --run_id "$RUN_ID" --no_save_process --episode_recorder_save_every 0 --reset_timeout 1200 --step_timeout 1200 --load_config_timeout 300 +``` + +Client: + +```bash +PY=/cpfs/shared/simulation/zhuzihou/dev/conda-managed/envs/embodied-eval-os-sim-isaacsim41-genmanip-py310/bin/python +ENV=/cpfs/shared/simulation/zhuzihou/dev/conda-managed/envs/embodied-eval-os-sim-isaacsim41-genmanip-py310 +export RUN_ID=labutopia_franka_smoke_clean8_20260622_100208 +export PYTHONNOUSERSITE=1 +export PYTHONPATH="$ENV/lib/python3.10:$ENV/lib/python3.10/site-packages:/cpfs/shared/simulation/zhuzihou/dev/genmanip-client/src:/root/.config/superpowers/worktrees/GenManip/labutopia-ebench-poc" +$PY - <<'PY' +import os +from genmanip_client.cli import main +raise SystemExit(main(['submit', 'ebench/labutopia_lab_poc/franka_poc', '--run_id', os.environ['RUN_ID'], '--host', '127.0.0.1', '--port', '18088'])) +PY +$PY - <<'PY' +import os +from genmanip_client.cli import main +raise SystemExit(main(['eval', '--worker_ids', '0', '--run_id', os.environ['RUN_ID'], '--host', '127.0.0.1', '--port', '18088', '--no_save_process', '--frame_save_interval', '0', '--chunk_size', '1'])) +PY +``` + +## Raw Evidence + +Client reset/final output: + +```text +Reset complete in 87.73s +Reset complete in 40.33s +Reset complete in 41.31s +Reset complete in 1.01s +Final Evaluation Result +(1/1)ebench/labutopia_lab.. 0.00% +(1/1)ebench/labutopia_lab.. 0.00% +(1/1)ebench/labutopia_lab.. 0.00% +Client closed. +``` + +Final status: + +```json +{"data":{"status":"complete","benchmark_id":"ebench","run_id":"labutopia_franka_smoke_clean8_20260622_100208","total_episodes":3,"completed_episodes":3,"in_progress_episodes":0,"active_workers":[],"results":{"ebench/labutopia_lab_poc/franka_poc/level1_pick":0.0,"ebench/labutopia_lab_poc/franka_poc/level1_place":0.0,"ebench/labutopia_lab_poc/franka_poc/level1_open_door":0.0}}} +``` + +Result files: + +```text +saved/eval_results/ebench/labutopia_franka_smoke_clean8_20260622_100208/ebench/labutopia_lab_poc/franka_poc/level1_open_door/000/result_info.json +saved/eval_results/ebench/labutopia_franka_smoke_clean8_20260622_100208/ebench/labutopia_lab_poc/franka_poc/level1_pick/000/result_info.json +saved/eval_results/ebench/labutopia_franka_smoke_clean8_20260622_100208/ebench/labutopia_lab_poc/franka_poc/level1_place/000/result_info.json +``` + +Key server log lines: + +```text +Using LabUtopia POC assets overlay. previous_ASSETS_DIR=/root/.config/superpowers/worktrees/GenManip/labutopia-ebench-poc/saved/assets ASSETS_DIR=/cpfs/shared/simulation/zhuzihou/dev/_datasets/EBench-Assets-Overlay/labutopia_level1_poc/assets runtime_scene=/cpfs/shared/simulation/zhuzihou/dev/_datasets/EBench-Assets-Overlay/labutopia_level1_poc/assets/scene_usds/labutopia/level1_poc/lab_001/scene.usda +Starting background reset for worker=0 info={'info': 0.0, 'termination_reason': 'non_finite_arm_state'} +Saving final result for benchmark_id=ebench +``` + +## Notes + +- The server was shut down after the run. +- Final port check: `18088=7`, `8087=0`; 18088 was offline and the separate EOS service on 8087 remained reachable. +- If `post_episode_process` raises an exception, the worker pool now fails fast and does not use the numeric done-info fallback to create a completed minimal result. +- USD material binding warnings and PhysX invalid-transform warnings appear during default-action smoke. They do not currently block reset/result completion, but they are relevant for baseline-policy scoring work. diff --git a/docs/records/2026-06-22-labutopia-ebench-lift2-baseline-lane-planning.md b/docs/records/2026-06-22-labutopia-ebench-lift2-baseline-lane-planning.md new file mode 100644 index 0000000..aa078d7 --- /dev/null +++ b/docs/records/2026-06-22-labutopia-ebench-lift2-baseline-lane-planning.md @@ -0,0 +1,73 @@ +# 2026-06-22 LabUtopia EBench Lift2 Baseline Lane Planning + +## Context + +Franka POC wiring smoke is closed with retained evidence: + +```text +run_id=labutopia_franka_smoke_clean8_20260622_100208 +status=complete +completed_episodes=3/3 +scores=0.0 for pick/place/open_door +claim_boundary=wiring smoke, not task solving +``` + +The next product question is whether LabUtopia can be moved toward an +EBench-official lift2 baseline evaluable lane without overclaiming. + +## Decision / Change + +Adopt an EOS-style staged lane: + +```text +P0 runtime asset and environment preflight +P1 lift2_candidate dry smoke +P2 official lift2 baseline discovery +P3 official baseline local contrast attempt +P4 closure and PM update +``` + +The plan is saved at: + +```text +docs/superpowers/plans/2026-06-22-ebench-labutopia-lift2-baseline-lane.md +``` + +## Files Touched + +Planning only: + +```text +docs/superpowers/plans/2026-06-22-ebench-labutopia-lift2-baseline-lane.md +docs/records/2026-06-22-labutopia-ebench-lift2-baseline-lane-planning.md +``` + +## Validation + +EOS reference patterns reviewed: + +```text +/cpfs/user/zhuzihou/dev/embodied-eval-os/CLAUDE.md +/cpfs/user/zhuzihou/dev/embodied-eval-os/adapters/ebench/README.md +/cpfs/user/zhuzihou/dev/embodied-eval-os/docs/records/2026-06-15-bpl8-ebench-owned-task-score-lane-planning.md +/cpfs/user/zhuzihou/dev/embodied-eval-os/docs/records/2026-06-19-bpl19m-official-style-online-policy-rollout-planning.md +/cpfs/user/zhuzihou/dev/embodied-eval-os/docs/records/2026-06-20-bpl19q-q2-official-native-smoke-closure.md +``` + +Local preflight observation: + +```text +LabUtopia overlay runtime scene exists. +LabUtopia overlay root does not contain robot_usds/lift2/robot.usd. +LabUtopia overlay root does not contain miscs/curobo/R5a/r5a_left_arm.yml. +``` + +## Known Limitations + +This record is a planning record. It does not claim lift2 dry smoke, official +baseline execution, official leaderboard comparability, or task success. + +## Next Actions + +Start P0 only. Do not run a long lift2 smoke until the composite runtime asset +root preflight passes. diff --git a/docs/records/2026-06-22-labutopia-ebench-weekly-report.md b/docs/records/2026-06-22-labutopia-ebench-weekly-report.md new file mode 100644 index 0000000..eea76b4 --- /dev/null +++ b/docs/records/2026-06-22-labutopia-ebench-weekly-report.md @@ -0,0 +1,188 @@ +# 2026-06-22 LabUtopia EBench 接入周报 + +HTML 版产品汇报页: +`docs/records/evidence/2026-06-22-labutopia-ebench-weekly-report/index.html` + +## 一句话进展 + +本周已经把 LabUtopia 的 Franka POC 跑通到端到端 smoke 阶段:任务可以提交、场景可以加载、三个 level-1 任务可以 reset/step、结果可以落盘、最终状态可以正常 complete。 + +这说明当前最关键的“接入链路”已经打通。需要注意的是,这还不是任务求解成功,也不是官方 baseline 成绩;当前 smoke 使用默认动作,所以三个任务分数都是 `0.0`。 + +## 本周完成了什么 + +### 1. LabUtopia POC 任务包已可运行 + +当前已经跑通 3 个 Franka POC 任务: + +| 任务 | 当前状态 | 说明 | +| --- | --- | --- | +| `level1_pick` | 已跑通 | 能 reset、step、写结果 | +| `level1_place` | 已跑通 | 能 reset、step、写结果 | +| `level1_open_door` | 已跑通 | 能 reset、step、写结果 | + +最新 smoke 结果: + +```text +run_id=labutopia_franka_smoke_clean8_20260622_100208 +status=complete +completed=3/3 +score=0.0 for all tasks +``` + +### 2. LabUtopia 场景资产加载已跑通 + +当前已经能正确加载 LabUtopia 的实验室场景资产,包括桌面、瓶子、烧杯、托盘、干燥箱、门把手等对象。 + +简单理解,资产加载现在是这样做的: + +1. 任务配置里只写相对路径,例如: + +```text +scene_usds/labutopia/level1_poc/lab_001/scene +``` + +2. 系统识别到这是 LabUtopia POC 任务后,会读取: + +```text +configs/tasks/ebench/labutopia_lab_poc/common/assets_manifest.json +``` + +3. 这个 manifest 告诉系统真正的 LabUtopia 资产目录在哪里: + +```text +/cpfs/shared/simulation/zhuzihou/dev/_datasets/EBench-Assets-Overlay/labutopia_level1_poc/assets +``` + +4. 运行时会把 `ASSETS_DIR` 临时切换到这个目录,所以相对路径会被解析成真实的 `scene.usda` 文件。 + +5. 加载完成后,manifest 还会把 LabUtopia 原始对象映射成 GenManip 运行时可以识别的对象名,例如: + +```text +conical_bottle02 -> obj_conical_bottle02 +beaker2 -> obj_beaker2 +DryingBox_01 -> obj_DryingBox_01 +table -> table +``` + +这部分已经通过实际 smoke 验证。 + +### 3. 结果记录链路已修复 + +之前任务虽然能跑,但最后可能因为结果没有正确写入而卡在结束阶段。现在已经修复: + +- 每个任务结束后都会写 `result_info.json` +- 最终会生成总结果 `result.json` +- server 不会再在最后一个任务结束后等待到超时 +- 如果后处理真的报错,系统会 fail fast,不会把错误伪装成“已完成” + +### 4. 与 EOS/其他工程师任务做了隔离 + +本次验证使用独立端口: + +```text +LabUtopia POC smoke: 18088 +EOS/其他工程师任务: 8087 +``` + +验证结束后: + +```text +18088 已关闭 +8087 仍正常在线 +``` + +所以当前 LabUtopia POC 测试没有干扰 EOS 那边正在跑的任务。 + +## 有没有渲染图验证? + +目前没有保留“产品可审阅”的渲染图或视频证据。 + +原因是这次 smoke 的目标是先验证接入链路,所以运行时使用了: + +```text +--no_save_process +--frame_save_interval 0 +``` + +这会关闭过程图片/视频保存,减少运行成本和干扰。因此当前证据主要是: + +- server/client 成功完成 +- 场景资产路径解析正确 +- reset/step 正常返回 +- 3 个任务都有 `result_info.json` +- 最终 status 是 `complete` + +这能证明“链路跑通”,但不能证明“画面渲染效果符合产品预期”。渲染图验收需要作为后续单独一项补上。 + +建议下一次渲染验收保留: + +```text +reset 后第一帧截图 +每个任务一张关键视角截图 +必要时保留短视频或 merged mp4 +``` + +## 本周遇到的问题和解决方式 + +| 问题 | 表现 | 解决方式 | 当前状态 | +| --- | --- | --- | --- | +| 资产根目录不对 | 系统一开始会去错误目录找 LabUtopia 场景 | 增加 LabUtopia manifest 识别逻辑,运行时切换到 overlay asset root | 已解决 | +| 缺少 `meta_info.pkl` | 传统 GenManip 任务依赖采集包里的元信息,LabUtopia POC 没有这份文件 | 针对 LabUtopia POC,从实时场景里自动生成最小可用元信息 | 已解决 | +| camera cleanup 字段缺失 | 切换任务时 camera 清理报字段错误 | 补齐 camera 配置里的 cleanup flags | 已解决 | +| 最终进度不 complete | 任务已跑完,但进度没有写入,导致客户端最后等待超时 | 任务结束时写最小 `result_info.json`,并修复进度统计 | 已解决 | +| 后处理异常可能被误记为完成 | 如果后处理报错,不能用 0 分兜底伪装成成功 | 增加 fail-fast 逻辑和回归测试 | 已解决 | +| 端口/结果混淆风险 | EOS 侧也有人在跑任务,担心互相影响 | 使用独立 worktree、独立 run_id、独立端口 18088,并复核 8087 状态 | 已解决 | + +## 当前进度判断 + +| 模块 | 产品视角状态 | 说明 | +| --- | --- | --- | +| LabUtopia 场景接入 | 已跑通 | 场景资产能加载,任务链路能跑完 | +| Franka POC smoke | 已完成 | 3 个任务全部 complete | +| 结果落盘 | 已完成 | per-task 和 final result 都能写出 | +| 渲染图/视频验收 | 未完成 | 本轮没有保存图片/视频,需要后续补 | +| 任务求解能力 | 未验证 | 当前默认动作得分 0.0,不代表策略能力 | +| 官方 baseline | 未进入本周结论 | 后续单独讨论和规划 | + +## 验证证据 + +本周保留的主要证据: + +```text +run_id=labutopia_franka_smoke_clean8_20260622_100208 +status=complete +completed_episodes=3 +total_episodes=3 +``` + +测试和校验: + +```text +python -m pytest tests/labutopia_poc -q +23 passed, 1 skipped + +python standalone_tools/labutopia_poc/validate_task_package.py +LabUtopia task package validation OK +``` + +结果文件: + +```text +saved/eval_results/ebench/labutopia_franka_smoke_clean8_20260622_100208/result.json +saved/eval_results/ebench/labutopia_franka_smoke_clean8_20260622_100208/.../level1_pick/000/result_info.json +saved/eval_results/ebench/labutopia_franka_smoke_clean8_20260622_100208/.../level1_place/000/result_info.json +saved/eval_results/ebench/labutopia_franka_smoke_clean8_20260622_100208/.../level1_open_door/000/result_info.json +``` + +## 下周建议 + +先补一个“渲染图验收”小闭环,再进入下一阶段机器人/baseline 讨论。 + +建议顺序: + +1. 开一轮保存图片的短 smoke。 +2. 为三个任务各保留 reset 后截图。 +3. 检查截图里场景、目标物、桌面、门/干燥箱是否可见且位置合理。 +4. 更新本周证据文档,明确“渲染验收通过/未通过”。 +5. 再进入下一阶段机器人和 baseline 的细化讨论。 diff --git a/docs/records/evidence/2026-06-22-labutopia-ebench-weekly-report/index.html b/docs/records/evidence/2026-06-22-labutopia-ebench-weekly-report/index.html new file mode 100644 index 0000000..601e12f --- /dev/null +++ b/docs/records/evidence/2026-06-22-labutopia-ebench-weekly-report/index.html @@ -0,0 +1,713 @@ + + + + + + + LabUtopia x EBench 接入周报 · 2026-06-22 + + + + + +
+
+
+

Weekly report · 2026-06-22 · PM-facing evidence page

+

LabUtopia x EBench 接入周报

+

本周 Franka POC 已经跑通端到端 smoke:任务能提交,LabUtopia 场景能加载,三个 level-1 任务能 reset/step,结果能落盘并正常 complete。当前结论是“接入链路跑通”,不是“策略已经会做任务”,也不是“官方 baseline 成绩”。

+
+ Franka POC complete + 3/3 episodes finalized + score=0.0 expected + Lift2 baseline lane planned +
+
+ +
+
+ +
+
+

当前进展总览

+
+
+
接入状态
+
链路跑通
+
server/client、reset、step、result 写入都已走完。
+
+
+
任务范围
+
3 个 level-1
+
pickplaceopen_door
+
+
+
最新 smoke
+
complete
+
labutopia_franka_smoke_clean8_20260622_100208
+
+
+
测试
+
23 passed
+
1 skipped,validator 通过。
+
+
+
渲染证据
+
未保留
+
本轮为轻量 smoke,关闭了过程图片和视频保存。
+
+
+
官方 Lift2 baseline
+
下一阶段
+
需要先完成复合资产根目录和官方 runner 发现。
+
+
+
+ +
+

本周完成了什么

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
事项产品视角状态说明证据边界
LabUtopia Franka POC已跑通三个 POC 任务能在本地评测 server 中启动、reset、step、结束。证明接入链路,不证明策略能力。
场景资产加载Franka POC 闭环通过 manifest 把 LabUtopia overlay 资产根目录接入运行时,并映射对象名。Franka POC 已验证;Lift2 还要补 robot/curobo 资产组合。
结果生命周期已修复每个任务写 result_info.json,最终写 result.json,后处理异常 fail fast。不会把后处理错误伪装成已完成。
并行任务隔离已隔离LabUtopia 用 18088,EOS/其他工程师侧 8087 未被干扰。run_id、worktree、端口均独立。
渲染图验收未完成本轮命令使用 --no_save_process--frame_save_interval 0暂不能对画面质量做产品验收结论。
+
+
+ +
+

资产加载是怎么做的

+

可以把资产加载理解成“任务配置只写短路径,运行时通过 manifest 找到真实资产仓库,再把对象名翻译成评测系统认识的名字”。这让任务包保持轻量,也避免把大资产直接放进代码仓库。

+ +
+
+
1
+
任务配置
+
写相对路径,例如 scene_usds/labutopia/level1_poc/lab_001/scene
+
+
+
2
+
读取 manifest
+
读取 configs/tasks/ebench/labutopia_lab_poc/common/assets_manifest.json
+
+
+
3
+
切换资产根目录
+
运行时临时把 ASSETS_DIR 指到 LabUtopia overlay。
+
+
+
4
+
解析 scene.usda
+
相对路径变成真实 scene.usda 文件并加载进 Isaac Sim。
+
+
+
5
+
对象名映射
+
例如 beaker2 映射成 obj_beaker2 供 evaluator 使用。
+
+
+ +
+ + LabUtopia asset loading and Lift2 readiness + + + Task config + short scene path + + + + Manifest override + LabUtopia overlay root + + + + Franka POC runtime + scene.usda + object map verified + + Lift2 next gate + scene overlay is not enough + + + + Missing for baseline + robot_usds/lift2 + cuRobo + + + + Composite root + P0 work item + +
+ +
+
+

已经闭环的部分

+

LabUtopia 场景 overlay 能被 Franka POC 加载,桌面、瓶子、烧杯、托盘、干燥箱、门把手等对象能通过运行时对象映射进入 evaluator。

+
+
+

还没闭环的部分

+

Lift2 官方 baseline 不能只用 scene overlay,它还需要默认资产里的 robot_usds/lift2miscs/curobo。因此下一步要做“复合资产根目录”,把 LabUtopia 场景资产和默认机器人资产组合起来。

+
+
+
+ +
+

验证证据

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
证据项结果说明
run_idlabutopia_franka_smoke_clean8_20260622_100208独立 smoke 运行记录。
episode 完成度3/3 completelevel1_picklevel1_placelevel1_open_door 均结束并写入结果。
score / sr0.0默认动作 smoke 的预期结果,只说明链路,不说明求解能力。
pytest23 passed, 1 skipped覆盖资产 override、fallback metadata、结果落盘、后处理 fail-fast 等回归点。
package validatorLabUtopia task package validation OK任务包、manifest、camera cleanup 配置通过静态校验。
端口隔离18088 closed, 8087 online没有和 EOS/其他工程师正在跑的任务混淆。
+
+
+ +
+

问题和处理结果

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
问题表现处理方式当前状态
资产根目录不对系统一开始会去默认目录找 LabUtopia 场景。增加 LabUtopia manifest 识别逻辑,运行时切换 overlay asset root。已解决
缺少 meta_info.pkl传统采集包依赖的 metadata 在 LabUtopia POC 中不存在。从实时场景自动生成最小可用 metadata。已解决
camera cleanup 字段缺失切换任务时 camera 清理报字段错误。补齐 POC camera 配置 cleanup flags。已解决
最终进度不 complete任务已跑完,但客户端等待最终结果直到超时。任务结束时写最小 result_info.json 并修复进度统计。已解决
后处理异常可能被误记为完成异常路径不能用 0 分兜底伪装为成功。增加 fail-fast 逻辑和回归测试。已解决
+
+
+ +
+

Claim Boundary

+
+
+

现在可以说

+
    +
  • LabUtopia Franka POC 已经能通过 GenManip/EBench server-client 链路本地跑完。
  • +
  • 三个 level-1 任务能完成 reset/step/finalize,并生成 per-task 和 final result。
  • +
  • Franka POC 的 LabUtopia scene overlay 加载路径已经验证。
  • +
+
+
+

现在不能说

+
    +
  • 不能说任务已经求解成功,当前默认动作得分是 0.0
  • +
  • 不能说官方 Lift2 baseline 已经可评,Lift2 复合资产和官方 runner 还没闭环。
  • +
  • 不能说画面渲染已验收,本轮没有保存图片或视频。
  • +
+
+
+
+ +
+

接下来几步

+
+
+ P0 +

Lift2 资产预检

+

构建复合资产根目录:LabUtopia scene overlay + 默认 robot_usds/lift2 + 默认 miscs/curobo

+
+
+ P1 +

Lift2 dry smoke

+

在独立端口运行 lift2_candidate,要求 3/3 complete,但仍保持 official_baseline_execution=false

+
+
+ P2 +

官方 runner 发现

+

定位官方 EBench/OpenPI/Lift2 runner 文件,记录路径、hash 和入口,不直接执行策略。

+
+
+ P3 +

官方式本地尝试

+

只有 P2 过后,才做一轮官方风格 online loop,并保留 terminal evidence。

+
+
+ P4 +

证据封口

+

补 artifact linkage、forbidden-claims、path-leakage 检查,再改变 PM 对外口径。

+
+
+
+ +
+

相关文档

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
文档用途链接
Markdown 周报文字版 PM 汇报。打开
Franka smoke 记录本次 smoke 的 run_id、命令、结果路径。打开
Lift2 baseline lane plan仿 EOS 的 P0-P4 证据门推进计划。打开
POC status plan本周 POC 状态和环境决策。打开
+
+
+ + +
+ + diff --git a/docs/superpowers/plans/2026-06-22-ebench-labutopia-lift2-baseline-lane.md b/docs/superpowers/plans/2026-06-22-ebench-labutopia-lift2-baseline-lane.md new file mode 100644 index 0000000..248e1e9 --- /dev/null +++ b/docs/superpowers/plans/2026-06-22-ebench-labutopia-lift2-baseline-lane.md @@ -0,0 +1,526 @@ +# EBench LabUtopia Lift2 Baseline Lane Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Move from Franka wiring smoke to an EOS-style, evidence-gated LabUtopia `lift2_candidate` lane that can truthfully support an "EBench official lift2 baseline is locally evaluable" claim after the required gates pass. + +**Architecture:** Treat LabUtopia as an EBench/GenManip compatibility lane with strict claim boundaries. Keep LabUtopia-specific asset composition, smoke scripts, official-baseline discovery, and evidence reports outside generic evaluator core; core runtime changes are allowed only when they preserve existing GenManip contracts. Use staged gates: preflight first, dry lift2 smoke second, official-baseline loop third, closure/evidence package last. + +**Tech Stack:** Python 3.10, Isaac Sim 4.1 runtime conda env, Ray Eval Server, GenManip client, LabUtopia POC config package, pytest, EOS-style evidence records. + +--- + +## EOS-Style Claim Boundary + +Allowed after P1 dry smoke: + +```text +LabUtopia lift2_candidate can be submitted, reset, stepped, and finalized locally through the GenManip/EBench server-client path. +``` + +Allowed only after P3 official-baseline gate: + +```text +The local environment can run one EBench official lift2 baseline-style online evaluation attempt on LabUtopia POC tasks and retain terminal source evidence. +``` + +Not allowed until a later benchmark-reproduction gate: + +```text +official leaderboard reproduction +leaderboard comparability +benchmark-wide model quality +model superiority +backend parity +real-world safety +hardware readiness +official EBench score release +``` + +## File Structure + +- Modify `genmanip/core/evaluator/labutopia_assets.py` + - Owns LabUtopia POC asset-root resolution. + - Next change: build or select a composite runtime asset root that contains LabUtopia scene overlay plus default GenManip robot/curobo assets needed by Lift2. +- Modify `genmanip/core/evaluator/isaac_worker_pool.py` + - Uses LabUtopia asset override through `resolve_labutopia_poc_assets_override`. + - Should not learn official-policy concepts. +- Modify `standalone_tools/labutopia_poc/validate_task_package.py` + - Owns static package validation. + - Next change: validate lift2 runtime robot/camera/curobo asset readiness under the effective asset root. +- Create `standalone_tools/labutopia_poc/run_lift2_smoke.py` + - Thin, reproducible wrapper around the proven server/client command sequence. + - Writes a machine-readable smoke report. +- Create `standalone_tools/labutopia_poc/discover_official_lift2_baseline.py` + - Discovers official EBench/OpenPI/lift2 runner files and records paths/hashes. + - Does not execute a policy. +- Create `tests/labutopia_poc/test_labutopia_composite_assets.py` + - TDD coverage for the composite asset root contract. +- Create `tests/labutopia_poc/test_lift2_smoke_report_contract.py` + - TDD coverage for the smoke report schema and claim flags. +- Create `docs/records/2026-06-22-labutopia-ebench-lift2-baseline-lane-planning.md` + - EOS-style dated planning record. +- Update `docs/superpowers/plans/2026-06-22-ebench-labutopia-poc.md` + - PM-facing status should point to this lane plan as the next execution path. + +## Current Known Preflight Finding + +The current LabUtopia overlay root contains the LabUtopia runtime scene but does not contain Lift2 robot assets: + +```text +overlay_root=/cpfs/shared/simulation/zhuzihou/dev/_datasets/EBench-Assets-Overlay/labutopia_level1_poc/assets +overlay_root/scene_usds/labutopia/level1_poc/lab_001/scene.usda exists +overlay_root/robot_usds/lift2/robot.usd missing +overlay_root/miscs/curobo/R5a/r5a_left_arm.yml missing +``` + +Therefore P0 must happen before any serious `lift2_candidate` smoke. Do not start by blindly running lift2 for hours. + +## P0: Runtime Asset And Environment Preflight + +**Files:** +- Modify: `genmanip/core/evaluator/labutopia_assets.py` +- Modify: `standalone_tools/labutopia_poc/validate_task_package.py` +- Test: `tests/labutopia_poc/test_labutopia_composite_assets.py` +- Test: `tests/labutopia_poc/test_validate_task_package.py` + +- [ ] **Step 1: Write failing composite asset root test** + +Create `tests/labutopia_poc/test_labutopia_composite_assets.py` with a test that builds a temporary default asset root and LabUtopia scene overlay root: + +```python +from pathlib import Path + +from genmanip.core.evaluator.labutopia_assets import build_labutopia_runtime_asset_root + + +def test_composite_asset_root_contains_overlay_scene_and_default_lift2_assets(tmp_path): + default_root = tmp_path / "default_assets" + overlay_root = tmp_path / "overlay_assets" + runtime_root = tmp_path / "runtime_assets" + + (default_root / "robot_usds/lift2").mkdir(parents=True) + (default_root / "robot_usds/lift2/robot.usd").write_text("# lift2", encoding="utf-8") + (default_root / "miscs/curobo/R5a").mkdir(parents=True) + (default_root / "miscs/curobo/R5a/r5a_left_arm.yml").write_text("robot_cfg: {}", encoding="utf-8") + (overlay_root / "scene_usds/labutopia/level1_poc/lab_001").mkdir(parents=True) + (overlay_root / "scene_usds/labutopia/level1_poc/lab_001/scene.usda").write_text("# scene", encoding="utf-8") + + result = build_labutopia_runtime_asset_root(default_root, overlay_root, runtime_root) + + assert (Path(result) / "scene_usds/labutopia/level1_poc/lab_001/scene.usda").exists() + assert (Path(result) / "robot_usds/lift2/robot.usd").exists() + assert (Path(result) / "miscs/curobo/R5a/r5a_left_arm.yml").exists() +``` + +- [ ] **Step 2: Run test and confirm RED** + +Run: + +```bash +python -m pytest tests/labutopia_poc/test_labutopia_composite_assets.py -q +``` + +Expected: + +```text +Module import or function missing failure for build_labutopia_runtime_asset_root +``` + +- [ ] **Step 3: Implement composite asset root** + +Implement `build_labutopia_runtime_asset_root(default_root, overlay_root, runtime_root)` in `genmanip/core/evaluator/labutopia_assets.py`. + +Contract: + +```text +runtime_root/scene_usds -> overlay_root/scene_usds +runtime_root/manifests -> overlay_root/manifests when present +runtime_root/miscs/mdl/labutopia -> overlay_root/miscs/mdl/labutopia when present +runtime_root/robot_usds -> default_root/robot_usds +runtime_root/miscs/curobo -> default_root/miscs/curobo +``` + +Use symlinks when possible. If symlink creation fails, copy directories with `shutil.copytree(..., dirs_exist_ok=True)`. + +- [ ] **Step 4: Route LabUtopia override through composite root** + +Update `resolve_labutopia_poc_assets_override()` so LabUtopia POC configs return the composite runtime root instead of the scene-only overlay root. + +Runtime root should be deterministic and gitignored: + +```text +saved/assets/labutopia_level1_poc_runtime +``` + +- [ ] **Step 5: Extend validator for lift2 runtime assets** + +Update `standalone_tools/labutopia_poc/validate_task_package.py` so `validate_task_package()` checks: + +```text +effective_labutopia_asset_root/scene_usds/labutopia/level1_poc/lab_001/scene.usda +effective_labutopia_asset_root/robot_usds/lift2/robot.usd +saved/assets/miscs/curobo/R5a/r5a_left_arm.yml +configs/cameras/fixed_camera_lift2_simbox.yml cleanup flags +``` + +- [ ] **Step 6: Verify P0** + +Run: + +```bash +python -m pytest tests/labutopia_poc/test_labutopia_composite_assets.py tests/labutopia_poc/test_validate_task_package.py -q +python standalone_tools/labutopia_poc/validate_task_package.py +``` + +Expected: + +```text +all selected tests pass +LabUtopia task package validation OK +``` + +## P1: Lift2 Candidate Dry Smoke + +**Files:** +- Create: `standalone_tools/labutopia_poc/run_lift2_smoke.py` +- Create: `tests/labutopia_poc/test_lift2_smoke_report_contract.py` +- Update: `docs/labutopia_lab_poc/franka_smoke.md` only if shared smoke conventions change +- Create: `docs/labutopia_lab_poc/lift2_smoke.md` + +- [ ] **Step 1: Write smoke report contract test** + +Create `tests/labutopia_poc/test_lift2_smoke_report_contract.py`: + +```python +from standalone_tools.labutopia_poc.run_lift2_smoke import build_smoke_report + + +def test_lift2_smoke_report_keeps_claim_boundary(): + report = build_smoke_report( + run_id="labutopia_lift2_smoke_example", + status="complete", + completed_episodes=3, + total_episodes=3, + results={ + "ebench/labutopia_lab_poc/lift2_candidate/level1_pick": 0.0, + "ebench/labutopia_lab_poc/lift2_candidate/level1_place": 0.0, + "ebench/labutopia_lab_poc/lift2_candidate/level1_open_door": 0.0, + }, + save_process=False, + ) + + assert report["profile"] == "lift2_candidate" + assert report["dry_smoke_status"] == "complete" + assert report["official_baseline_execution"] is False + assert report["official_benchmark_reproduction"] is False + assert report["official_leaderboard_comparable"] is False + assert report["standard_model_score"] is None +``` + +- [ ] **Step 2: Run test and confirm RED** + +Run: + +```bash +python -m pytest tests/labutopia_poc/test_lift2_smoke_report_contract.py -q +``` + +Expected: + +```text +Module import or function missing failure for run_lift2_smoke +``` + +- [ ] **Step 3: Implement report builder and CLI skeleton** + +Create `standalone_tools/labutopia_poc/run_lift2_smoke.py` with: + +```python +from __future__ import annotations + +import argparse +import json +from pathlib import Path +from typing import Any + + +def build_smoke_report( + *, + run_id: str, + status: str, + completed_episodes: int, + total_episodes: int, + results: dict[str, float], + save_process: bool, +) -> dict[str, Any]: + return { + "profile": "lift2_candidate", + "config": "ebench/labutopia_lab_poc/lift2_candidate", + "run_id": run_id, + "dry_smoke_status": status, + "completed_episodes": completed_episodes, + "total_episodes": total_episodes, + "results": results, + "save_process": save_process, + "official_baseline_execution": False, + "official_benchmark_reproduction": False, + "official_leaderboard_comparable": False, + "standard_model_score": None, + } +``` + +The first version may require the operator to run server/client commands manually and then pass `--status-json`; it must not hide process execution details. + +- [ ] **Step 4: Run isolated lift2 dry smoke** + +Use the same isolation contract as Franka: + +```bash +PY=/cpfs/shared/simulation/zhuzihou/dev/conda-managed/envs/embodied-eval-os-sim-isaacsim41-genmanip-py310/bin/python +ENV=/cpfs/shared/simulation/zhuzihou/dev/conda-managed/envs/embodied-eval-os-sim-isaacsim41-genmanip-py310 +WORKTREE=/root/.config/superpowers/worktrees/GenManip/labutopia-ebench-poc +CUROBO_SRC=/cpfs/shared/simulation/mamengchen/curobo-wbc-backup/src +CUDA11_LIB=/isaac-sim/exts/omni.isaac.ml_archive/pip_prebundle/nvidia/cuda_runtime/lib +RUN_ID=labutopia_lift2_smoke_$(date -u +%Y%m%d_%H%M%S) +export ACCEPT_EULA=Y OMNI_KIT_ACCEPT_EULA=YES PYTHONNOUSERSITE=1 +export RAY_ADDRESS=local RAY_USAGE_STATS_ENABLED=0 RAY_TMPDIR=/tmp/gm_lift2 +export PYTHONPATH="$CUROBO_SRC:$WORKTREE" +export LD_LIBRARY_PATH="$CUDA11_LIB:$ENV/lib/python3.10/site-packages/torch/lib:${LD_LIBRARY_PATH:-}" +"$PY" ray_eval_server.py --host 127.0.0.1 --port 18088 --run_id "$RUN_ID" --no_save_process --episode_recorder_save_every 0 --reset_timeout 1200 --step_timeout 1200 --load_config_timeout 300 +``` + +In a separate shell: + +```bash +PY=/cpfs/shared/simulation/zhuzihou/dev/conda-managed/envs/embodied-eval-os-sim-isaacsim41-genmanip-py310/bin/python +ENV=/cpfs/shared/simulation/zhuzihou/dev/conda-managed/envs/embodied-eval-os-sim-isaacsim41-genmanip-py310 +WORKTREE=/root/.config/superpowers/worktrees/GenManip/labutopia-ebench-poc +export PYTHONNOUSERSITE=1 +export PYTHONPATH="$ENV/lib/python3.10:$ENV/lib/python3.10/site-packages:/cpfs/shared/simulation/zhuzihou/dev/genmanip-client/src:$WORKTREE" +"$PY" - <<'PY' +import os +from genmanip_client.cli import main +raise SystemExit(main(['submit', 'ebench/labutopia_lab_poc/lift2_candidate', '--run_id', os.environ['RUN_ID'], '--host', '127.0.0.1', '--port', '18088'])) +PY +"$PY" - <<'PY' +import os +from genmanip_client.cli import main +raise SystemExit(main(['eval', '--worker_ids', '0', '--run_id', os.environ['RUN_ID'], '--host', '127.0.0.1', '--port', '18088', '--no_save_process', '--frame_save_interval', '0', '--chunk_size', '1'])) +PY +``` + +- [ ] **Step 5: P1 acceptance** + +Accept P1 only if all are true: + +```text +server did not attach to or disrupt 8087 +18088 is down after cleanup +8087 remains reachable after cleanup +status=complete +completed_episodes=3 +result_info.json exists for level1_pick, level1_place, level1_open_door +official_baseline_execution=false +standard_model_score=null +``` + +If any condition fails, create `docs/labutopia_lab_poc/lift2_smoke.md` with `dry_smoke_status=blocked_with_evidence` and list the exact blocker. + +## P2: Official Lift2 Baseline Discovery Gate + +**Files:** +- Create: `standalone_tools/labutopia_poc/discover_official_lift2_baseline.py` +- Create: `tests/labutopia_poc/test_official_lift2_baseline_discovery_contract.py` +- Create: `docs/labutopia_lab_poc/official_lift2_baseline_discovery.md` + +- [ ] **Step 1: Write discovery contract test** + +Create `tests/labutopia_poc/test_official_lift2_baseline_discovery_contract.py`: + +```python +from standalone_tools.labutopia_poc.discover_official_lift2_baseline import build_discovery_report + + +def test_discovery_report_is_no_execution_and_no_score(): + report = build_discovery_report( + source_root="/tmp/EBench", + discovered_files=["baselines/openpi/scripts/pi_eval_client_online.py"], + missing_files=[], + ) + + assert report["discovery_status"] == "passed" + assert report["official_baseline_execution_attempted"] is False + assert report["standard_model_score"] is None + assert report["official_benchmark_reproduction"] is False + assert report["official_leaderboard_comparable"] is False +``` + +- [ ] **Step 2: Implement discovery report builder** + +The discovery script must check candidate roots in this order: + +```text +$EBENCH_ROOT +/cpfs/shared/simulation/zhuzihou/dev/EBench +/cpfs/user/zhuzihou/dev/EBench +/cpfs/user/zhuzihou/dev/embodied-eval-os/.external/EBench +``` + +It must record whether these files exist: + +```text +scripts/launch_pi_onlineeval.sh +baselines/openpi/scripts/pi_eval_client_online.py +baselines/openpi/third_party/openpi/scripts/serve_policy.py +baselines/openpi/src/openpi/training/config.py +``` + +- [ ] **Step 3: Run discovery** + +Run: + +```bash +python standalone_tools/labutopia_poc/discover_official_lift2_baseline.py --output docs/labutopia_lab_poc/official_lift2_baseline_discovery.json +``` + +Acceptance: + +```text +discovery_status=passed or blocked_with_evidence +official_baseline_execution_attempted=false +standard_model_score=null +``` + +## P3: Official Baseline Local Contrast Attempt + +**Files:** +- Create: `standalone_tools/labutopia_poc/run_official_lift2_baseline_contrast.py` +- Create: `tests/labutopia_poc/test_official_lift2_baseline_contrast_report.py` +- Create: `docs/labutopia_lab_poc/official_lift2_baseline_contrast.md` + +- [ ] **Step 1: Mirror EOS BPL-19Q shape** + +The local contrast runner must follow this loop shape: + +```text +GenManip reset_result +-> official client observation prep +-> policy inference +-> official action prep +-> GenManip step or step_chunk +-> repeat until terminal result or max cycle +``` + +Do not implement fixed action replay as a substitute for online control. + +- [ ] **Step 2: Add report contract test** + +Create `tests/labutopia_poc/test_official_lift2_baseline_contrast_report.py`: + +```python +from standalone_tools.labutopia_poc.run_official_lift2_baseline_contrast import build_contrast_report + + +def test_official_contrast_report_keeps_source_score_separate(): + report = build_contrast_report( + run_id="example", + status="executed", + policy_cycle_count=2, + terminal_result_present=True, + source_metric_score_value=0.0, + ) + + assert report["official_baseline_execution_attempted"] is True + assert report["official_eval_loop_status"] == "executed" + assert report["source_metric_score_value"] == 0.0 + assert report["standard_model_score"] is None + assert report["score_source_from_official_runner"] is False + assert report["official_benchmark_reproduction"] is False + assert report["official_leaderboard_comparable"] is False +``` + +- [ ] **Step 3: Execute only after P2 passed** + +Run the official contrast attempt only when P2 discovery has retained runner paths and environment requirements. + +Acceptance: + +```text +official_baseline_execution_attempted=true +official_eval_loop_status=executed or blocked_with_evidence +terminal_result_present=true for executed attempts +source_metric_score_value retained separately +standard_model_score=null +score_source_from_official_runner=false +official_benchmark_reproduction=false +official_leaderboard_comparable=false +artifact_linkage_audit_status=passed +forbidden_claims_audit_status=passed +path_leakage_audit_status=passed +``` + +## P4: Closure Record And PM Update + +**Files:** +- Create: `docs/records/2026-06-22-labutopia-lift2-dry-smoke-closure.md` after P1 +- Create: `docs/records/2026-06-22-labutopia-official-lift2-baseline-contrast-closure.md` after P3 +- Update: `docs/superpowers/plans/2026-06-22-ebench-labutopia-poc.md` + +- [ ] **Step 1: Write closure record** + +Use this exact structure: + +```markdown +# 2026-06-22 LabUtopia Lift2 Dry Smoke Closure + +## Context +## Decision / Change +## Files touched +## Validation +## Known limitations +## Next actions +``` + +- [ ] **Step 2: Update PM-facing plan** + +Only use these words if P1 passed: + +```text +Lift2 dry smoke completed. +``` + +Only use these words if P3 passed: + +```text +Official lift2 baseline local contrast executed with retained terminal source evidence. +``` + +Do not use: + +```text +official leaderboard comparable +official benchmark reproduced +official score released +baseline solved LabUtopia +``` + +## Verification Before Handoff + +Run all before claiming the lane is ready for PM update: + +```bash +python -m pytest tests/labutopia_poc -q +python standalone_tools/labutopia_poc/validate_task_package.py +curl -fsS http://127.0.0.1:18088/docs >/tmp/labutopia_18088_check.out 2>&1; echo "18088=$?" +curl -fsS http://127.0.0.1:8087/docs >/tmp/labutopia_8087_check.out 2>&1; echo "8087=$?" +``` + +Expected: + +```text +tests pass +LabUtopia task package validation OK +18088=7 after cleanup +8087=0 +``` diff --git a/docs/superpowers/plans/2026-06-22-ebench-labutopia-poc.md b/docs/superpowers/plans/2026-06-22-ebench-labutopia-poc.md new file mode 100644 index 0000000..9a0da64 --- /dev/null +++ b/docs/superpowers/plans/2026-06-22-ebench-labutopia-poc.md @@ -0,0 +1,53 @@ +# EBench LabUtopia POC Status + +Updated: 2026-06-22 + +## PM Summary + +EBench LabUtopia POC has reached a runnable end-to-end smoke state inside the GenManip evaluation server. The current Franka POC profile can be submitted through the client, reset three LabUtopia level-1 tasks, run one action step per task, record per-task results, and produce a final evaluation result without timing out at the end. + +This is a wiring and platform-readiness milestone, not a task-solving milestone. The smoke uses the client default action, so all three task scores are 0.0. That is expected for this check: it proves the benchmark package, scene assets, server lifecycle, progress accounting, and result writing path are connected. + +## Current State + +| Area | Status | Notes | +| --- | --- | --- | +| Franka POC package | Ready for smoke | `ebench/labutopia_lab_poc/franka_poc` includes `level1_pick`, `level1_place`, `level1_open_door`. | +| Asset loading | Ready for smoke | LabUtopia POC overrides `ASSETS_DIR` to the EBench overlay and resolves the runtime `scene.usda`. | +| Scene metadata | Ready for smoke | Missing collected-package `meta_info.pkl` is synthesized from the live LabUtopia scene for POC tasks. | +| Camera cleanup | Ready for smoke | POC camera configs now include cleanup flags required by GenManip camera reset. | +| Result lifecycle | Ready for smoke | Episodes that terminate before recorder finalize persist minimal `result_info.json`; real post-processing exceptions now fail fast instead of being hidden as completed results. | +| Lift2 official baseline | Not yet proven | Architecture is prepared to add/evaluate the lift2 candidate profile, but the official baseline smoke still needs to be run. | + +## Evidence + +Latest isolated smoke: + +- Run ID: `labutopia_franka_smoke_clean8_20260622_100208` +- Server port: `18088` +- Existing EOS/other-engineer port: `8087`, left untouched and still online after the run +- Final status: `complete`, `3/3` episodes completed +- Final result: all three tasks recorded `score=0.0`, `sr=0.0` +- Regression tests: `python -m pytest tests/labutopia_poc -q` -> `23 passed, 1 skipped` +- Package validator: `python standalone_tools/labutopia_poc/validate_task_package.py` -> `LabUtopia task package validation OK` + +Details are recorded in `docs/labutopia_lab_poc/franka_smoke.md`. + +## Runtime Decision + +Use this conda environment for current testing: + +`/cpfs/shared/simulation/zhuzihou/dev/conda-managed/envs/embodied-eval-os-sim-isaacsim41-genmanip-py310` + +Reason: it is already compatible with Isaac Sim 4.1, Ray, GenManip client, cuRobo source injection, and the EBench/LabUtopia asset overlay. We do not source `/isaac-sim/setup_python_env.sh`; the clean conda environment plus explicit `PYTHONPATH`/`LD_LIBRARY_PATH` is the tested path. + +## Risks And Next Steps + +The next lane is planned in +`docs/superpowers/plans/2026-06-22-ebench-labutopia-lift2-baseline-lane.md`. + +1. P0 runtime asset preflight: build/verify a LabUtopia composite asset root before running lift2. Current observation: the scene overlay exists, but lift2 robot and cuRobo assets are not present under the scene-only overlay root. +2. P1 lift2 dry smoke: run `ebench/labutopia_lab_poc/lift2_candidate` on isolated port `18088`, require `complete` and `3/3` result files, keep `official_baseline_execution=false`. +3. P2 official baseline discovery: locate official EBench lift2/OpenPI runner files, retain paths and hashes, do not execute policy yet. +4. P3 official baseline local contrast: run one official-style online loop only after P2 passes, retain source terminal evidence, keep `standard_model_score=null` until a separate score-release gate. +5. Closure: write EOS-style dated records with artifact linkage, forbidden-claims, and path-leakage checks before changing PM-facing claims. diff --git a/genmanip/core/evaluator/env.py b/genmanip/core/evaluator/env.py index 87527d5..a7e7cc5 100644 --- a/genmanip/core/evaluator/env.py +++ b/genmanip/core/evaluator/env.py @@ -13,6 +13,9 @@ parse_embodiment_action, remove_dir_best_effort, ) +from genmanip.core.evaluator.labutopia_layout import ( + load_or_build_labutopia_poc_meta_info, +) from genmanip.utils.loader.domain_randomization import ( random_texture_for_eval, reset_scene, @@ -22,7 +25,6 @@ recovery_scene, ) from genmanip.utils.standalone.file_utils import ( - load_dict_from_pkl, make_dir, ) from genmanip.utils.standalone.io_utils import serialize_data, _jpeg @@ -653,8 +655,12 @@ def _setup_episode_layout(self): task_dir = self.default_config["TASKS_DIR"] task_name = scene_config.task_name # Load data - meta_info = load_dict_from_pkl( - os.path.join(task_dir, task_name, f"{seed}/meta_info.pkl") + meta_info = load_or_build_labutopia_poc_meta_info( + os.path.join(task_dir, task_name, f"{seed}/meta_info.pkl"), + task_name, + seed, + scene, + scene_config, ) reset_scene(scene) diff --git a/genmanip/core/evaluator/episode_result.py b/genmanip/core/evaluator/episode_result.py new file mode 100644 index 0000000..5c9a5e2 --- /dev/null +++ b/genmanip/core/evaluator/episode_result.py @@ -0,0 +1,37 @@ +from __future__ import annotations + +from numbers import Real +from typing import Any + + +def _coerce_score(value: Any) -> float | None: + if isinstance(value, bool): + return None + if isinstance(value, Real): + return float(value) + return None + + +def resolve_episode_score( + worker_post_process_result: Any, + done_info: dict[str, Any] | None, + *, + allow_done_info_fallback: bool = True, +) -> float | None: + """Resolve an episode score from finalize output, then done info fallback.""" + if isinstance(worker_post_process_result, dict): + score = _coerce_score(worker_post_process_result.get("score")) + if score is not None: + return score + else: + score = _coerce_score(worker_post_process_result) + if score is not None: + return score + + if ( + not allow_done_info_fallback + or not isinstance(done_info, dict) + or "error" in done_info + ): + return None + return _coerce_score(done_info.get("info")) diff --git a/genmanip/core/evaluator/isaac_worker.py b/genmanip/core/evaluator/isaac_worker.py index 39a9306..49e78a9 100644 --- a/genmanip/core/evaluator/isaac_worker.py +++ b/genmanip/core/evaluator/isaac_worker.py @@ -1,4 +1,5 @@ import os +import asyncio import logging import sys import time @@ -33,6 +34,40 @@ def import_modules(): from pydantic import BaseModel, Field +def _ensure_isaac_compatible_asyncio_loop(logger: logging.Logger | None = None) -> None: + """Reset this worker thread to a CPython asyncio loop before Isaac starts.""" + try: + current_loop = asyncio.get_event_loop() + except RuntimeError: + current_loop = None + + required_private_attrs = ("_check_closed", "_ready", "_scheduled") + if current_loop is not None and all( + hasattr(current_loop, attr) for attr in required_private_attrs + ): + return + + original_policy = asyncio.get_event_loop_policy().__class__.__module__ + "." + ( + asyncio.get_event_loop_policy().__class__.__name__ + ) + original_loop = ( + "" + if current_loop is None + else current_loop.__class__.__module__ + "." + current_loop.__class__.__name__ + ) + + asyncio.set_event_loop_policy(asyncio.DefaultEventLoopPolicy()) + asyncio.set_event_loop(asyncio.new_event_loop()) + + if logger is not None: + logger.info( + "Reset asyncio loop for Isaac Sim compatibility. " + "original_policy=%s original_loop=%s", + original_policy, + original_loop, + ) + + def _read_proc_status_memory_kb() -> dict[str, int]: memory_fields = ("VmRSS", "RssAnon", "RssFile", "VmSize", "VmSwap") result: dict[str, int] = {} @@ -97,6 +132,7 @@ def __init__( # ---------- Isaac modules ---------- import_modules() + _ensure_isaac_compatible_asyncio_loop(self.logger) from isaacsim import SimulationApp # type: ignore diff --git a/genmanip/core/evaluator/isaac_worker_pool.py b/genmanip/core/evaluator/isaac_worker_pool.py index b3d6b1c..cbb2c96 100644 --- a/genmanip/core/evaluator/isaac_worker_pool.py +++ b/genmanip/core/evaluator/isaac_worker_pool.py @@ -21,7 +21,11 @@ ProgressManager, HEARTBEAT_INTERVAL_SECONDS, ) +from genmanip.core.evaluator.episode_result import resolve_episode_score from genmanip.core.evaluator.isaac_worker import IsaacWorker +from genmanip.core.evaluator.labutopia_assets import ( + resolve_labutopia_poc_assets_override, +) from genmanip.core.evaluator.utils import ( finalize_episode_recorder_payload, parse_configs_and_benchmark_id, @@ -103,6 +107,7 @@ def __init__(self, args, current_dir, world_size=None): self.default_config = load_default_config( self.current_dir, "__None__.json", "local" if self.args.local else "default" ) + self._default_assets_dir = self.default_config.get("ASSETS_DIR") # worker env timeout self.step_call_timeout = 1200 self.reset_call_timeout = 1200 @@ -213,6 +218,32 @@ def _resolve_worker_env_vars( return env_vars_reference if env_vars_reference is not None else {} + def _reset_assets_dir_override(self) -> None: + if self._default_assets_dir is None: + self.default_config.pop("ASSETS_DIR", None) + else: + self.default_config["ASSETS_DIR"] = self._default_assets_dir + + def _maybe_apply_labutopia_assets_override( + self, evaluation_config_path_or_group: str + ) -> None: + override = resolve_labutopia_poc_assets_override( + self.current_dir, evaluation_config_path_or_group + ) + if override is None: + return + + previous_assets_dir = self.default_config.get("ASSETS_DIR", "") + self.default_config["ASSETS_DIR"] = override.overlay_root + if self.logger is not None: + self.logger.info( + "Using LabUtopia POC assets overlay. previous_ASSETS_DIR=%s " + "ASSETS_DIR=%s runtime_scene=%s", + previous_assets_dir, + override.overlay_root, + override.runtime_scene, + ) + def start_new_job(self, run_id: str, evaluation_config_path_list: list[str]): """ Start a new evaluation job. @@ -254,12 +285,14 @@ def start_new_job(self, run_id: str, evaluation_config_path_list: list[str]): effective_run_id = self._validate_run_id(effective_run_id) self.args.run_id = effective_run_id self.configure_logging(effective_run_id) + self._reset_assets_dir_override() # Parse configs and determine benchmark_id self.benchmark_id = None all_configs: list[tuple[list[dict], dict[str, list[str]], dict[str, int]]] = [] for evaluation_config_path_or_group in evaluation_config_path_list: + self._maybe_apply_labutopia_assets_override(evaluation_config_path_or_group) evaluation_config_list, benchmark_id, is_genmanip_package = ( parse_configs_and_benchmark_id( evaluation_config_path_or_group, self.current_dir @@ -1145,11 +1178,12 @@ def _handle_done(self, w_id: str, info: dict) -> dict: } # Get result from worker - result = None + post_episode_result = None + post_episode_error: Exception | None = None finalize_payload = None if "error" not in info and "info" in info and info["info"] != "Done": try: - result = self._ray_get_with_timeout( + post_episode_result = self._ray_get_with_timeout( call_remote=lambda: self.workers[ w_id ].post_episode_process.remote(), @@ -1157,15 +1191,26 @@ def _handle_done(self, w_id: str, info: dict) -> dict: context=f"Worker {w_id} post_episode_process", ) except Exception as e: + post_episode_error = e if self._is_worker_unavailable_error(e): self._replace_worker(w_id, reason=e) if self.logger is not None: self.logger.exception( "Post-episode processing failed for worker=%s", w_id ) - if isinstance(result, dict): - finalize_payload = result.get("finalize_payload") - result = result.get("score") + if isinstance(post_episode_result, dict): + finalize_payload = post_episode_result.get("finalize_payload") + result = resolve_episode_score( + post_episode_result, + info, + allow_done_info_fallback=post_episode_error is None, + ) + if post_episode_error is not None: + self.progress_manager.release_worker_task(str(w_id)) + raise RuntimeError( + f"Post-episode processing failed for worker={w_id}: " + f"{type(post_episode_error).__name__}: {post_episode_error}" + ) from post_episode_error if self._worker_cancelled(w_id): return { diff --git a/genmanip/core/evaluator/labutopia_assets.py b/genmanip/core/evaluator/labutopia_assets.py new file mode 100644 index 0000000..da5150a --- /dev/null +++ b/genmanip/core/evaluator/labutopia_assets.py @@ -0,0 +1,77 @@ +from __future__ import annotations + +from dataclasses import dataclass +import json +from pathlib import Path +from typing import Any + + +LABUTOPIA_POC_CONFIG_PREFIX = "ebench/labutopia_lab_poc" +LABUTOPIA_POC_MANIFEST = ( + "configs/tasks/ebench/labutopia_lab_poc/common/assets_manifest.json" +) + + +@dataclass(frozen=True) +class LabUtopiaAssetsOverride: + overlay_root: str + runtime_scene: str + + +def _is_labutopia_poc_config_ref(value: str) -> bool: + normalized = value.strip().replace("\\", "/") + return ( + normalized == LABUTOPIA_POC_CONFIG_PREFIX + or normalized.startswith(f"{LABUTOPIA_POC_CONFIG_PREFIX}/") + or f"/{LABUTOPIA_POC_CONFIG_PREFIX}/" in normalized + or normalized.endswith(f"/{LABUTOPIA_POC_CONFIG_PREFIX}") + ) + + +def _manifest_string(manifest: dict[str, Any], key: str, manifest_path: Path) -> str: + value = manifest.get(key) + if not isinstance(value, str) or value.strip() == "": + raise ValueError(f"{manifest_path}: {key} must be a non-empty string") + return value + + +def resolve_labutopia_poc_assets_override( + current_dir: str | Path, config_path_or_group: str +) -> LabUtopiaAssetsOverride | None: + """Return a LabUtopia POC ASSETS_DIR override for matching task packages.""" + if not _is_labutopia_poc_config_ref(config_path_or_group): + return None + + repo_root = Path(current_dir) + manifest_path = repo_root / LABUTOPIA_POC_MANIFEST + if not manifest_path.exists(): + raise FileNotFoundError( + f"LabUtopia POC assets manifest does not exist: {manifest_path}" + ) + + with manifest_path.open(encoding="utf-8") as handle: + manifest = json.load(handle) + if not isinstance(manifest, dict): + raise ValueError(f"{manifest_path}: expected JSON object") + + overlay_root = Path(_manifest_string(manifest, "overlay_root", manifest_path)) + if not overlay_root.is_absolute(): + overlay_root = repo_root / overlay_root + overlay_root = overlay_root.expanduser() + + runtime_usd_name = _manifest_string(manifest, "runtime_usd_name", manifest_path) + runtime_usd_path = ( + runtime_usd_name + if runtime_usd_name.endswith(".usda") + else f"{runtime_usd_name}.usda" + ) + runtime_scene = overlay_root / runtime_usd_path + if not runtime_scene.exists(): + raise FileNotFoundError( + f"LabUtopia POC runtime scene does not exist: {runtime_scene}" + ) + + return LabUtopiaAssetsOverride( + overlay_root=str(overlay_root), + runtime_scene=str(runtime_scene), + ) diff --git a/genmanip/core/evaluator/labutopia_layout.py b/genmanip/core/evaluator/labutopia_layout.py new file mode 100644 index 0000000..a9ebf3e --- /dev/null +++ b/genmanip/core/evaluator/labutopia_layout.py @@ -0,0 +1,106 @@ +from __future__ import annotations + +import copy +import pickle +from pathlib import Path +from typing import Any + + +LABUTOPIA_POC_TASK_PREFIX = "ebench/labutopia_lab_poc/" + + +def is_labutopia_poc_task_name(task_name: str) -> bool: + return isinstance(task_name, str) and task_name.startswith(LABUTOPIA_POC_TASK_PREFIX) + + +def _object_world_pose(obj: Any) -> tuple[Any, Any]: + position, orientation = obj.get_world_pose() + return position, orientation + + +def _build_initial_layout(scene: Any) -> dict[str, dict[str, Any]]: + layout: dict[str, dict[str, Any]] = {} + cache_library = getattr(scene, "cache_library", None) + usd_path_list = getattr(cache_library, "preloaded_object_path_list", {}) or {} + preload_object_meta_info = ( + getattr(cache_library, "preload_object_meta_info", {}) or {} + ) + + for key, obj in scene.object_list.items(): + prim = getattr(obj, "prim", None) + if prim is not None and hasattr(prim, "IsActive") and not prim.IsActive(): + continue + position, orientation = _object_world_pose(obj) + object_meta = preload_object_meta_info.get(key, {}) + layout[key] = { + "type": "object", + "position": position, + "orientation": orientation, + "scale": obj.get_local_scale(), + "path": usd_path_list.get(key, ""), + "add_colliders": object_meta.get("add_colliders", True), + "add_rigid_body": object_meta.get("add_rigid_body", True), + "prim_path": obj.prim_path, + "is_articulation_part": key in scene.articulation_part_list, + } + + for key, articulation in scene.articulation_list.items(): + position, orientation = articulation.get_world_pose() + layout[key] = { + "type": "articulation", + "position": position, + "orientation": orientation, + "scale": articulation.get_local_scale(), + "joint_positions": articulation.get_joint_positions(), + "prim_path": articulation.prim_path, + } + + for embodiment in scene.robot_list: + robot = embodiment.robot + position, orientation = robot.get_world_pose() + layout[robot.name] = { + "type": "robot", + "position": position, + "orientation": orientation, + "joint_positions": robot.get_joint_positions(), + } + + return layout + + +def build_labutopia_poc_meta_info( + scene: Any, scene_config: Any, seed: str +) -> dict[str, Any]: + """Build minimal eval metadata for LabUtopia POC tasks from the live scene.""" + task_data = { + "initial_scene_graph": None, + "initial_layout": _build_initial_layout(scene), + "goal": copy.deepcopy(scene_config.generation_config.goal), + "instruction": scene_config.instruction, + "frame_status": {}, + } + return { + "max_size": 0, + "num_steps": 0, + "language_instruction": scene_config.instruction, + "task_data": task_data, + "keys": {}, + "task_name": scene_config.task_name, + "episode_name": seed, + } + + +def load_or_build_labutopia_poc_meta_info( + meta_info_path: str | Path, + task_name: str, + seed: str, + scene: Any, + scene_config: Any, +) -> dict[str, Any]: + path = Path(meta_info_path) + if path.exists(): + with path.open("rb") as handle: + return pickle.load(handle) + if not is_labutopia_poc_task_name(task_name): + raise FileNotFoundError(f"meta_info.pkl does not exist: {path}") + return build_labutopia_poc_meta_info(scene, scene_config, seed) diff --git a/genmanip/core/evaluator/progress_manager.py b/genmanip/core/evaluator/progress_manager.py index 148dea8..15e6c01 100644 --- a/genmanip/core/evaluator/progress_manager.py +++ b/genmanip/core/evaluator/progress_manager.py @@ -313,12 +313,31 @@ def record_result( self.result_list[task_name][task_seed] = score self.worker_to_task_map.pop(worker_id, None) + if score is not None and release_lock: + self._write_minimal_result_info(task_name, task_seed, float(score)) + # Release the episode lock unless the caller keeps it for async finalize. if release_lock: self.release_episode_lock(task_name, task_seed) return episode_result + def _write_minimal_result_info( + self, task_name: str, task_seed: str, score: float + ) -> None: + self._atomic_json_write( + str(self._get_result_info_path(task_name, task_seed)), + { + "score": score, + "success_rate": 1 if abs(score - 1) < 1e-6 else 0, + "log_info": { + "episode_start_time": None, + "episode_end_time": None, + "metric_score": [], + }, + }, + ) + def get_worker_task(self, worker_id: str) -> tuple[str, str, dict] | None: """Get the current task assigned to a worker.""" with self.lock: diff --git a/standalone_tools/labutopia_poc/validate_task_package.py b/standalone_tools/labutopia_poc/validate_task_package.py index cf9100c..d03a71a 100644 --- a/standalone_tools/labutopia_poc/validate_task_package.py +++ b/standalone_tools/labutopia_poc/validate_task_package.py @@ -51,6 +51,13 @@ "camera_config": "configs/cameras/fixed_camera_lift2_simbox.yml", }, } +CAMERA_CLEANUP_FLAGS = { + "with_bbox2d", + "with_bbox3d", + "with_motion_vector", + "with_semantic", + "with_distance", +} ALLOWED_METRICS = { "manip/labutopia/object_height_delta": { "obj_uid", @@ -161,6 +168,17 @@ def _validate_assets_manifest() -> None: manifest.get("runtime_usd_name") == RUNTIME_USD_NAME, f"{path}: runtime_usd_name must be {RUNTIME_USD_NAME!r}", ) + overlay_root = manifest.get("overlay_root") + _assert( + isinstance(overlay_root, str) and overlay_root, + f"{path}: overlay_root must be a non-empty path", + ) + overlay_path = Path(overlay_root).expanduser() + if not overlay_path.is_absolute(): + overlay_path = ROOT / overlay_path + runtime_scene = overlay_path / f"{RUNTIME_USD_NAME}.usda" + if not runtime_scene.exists(): + raise FileNotFoundError(f"{path}: runtime scene does not exist: {runtime_scene}") _assert( manifest.get("wrapper_prim_paths") == EXPECTED_WRAPPER_PRIM_PATHS, f"{path}: wrapper_prim_paths must preserve GenManip key stripping", @@ -213,6 +231,23 @@ def _validate_task_semantics() -> None: _assert(key in settings, f"{path}: open_door preferred metric missing {key}") +def _validate_camera_configs() -> None: + for profile, expected in PROFILE_EXPECTATIONS.items(): + path = ROOT / expected["camera_config"] + data = _load_yaml(path) + _assert(isinstance(data, dict), f"{path}: expected camera mapping") + for camera_name, camera in data.items(): + _assert( + isinstance(camera, dict), + f"{path}:{camera_name}: expected camera settings mapping", + ) + missing = CAMERA_CLEANUP_FLAGS - set(camera) + _assert( + not missing, + f"{path}:{camera_name}: {profile} camera missing cleanup flags {missing}", + ) + + def _walk_goal_dicts(value: Any, path: Path) -> list[dict[str, Any]]: if isinstance(value, dict): return [value] @@ -335,6 +370,7 @@ def _validate_metrics_manager_lazy_registration() -> None: def validate_task_package() -> None: _validate_assets_manifest() _validate_task_semantics() + _validate_camera_configs() for path in _indexed_task_yaml_paths(): _validate_runtime_task(path) _validate_metrics_manager_lazy_registration() diff --git a/tests/labutopia_poc/test_episode_result_completion.py b/tests/labutopia_poc/test_episode_result_completion.py new file mode 100644 index 0000000..c97dcc9 --- /dev/null +++ b/tests/labutopia_poc/test_episode_result_completion.py @@ -0,0 +1,60 @@ +import json + +from genmanip.core.evaluator.episode_result import resolve_episode_score +from genmanip.core.evaluator.progress_manager import ProgressManager + + +def test_resolve_episode_score_falls_back_to_done_info_numeric_score(): + assert resolve_episode_score(None, {"info": 0.0}) == 0.0 + assert resolve_episode_score(None, {"info": 1}) == 1.0 + + +def test_resolve_episode_score_prefers_worker_post_process_result(): + assert ( + resolve_episode_score( + {"score": 0.5, "finalize_payload": {"episode": "payload"}}, + {"info": 0.0}, + ) + == 0.5 + ) + + +def test_resolve_episode_score_ignores_non_score_done_info(): + assert resolve_episode_score(None, {"info": "Done"}) is None + assert resolve_episode_score(None, {"info": True}) is None + assert resolve_episode_score(None, {"error": "lock_lost", "info": 0.0}) is None + + +def test_resolve_episode_score_can_disable_done_info_fallback_after_post_process_error(): + assert ( + resolve_episode_score( + None, + {"info": 0.0, "termination_reason": "non_finite_arm_state"}, + allow_done_info_fallback=False, + ) + is None + ) + + +def test_record_result_persists_result_info_without_async_finalize(tmp_path): + progress = ProgressManager( + result_base_dir=str(tmp_path), + benchmark_id="ebench", + run_id="run", + ) + progress.add_evaluation_config( + [{"task_name": "task_a"}], + {"task_a": ["000"]}, + {"task_a": 1}, + ) + config, seed = progress.get_next_task("worker-0") + assert config["task_name"] == "task_a" + assert seed == "000" + + episode_result = progress.record_result("worker-0", 0.0, release_lock=True) + + result_path = tmp_path / "ebench" / "run" / "task_a" / "000" / "result_info.json" + assert episode_result["score"] == 0.0 + assert json.loads(result_path.read_text(encoding="utf-8"))["score"] == 0.0 + assert progress.reconcile_task_state_from_filesystem() is True + assert progress.check_finished() diff --git a/tests/labutopia_poc/test_isaac_worker_asyncio.py b/tests/labutopia_poc/test_isaac_worker_asyncio.py new file mode 100644 index 0000000..cb6f9a7 --- /dev/null +++ b/tests/labutopia_poc/test_isaac_worker_asyncio.py @@ -0,0 +1,41 @@ +import asyncio + +import pytest + + +def test_isaac_worker_replaces_uvloop_with_cpython_loop(): + uvloop = pytest.importorskip("uvloop") + worker_module = pytest.importorskip("genmanip.core.evaluator.isaac_worker") + + old_policy = asyncio.get_event_loop_policy() + try: + old_loop = asyncio.get_event_loop() + except RuntimeError: + old_loop = None + + created_loops = [] + try: + asyncio.set_event_loop_policy(uvloop.EventLoopPolicy()) + incompatible_loop = asyncio.new_event_loop() + created_loops.append(incompatible_loop) + asyncio.set_event_loop(incompatible_loop) + + assert not hasattr(asyncio.get_event_loop(), "_check_closed") + + worker_module._ensure_isaac_compatible_asyncio_loop() + fixed_loop = asyncio.get_event_loop() + created_loops.append(fixed_loop) + + assert hasattr(fixed_loop, "_check_closed") + assert hasattr(fixed_loop, "_ready") + assert hasattr(fixed_loop, "_scheduled") + finally: + for loop in created_loops: + if loop is old_loop or loop.is_closed(): + continue + loop.close() + asyncio.set_event_loop_policy(old_policy) + if old_loop is not None and not old_loop.is_closed(): + asyncio.set_event_loop(old_loop) + else: + asyncio.set_event_loop(None) diff --git a/tests/labutopia_poc/test_labutopia_assets_override.py b/tests/labutopia_poc/test_labutopia_assets_override.py new file mode 100644 index 0000000..8c0145c --- /dev/null +++ b/tests/labutopia_poc/test_labutopia_assets_override.py @@ -0,0 +1,61 @@ +from pathlib import Path +import json + +import pytest + +from genmanip.core.evaluator.labutopia_assets import ( + resolve_labutopia_poc_assets_override, +) + + +def _write_manifest(repo_root: Path, overlay_root: Path, runtime_usd_name: str) -> None: + manifest_path = ( + repo_root + / "configs/tasks/ebench/labutopia_lab_poc/common/assets_manifest.json" + ) + manifest_path.parent.mkdir(parents=True, exist_ok=True) + manifest_path.write_text( + json.dumps( + { + "overlay_root": str(overlay_root), + "runtime_usd_name": runtime_usd_name, + } + ), + encoding="utf-8", + ) + + +def test_assets_override_ignores_non_labutopia_config(tmp_path): + override = resolve_labutopia_poc_assets_override( + tmp_path, "ebench/some_other_package/config.json" + ) + + assert override is None + + +def test_assets_override_resolves_overlay_runtime_scene(tmp_path): + runtime_usd_name = "scene_usds/labutopia/level1_poc/lab_001/scene" + overlay_root = tmp_path / "overlay/assets" + runtime_scene = overlay_root / f"{runtime_usd_name}.usda" + runtime_scene.parent.mkdir(parents=True) + runtime_scene.write_text("#usda 1.0\n", encoding="utf-8") + _write_manifest(tmp_path, overlay_root, runtime_usd_name) + + override = resolve_labutopia_poc_assets_override( + tmp_path, "ebench/labutopia_lab_poc/franka_poc/franka_poc.json" + ) + + assert override is not None + assert override.overlay_root == str(overlay_root) + assert override.runtime_scene == str(runtime_scene) + + +def test_assets_override_rejects_missing_runtime_scene(tmp_path): + runtime_usd_name = "scene_usds/labutopia/level1_poc/lab_001/scene" + overlay_root = tmp_path / "overlay/assets" + _write_manifest(tmp_path, overlay_root, runtime_usd_name) + + with pytest.raises(FileNotFoundError, match="runtime scene"): + resolve_labutopia_poc_assets_override( + tmp_path, "ebench/labutopia_lab_poc/lift2_candidate/lift2_candidate.json" + ) diff --git a/tests/labutopia_poc/test_labutopia_layout_fallback.py b/tests/labutopia_poc/test_labutopia_layout_fallback.py new file mode 100644 index 0000000..a4409bc --- /dev/null +++ b/tests/labutopia_poc/test_labutopia_layout_fallback.py @@ -0,0 +1,110 @@ +import pytest + +from genmanip.core.evaluator.labutopia_layout import ( + build_labutopia_poc_meta_info, + load_or_build_labutopia_poc_meta_info, +) + + +class _Prim: + def __init__(self, active=True): + self._active = active + + def IsActive(self): + return self._active + + +class _Object: + def __init__(self, prim_path, position, orientation, scale, active=True): + self.prim_path = prim_path + self.prim = _Prim(active) + self._position = position + self._orientation = orientation + self._scale = scale + + def get_world_pose(self): + return self._position, self._orientation + + def get_local_scale(self): + return self._scale + + +class _RobotHandle: + def __init__(self): + self.name = "franka" + + def get_world_pose(self): + return [0.0, 0.0, 0.71], [1.0, 0.0, 0.0, 0.0] + + def get_joint_positions(self): + return [0.1, 0.2, 0.3] + + +class _Robot: + def __init__(self): + self.robot = _RobotHandle() + + +class _GenerationConfig: + goal = [[{"type": "manip/labutopia/object_height_delta", "obj_uid": "obj"}]] + + +class _SceneConfig: + task_name = "ebench/labutopia_lab_poc/franka_poc/level1_pick" + instruction = "Pick up the object." + generation_config = _GenerationConfig() + + +class _CacheLibrary: + preloaded_object_path_list = {"obj": "objects/obj.usd"} + preload_object_meta_info = { + "obj": {"add_colliders": False, "add_rigid_body": True} + } + + +class _Scene: + object_list = { + "obj": _Object( + "/World/labutopia_level1_poc/obj_obj", + [1.0, 2.0, 3.0], + [1.0, 0.0, 0.0, 0.0], + [1.0, 1.0, 1.0], + ), + "inactive": _Object( + "/World/labutopia_level1_poc/obj_inactive", + [0.0, 0.0, 0.0], + [1.0, 0.0, 0.0, 0.0], + [1.0, 1.0, 1.0], + active=False, + ), + } + articulation_list = {} + articulation_part_list = {"obj": object()} + robot_list = [_Robot()] + cache_library = _CacheLibrary() + + +def test_build_labutopia_poc_meta_info_from_current_scene(): + meta_info = build_labutopia_poc_meta_info(_Scene(), _SceneConfig(), "000") + + task_data = meta_info["task_data"] + assert meta_info["task_name"] == _SceneConfig.task_name + assert meta_info["episode_name"] == "000" + assert task_data["instruction"] == "Pick up the object." + assert task_data["goal"] == _GenerationConfig.goal + assert set(task_data["initial_layout"]) == {"obj", "franka"} + assert task_data["initial_layout"]["obj"]["is_articulation_part"] is True + assert task_data["initial_layout"]["obj"]["path"] == "objects/obj.usd" + assert task_data["initial_layout"]["obj"]["add_colliders"] is False + assert task_data["initial_layout"]["franka"]["joint_positions"] == [0.1, 0.2, 0.3] + + +def test_load_or_build_rejects_missing_non_labutopia_meta_info(tmp_path): + with pytest.raises(FileNotFoundError, match="meta_info.pkl"): + load_or_build_labutopia_poc_meta_info( + tmp_path / "meta_info.pkl", + "ebench/official_task/level1_pick", + "000", + _Scene(), + _SceneConfig(), + ) diff --git a/tests/labutopia_poc/test_validate_task_package.py b/tests/labutopia_poc/test_validate_task_package.py index f6f0c55..28127e6 100644 --- a/tests/labutopia_poc/test_validate_task_package.py +++ b/tests/labutopia_poc/test_validate_task_package.py @@ -13,6 +13,13 @@ "ebench/labutopia_lab_poc/lift2_candidate/lift2_candidate.json", ] EXPECTED_TASKS = ["level1_pick", "level1_place", "level1_open_door"] +CAMERA_CLEANUP_FLAGS = { + "with_bbox2d", + "with_bbox3d", + "with_motion_vector", + "with_semantic", + "with_distance", +} def _write_json(path, data): @@ -87,9 +94,54 @@ def test_validate_task_package_cli_reports_success(): assert "LabUtopia task package validation OK" in result.stdout +def test_assets_manifest_rejects_missing_overlay_runtime_scene(tmp_path, monkeypatch): + package_root = tmp_path / "tasks/ebench/labutopia_lab_poc" + common_root = package_root / "common" + common_root.mkdir(parents=True) + overlay_root = tmp_path / "overlay/assets" + generated_manifest = tmp_path / "generated_manifest.json" + generated_manifest.write_text( + json.dumps( + { + "usd_name": validate_task_package.RUNTIME_USD_NAME, + "scene_uid": validate_task_package.SCENE_UID, + "runtime_object_keys": [], + "wrapper_prim_paths": validate_task_package.EXPECTED_WRAPPER_PRIM_PATHS, + "source_to_runtime_object_key": {}, + } + ), + encoding="utf-8", + ) + _write_json( + common_root / "assets_manifest.json", + { + "overlay_root": str(overlay_root), + "runtime_usd_name": validate_task_package.RUNTIME_USD_NAME, + "generated_manifest": str(generated_manifest), + "scene_uid": validate_task_package.SCENE_UID, + "runtime_object_keys": [], + "wrapper_prim_paths": validate_task_package.EXPECTED_WRAPPER_PRIM_PATHS, + "source_to_runtime_object_key": {}, + }, + ) + monkeypatch.setattr(validate_task_package, "PACKAGE_ROOT", package_root) + + with pytest.raises(FileNotFoundError, match="runtime scene"): + validate_task_package._validate_assets_manifest() + + def test_labutopia_tasks_define_runtime_articulation_contract(): for path in validate_task_package._indexed_task_yaml_paths(): data = validate_task_package._load_yaml(path) cfg = data["evaluation_configs"][0] assert "articulation" in cfg["generation_config"], str(path) + + +def test_labutopia_camera_configs_define_cleanup_flags(): + for expectation in validate_task_package.PROFILE_EXPECTATIONS.values(): + camera_path = validate_task_package.ROOT / expectation["camera_config"] + cameras = validate_task_package._load_yaml(camera_path) + for camera_name, camera in cameras.items(): + missing = CAMERA_CLEANUP_FLAGS - set(camera) + assert not missing, f"{camera_path}:{camera_name} missing {missing}" From 391db63f410909f82ebe649616afd4a525de9f9d Mon Sep 17 00:00:00 2001 From: Codex Date: Mon, 22 Jun 2026 22:54:29 +0800 Subject: [PATCH 12/30] Fix LabUtopia Pages workflow --- .github/workflows/deploy-labutopia-report.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/deploy-labutopia-report.yml b/.github/workflows/deploy-labutopia-report.yml index 97f6700..aa56d0e 100644 --- a/.github/workflows/deploy-labutopia-report.yml +++ b/.github/workflows/deploy-labutopia-report.yml @@ -36,8 +36,6 @@ jobs: - name: Configure Pages uses: actions/configure-pages@v5 - with: - enablement: true - name: Upload docs/ as Pages artifact uses: actions/upload-pages-artifact@v3 From 8316c0e2da974f8f0408cd4d3c7c72a8ecf10b31 Mon Sep 17 00:00:00 2001 From: Codex Date: Tue, 23 Jun 2026 00:26:36 +0800 Subject: [PATCH 13/30] Add LabUtopia EBench render evidence --- docs/labutopia_lab_poc/franka_render_smoke.md | 86 ++++++++++++ ...26-06-22-labutopia-ebench-weekly-report.md | 53 ++++---- .../labutopia-franka-level1-open-door.jpg | Bin 0 -> 17365 bytes .../assets/labutopia-franka-level1-pick.jpg | Bin 0 -> 26043 bytes .../assets/labutopia-franka-level1-place.jpg | Bin 0 -> 15013 bytes .../index.html | 127 +++++++++++++++--- 6 files changed, 222 insertions(+), 44 deletions(-) create mode 100644 docs/labutopia_lab_poc/franka_render_smoke.md create mode 100644 docs/records/evidence/2026-06-22-labutopia-ebench-weekly-report/assets/labutopia-franka-level1-open-door.jpg create mode 100644 docs/records/evidence/2026-06-22-labutopia-ebench-weekly-report/assets/labutopia-franka-level1-pick.jpg create mode 100644 docs/records/evidence/2026-06-22-labutopia-ebench-weekly-report/assets/labutopia-franka-level1-place.jpg diff --git a/docs/labutopia_lab_poc/franka_render_smoke.md b/docs/labutopia_lab_poc/franka_render_smoke.md new file mode 100644 index 0000000..5690577 --- /dev/null +++ b/docs/labutopia_lab_poc/franka_render_smoke.md @@ -0,0 +1,86 @@ +# LabUtopia Franka POC render smoke + +Date: 2026-06-22 + +## Scope + +This record tracks the render evidence added for the LabUtopia Franka POC weekly report. + +The render evidence is PM-facing asset-visibility evidence only. It does not prove task success, official baseline readiness, video recording readiness, or correct reset-time task layout. + +## Environment + +Conda environment: + +```text +/cpfs/shared/simulation/zhuzihou/dev/conda-managed/envs/embodied-eval-os-sim-isaacsim41-genmanip-py310 +``` + +Task package: + +```text +ebench/labutopia_lab_poc/franka_poc +``` + +LabUtopia overlay root: + +```text +/cpfs/shared/simulation/zhuzihou/dev/_datasets/EBench-Assets-Overlay/labutopia_level1_poc/assets +``` + +## Render smoke + +EBench server/client render smoke run: + +```text +run_id=labutopia_franka_render_smoke_20260622_150819 +config=ebench/labutopia_lab_poc/franka_poc +port=18090 +save_process=true +frame_save_interval=1 +``` + +Result summary: + +```text +level1_pick score=0.0 sr=0.0 +level1_place score=0.0 sr=0.0 +level1_open_door score=0.0 sr=0.0 +``` + +The `0.0` score is expected for this wiring smoke because it uses default/no-op behavior. It should not be interpreted as a policy result. + +## Camera finding + +The eval recorder wrote per-task PNG frames under the render smoke run directory, but the recorded `camera2` RGB frames were black. + +Observed implication: + +```text +eval recorder camera2: not usable for PM visual evidence yet +``` + +To keep the weekly report useful while preserving claim boundaries, the report uses static direct-render screenshots from the same EBench/GenManip-loaded LabUtopia stage and the same overlay asset root. The direct render only changes report lighting and camera viewpoint. It does not change task configuration, evaluator logic, or result scores. + +## Report images + +Committed report assets: + +```text +docs/records/evidence/2026-06-22-labutopia-ebench-weekly-report/assets/labutopia-franka-level1-pick.jpg +docs/records/evidence/2026-06-22-labutopia-ebench-weekly-report/assets/labutopia-franka-level1-place.jpg +docs/records/evidence/2026-06-22-labutopia-ebench-weekly-report/assets/labutopia-franka-level1-open-door.jpg +``` + +Interpretation: + +- `level1_pick`: static visibility of the loaded local tabletop target asset. +- `level1_place`: static visibility of the loaded target platform asset. +- `level1_open_door`: static visibility of the loaded drying-box geometry. + +## Open risks + +- Fix eval recorder `camera2` black frames. +- Close the gap between visible mesh bbox, wrapper pose, and task semantic coordinates. +- Capture reset-time keyframes for all three tasks through the normal eval recording path. +- Keep official Lift2 baseline claims blocked until Lift2 composite assets and official runner discovery are complete. diff --git a/docs/records/2026-06-22-labutopia-ebench-weekly-report.md b/docs/records/2026-06-22-labutopia-ebench-weekly-report.md index eea76b4..3c799e8 100644 --- a/docs/records/2026-06-22-labutopia-ebench-weekly-report.md +++ b/docs/records/2026-06-22-labutopia-ebench-weekly-report.md @@ -96,32 +96,21 @@ EOS/其他工程师任务: 8087 ## 有没有渲染图验证? -目前没有保留“产品可审阅”的渲染图或视频证据。 - -原因是这次 smoke 的目标是先验证接入链路,所以运行时使用了: +已补三张静态渲染图,放在 HTML 汇报页里: ```text ---no_save_process ---frame_save_interval 0 +docs/records/evidence/2026-06-22-labutopia-ebench-weekly-report/assets/labutopia-franka-level1-pick.jpg +docs/records/evidence/2026-06-22-labutopia-ebench-weekly-report/assets/labutopia-franka-level1-place.jpg +docs/records/evidence/2026-06-22-labutopia-ebench-weekly-report/assets/labutopia-franka-level1-open-door.jpg ``` -这会关闭过程图片/视频保存,减少运行成本和干扰。因此当前证据主要是: - -- server/client 成功完成 -- 场景资产路径解析正确 -- reset/step 正常返回 -- 3 个任务都有 `result_info.json` -- 最终 status 是 `complete` - -这能证明“链路跑通”,但不能证明“画面渲染效果符合产品预期”。渲染图验收需要作为后续单独一项补上。 +这三张图来自同一个 LabUtopia Franka POC 任务包和同一个 LabUtopia overlay 资产根目录,均在 Isaac/Replicator 中从 EBench/GenManip 加载后的 stage 抓取。它们可以作为“资产可见性证据”,但还不能作为“画面/视频/任务布局已经验收”的结论。 -建议下一次渲染验收保留: +需要特别说明: -```text -reset 后第一帧截图 -每个任务一张关键视角截图 -必要时保留短视频或 merged mp4 -``` +- `run_id=labutopia_franka_render_smoke_20260622_150819` 跑过保存帧 smoke,但 eval recorder 的 `camera2` 帧当前是黑屏。 +- 因此 HTML 里使用的是同一加载路径下的直渲静态截图,只补了报告用光照和相机视角,不改变任务配置、结果分数或 evaluator 逻辑。 +- 当前还观察到部分对象的可见 mesh bbox、wrapper pose、任务语义坐标没有完全对齐;下一步必须修复这个问题,再做 reset 后关键帧验收。 ## 本周遇到的问题和解决方式 @@ -130,6 +119,8 @@ reset 后第一帧截图 | 资产根目录不对 | 系统一开始会去错误目录找 LabUtopia 场景 | 增加 LabUtopia manifest 识别逻辑,运行时切换到 overlay asset root | 已解决 | | 缺少 `meta_info.pkl` | 传统 GenManip 任务依赖采集包里的元信息,LabUtopia POC 没有这份文件 | 针对 LabUtopia POC,从实时场景里自动生成最小可用元信息 | 已解决 | | camera cleanup 字段缺失 | 切换任务时 camera 清理报字段错误 | 补齐 camera 配置里的 cleanup flags | 已解决 | +| eval recorder `camera2` 黑屏 | 保存过程帧时 `camera2` 输出黑屏 | 已补同一加载路径下的直渲静态图用于 PM 可见性汇报 | 待修复 | +| 可见 mesh 与 wrapper pose 未完全对齐 | 部分对象 bbox、wrapper pose、任务语义坐标存在偏移 | 先降级为“资产可见性证据”,不宣称任务布局验收完成 | 待闭环 | | 最终进度不 complete | 任务已跑完,但进度没有写入,导致客户端最后等待超时 | 任务结束时写最小 `result_info.json`,并修复进度统计 | 已解决 | | 后处理异常可能被误记为完成 | 如果后处理报错,不能用 0 分兜底伪装成成功 | 增加 fail-fast 逻辑和回归测试 | 已解决 | | 端口/结果混淆风险 | EOS 侧也有人在跑任务,担心互相影响 | 使用独立 worktree、独立 run_id、独立端口 18088,并复核 8087 状态 | 已解决 | @@ -141,7 +132,7 @@ reset 后第一帧截图 | LabUtopia 场景接入 | 已跑通 | 场景资产能加载,任务链路能跑完 | | Franka POC smoke | 已完成 | 3 个任务全部 complete | | 结果落盘 | 已完成 | per-task 和 final result 都能写出 | -| 渲染图/视频验收 | 未完成 | 本轮没有保存图片/视频,需要后续补 | +| 渲染图/视频验收 | 已补静态可见性证据 | 已有三张直渲图;`camera2`、视频和 reset 后任务布局仍需闭环 | | 任务求解能力 | 未验证 | 当前默认动作得分 0.0,不代表策略能力 | | 官方 baseline | 未进入本周结论 | 后续单独讨论和规划 | @@ -175,14 +166,22 @@ saved/eval_results/ebench/labutopia_franka_smoke_clean8_20260622_100208/.../leve saved/eval_results/ebench/labutopia_franka_smoke_clean8_20260622_100208/.../level1_open_door/000/result_info.json ``` +渲染补证: + +```text +run_id=labutopia_franka_render_smoke_20260622_150819 +eval recorder camera2 frames: black +static direct-render evidence: 3 jpg files under docs/records/evidence/.../assets/ +``` + ## 下周建议 -先补一个“渲染图验收”小闭环,再进入下一阶段机器人/baseline 讨论。 +先把“渲染和布局验收”补成真正闭环,再进入下一阶段机器人/baseline 讨论。 建议顺序: -1. 开一轮保存图片的短 smoke。 -2. 为三个任务各保留 reset 后截图。 -3. 检查截图里场景、目标物、桌面、门/干燥箱是否可见且位置合理。 -4. 更新本周证据文档,明确“渲染验收通过/未通过”。 -5. 再进入下一阶段机器人和 baseline 的细化讨论。 +1. 修复 eval recorder 的 `camera2` 黑屏问题。 +2. 修复或解释可见 mesh bbox、wrapper pose、任务语义坐标的偏移。 +3. 为三个任务各保留 reset 后关键截图。 +4. 检查截图里目标物、桌面、门/干燥箱是否可见且位置合理。 +5. 再进入 Lift2 复合资产预检和官方 baseline 路线。 diff --git a/docs/records/evidence/2026-06-22-labutopia-ebench-weekly-report/assets/labutopia-franka-level1-open-door.jpg b/docs/records/evidence/2026-06-22-labutopia-ebench-weekly-report/assets/labutopia-franka-level1-open-door.jpg new file mode 100644 index 0000000000000000000000000000000000000000..86bf54c7d4e62945cb5456d21bdd676c35d51430 GIT binary patch literal 17365 zcmb7r2|QG7`~R6S7+c1!Y(pZ_kVJ`WDMeW#dF<`VnmyZC6J-famLgH1Eb-X0CL-BV zB>OIvY{`=Ubw<6<`~E)f^88Q7%$alU`?|l^_xfJfea}tnUF!n~;pm|whY$<~K``(S zS?@tK5F8C0So9T#!_m?)($Ue<(lO&1=ovRLvu@hN%)-LT#?8S>;9_H8;n>2##lyRK z^JZ3dJ^?;n0dC&Syr>Zjbfu%EW1^#D;w7*Uc>lkT^%{f?k8PzzXfPrOmJLJ0hFO1s z@B>r~f~5hdzYh$Sj-Ccbi-)J2s2~4&tbah5p*5C3LjV`QM*Zv4zxj#grKb=_s6hC) zW$kMMJ@0#6hYz&8s>(%Z7OVcIg@0~F$5Nrox$kS^q%XJoHA~mz(3N$Bh$XAsOloSK z+QjTm!A|+EfsN1Nbwyj>FCzMxp-FVJ73 z=rxwqf5W@&2^q%?&$J8yvK1n%cM9=T&GkI+s9wPTjpEkBQ<2Yqh~E-=w9uaPcC00S zo`sBB`tJvB$GfF#TNg@O$ai~c6)%kPuY`F=mm^qo9JR9uB5Z|VXvs)$=qW)LE?NY+ z{Bi!U^q7y5ip~{>SC$@Y#-poiZ;m&33uSw@lmGirf~+^Luv%t&F!Y+Kq*v_g^41=z zeHDswgS+GKM~h zd&DL|4?(zSk0P}1=sQQsx$pKjtB_XeKUv=Y;hZ2aGTs3dVqR1p6`4 z`Rli%ZtoRVd#8n-_*RaR{_qWXZrQX+Yr*Fh`Tvf~>^(f?v7}44-~EP5vcUcmLElb! z`94ALr=TI0n<@~hm$+bvU(_jt0E$8AlMsxaAtz!Q{m{mMjL@pVORi@{eYHSj^D@(g zspPUB3Nb7lA|!R=yPEcZWtKcg%2X! zFqkS@A^?P%72fbw27Ue&xrTN<+JKZ4gqBM0&D}@Q=slLyL!|SkB6ORsW+$hX7>^1S z_�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 literal 0 HcmV?d00001 diff --git a/docs/records/evidence/2026-06-22-labutopia-ebench-weekly-report/assets/labutopia-franka-level1-place.jpg b/docs/records/evidence/2026-06-22-labutopia-ebench-weekly-report/assets/labutopia-franka-level1-place.jpg new file mode 100644 index 0000000000000000000000000000000000000000..f9fb7738e94aabb9f5bbb1bb2abf2108cd4e5959 GIT binary patch literal 15013 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 literal 0 HcmV?d00001 diff --git a/docs/records/evidence/2026-06-22-labutopia-ebench-weekly-report/index.html b/docs/records/evidence/2026-06-22-labutopia-ebench-weekly-report/index.html index 601e12f..9241f6b 100644 --- a/docs/records/evidence/2026-06-22-labutopia-ebench-weekly-report/index.html +++ b/docs/records/evidence/2026-06-22-labutopia-ebench-weekly-report/index.html @@ -81,6 +81,7 @@ gap: 24px; align-items: end; } + .hero > * { min-width: 0; } .eyebrow { margin: 0 0 8px; color: var(--green); @@ -88,18 +89,23 @@ font-weight: 750; text-transform: uppercase; letter-spacing: 0; + overflow-wrap: anywhere; } h1 { margin: 0 0 12px; font-size: clamp(32px, 4.4vw, 54px); line-height: 1.12; letter-spacing: 0; + overflow-wrap: anywhere; } + h1 span { display: inline; } + h1 span + span::before { content: " "; } .lede { max-width: 840px; margin: 0; color: var(--muted); font-size: 18px; + overflow-wrap: anywhere; } .hero-note { background: var(--amber-soft); @@ -107,6 +113,8 @@ border-radius: 8px; padding: 16px; color: #563b08; + min-width: 0; + overflow-wrap: anywhere; } .hero-note b { display: block; margin-bottom: 6px; } main { @@ -147,6 +155,7 @@ border-radius: 8px; padding: 14px; background: #ffffff; + min-width: 0; } .label { color: var(--muted); @@ -276,6 +285,39 @@ height: auto; min-height: 260px; } + .render-grid { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 14px; + margin-top: 16px; + } + .render-card { + margin: 0; + border: 1px solid var(--border); + border-radius: 8px; + background: #ffffff; + overflow: hidden; + } + .render-card img { + display: block; + width: 100%; + aspect-ratio: 16 / 9; + object-fit: cover; + background: #101820; + } + .render-card figcaption { + min-height: 116px; + padding: 12px; + color: var(--muted); + font-size: 13px; + line-height: 1.45; + } + .render-card figcaption b { + display: block; + margin-bottom: 5px; + color: var(--ink); + font-size: 15px; + } .timeline { display: grid; grid-template-columns: repeat(5, minmax(150px, 1fr)); @@ -316,6 +358,7 @@ .split { grid-template-columns: 1fr; } .flow { grid-template-columns: 1fr; } .flow-step::after { display: none; } + .render-grid { grid-template-columns: 1fr; } .timeline { grid-template-columns: 1fr; } .nav { display: none; } } @@ -324,6 +367,9 @@ .hero, main { padding-left: 16px; padding-right: 16px; } section { padding: 16px; } .metric { min-height: auto; } + h1 { font-size: 28px; line-height: 1.18; } + h1 span { display: block; } + h1 span + span::before { content: ""; } .value { font-size: 21px; } th, td { min-width: 130px; } } @@ -335,6 +381,7 @@ @@ -344,7 +391,7 @@

Weekly report · 2026-06-22 · PM-facing evidence page

-

LabUtopia x EBench 接入周报

+

LabUtopia x EBench接入周报

本周 Franka POC 已经跑通端到端 smoke:任务能提交,LabUtopia 场景能加载,三个 level-1 任务能 reset/step,结果能落盘并正常 complete。当前结论是“接入链路跑通”,不是“策略已经会做任务”,也不是“官方 baseline 成绩”。

Franka POC complete @@ -355,7 +402,7 @@

LabUtopia x EBench 接入周报

@@ -386,8 +433,8 @@

当前进展总览

渲染证据
-
未保留
-
本轮为轻量 smoke,关闭了过程图片和视频保存。
+
已补 3 张
+
静态可见性截图已补;eval recorder 的 camera2 仍需修复。
官方 Lift2 baseline
@@ -436,9 +483,9 @@

本周完成了什么

渲染图验收 - 未完成 - 本轮命令使用 --no_save_process--frame_save_interval 0。 - 暂不能对画面质量做产品验收结论。 + 已补静态证据 + 已从 EBench/GenManip 加载后的 Isaac stage 抓取三张静态图。 + 证明可见资产能被加载;暂不能证明任务布局、视频链路或策略成功。 @@ -522,6 +569,29 @@

还没闭环的部分

+
+

EBench 加载后的三任务渲染图

+

下面三张图来自同一个 LabUtopia Franka POC 任务包和同一个 LabUtopia overlay 资产根目录,均在 Isaac/Replicator 中从 EBench/GenManip 加载后的 stage 抓取。它们是“资产可见性证据”,不是策略成功率、任务布局或官方 baseline 的验收结论。

+
+

渲染证据边界

+

自动 eval recorder 保存的 camera2 帧当前为黑屏;因此这里采用同一任务包、同一 overlay 加载路径下的直渲截图。直渲过程中只补了报告用光照和相机视角,不改变任务配置、结果分数或 evaluator 逻辑。

+
+
+
+ level1_pick task render after EBench LabUtopia asset loading +
level1_pickPick up the conical bottle. 当前能看到加载后的局部桌面目标资产;后续还要修复 wrapper pose 与可见 mesh bbox 的一致性。
+
+
+ level1_place target platform render after EBench LabUtopia asset loading +
level1_placePick up beaker and place on target platform. 当前 target platform 可见;beaker 与 target 的相对布局仍需做 reset 后截图闭环。
+
+
+ level1_open_door drying box render after EBench LabUtopia asset loading +
level1_open_doorOpen the drying box door. 当前干燥箱几何可见;door handle 与 joint/metric 的可评估闭环仍需要下一轮修复。
+
+
+
+

验证证据

@@ -549,6 +619,11 @@

验证证据

0.0 默认动作 smoke 的预期结果,只说明链路,不说明求解能力。 + + render smoke + labutopia_franka_render_smoke_20260622_150819 + 同一任务包跑过保存帧 smoke;eval recorder 的 camera2 帧为黑屏,静态直渲图作为补充可见性证据。 + pytest 23 passed, 1 skipped @@ -600,6 +675,18 @@

问题和处理结果

补齐 POC camera 配置 cleanup flags。 已解决 + + eval recorder camera2 黑屏 + 保存过程帧时 camera2 输出为黑屏。 + 已补同一加载路径的直渲静态图,先满足 PM 可见性汇报。 + 待修复 + + + 可见 mesh 与 wrapper pose 未完全对齐 + 部分对象 bbox、wrapper pose、任务语义坐标存在偏移,影响截图和后续可评性判断。 + 报告中降级为“资产可见性证据”,不宣称任务布局验收完成。 + 待闭环 + 最终进度不 complete 任务已跑完,但客户端等待最终结果直到超时。 @@ -626,6 +713,7 @@

现在可以说

  • LabUtopia Franka POC 已经能通过 GenManip/EBench server-client 链路本地跑完。
  • 三个 level-1 任务能完成 reset/step/finalize,并生成 per-task 和 final result。
  • Franka POC 的 LabUtopia scene overlay 加载路径已经验证。
  • +
  • 已补三张通过同一 EBench/GenManip 加载路径抓取的静态可见性截图。
  • @@ -633,7 +721,7 @@

    现在不能说

    • 不能说任务已经求解成功,当前默认动作得分是 0.0
    • 不能说官方 Lift2 baseline 已经可评,Lift2 复合资产和官方 runner 还没闭环。
    • -
    • 不能说画面渲染已验收,本轮没有保存图片或视频。
    • +
    • 不能说画面/视频/任务布局已经验收,当前只有静态可见性补证。
    @@ -644,28 +732,28 @@

    接下来几步

    P0 +

    渲染和布局闭环

    +

    先修复 camera2 黑屏、对象 bbox/pose 偏移和 reset 后关键截图,再进入 Lift2 资产预检。

    +
    +
    + P1

    Lift2 资产预检

    构建复合资产根目录:LabUtopia scene overlay + 默认 robot_usds/lift2 + 默认 miscs/curobo

    - P1 + P2

    Lift2 dry smoke

    在独立端口运行 lift2_candidate,要求 3/3 complete,但仍保持 official_baseline_execution=false

    - P2 + P3

    官方 runner 发现

    定位官方 EBench/OpenPI/Lift2 runner 文件,记录路径、hash 和入口,不直接执行策略。

    -
    - P3 -

    官方式本地尝试

    -

    只有 P2 过后,才做一轮官方风格 online loop,并保留 terminal evidence。

    -
    P4 -

    证据封口

    -

    补 artifact linkage、forbidden-claims、path-leakage 检查,再改变 PM 对外口径。

    +

    官方式本地尝试

    +

    只有 P3 过后,才做一轮官方风格 online loop,并保留 terminal evidence。

    @@ -692,6 +780,11 @@

    相关文档

    本次 smoke 的 run_id、命令、结果路径。 打开 + + Franka render smoke 记录 + 三任务渲染补证、camera2 黑屏和当前风险边界。 + 打开 + Lift2 baseline lane plan 仿 EOS 的 P0-P4 证据门推进计划。 From 2517e8e625e2ac2b68d5d4f815a7ede04337cf8b Mon Sep 17 00:00:00 2001 From: Codex Date: Tue, 23 Jun 2026 11:52:13 +0800 Subject: [PATCH 14/30] Document LabUtopia render QA blockers --- docs/labutopia_lab_poc/franka_render_smoke.md | 21 +- .../render_visual_investigation_20260623.md | 140 +++++ ...26-06-22-labutopia-ebench-weekly-report.md | 45 +- .../index.html | 103 ++-- ...-labutopia-ebench-render-layout-closure.md | 527 ++++++++++++++++++ 5 files changed, 774 insertions(+), 62 deletions(-) create mode 100644 docs/labutopia_lab_poc/render_visual_investigation_20260623.md create mode 100644 docs/superpowers/plans/2026-06-23-labutopia-ebench-render-layout-closure.md diff --git a/docs/labutopia_lab_poc/franka_render_smoke.md b/docs/labutopia_lab_poc/franka_render_smoke.md index 5690577..e51eb96 100644 --- a/docs/labutopia_lab_poc/franka_render_smoke.md +++ b/docs/labutopia_lab_poc/franka_render_smoke.md @@ -6,7 +6,7 @@ Date: 2026-06-22 This record tracks the render evidence added for the LabUtopia Franka POC weekly report. -The render evidence is PM-facing asset-visibility evidence only. It does not prove task success, official baseline readiness, video recording readiness, or correct reset-time task layout. +The render evidence is historical failed evidence. It does not prove asset visibility, task success, official baseline readiness, video recording readiness, or correct reset-time task layout. ## Environment @@ -60,7 +60,7 @@ Observed implication: eval recorder camera2: not usable for PM visual evidence yet ``` -To keep the weekly report useful while preserving claim boundaries, the report uses static direct-render screenshots from the same EBench/GenManip-loaded LabUtopia stage and the same overlay asset root. The direct render only changes report lighting and camera viewpoint. It does not change task configuration, evaluator logic, or result scores. +The initial weekly report used static direct-render screenshots from the same EBench/GenManip-loaded LabUtopia stage and the same overlay asset root. Follow-up visual QA on 2026-06-23 found those screenshots are not acceptable as task-scene evidence. The direct render changed report lighting and camera viewpoint, and it did not prove task configuration, evaluator logic, result scores, or reset-time visual correctness. ## Report images @@ -72,15 +72,22 @@ docs/records/evidence/2026-06-22-labutopia-ebench-weekly-report/assets/labutopia docs/records/evidence/2026-06-22-labutopia-ebench-weekly-report/assets/labutopia-franka-level1-open-door.jpg ``` -Interpretation: +Follow-up visual QA: -- `level1_pick`: static visibility of the loaded local tabletop target asset. -- `level1_place`: static visibility of the loaded target platform asset. -- `level1_open_door`: static visibility of the loaded drying-box geometry. +- `level1_pick`: WARN. Some object-like shapes are visible, but the pick target is not clearly identifiable. +- `level1_place`: FAIL. Only the target platform/slab is visible; the beaker and place relation are missing. +- `level1_open_door`: FAIL. Only a drying-box corner/cuboid is visible; door, handle, hinge, and open state are not identifiable. ## Open risks -- Fix eval recorder `camera2` black frames. +- Fix eval recorder `camera2` black frames and prove camera readback before recorder writing. - Close the gap between visible mesh bbox, wrapper pose, and task semantic coordinates. +- Make LabUtopia reset-time task layout explicit instead of relying on fallback live-scene metadata. - Capture reset-time keyframes for all three tasks through the normal eval recording path. +- Replace the current report images only after independent visual QA passes. - Keep official Lift2 baseline claims blocked until Lift2 composite assets and official runner discovery are complete. + +## Follow-up Records + +- [docs/labutopia_lab_poc/render_visual_investigation_20260623.md](render_visual_investigation_20260623.md) +- [docs/superpowers/plans/2026-06-23-labutopia-ebench-render-layout-closure.md](../superpowers/plans/2026-06-23-labutopia-ebench-render-layout-closure.md) diff --git a/docs/labutopia_lab_poc/render_visual_investigation_20260623.md b/docs/labutopia_lab_poc/render_visual_investigation_20260623.md new file mode 100644 index 0000000..5586c84 --- /dev/null +++ b/docs/labutopia_lab_poc/render_visual_investigation_20260623.md @@ -0,0 +1,140 @@ +# LabUtopia EBench Render Visual Investigation + +Date: 2026-06-23 + +## Scope + +This record summarizes the visual and technical investigation for the three LabUtopia Franka POC render images that were added to the 2026-06-22 weekly report. + +The conclusion is that the current three images are not acceptable as task-scene render evidence. They should be treated as failed visual evidence until the normal eval recorder path produces task-relevant reset-time frames. + +## Reviewed Images + +Committed report assets: + +```text +docs/records/evidence/2026-06-22-labutopia-ebench-weekly-report/assets/labutopia-franka-level1-pick.jpg +docs/records/evidence/2026-06-22-labutopia-ebench-weekly-report/assets/labutopia-franka-level1-place.jpg +docs/records/evidence/2026-06-22-labutopia-ebench-weekly-report/assets/labutopia-franka-level1-open-door.jpg +``` + +## Multi-Agent Review + +### Visual QA Reviewer + +Verdict: FAIL overall. + +| Task | Verdict | Visible evidence | Main risk | +| --- | --- | --- | --- | +| `level1_pick` | WARN | Some tabletop-like surface and white object shapes are visible. | Pick target is not clearly identifiable; material contrast and clipping make this weak evidence. | +| `level1_place` | FAIL | Only a white rectangular platform/slab is visible. | No beaker or place relation is visible. | +| `level1_open_door` | FAIL | A close-up of a dark cuboid/corner with white top is visible. | Door, handle, hinge, and open state are not identifiable. | + +Retake requirements: + +- `level1_pick`: target object centered, separated from the support surface, with enough contrast to identify it. +- `level1_place`: both the beaker and target platform visible in one frame, with their spatial relation clear. +- `level1_open_door`: wider frontal or three-quarter view showing drying box, door face, handle, hinge area, and open/closed state. + +### Technical Reviewer + +The report JPGs are documented, but there is no committed producer script or exact capture command for them. They are not normal eval recorder frames. + +Key findings: + +- `run_id=labutopia_franka_render_smoke_20260622_150819` saved process frames, but all checked `camera2` frames are pure black. +- The three report JPGs were direct-render static screenshots with report-specific lighting and camera viewpoint. +- Current task YAMLs use `object_config: {}` and `layout_config.type: null`, so LabUtopia source task position ranges are recorded but not actively applied as GenManip reset-time layouts. +- The shared POC camera config does not preserve the original LabUtopia `level1_open_door` camera viewpoints. +- The overlay wrapper proves selected prims can be discovered, but it does not by itself prove task-relevant object placement, visible bbox alignment, or baseline readiness. + +### Product Reviewer + +The PM-safe status is: + +```text +Backend integration smoke is complete, but visual/layout validation is blocked. +``` + +The current images should be called debug screenshots or historical failed evidence, not PM-ready render evidence. + +## Evidence Collected + +Eval recorder frame stats from the render smoke run: + +```text +run_id=labutopia_franka_render_smoke_20260622_150819 +level1_pick camera2 frames: 32 checked path entries, sampled min=max=mean=0 +level1_place camera2 frames: 32 checked path entries, sampled min=max=mean=0 +level1_open_door camera2 frames: 32 checked path entries, sampled min=max=mean=0 +``` + +Representative paths: + +```text +saved/eval_results/ebench/labutopia_franka_render_smoke_20260622_150819/ebench/labutopia_lab_poc/franka_poc/level1_pick/000/*/camera2/00000.png +saved/eval_results/ebench/labutopia_franka_render_smoke_20260622_150819/ebench/labutopia_lab_poc/franka_poc/level1_place/000/*/camera2/00000.png +saved/eval_results/ebench/labutopia_franka_render_smoke_20260622_150819/ebench/labutopia_lab_poc/franka_poc/level1_open_door/000/*/camera2/00000.png +``` + +Relevant configuration: + +```text +configs/tasks/ebench/labutopia_lab_poc/franka_poc/level1_pick.yml +configs/tasks/ebench/labutopia_lab_poc/franka_poc/level1_place.yml +configs/tasks/ebench/labutopia_lab_poc/franka_poc/level1_open_door.yml +configs/cameras/labutopia_franka_poc.yml +configs/tasks/ebench/labutopia_lab_poc/common/task_semantics.yml +standalone_tools/labutopia_poc/build_asset_overlay.py +genmanip/core/evaluator/labutopia_layout.py +genmanip/utils/loader/scene.py +``` + +## Ranked Root-Cause Hypotheses + +1. Eval recorder camera path is broken before writing. + - The saved PNGs are pure black, so the issue is upstream of report rendering and likely upstream of image file writing. + - Candidate causes: camera pose points at empty/dark view, render product not bound to the intended Isaac camera prim, missing lighting in the normal eval path, or camera readback before valid rendering. +2. Task layout is not yet a real LabUtopia reset layout. + - LabUtopia source position ranges are in `task_semantics.yml`, but the runtime task YAMLs have empty `object_config` and null layout. + - The fallback metadata captures the live loaded scene rather than sampling or enforcing LabUtopia task-specific reset poses. +3. Direct-render report screenshots used non-eval camera choices. + - The report images were captured with changed lighting and viewpoint and no committed producer script. + - This makes them unsuitable for proving task-scene correctness. +4. Overlay wrapper and task semantics are not yet aligned enough for visual or metric claims. + - The wrapper payload exposes prims under GenManip-friendly names, but visible mesh bbox, wrapper pose, and semantic coordinates still need a measured consistency check. +5. The `open_door` camera is using a shared POC view instead of the source task's task-specific view. + - Original LabUtopia `level1_open_door.yaml` uses different camera positions from the shared POC camera config. + +## Claim Boundary + +Allowed now: + +```text +LabUtopia Franka POC can run through the local GenManip/EBench server-client smoke path and finalize 3/3 tasks with result files. +``` + +Not allowed now: + +```text +The three task render images are accepted. +The eval video path works. +Task reset layouts are visually verified. +The official Lift2 baseline is evaluable. +The current screenshots prove task correctness. +``` + +## Required Next Diagnostics + +Run these in an isolated port/run_id so EOS or another engineer's run is not confused with this work: + +1. Dump `camera1` and `camera2` world poses, render product paths, RGB min/max/mean/nonzero stats immediately after `get_eval_camera_data()`. +2. Save one raw camera frame before `EpisodeRecorder.record_obs()` to isolate camera readback from recorder output. +3. Change only `camera2` pose to a known direct-render viewpoint, then only lighting, to separate pose failure from lighting/readback failure. +4. Dump `scene.object_list.keys()`, wrapper world poses, object extents/bbox centers, and target object projections into `camera2`. +5. Add a reproducible render/capture script that writes all diagnostics and images under a unique `run_id`. +6. Write an evidence manifest for any future PM image. It must include `run_id`, task name, source eval frame path, report image path, sha256, camera config, asset root, commit hash, and `direct_render=false`. + +## Documentation Decision + +The weekly report must say the current images failed visual review. The images may remain as historical failed evidence only if clearly marked. They must not be presented as accepted render evidence until P0 render/layout closure passes. diff --git a/docs/records/2026-06-22-labutopia-ebench-weekly-report.md b/docs/records/2026-06-22-labutopia-ebench-weekly-report.md index 3c799e8..951e328 100644 --- a/docs/records/2026-06-22-labutopia-ebench-weekly-report.md +++ b/docs/records/2026-06-22-labutopia-ebench-weekly-report.md @@ -7,7 +7,7 @@ HTML 版产品汇报页: 本周已经把 LabUtopia 的 Franka POC 跑通到端到端 smoke 阶段:任务可以提交、场景可以加载、三个 level-1 任务可以 reset/step、结果可以落盘、最终状态可以正常 complete。 -这说明当前最关键的“接入链路”已经打通。需要注意的是,这还不是任务求解成功,也不是官方 baseline 成绩;当前 smoke 使用默认动作,所以三个任务分数都是 `0.0`。 +这说明当前最关键的“接入链路”已经打通。需要注意的是,这还不是任务求解成功,也不是官方 baseline 成绩;当前 smoke 使用默认动作,所以三个任务分数都是 `0.0`。2026-06-23 复核后还要补充一点:当前渲染/布局验收是 blocked,三张静态渲染图未通过视觉 QA,不能继续作为 PM-ready 渲染证据。 ## 本周完成了什么 @@ -65,7 +65,7 @@ DryingBox_01 -> obj_DryingBox_01 table -> table ``` -这部分已经通过实际 smoke 验证。 +这部分已经通过实际 smoke 验证。这里的“资产加载”只表示 scene overlay 和对象名映射能进入 runtime,不表示 reset 后任务布局、相机画面或可评测视频已经验收。 ### 3. 结果记录链路已修复 @@ -96,7 +96,7 @@ EOS/其他工程师任务: 8087 ## 有没有渲染图验证? -已补三张静态渲染图,放在 HTML 汇报页里: +2026-06-23 复核结论:当前三张静态渲染图未通过视觉验收,不能继续作为“任务渲染已闭环”的证据。它们保留为历史问题样例,用来说明下一步必须修复 eval camera、任务布局和可复现截图链路。 ```text docs/records/evidence/2026-06-22-labutopia-ebench-weekly-report/assets/labutopia-franka-level1-pick.jpg @@ -104,13 +104,14 @@ docs/records/evidence/2026-06-22-labutopia-ebench-weekly-report/assets/labutopia docs/records/evidence/2026-06-22-labutopia-ebench-weekly-report/assets/labutopia-franka-level1-open-door.jpg ``` -这三张图来自同一个 LabUtopia Franka POC 任务包和同一个 LabUtopia overlay 资产根目录,均在 Isaac/Replicator 中从 EBench/GenManip 加载后的 stage 抓取。它们可以作为“资产可见性证据”,但还不能作为“画面/视频/任务布局已经验收”的结论。 +这三张图来自同一个 LabUtopia Franka POC 任务包和同一个 LabUtopia overlay 资产根目录,但它们不是正常 eval recorder 产物,而是后补 direct-render 静态图。视觉复核后判定:`level1_pick` 只能算 WARN,`level1_place` 和 `level1_open_door` 是 FAIL。 需要特别说明: - `run_id=labutopia_franka_render_smoke_20260622_150819` 跑过保存帧 smoke,但 eval recorder 的 `camera2` 帧当前是黑屏。 -- 因此 HTML 里使用的是同一加载路径下的直渲静态截图,只补了报告用光照和相机视角,不改变任务配置、结果分数或 evaluator 逻辑。 -- 当前还观察到部分对象的可见 mesh bbox、wrapper pose、任务语义坐标没有完全对齐;下一步必须修复这个问题,再做 reset 后关键帧验收。 +- 三个任务各 32 帧的 eval `camera2` 抽样统计均为纯黑:RGB min/max/mean 都是 0。 +- 当前 task YAML 仍是 `object_config: {}`、`layout_config.type: null`,还没有把 LabUtopia 原始 position_range 真正落成 GenManip reset-time 布局。 +- 因此下一步必须先修复 eval camera/readback、任务布局和可复现截图脚本,再做 reset 后关键帧验收。 ## 本周遇到的问题和解决方式 @@ -119,8 +120,9 @@ docs/records/evidence/2026-06-22-labutopia-ebench-weekly-report/assets/labutopia | 资产根目录不对 | 系统一开始会去错误目录找 LabUtopia 场景 | 增加 LabUtopia manifest 识别逻辑,运行时切换到 overlay asset root | 已解决 | | 缺少 `meta_info.pkl` | 传统 GenManip 任务依赖采集包里的元信息,LabUtopia POC 没有这份文件 | 针对 LabUtopia POC,从实时场景里自动生成最小可用元信息 | 已解决 | | camera cleanup 字段缺失 | 切换任务时 camera 清理报字段错误 | 补齐 camera 配置里的 cleanup flags | 已解决 | -| eval recorder `camera2` 黑屏 | 保存过程帧时 `camera2` 输出黑屏 | 已补同一加载路径下的直渲静态图用于 PM 可见性汇报 | 待修复 | -| 可见 mesh 与 wrapper pose 未完全对齐 | 部分对象 bbox、wrapper pose、任务语义坐标存在偏移 | 先降级为“资产可见性证据”,不宣称任务布局验收完成 | 待闭环 | +| eval recorder `camera2` 黑屏 | 保存过程帧时 `camera2` 输出纯黑 | 已确认为 P0 阻塞;直渲截图不能替代 eval-path 证据 | 待修复 | +| 当前三张渲染图未通过视觉验收 | `pick` 目标不清,`place` 缺 beaker,`open_door` 缺门/把手/开门状态 | 降级为历史失败样例,新增调研记录和 P0-P2 修复计划 | 待重拍 | +| 可见 mesh 与 wrapper pose 未完全对齐 | 部分对象 bbox、wrapper pose、任务语义坐标存在偏移 | 需要输出 object pose/bbox/投影诊断,再决定布局修复 | 待闭环 | | 最终进度不 complete | 任务已跑完,但进度没有写入,导致客户端最后等待超时 | 任务结束时写最小 `result_info.json`,并修复进度统计 | 已解决 | | 后处理异常可能被误记为完成 | 如果后处理报错,不能用 0 分兜底伪装成成功 | 增加 fail-fast 逻辑和回归测试 | 已解决 | | 端口/结果混淆风险 | EOS 侧也有人在跑任务,担心互相影响 | 使用独立 worktree、独立 run_id、独立端口 18088,并复核 8087 状态 | 已解决 | @@ -132,7 +134,7 @@ docs/records/evidence/2026-06-22-labutopia-ebench-weekly-report/assets/labutopia | LabUtopia 场景接入 | 已跑通 | 场景资产能加载,任务链路能跑完 | | Franka POC smoke | 已完成 | 3 个任务全部 complete | | 结果落盘 | 已完成 | per-task 和 final result 都能写出 | -| 渲染图/视频验收 | 已补静态可见性证据 | 已有三张直渲图;`camera2`、视频和 reset 后任务布局仍需闭环 | +| 渲染图/视频验收 | 未通过 | 当前三张直渲图视觉 QA 不合格;`camera2`、视频和 reset 后任务布局仍需闭环 | | 任务求解能力 | 未验证 | 当前默认动作得分 0.0,不代表策略能力 | | 官方 baseline | 未进入本周结论 | 后续单独讨论和规划 | @@ -166,13 +168,13 @@ saved/eval_results/ebench/labutopia_franka_smoke_clean8_20260622_100208/.../leve saved/eval_results/ebench/labutopia_franka_smoke_clean8_20260622_100208/.../level1_open_door/000/result_info.json ``` -渲染补证: +渲染补证复核: -```text -run_id=labutopia_franka_render_smoke_20260622_150819 -eval recorder camera2 frames: black -static direct-render evidence: 3 jpg files under docs/records/evidence/.../assets/ -``` +- `run_id=labutopia_franka_render_smoke_20260622_150819` +- eval recorder `camera2` frames: black, sampled RGB min/max/mean all 0 +- static direct-render evidence: visual QA failed on 2026-06-23 +- investigation: [docs/labutopia_lab_poc/render_visual_investigation_20260623.md](../labutopia_lab_poc/render_visual_investigation_20260623.md) +- plan: [docs/superpowers/plans/2026-06-23-labutopia-ebench-render-layout-closure.md](../superpowers/plans/2026-06-23-labutopia-ebench-render-layout-closure.md) ## 下周建议 @@ -180,8 +182,13 @@ static direct-render evidence: 3 jpg files under docs/records/evidence/.../asset 建议顺序: -1. 修复 eval recorder 的 `camera2` 黑屏问题。 -2. 修复或解释可见 mesh bbox、wrapper pose、任务语义坐标的偏移。 -3. 为三个任务各保留 reset 后关键截图。 -4. 检查截图里目标物、桌面、门/干燥箱是否可见且位置合理。 +1. 按 `2026-06-23-labutopia-ebench-render-layout-closure.md` 执行 P0:定位 eval recorder `camera2` 黑屏是 pose、lighting、render product 还是 readback 问题。 +2. 执行 P1:把 LabUtopia task position_range、必看对象和任务特定相机要求落成可验证的 reset-time 布局。 +3. 执行 P2:用正常 eval-path 重新抓三任务关键帧,并通过独立视觉 QA。 +4. 图像验收通过后再替换 HTML 里的三张图,并恢复 PM 可见性汇报。 5. 再进入 Lift2 复合资产预检和官方 baseline 路线。 + +## 新增调研和计划文档 + +- [docs/labutopia_lab_poc/render_visual_investigation_20260623.md](../labutopia_lab_poc/render_visual_investigation_20260623.md) +- [docs/superpowers/plans/2026-06-23-labutopia-ebench-render-layout-closure.md](../superpowers/plans/2026-06-23-labutopia-ebench-render-layout-closure.md) diff --git a/docs/records/evidence/2026-06-22-labutopia-ebench-weekly-report/index.html b/docs/records/evidence/2026-06-22-labutopia-ebench-weekly-report/index.html index 9241f6b..f165a28 100644 --- a/docs/records/evidence/2026-06-22-labutopia-ebench-weekly-report/index.html +++ b/docs/records/evidence/2026-06-22-labutopia-ebench-weekly-report/index.html @@ -392,17 +392,17 @@

    Weekly report · 2026-06-22 · PM-facing evidence page

    LabUtopia x EBench接入周报

    -

    本周 Franka POC 已经跑通端到端 smoke:任务能提交,LabUtopia 场景能加载,三个 level-1 任务能 reset/step,结果能落盘并正常 complete。当前结论是“接入链路跑通”,不是“策略已经会做任务”,也不是“官方 baseline 成绩”。

    +

    本周 Franka POC 已经跑通端到端 smoke:任务能提交,LabUtopia 场景能加载,三个 level-1 任务能 reset/step,结果能落盘并正常 complete。当前结论是“接入链路跑通”;渲染/布局验收仍是 blocked,当前三张渲染图未通过视觉验收。

    - Franka POC complete + wiring smoke complete 3/3 episodes finalized score=0.0 expected - Lift2 baseline lane planned + render/layout blocked
    @@ -433,8 +433,8 @@

    当前进展总览

    渲染证据
    -
    已补 3 张
    -
    静态可见性截图已补;eval recorder 的 camera2 仍需修复。
    +
    未通过
    +
    三张直渲图视觉 QA 不合格;eval recorder 的 camera2 仍为黑屏。
    官方 Lift2 baseline
    @@ -467,7 +467,7 @@

    本周完成了什么

    场景资产加载 Franka POC 闭环 通过 manifest 把 LabUtopia overlay 资产根目录接入运行时,并映射对象名。 - Franka POC 已验证;Lift2 还要补 robot/curobo 资产组合。 + 证明 scene overlay 可加载;不证明 reset 后布局或相机画面已验收。 结果生命周期 @@ -483,9 +483,9 @@

    本周完成了什么

    渲染图验收 - 已补静态证据 - 已从 EBench/GenManip 加载后的 Isaac stage 抓取三张静态图。 - 证明可见资产能被加载;暂不能证明任务布局、视频链路或策略成功。 + 未通过视觉 QA + 当前三张图保留为失败样例:pick WARN,place/open_door FAIL。 + 不能作为 PM-ready 渲染证据;需要 eval-path reset 帧重拍。 @@ -542,8 +542,8 @@

    资产加载是怎么做的

    Franka POC runtime scene.usda + object map verified - Lift2 next gate - scene overlay is not enough + Lift2 later gate + after render QA passes @@ -553,41 +553,41 @@

    资产加载是怎么做的

    Composite root - P0 work item + P3+ work item

    已经闭环的部分

    -

    LabUtopia 场景 overlay 能被 Franka POC 加载,桌面、瓶子、烧杯、托盘、干燥箱、门把手等对象能通过运行时对象映射进入 evaluator。

    +

    LabUtopia 场景 overlay 能被 Franka POC 加载,相关对象名能通过运行时对象映射进入 evaluator。这只证明接入路径,不证明视觉截图、reset 布局或可评测视频。

    还没闭环的部分

    -

    Lift2 官方 baseline 不能只用 scene overlay,它还需要默认资产里的 robot_usds/lift2miscs/curobo。因此下一步要做“复合资产根目录”,把 LabUtopia 场景资产和默认机器人资产组合起来。

    +

    Lift2 官方 baseline 不能只用 scene overlay,它还需要默认资产里的 robot_usds/lift2miscs/curobo。这一步排在 P0-P2 渲染/布局闭环之后,再做“复合资产根目录”。

    EBench 加载后的三任务渲染图

    -

    下面三张图来自同一个 LabUtopia Franka POC 任务包和同一个 LabUtopia overlay 资产根目录,均在 Isaac/Replicator 中从 EBench/GenManip 加载后的 stage 抓取。它们是“资产可见性证据”,不是策略成功率、任务布局或官方 baseline 的验收结论。

    -
    -

    渲染证据边界

    -

    自动 eval recorder 保存的 camera2 帧当前为黑屏;因此这里采用同一任务包、同一 overlay 加载路径下的直渲截图。直渲过程中只补了报告用光照和相机视角,不改变任务配置、结果分数或 evaluator 逻辑。

    +

    2026-06-23 复核结论:当前三张渲染图未通过视觉验收,不能作为任务渲染已闭环的证据。下面保留为历史失败样例,方便说明 P0 修复范围。

    +
    +

    渲染证据状态

    +

    自动 eval recorder 保存的 camera2 帧为纯黑;三张 direct-render 静态图不是正常 eval recorder 产物,也没有可复现生产脚本。视觉 QA 结果为 level1_pick=WARNlevel1_place=FAILlevel1_open_door=FAIL

    level1_pick task render after EBench LabUtopia asset loading -
    level1_pickPick up the conical bottle. 当前能看到加载后的局部桌面目标资产;后续还要修复 wrapper pose 与可见 mesh bbox 的一致性。
    +
    level1_pick · WARN目标物不够清晰,材质和裁切让 pick object 难以确认;只能作为失败样例,不能作为验收图。
    level1_place target platform render after EBench LabUtopia asset loading -
    level1_placePick up beaker and place on target platform. 当前 target platform 可见;beaker 与 target 的相对布局仍需做 reset 后截图闭环。
    +
    level1_place · FAIL只看到 target platform,看不到 beaker,也看不到放置关系;必须通过 eval-path reset 帧重拍。
    level1_open_door drying box render after EBench LabUtopia asset loading -
    level1_open_doorOpen the drying box door. 当前干燥箱几何可见;door handle 与 joint/metric 的可评估闭环仍需要下一轮修复。
    +
    level1_open_door · FAIL只看到箱体角落,看不到门、把手、铰链或开门状态;必须使用任务相关视角重拍。
    @@ -622,7 +622,12 @@

    验证证据

    render smoke labutopia_franka_render_smoke_20260622_150819 - 同一任务包跑过保存帧 smoke;eval recorder 的 camera2 帧为黑屏,静态直渲图作为补充可见性证据。 + 同一任务包跑过保存帧 smoke;eval recorder 的 camera2 帧为纯黑,静态直渲图已被复核为视觉证据不合格。 + + + visual QA + WARN / FAIL / FAIL + pick WARN,place FAIL,open_door FAIL;详见 2026-06-23 调研记录。 pytest @@ -678,13 +683,19 @@

    问题和处理结果

    eval recorder camera2 黑屏 保存过程帧时 camera2 输出为黑屏。 - 已补同一加载路径的直渲静态图,先满足 PM 可见性汇报。 - 待修复 + 确认三个任务各 32 帧抽样均为纯黑;直渲截图不能替代 eval-path 证据。 + P0 阻塞 + + + 当前三张渲染图未通过视觉验收 + pick 目标不清,place 缺 beaker,open_door 缺门/把手/开门状态。 + 降级为历史失败样例;新增调研记录和 P0-P2 render/layout closure 计划。 + 待重拍 可见 mesh 与 wrapper pose 未完全对齐 部分对象 bbox、wrapper pose、任务语义坐标存在偏移,影响截图和后续可评性判断。 - 报告中降级为“资产可见性证据”,不宣称任务布局验收完成。 + 报告中降级为“历史失败样例”,不宣称任务布局或资产可见性验收完成。 待闭环 @@ -713,7 +724,7 @@

    现在可以说

  • LabUtopia Franka POC 已经能通过 GenManip/EBench server-client 链路本地跑完。
  • 三个 level-1 任务能完成 reset/step/finalize,并生成 per-task 和 final result。
  • Franka POC 的 LabUtopia scene overlay 加载路径已经验证。
  • -
  • 已补三张通过同一 EBench/GenManip 加载路径抓取的静态可见性截图。
  • +
  • 当前三张渲染图已经完成复核,并明确判定为不合格历史样例。
  • @@ -721,7 +732,7 @@

    现在不能说

    • 不能说任务已经求解成功,当前默认动作得分是 0.0
    • 不能说官方 Lift2 baseline 已经可评,Lift2 复合资产和官方 runner 还没闭环。
    • -
    • 不能说画面/视频/任务布局已经验收,当前只有静态可见性补证。
    • +
    • 不能说画面/视频/任务布局已经验收;当前三张图视觉 QA 未通过,eval camera2 仍是黑屏。
    @@ -732,28 +743,38 @@

    接下来几步

    P0 -

    渲染和布局闭环

    -

    先修复 camera2 黑屏、对象 bbox/pose 偏移和 reset 后关键截图,再进入 Lift2 资产预检。

    +

    Camera 黑屏诊断

    +

    定位 eval recorder camera2 黑屏是 pose、lighting、render product 绑定还是 readback 问题,并输出 object pose/bbox/projection 诊断。

    P1 -

    Lift2 资产预检

    -

    构建复合资产根目录:LabUtopia scene overlay + 默认 robot_usds/lift2 + 默认 miscs/curobo

    +

    Reset 布局显式化

    +

    把 task position_range、必看对象和任务特定相机要求落成可验证的 reset-time 布局,先让三任务关键对象进入画面。

    P2 +

    Eval-path 重拍验收

    +

    用正常 eval recorder 重新抓三任务关键帧,写 evidence manifest,并要求三张报告图独立视觉 QA 全部 PASS

    +
    +
    + P3 +

    Lift2 资产预检

    +

    渲染/布局证据通过后,再构建复合资产根目录:LabUtopia scene overlay + 默认 robot_usds/lift2 + 默认 miscs/curobo

    +
    +
    + P4

    Lift2 dry smoke

    在独立端口运行 lift2_candidate,要求 3/3 complete,但仍保持 official_baseline_execution=false

    - P3 + P5

    官方 runner 发现

    定位官方 EBench/OpenPI/Lift2 runner 文件,记录路径、hash 和入口,不直接执行策略。

    - P4 + P6

    官方式本地尝试

    -

    只有 P3 过后,才做一轮官方风格 online loop,并保留 terminal evidence。

    +

    只有 P5 过后,才做一轮官方风格 online loop,并保留 terminal evidence。

    @@ -782,9 +803,19 @@

    相关文档

    Franka render smoke 记录 - 三任务渲染补证、camera2 黑屏和当前风险边界。 + 三任务失败渲染样例、camera2 黑屏和当前风险边界。 打开 + + Render visual investigation + 6/23 多角度复核:当前三张图未通过视觉验收,camera2 为纯黑。 + 打开 + + + Render/layout closure plan + P0-P2 修复计划:camera 诊断、reset 布局、eval-path 重拍和视觉 QA。 + 打开 + Lift2 baseline lane plan 仿 EOS 的 P0-P4 证据门推进计划。 diff --git a/docs/superpowers/plans/2026-06-23-labutopia-ebench-render-layout-closure.md b/docs/superpowers/plans/2026-06-23-labutopia-ebench-render-layout-closure.md new file mode 100644 index 0000000..bd1b844 --- /dev/null +++ b/docs/superpowers/plans/2026-06-23-labutopia-ebench-render-layout-closure.md @@ -0,0 +1,527 @@ +# LabUtopia EBench Render/Layout Closure Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Replace the failed LabUtopia render evidence with reproducible eval-path frames that show task-relevant reset layouts for `level1_pick`, `level1_place`, and `level1_open_door`. + +**Architecture:** Treat render/layout closure as a P0 gate before Lift2 baseline work. First instrument the normal eval camera path, then make task reset layout explicit, then regenerate visual evidence through a reproducible script and independent visual review. Keep all runs isolated by worktree, port, run_id, and result directory to avoid confusion with EOS or another engineer's work. + +**Tech Stack:** Python 3.10, Isaac Sim 4.1 conda env, GenManip evaluator, EBench server/client, Pillow-based image stats, pytest, static GitHub Pages docs. + +--- + +## Claim Boundary + +Allowed before this plan is complete: + +```text +LabUtopia Franka POC server/client smoke runs complete and result files are written. +``` + +Allowed only after this plan is complete: + +```text +The three Franka POC tasks have reproducible eval-path reset render evidence that passes visual QA. +``` + +Still not allowed after this plan: + +```text +official Lift2 baseline score +leaderboard comparability +official policy quality claim +``` + +## File Structure + +- Create `standalone_tools/labutopia_poc/capture_eval_render_diagnostics.py` + - Runs one controlled LabUtopia POC reset/camera capture in the Isaac environment. + - Writes camera poses, render product paths, RGB stats, object pose/bbox diagnostics, and PNG frames under a unique output directory. +- Create `tests/labutopia_poc/test_render_diagnostics_contract.py` + - Tests the diagnostics JSON schema and visual QA status logic without launching Isaac. +- Modify `configs/tasks/ebench/labutopia_lab_poc/franka_poc/level1_pick.yml` + - Add render/layout readiness metadata and later explicit placement once diagnostics proves the coordinate contract. +- Modify `configs/tasks/ebench/labutopia_lab_poc/franka_poc/level1_place.yml` + - Add render/layout readiness metadata and later explicit beaker/platform placement. +- Modify `configs/tasks/ebench/labutopia_lab_poc/franka_poc/level1_open_door.yml` + - Add render/layout readiness metadata and later explicit drying-box placement plus task-specific camera requirements. +- Modify or create a task-specific camera config under `configs/cameras/` + - Preserve the original LabUtopia `open_door` view or an equivalent EBench-safe view. +- Update `docs/labutopia_lab_poc/render_visual_investigation_20260623.md` + - Append diagnostic outputs and visual review verdicts. +- Update weekly Markdown and HTML reports only after evidence state changes. + +## P0: Camera Black-Frame Root Cause + +**Files:** +- Create: `standalone_tools/labutopia_poc/capture_eval_render_diagnostics.py` +- Create: `tests/labutopia_poc/test_render_diagnostics_contract.py` + +- [ ] **Step 1: Write diagnostics contract test** + +Create `tests/labutopia_poc/test_render_diagnostics_contract.py` with this contract: + +```python +from standalone_tools.labutopia_poc.capture_eval_render_diagnostics import ( + build_camera_frame_stats, + classify_frame_stats, +) + + +def test_classify_black_frame_as_failed(): + stats = build_camera_frame_stats( + camera_name="camera2", + frame_path="camera2/00000.png", + width=256, + height=256, + channel_min=[0, 0, 0], + channel_max=[0, 0, 0], + channel_mean=[0.0, 0.0, 0.0], + nonzero_pixels=0, + ) + + assert classify_frame_stats(stats) == "black_frame_fail" + + +def test_classify_visible_frame_as_pass(): + stats = build_camera_frame_stats( + camera_name="camera2", + frame_path="camera2/00000.png", + width=256, + height=256, + channel_min=[0, 1, 0], + channel_max=[180, 190, 170], + channel_mean=[72.0, 80.0, 69.0], + nonzero_pixels=42000, + ) + + assert classify_frame_stats(stats) == "visible_frame" +``` + +- [ ] **Step 2: Run the test and confirm RED** + +Run: + +```bash +python -m pytest tests/labutopia_poc/test_render_diagnostics_contract.py -q +``` + +Expected: + +```text +ImportError or missing function failure +``` + +- [ ] **Step 3: Implement the pure-Python diagnostics helpers** + +Create `standalone_tools/labutopia_poc/capture_eval_render_diagnostics.py` with: + +```python +from __future__ import annotations + +from dataclasses import asdict, dataclass +from typing import Literal + + +@dataclass(frozen=True) +class CameraFrameStats: + camera_name: str + frame_path: str + width: int + height: int + channel_min: list[int] + channel_max: list[int] + channel_mean: list[float] + nonzero_pixels: int + + +def build_camera_frame_stats( + *, + camera_name: str, + frame_path: str, + width: int, + height: int, + channel_min: list[int], + channel_max: list[int], + channel_mean: list[float], + nonzero_pixels: int, +) -> dict[str, object]: + return asdict( + CameraFrameStats( + camera_name=camera_name, + frame_path=frame_path, + width=width, + height=height, + channel_min=channel_min, + channel_max=channel_max, + channel_mean=channel_mean, + nonzero_pixels=nonzero_pixels, + ) + ) + + +def classify_frame_stats(stats: dict[str, object]) -> Literal["black_frame_fail", "visible_frame"]: + channel_max = stats["channel_max"] + nonzero_pixels = int(stats["nonzero_pixels"]) + if not isinstance(channel_max, list): + raise TypeError("channel_max must be a list") + if max(int(value) for value in channel_max) == 0 or nonzero_pixels == 0: + return "black_frame_fail" + return "visible_frame" +``` + +- [ ] **Step 4: Run the test and confirm GREEN** + +Run: + +```bash +python -m pytest tests/labutopia_poc/test_render_diagnostics_contract.py -q +``` + +Expected: + +```text +2 passed +``` + +- [ ] **Step 5: Add Isaac runtime capture mode** + +Extend `capture_eval_render_diagnostics.py` with an `argparse` CLI that accepts: + +```text +--config ebench/labutopia_lab_poc/franka_poc +--task level1_pick +--run-id labutopia_render_diag_YYYYMMDD_HHMMSS +--port 18091 +--output-dir saved/diagnostics/labutopia_render_diag_YYYYMMDD_HHMMSS +--save-one-step +``` + +The CLI must write: + +```text +diagnostics.json +camera1/00000.png +camera2/00000.png +``` + +`diagnostics.json` must include: + +```json +{ + "run_id": "labutopia_render_diag_YYYYMMDD_HHMMSS", + "task": "level1_pick", + "camera_frames": [], + "camera_poses": {}, + "render_products": {}, + "render_product_binding": {}, + "object_world_poses": {}, + "object_extents": {}, + "projected_object_centers": {}, + "articulation_state": {}, + "claim_boundary": { + "task_render_accepted": false, + "official_baseline_evaluable": false + } +} +``` + +- [ ] **Step 6: Run isolated camera diagnostics** + +Use the conda Python: + +```bash +/cpfs/shared/simulation/zhuzihou/dev/conda-managed/envs/embodied-eval-os-sim-isaacsim41-genmanip-py310/bin/python \ + standalone_tools/labutopia_poc/capture_eval_render_diagnostics.py \ + --config ebench/labutopia_lab_poc/franka_poc \ + --task level1_pick \ + --run-id labutopia_render_diag_$(date +%Y%m%d_%H%M%S) \ + --port 18091 \ + --output-dir saved/diagnostics/labutopia_render_diag_pick \ + --save-one-step +``` + +Expected: + +```text +diagnostics.json written +camera frame stats recorded for camera1 and camera2 +camera prim path, render product path, world pose, focal data, RGB stats, and nonzero count recorded immediately after get_eval_camera_data() +no process remains on port 18091 after completion +``` + +- [ ] **Step 7: Repeat diagnostics for all tasks** + +Run the same command for: + +```text +level1_pick +level1_place +level1_open_door +``` + +Expected: + +```text +Each task has camera frame stats and object pose diagnostics. +If camera2 is black, diagnostics identify whether camera1 is also black. +level1_open_door additionally records articulation names, drying-box pose, handle pose, and handle/object projection into camera2. +``` + +## P1: Reset-Time Task Layout Closure + +**Files:** +- Modify: `configs/tasks/ebench/labutopia_lab_poc/franka_poc/level1_pick.yml` +- Modify: `configs/tasks/ebench/labutopia_lab_poc/franka_poc/level1_place.yml` +- Modify: `configs/tasks/ebench/labutopia_lab_poc/franka_poc/level1_open_door.yml` +- Modify: `configs/cameras/labutopia_franka_poc.yml` or create task-specific camera configs +- Test: `tests/labutopia_poc/test_validate_task_package.py` + +- [ ] **Step 1: Add static validation for render-layout readiness** + +Extend `standalone_tools/labutopia_poc/validate_task_package.py` so each Franka POC task must declare a render/layout readiness block: + +```yaml +labutopia_render_validation: + required_visible_objects: + - obj_conical_bottle02 + required_camera_names: + - camera1 + - camera2 + task_visual_goal: pick_target_visible +``` + +Per-task required objects: + +```text +level1_pick: obj_conical_bottle02 +level1_place: obj_beaker2, obj_target_plat +level1_open_door: obj_DryingBox_01, obj_DryingBox_01_handle +``` + +- [ ] **Step 2: Add failing tests for missing render-validation block** + +Update `tests/labutopia_poc/test_validate_task_package.py` with assertions that every Franka POC YAML includes the required block and object list. + +Run: + +```bash +python -m pytest tests/labutopia_poc/test_validate_task_package.py -q +``` + +Expected: + +```text +Failure showing missing labutopia_render_validation +``` + +- [ ] **Step 3: Add render-validation metadata to the three task YAMLs** + +Add to `level1_pick.yml`: + +```yaml + labutopia_render_validation: + required_visible_objects: + - obj_conical_bottle02 + required_camera_names: + - camera1 + - camera2 + task_visual_goal: pick_target_visible +``` + +Add to `level1_place.yml`: + +```yaml + labutopia_render_validation: + required_visible_objects: + - obj_beaker2 + - obj_target_plat + required_camera_names: + - camera1 + - camera2 + task_visual_goal: beaker_and_target_visible +``` + +Add to `level1_open_door.yml`: + +```yaml + labutopia_render_validation: + required_visible_objects: + - obj_DryingBox_01 + - obj_DryingBox_01_handle + required_camera_names: + - camera1 + - camera2 + task_visual_goal: door_and_handle_visible +``` + +- [ ] **Step 4: Decide camera fix from P0 diagnostics** + +Use the P0 diagnostics to choose one outcome: + +```text +Outcome A: render product/readback broken -> fix camera creation/readback before changing poses. +Outcome B: pose/lighting broken only -> update camera pose and lighting config. +Outcome C: task-specific view needed -> create task-specific camera config for open_door. +``` + +Do not change all variables at once. + +- [ ] **Step 5: Run package validation** + +Run: + +```bash +python standalone_tools/labutopia_poc/validate_task_package.py +``` + +Expected: + +```text +LabUtopia task package validation OK +``` + +## P2: Reproducible Evidence Regeneration + +**Files:** +- Create: `docs/labutopia_lab_poc/evidence_manifests/render_layout_closure_YYYYMMDD_HHMMSS.json` +- Modify: `docs/labutopia_lab_poc/render_visual_investigation_20260623.md` +- Modify: `docs/labutopia_lab_poc/franka_render_smoke.md` +- Modify: `docs/records/2026-06-22-labutopia-ebench-weekly-report.md` +- Modify: `docs/records/evidence/2026-06-22-labutopia-ebench-weekly-report/index.html` + +- [ ] **Step 1: Run eval-path capture after P0/P1 fixes** + +Run the diagnostic/capture script for all three tasks and save outputs under: + +```text +saved/diagnostics/labutopia_render_closure_/ +``` + +Expected: + +```text +camera frames are not black +required objects are visible in at least one eval-path frame per task +diagnostics.json includes frame stats and claim_boundary.task_render_accepted=false until visual review passes +``` + +- [ ] **Step 2: Run visual QA review** + +Use `render-visual-reviewer` on the three regenerated images. + +Acceptance: + +```text +level1_pick: PASS only if the pick target is clearly identifiable +level1_place: PASS only if beaker and target platform are visible together +level1_open_door: PASS only if drying box, door face, and handle are visible +common reject conditions: black frame, near-all-white/flat frame, target too tiny, severe clipping, wrong dimensions/channels, reused identical frame across different tasks +``` + +- [ ] **Step 3: Write evidence manifest** + +Create `docs/labutopia_lab_poc/evidence_manifests/render_layout_closure_YYYYMMDD_HHMMSS.json`: + +```json +{ + "run_id": "labutopia_render_closure_YYYYMMDD_HHMMSS", + "commit": "git-commit-sha", + "direct_render": false, + "official_baseline_execution": false, + "task_render_accepted": true, + "camera_config": "configs/cameras/labutopia_franka_poc.yml", + "asset_root": "/cpfs/shared/simulation/zhuzihou/dev/_datasets/EBench-Assets-Overlay/labutopia_level1_poc/assets", + "images": { + "level1_pick": { + "source_frame": "saved/eval_results/ebench//.../level1_pick/.../camera2/00000.png", + "report_image": "docs/records/evidence/2026-06-22-labutopia-ebench-weekly-report/assets/labutopia-franka-level1-pick.jpg", + "sha256": "sha256", + "visual_qa": "PASS" + }, + "level1_place": { + "source_frame": "saved/eval_results/ebench//.../level1_place/.../camera2/00000.png", + "report_image": "docs/records/evidence/2026-06-22-labutopia-ebench-weekly-report/assets/labutopia-franka-level1-place.jpg", + "sha256": "sha256", + "visual_qa": "PASS" + }, + "level1_open_door": { + "source_frame": "saved/eval_results/ebench//.../level1_open_door/.../camera2/00000.png", + "report_image": "docs/records/evidence/2026-06-22-labutopia-ebench-weekly-report/assets/labutopia-franka-level1-open-door.jpg", + "sha256": "sha256", + "visual_qa": "PASS" + } + } +} +``` + +Reject the manifest if any image has `direct_render=true`, missing `source_frame`, missing `sha256`, or `visual_qa` not equal to `PASS`. + +- [ ] **Step 4: Replace report images only after visual QA passes** + +Replace: + +```text +docs/records/evidence/2026-06-22-labutopia-ebench-weekly-report/assets/labutopia-franka-level1-pick.jpg +docs/records/evidence/2026-06-22-labutopia-ebench-weekly-report/assets/labutopia-franka-level1-place.jpg +docs/records/evidence/2026-06-22-labutopia-ebench-weekly-report/assets/labutopia-franka-level1-open-door.jpg +``` + +Do not replace them with direct-render images unless the report clearly labels them as non-eval-path diagnostic images. + +- [ ] **Step 5: Update PM report wording** + +Allowed wording after the gate passes: + +```text +三任务已有可复现的 eval-path reset 渲染证据,能看到各自任务关键对象;这证明渲染/布局闭环,不代表策略求解成功,也不代表官方 Lift2 baseline 成绩。 +``` + +- [ ] **Step 6: Verify docs and tests** + +Run: + +```bash +git diff --check +python -m pytest tests/labutopia_poc -q +python standalone_tools/labutopia_poc/validate_task_package.py +python - <<'PY' +from pathlib import Path +html = Path('docs/records/evidence/2026-06-22-labutopia-ebench-weekly-report/index.html').read_text(encoding='utf-8') +for text in [ + '当前三张渲染图未通过视觉验收', + 'render_visual_investigation_20260623.md', + '2026-06-23-labutopia-ebench-render-layout-closure.md', +]: + assert text in html, text +print('HTML evidence links OK') +PY +``` + +Expected: + +```text +no whitespace errors +tests pass +validator OK +HTML evidence links OK +``` + +## Confusion Avoidance + +Use these conventions for every new run: + +```text +port: 18091 or above, never 8087 +run_id prefix: labutopia_render_diag_ or labutopia_render_closure_ +output root: saved/diagnostics/ +report title: LabUtopia render/layout closure, not EOS +``` + +Before and after any Isaac run: + +```bash +ps -eo pid,ppid,cmd | rg '18091|labutopia_render_diag|labutopia_render_closure|SimulationApp|kit/kit|ray' || true +``` + +Never delete or stop the existing EOS/other-engineer process on port `8087`. From 9e60237cd088c279b64f1ab0e1d3c14f911d09cd Mon Sep 17 00:00:00 2001 From: Codex Date: Tue, 23 Jun 2026 15:29:39 +0800 Subject: [PATCH 15/30] docs: record LabUtopia render readback diagnostics --- docs/index.html | 5 +- .../render_diagnostics_20260623.json | 94 ++++ docs/labutopia_lab_poc/franka_render_smoke.md | 28 +- .../render_visual_investigation_20260623.md | 81 ++- ...26-06-22-labutopia-ebench-weekly-report.md | 24 +- .../index.html | 59 +- .../plans/2026-06-22-ebench-labutopia-poc.md | 18 +- ...-labutopia-ebench-render-layout-closure.md | 90 ++- .../capture_eval_render_diagnostics.py | 518 ++++++++++++++++++ .../test_render_diagnostics_contract.py | 34 ++ 10 files changed, 880 insertions(+), 71 deletions(-) create mode 100644 docs/labutopia_lab_poc/evidence_manifests/render_diagnostics_20260623.json create mode 100644 standalone_tools/labutopia_poc/capture_eval_render_diagnostics.py create mode 100644 tests/labutopia_poc/test_render_diagnostics_contract.py diff --git a/docs/index.html b/docs/index.html index f8976e7..98d1c36 100644 --- a/docs/index.html +++ b/docs/index.html @@ -49,10 +49,11 @@

    GenManip LabUtopia Reports

    Zero-build static report surface for LabUtopia x EBench integration evidence.

    LabUtopia x EBench 接入周报 · 2026-06-22 -

    Franka POC end-to-end smoke has completed; Lift2 official baseline readiness remains behind P0-P4 evidence gates.

    +

    Franka POC end-to-end smoke has completed; render readback/layout closure and Lift2 official baseline readiness remain behind evidence gates.

    3/3 episodes complete - 23 passed, 1 skipped + 25 passed, 1 skipped + readback black before recorder Franka POC only GitHub Pages ready
    diff --git a/docs/labutopia_lab_poc/evidence_manifests/render_diagnostics_20260623.json b/docs/labutopia_lab_poc/evidence_manifests/render_diagnostics_20260623.json new file mode 100644 index 0000000..0be6666 --- /dev/null +++ b/docs/labutopia_lab_poc/evidence_manifests/render_diagnostics_20260623.json @@ -0,0 +1,94 @@ +{ + "date": "2026-06-23", + "commit": "2517e8e625e2ac2b68d5d4f815a7ede04337cf8b", + "direct_render": false, + "official_baseline_execution": false, + "task_render_accepted": false, + "official_baseline_evaluable": false, + "summary": "Three LabUtopia Franka POC eval-path diagnostics prove camera2 RGB is already black immediately after get_eval_camera_data(), before EpisodeRecorder writes PNG files.", + "environment": { + "conda_env": "/cpfs/shared/simulation/zhuzihou/dev/conda-managed/envs/embodied-eval-os-sim-isaacsim41-genmanip-py310", + "config": "ebench/labutopia_lab_poc/franka_poc", + "port_label": 18091, + "asset_root": "/cpfs/shared/simulation/zhuzihou/dev/_datasets/EBench-Assets-Overlay/labutopia_level1_poc/assets", + "camera_config": "configs/cameras/labutopia_franka_poc.yml" + }, + "diagnostics": { + "level1_pick": { + "run_id": "labutopia_render_diag_pick_20260623_070712", + "diagnostics_json": "saved/diagnostics/labutopia_render_diag_pick_20260623_070712/level1_pick/diagnostics.json", + "boundary_classification": "readback_black_before_recorder", + "readback_frame": "saved/diagnostics/labutopia_render_diag_pick_20260623_070712/level1_pick/readback_after_get_eval_camera_data/camera2/00000.png", + "recorder_frame": "saved/diagnostics/labutopia_render_diag_pick_20260623_070712/level1_pick/recorder_png/camera2/00000.png", + "sha256": "3f4d71b63cd17da7aeda885c59160ea2e69ed17cfc27915ffaffe7f874e1b95c", + "channel_max": [0.0, 0.0, 0.0], + "nonzero_pixels": 0, + "visual_qa": "FAIL" + }, + "level1_place": { + "run_id": "labutopia_render_diag_level1_place_20260623_070855", + "diagnostics_json": "saved/diagnostics/labutopia_render_diag_level1_place_20260623_070855/level1_place/diagnostics.json", + "run_log": "saved/diagnostics/labutopia_render_diag_level1_place_20260623_070855/level1_place/run.log", + "boundary_classification": "readback_black_before_recorder", + "readback_frame": "saved/diagnostics/labutopia_render_diag_level1_place_20260623_070855/level1_place/readback_after_get_eval_camera_data/camera2/00000.png", + "recorder_frame": "saved/diagnostics/labutopia_render_diag_level1_place_20260623_070855/level1_place/recorder_png/camera2/00000.png", + "sha256": "3f4d71b63cd17da7aeda885c59160ea2e69ed17cfc27915ffaffe7f874e1b95c", + "channel_max": [0.0, 0.0, 0.0], + "nonzero_pixels": 0, + "visual_qa": "FAIL" + }, + "level1_open_door": { + "run_id": "labutopia_render_diag_level1_open_door_20260623_070933", + "diagnostics_json": "saved/diagnostics/labutopia_render_diag_level1_open_door_20260623_070933/level1_open_door/diagnostics.json", + "run_log": "saved/diagnostics/labutopia_render_diag_level1_open_door_20260623_070933/level1_open_door/run.log", + "boundary_classification": "readback_black_before_recorder", + "readback_frame": "saved/diagnostics/labutopia_render_diag_level1_open_door_20260623_070933/level1_open_door/readback_after_get_eval_camera_data/camera2/00000.png", + "recorder_frame": "saved/diagnostics/labutopia_render_diag_level1_open_door_20260623_070933/level1_open_door/recorder_png/camera2/00000.png", + "sha256": "3f4d71b63cd17da7aeda885c59160ea2e69ed17cfc27915ffaffe7f874e1b95c", + "channel_max": [0.0, 0.0, 0.0], + "nonzero_pixels": 0, + "visual_qa": "FAIL" + } + }, + "camera2": { + "prim_path": "/Camera/LabUtopiaCamera2", + "render_product_path": "/Render/RenderProduct_Replicator_01", + "render_product_camera_relationship_target": "/Camera/LabUtopiaCamera2", + "world_position": [0.10000000149011612, 0.0, 2.5], + "world_orientation": [-0.7071067811865475, -3.061617183860266e-17, 3.061617183860266e-17, 0.7071067811865476] + }, + "layout_red_flags": { + "obj_conical_bottle02": { + "world_position": [10.23592472076416, 0.12754562497138977, 0.285481333732605], + "local_scale": [0.00009999999747378752, 0.00009999999747378752, 0.00009999999747378752] + }, + "obj_beaker2": { + "world_position": [9.747695922851562, 0.6008090972900391, 0.07486330717802048], + "local_scale": [1.0, 1.0, 1.0] + }, + "obj_target_plat": { + "world_position": [8.589698791503906, 0.0, 0.30000001192092896], + "local_scale": [0.10000000149011612, 0.10000000149011612, 0.00009999999747378752] + }, + "obj_DryingBox_01": { + "world_position": [45.883541107177734, 1.9118263721466064, 0.0000010597782420518342], + "local_scale": [0.0010000000474974513, 0.0010000000474974513, 0.0010000000474974513] + }, + "obj_DryingBox_01_handle": { + "world_position": [-148.7632293701172, -294.39349365234375, 328.591552734375], + "local_scale": [1.0, 1.0, 1.0] + } + }, + "claim_boundary": { + "allowed": [ + "LabUtopia Franka POC wiring smoke completes and writes result files.", + "Runtime diagnostics prove camera2 black frames are upstream of EpisodeRecorder writing." + ], + "blocked": [ + "PM-ready task render evidence.", + "Eval video readiness.", + "Reset-time task layout acceptance.", + "Official Lift2 baseline evaluability or score." + ] + } +} diff --git a/docs/labutopia_lab_poc/franka_render_smoke.md b/docs/labutopia_lab_poc/franka_render_smoke.md index e51eb96..0d5bf8e 100644 --- a/docs/labutopia_lab_poc/franka_render_smoke.md +++ b/docs/labutopia_lab_poc/franka_render_smoke.md @@ -60,6 +60,30 @@ Observed implication: eval recorder camera2: not usable for PM visual evidence yet ``` +Follow-up eval-path diagnostics on 2026-06-23 narrowed the failure boundary: + +```text +level1_pick: readback_black_before_recorder +level1_place: readback_black_before_recorder +level1_open_door: readback_black_before_recorder +``` + +That means the frame is already pure black immediately after `get_eval_camera_data()` and before `EpisodeRecorder` writes PNG files. Recorder writing is therefore ruled out as the primary black-frame source. + +Diagnostic runs: + +```text +saved/diagnostics/labutopia_render_diag_pick_20260623_070712/level1_pick/diagnostics.json +saved/diagnostics/labutopia_render_diag_level1_place_20260623_070855/level1_place/diagnostics.json +saved/diagnostics/labutopia_render_diag_level1_open_door_20260623_070933/level1_open_door/diagnostics.json +``` + +Evidence manifest: + +```text +docs/labutopia_lab_poc/evidence_manifests/render_diagnostics_20260623.json +``` + The initial weekly report used static direct-render screenshots from the same EBench/GenManip-loaded LabUtopia stage and the same overlay asset root. Follow-up visual QA on 2026-06-23 found those screenshots are not acceptable as task-scene evidence. The direct render changed report lighting and camera viewpoint, and it did not prove task configuration, evaluator logic, result scores, or reset-time visual correctness. ## Report images @@ -80,9 +104,11 @@ Follow-up visual QA: ## Open risks -- Fix eval recorder `camera2` black frames and prove camera readback before recorder writing. +- Fix eval recorder `camera2` black frames at the source. Readback is now proven black before recorder writing. - Close the gap between visible mesh bbox, wrapper pose, and task semantic coordinates. - Make LabUtopia reset-time task layout explicit instead of relying on fallback live-scene metadata. +- Fix LabUtopia asset import/layout red flags: selected objects are still in source-lab coordinates, and the open-door handle is imported as an invalid independent child transform. +- Add deterministic lighting to the runtime overlay before accepting any render evidence. - Capture reset-time keyframes for all three tasks through the normal eval recording path. - Replace the current report images only after independent visual QA passes. - Keep official Lift2 baseline claims blocked until Lift2 composite assets and official runner discovery are complete. diff --git a/docs/labutopia_lab_poc/render_visual_investigation_20260623.md b/docs/labutopia_lab_poc/render_visual_investigation_20260623.md index 5586c84..dc14461 100644 --- a/docs/labutopia_lab_poc/render_visual_investigation_20260623.md +++ b/docs/labutopia_lab_poc/render_visual_investigation_20260623.md @@ -77,6 +77,55 @@ saved/eval_results/ebench/labutopia_franka_render_smoke_20260622_150819/ebench/l saved/eval_results/ebench/labutopia_franka_render_smoke_20260622_150819/ebench/labutopia_lab_poc/franka_poc/level1_open_door/000/*/camera2/00000.png ``` +Runtime diagnostics added on 2026-06-23: + +| Task | Run ID | Boundary | Readback stats | Recorder stats | +| --- | --- | --- | --- | --- | +| `level1_pick` | `labutopia_render_diag_pick_20260623_070712` | `readback_black_before_recorder` | `channel_max=[0,0,0]`, `nonzero=0` | `channel_max=[0,0,0]`, `nonzero=0` | +| `level1_place` | `labutopia_render_diag_level1_place_20260623_070855` | `readback_black_before_recorder` | `channel_max=[0,0,0]`, `nonzero=0` | `channel_max=[0,0,0]`, `nonzero=0` | +| `level1_open_door` | `labutopia_render_diag_level1_open_door_20260623_070933` | `readback_black_before_recorder` | `channel_max=[0,0,0]`, `nonzero=0` | `channel_max=[0,0,0]`, `nonzero=0` | + +Diagnostic paths: + +```text +saved/diagnostics/labutopia_render_diag_pick_20260623_070712/level1_pick/diagnostics.json +saved/diagnostics/labutopia_render_diag_level1_place_20260623_070855/level1_place/diagnostics.json +saved/diagnostics/labutopia_render_diag_level1_open_door_20260623_070933/level1_open_door/diagnostics.json +docs/labutopia_lab_poc/evidence_manifests/render_diagnostics_20260623.json +``` + +Independent visual review of the three diagnostic readback images returned FAIL for all three because each image is a uniform black frame. + +Recorder boundary conclusion: + +```text +EpisodeRecorder is not the primary black-frame source. The RGB array is already black immediately after get_eval_camera_data(), and the same raw frame is passed into EpisodeRecorder.record_obs(). +``` + +Camera/render evidence: + +```text +camera2 prim_path: /Camera/LabUtopiaCamera2 +camera2 render_product_path: /Render/RenderProduct_Replicator_01 +render product camera relationship target: /Camera/LabUtopiaCamera2 +camera2 position: [0.1, 0.0, 2.5] +camera2 orientation in diagnostics: [-0.7071, ~0, ~0, 0.7071] +``` + +The render product binding appears valid, so the most likely immediate camera-side problem is camera pose/axes or lighting/readback timing, not recorder file writing. + +Layout evidence: + +```text +obj_conical_bottle02 position ~= [10.236, 0.128, 0.285], scale ~= [1e-4, 1e-4, 1e-4] +obj_beaker2 position ~= [9.748, 0.601, 0.075] +obj_target_plat position ~= [8.590, 0.000, 0.300], scale z ~= 1e-4 +obj_DryingBox_01 position ~= [45.884, 1.912, 0.000001], scale ~= [0.001, 0.001, 0.001] +obj_DryingBox_01_handle position ~= [-148.763, -294.393, 328.592] +``` + +This means the asset import/layout is also faulty. A camera-only fix may make a frame non-black, but it would not make the Franka task or official Lift2 baseline evaluable because task objects are not normalized into a valid robot workspace. + Relevant configuration: ```text @@ -92,18 +141,23 @@ genmanip/utils/loader/scene.py ## Ranked Root-Cause Hypotheses -1. Eval recorder camera path is broken before writing. - - The saved PNGs are pure black, so the issue is upstream of report rendering and likely upstream of image file writing. - - Candidate causes: camera pose points at empty/dark view, render product not bound to the intended Isaac camera prim, missing lighting in the normal eval path, or camera readback before valid rendering. -2. Task layout is not yet a real LabUtopia reset layout. +1. Asset import/layout is not valid for GenManip/Franka task execution. + - The selected objects remain in source LabUtopia lab coordinates, far from the Franka/table workspace. + - The open-door handle is imported as a direct child payload and appears to lose the parent drying-box transform/scale. + - This is a baseline-evaluability blocker, not just a visual defect. +2. Eval camera readback is black before recorder writing. + - The diagnostics prove the black frame exists immediately after `get_eval_camera_data()`. + - Candidate causes: camera axes/pose points at empty/dark view, missing deterministic lighting, or readback before valid render accumulation. + - Render product binding to `/Camera/LabUtopiaCamera2` appears valid in diagnostics. +3. Task layout is not yet a real LabUtopia reset layout. - LabUtopia source position ranges are in `task_semantics.yml`, but the runtime task YAMLs have empty `object_config` and null layout. - The fallback metadata captures the live loaded scene rather than sampling or enforcing LabUtopia task-specific reset poses. -3. Direct-render report screenshots used non-eval camera choices. +4. Direct-render report screenshots used non-eval camera choices. - The report images were captured with changed lighting and viewpoint and no committed producer script. - This makes them unsuitable for proving task-scene correctness. -4. Overlay wrapper and task semantics are not yet aligned enough for visual or metric claims. +5. Overlay wrapper and task semantics are not yet aligned enough for visual or metric claims. - The wrapper payload exposes prims under GenManip-friendly names, but visible mesh bbox, wrapper pose, and semantic coordinates still need a measured consistency check. -5. The `open_door` camera is using a shared POC view instead of the source task's task-specific view. +6. The `open_door` camera is using a shared POC view instead of the source task's task-specific view. - Original LabUtopia `level1_open_door.yaml` uses different camera positions from the shared POC camera config. ## Claim Boundary @@ -128,12 +182,13 @@ The current screenshots prove task correctness. Run these in an isolated port/run_id so EOS or another engineer's run is not confused with this work: -1. Dump `camera1` and `camera2` world poses, render product paths, RGB min/max/mean/nonzero stats immediately after `get_eval_camera_data()`. -2. Save one raw camera frame before `EpisodeRecorder.record_obs()` to isolate camera readback from recorder output. -3. Change only `camera2` pose to a known direct-render viewpoint, then only lighting, to separate pose failure from lighting/readback failure. -4. Dump `scene.object_list.keys()`, wrapper world poses, object extents/bbox centers, and target object projections into `camera2`. -5. Add a reproducible render/capture script that writes all diagnostics and images under a unique `run_id`. -6. Write an evidence manifest for any future PM image. It must include `run_id`, task name, source eval frame path, report image path, sha256, camera config, asset root, commit hash, and `direct_render=false`. +1. Fix/validate camera axes handling in the free-camera loader path, then rerun diagnostics with only that variable changed. +2. Add deterministic lighting to the runtime overlay, then rerun diagnostics with only lighting changed. +3. Rebuild or supplement the overlay so nested LabUtopia parts preserve composed transforms, especially `DryingBox_01/handle`. +4. Normalize selected task objects into the GenManip/Franka workspace and record explicit task reset layouts instead of relying on fallback live-scene metadata. +5. Add static USD validation for object centers, bounds, scales, nested-part transform preservation, and at least one light. +6. After layout and camera/lighting are fixed, regenerate all three eval-path reset frames and run independent visual QA. +7. Write an evidence manifest for any future PM image. It must include `run_id`, task name, source eval frame path, report image path, sha256, camera config, asset root, commit hash, and `direct_render=false`. ## Documentation Decision diff --git a/docs/records/2026-06-22-labutopia-ebench-weekly-report.md b/docs/records/2026-06-22-labutopia-ebench-weekly-report.md index 951e328..58ea76f 100644 --- a/docs/records/2026-06-22-labutopia-ebench-weekly-report.md +++ b/docs/records/2026-06-22-labutopia-ebench-weekly-report.md @@ -110,6 +110,8 @@ docs/records/evidence/2026-06-22-labutopia-ebench-weekly-report/assets/labutopia - `run_id=labutopia_franka_render_smoke_20260622_150819` 跑过保存帧 smoke,但 eval recorder 的 `camera2` 帧当前是黑屏。 - 三个任务各 32 帧的 eval `camera2` 抽样统计均为纯黑:RGB min/max/mean 都是 0。 +- 2026-06-23 新增 runtime 诊断后,三个任务都定位为 `readback_black_before_recorder`:黑帧在 `get_eval_camera_data()` 后就已经存在,recorder 写盘不是根因。 +- 进一步检查发现资产/layout 也有硬问题:瓶子/烧杯/托盘仍在源 LabUtopia 坐标的 x≈8-10,干燥箱在 x≈45,把手甚至出现在百米量级坐标。这说明不能只调相机,必须同步修复导入布局。 - 当前 task YAML 仍是 `object_config: {}`、`layout_config.type: null`,还没有把 LabUtopia 原始 position_range 真正落成 GenManip reset-time 布局。 - 因此下一步必须先修复 eval camera/readback、任务布局和可复现截图脚本,再做 reset 后关键帧验收。 @@ -120,9 +122,9 @@ docs/records/evidence/2026-06-22-labutopia-ebench-weekly-report/assets/labutopia | 资产根目录不对 | 系统一开始会去错误目录找 LabUtopia 场景 | 增加 LabUtopia manifest 识别逻辑,运行时切换到 overlay asset root | 已解决 | | 缺少 `meta_info.pkl` | 传统 GenManip 任务依赖采集包里的元信息,LabUtopia POC 没有这份文件 | 针对 LabUtopia POC,从实时场景里自动生成最小可用元信息 | 已解决 | | camera cleanup 字段缺失 | 切换任务时 camera 清理报字段错误 | 补齐 camera 配置里的 cleanup flags | 已解决 | -| eval recorder `camera2` 黑屏 | 保存过程帧时 `camera2` 输出纯黑 | 已确认为 P0 阻塞;直渲截图不能替代 eval-path 证据 | 待修复 | +| eval recorder `camera2` 黑屏 | 保存过程帧时 `camera2` 输出纯黑 | 已定位到 `readback_black_before_recorder`;recorder 写盘被排除,下一步修 camera axes/pose、lighting 和 readback 源头 | 待修复 | | 当前三张渲染图未通过视觉验收 | `pick` 目标不清,`place` 缺 beaker,`open_door` 缺门/把手/开门状态 | 降级为历史失败样例,新增调研记录和 P0-P2 修复计划 | 待重拍 | -| 可见 mesh 与 wrapper pose 未完全对齐 | 部分对象 bbox、wrapper pose、任务语义坐标存在偏移 | 需要输出 object pose/bbox/投影诊断,再决定布局修复 | 待闭环 | +| 资产导入/layout 坐标不闭环 | 任务物体仍在源 lab 坐标,open-door handle 导入后坐标异常放大 | 需要重建或补充 overlay,保留 nested part composed transform,并把任务物体归一到机器人工作空间 | 待闭环 | | 最终进度不 complete | 任务已跑完,但进度没有写入,导致客户端最后等待超时 | 任务结束时写最小 `result_info.json`,并修复进度统计 | 已解决 | | 后处理异常可能被误记为完成 | 如果后处理报错,不能用 0 分兜底伪装成成功 | 增加 fail-fast 逻辑和回归测试 | 已解决 | | 端口/结果混淆风险 | EOS 侧也有人在跑任务,担心互相影响 | 使用独立 worktree、独立 run_id、独立端口 18088,并复核 8087 状态 | 已解决 | @@ -153,7 +155,10 @@ total_episodes=3 ```text python -m pytest tests/labutopia_poc -q -23 passed, 1 skipped +25 passed, 1 skipped + +python -m pytest tests/labutopia_poc/test_render_diagnostics_contract.py -q +2 passed python standalone_tools/labutopia_poc/validate_task_package.py LabUtopia task package validation OK @@ -172,6 +177,8 @@ saved/eval_results/ebench/labutopia_franka_smoke_clean8_20260622_100208/.../leve - `run_id=labutopia_franka_render_smoke_20260622_150819` - eval recorder `camera2` frames: black, sampled RGB min/max/mean all 0 +- runtime diagnostics: `level1_pick/place/open_door` all `readback_black_before_recorder` +- evidence manifest: [docs/labutopia_lab_poc/evidence_manifests/render_diagnostics_20260623.json](../labutopia_lab_poc/evidence_manifests/render_diagnostics_20260623.json) - static direct-render evidence: visual QA failed on 2026-06-23 - investigation: [docs/labutopia_lab_poc/render_visual_investigation_20260623.md](../labutopia_lab_poc/render_visual_investigation_20260623.md) - plan: [docs/superpowers/plans/2026-06-23-labutopia-ebench-render-layout-closure.md](../superpowers/plans/2026-06-23-labutopia-ebench-render-layout-closure.md) @@ -182,11 +189,12 @@ saved/eval_results/ebench/labutopia_franka_smoke_clean8_20260622_100208/.../leve 建议顺序: -1. 按 `2026-06-23-labutopia-ebench-render-layout-closure.md` 执行 P0:定位 eval recorder `camera2` 黑屏是 pose、lighting、render product 还是 readback 问题。 -2. 执行 P1:把 LabUtopia task position_range、必看对象和任务特定相机要求落成可验证的 reset-time 布局。 -3. 执行 P2:用正常 eval-path 重新抓三任务关键帧,并通过独立视觉 QA。 -4. 图像验收通过后再替换 HTML 里的三张图,并恢复 PM 可见性汇报。 -5. 再进入 Lift2 复合资产预检和官方 baseline 路线。 +1. P0a:修相机 axes/pose 支持,让 `camera2` 从 eval readback 直接产出非黑帧。 +2. P0b:补 deterministic lighting,避免只靠 direct-render 临时灯光。 +3. P1:重建或补充 LabUtopia overlay,把任务对象和 nested handle 保持正确 transform,并放到 Franka 工作空间。 +4. P2:用正常 eval-path 重新抓三任务关键帧,写 evidence manifest,并通过独立视觉 QA。 +5. P3:图像验收通过后再替换 HTML 里的三张图,并恢复 PM 可见性汇报。 +6. P4:再进入 Lift2 复合资产预检和官方 baseline 路线。 ## 新增调研和计划文档 diff --git a/docs/records/evidence/2026-06-22-labutopia-ebench-weekly-report/index.html b/docs/records/evidence/2026-06-22-labutopia-ebench-weekly-report/index.html index f165a28..54959ea 100644 --- a/docs/records/evidence/2026-06-22-labutopia-ebench-weekly-report/index.html +++ b/docs/records/evidence/2026-06-22-labutopia-ebench-weekly-report/index.html @@ -402,7 +402,7 @@

    LabUtopia x EBench接入周报

    @@ -428,13 +428,13 @@

    当前进展总览

    测试
    -
    23 passed
    -
    1 skipped,validator 通过。
    +
    25 passed
    +
    1 skipped,诊断合同 2 passed,validator 通过。
    渲染证据
    未通过
    -
    三张直渲图视觉 QA 不合格;eval recorder 的 camera2 仍为黑屏。
    +
    三张直渲图视觉 QA 不合格;camera2 已定位为 readback 后、recorder 前黑屏。
    官方 Lift2 baseline
    @@ -564,7 +564,7 @@

    已经闭环的部分

    还没闭环的部分

    -

    Lift2 官方 baseline 不能只用 scene overlay,它还需要默认资产里的 robot_usds/lift2miscs/curobo。这一步排在 P0-P2 渲染/布局闭环之后,再做“复合资产根目录”。

    +

    Lift2 官方 baseline 不能只用 scene overlay,它还需要默认资产里的 robot_usds/lift2miscs/curobo。这一步排在 camera readback、资产/layout 和 P2 渲染证据闭环之后,再做“复合资产根目录”。

    @@ -574,7 +574,7 @@

    EBench 加载后的三任务渲染图

    2026-06-23 复核结论:当前三张渲染图未通过视觉验收,不能作为任务渲染已闭环的证据。下面保留为历史失败样例,方便说明 P0 修复范围。

    渲染证据状态

    -

    自动 eval recorder 保存的 camera2 帧为纯黑;三张 direct-render 静态图不是正常 eval recorder 产物,也没有可复现生产脚本。视觉 QA 结果为 level1_pick=WARNlevel1_place=FAILlevel1_open_door=FAIL

    +

    自动 eval recorder 保存的 camera2 帧为纯黑;2026-06-23 runtime 诊断已确认三个任务都是 readback_black_before_recorder,recorder 写盘不是根因。三张 direct-render 静态图不是正常 eval recorder 产物,也没有可复现生产脚本。视觉 QA 结果为 level1_pick=WARNlevel1_place=FAILlevel1_open_door=FAIL

    @@ -622,7 +622,12 @@

    验证证据

    render smoke labutopia_franka_render_smoke_20260622_150819 - 同一任务包跑过保存帧 smoke;eval recorder 的 camera2 帧为纯黑,静态直渲图已被复核为视觉证据不合格。 + 同一任务包跑过保存帧 smoke;eval recorder 的 camera2 帧为纯黑,后续 runtime 诊断确认黑帧发生在 get_eval_camera_data() 后、recorder 写盘前。 + + + render diagnostics + readback_black_before_recorder + 三个任务均为 readback 后全黑:channel_max=[0,0,0]nonzero=0;manifest 已记录。 visual QA @@ -631,8 +636,13 @@

    验证证据

    pytest - 23 passed, 1 skipped - 覆盖资产 override、fallback metadata、结果落盘、后处理 fail-fast 等回归点。 + 25 passed, 1 skipped + 覆盖资产 override、fallback metadata、结果落盘、后处理 fail-fast 和 render diagnostic 合同等回归点。 + + + diagnostic contract + 2 passed + 纯 Python 帧统计和黑帧分类接口不依赖 Isaac,可快速回归。 package validator @@ -683,7 +693,7 @@

    问题和处理结果

    eval recorder camera2 黑屏 保存过程帧时 camera2 输出为黑屏。 - 确认三个任务各 32 帧抽样均为纯黑;直渲截图不能替代 eval-path 证据。 + 已定位为 readback_black_before_recorder;recorder 写盘被排除,下一步修 camera axes/pose、lighting 和 readback 源头。 P0 阻塞 @@ -693,9 +703,9 @@

    问题和处理结果

    待重拍 - 可见 mesh 与 wrapper pose 未完全对齐 - 部分对象 bbox、wrapper pose、任务语义坐标存在偏移,影响截图和后续可评性判断。 - 报告中降级为“历史失败样例”,不宣称任务布局或资产可见性验收完成。 + 资产导入/layout 坐标不闭环 + 任务物体仍在源 lab 坐标,open-door handle 导入后坐标异常放大。 + 需要重建或补充 overlay,保留 nested part composed transform,并把任务物体归一到机器人工作空间。 待闭环 @@ -725,6 +735,7 @@

    现在可以说

  • 三个 level-1 任务能完成 reset/step/finalize,并生成 per-task 和 final result。
  • Franka POC 的 LabUtopia scene overlay 加载路径已经验证。
  • 当前三张渲染图已经完成复核,并明确判定为不合格历史样例。
  • +
  • 黑帧边界已经定位到 camera readback 之后、recorder 写盘之前。
  • @@ -732,7 +743,8 @@

    现在不能说

    • 不能说任务已经求解成功,当前默认动作得分是 0.0
    • 不能说官方 Lift2 baseline 已经可评,Lift2 复合资产和官方 runner 还没闭环。
    • -
    • 不能说画面/视频/任务布局已经验收;当前三张图视觉 QA 未通过,eval camera2 仍是黑屏。
    • +
    • 不能说画面/视频/任务布局已经验收;当前三张图视觉 QA 未通过,eval camera2 的 readback 仍是黑屏。
    • +
    • 不能说资产导入布局已闭环;当前对象坐标和 open-door handle transform 仍需修复。
    @@ -743,13 +755,13 @@

    接下来几步

    P0 -

    Camera 黑屏诊断

    -

    定位 eval recorder camera2 黑屏是 pose、lighting、render product 绑定还是 readback 问题,并输出 object pose/bbox/projection 诊断。

    +

    Camera readback 源头修复

    +

    已定位为 readback_black_before_recorder;下一步逐项修 camera axes/pose、deterministic lighting 和 readback 源头,直到 eval readback 非黑。

    P1 -

    Reset 布局显式化

    -

    把 task position_range、必看对象和任务特定相机要求落成可验证的 reset-time 布局,先让三任务关键对象进入画面。

    +

    资产/layout 正规化

    +

    重建或补充 LabUtopia overlay,保留 nested part transform,把任务对象归一到 Franka 工作空间,并用静态 USD 校验 object center/bounds/scale/light。

    P2 @@ -759,7 +771,7 @@

    Eval-path 重拍验收

    P3

    Lift2 资产预检

    -

    渲染/布局证据通过后,再构建复合资产根目录:LabUtopia scene overlay + 默认 robot_usds/lift2 + 默认 miscs/curobo

    +

    camera readback、资产/layout 和三图视觉 QA 通过后,再构建复合资产根目录:LabUtopia scene overlay + 默认 robot_usds/lift2 + 默认 miscs/curobo

    P4 @@ -803,14 +815,19 @@

    相关文档

    Franka render smoke 记录 - 三任务失败渲染样例、camera2 黑屏和当前风险边界。 + 三任务失败渲染样例、camera2 黑屏和 readback 边界。 打开 Render visual investigation - 6/23 多角度复核:当前三张图未通过视觉验收,camera2 为纯黑。 + 6/23 多角度复核:当前三张图未通过视觉验收,camera2 为 readback 后、recorder 前黑屏。 打开 + + Render diagnostics manifest + 三任务 runtime 诊断、帧 hash、claim boundary 和 layout red flags。 + 打开 + Render/layout closure plan P0-P2 修复计划:camera 诊断、reset 布局、eval-path 重拍和视觉 QA。 diff --git a/docs/superpowers/plans/2026-06-22-ebench-labutopia-poc.md b/docs/superpowers/plans/2026-06-22-ebench-labutopia-poc.md index 9a0da64..08d13d0 100644 --- a/docs/superpowers/plans/2026-06-22-ebench-labutopia-poc.md +++ b/docs/superpowers/plans/2026-06-22-ebench-labutopia-poc.md @@ -17,6 +17,8 @@ This is a wiring and platform-readiness milestone, not a task-solving milestone. | Scene metadata | Ready for smoke | Missing collected-package `meta_info.pkl` is synthesized from the live LabUtopia scene for POC tasks. | | Camera cleanup | Ready for smoke | POC camera configs now include cleanup flags required by GenManip camera reset. | | Result lifecycle | Ready for smoke | Episodes that terminate before recorder finalize persist minimal `result_info.json`; real post-processing exceptions now fail fast instead of being hidden as completed results. | +| Render/readback evidence | Blocked | 2026-06-23 diagnostics show all three tasks are `readback_black_before_recorder`; recorder writing is not the root cause. | +| Asset/layout acceptance | Blocked | Selected objects remain in source LabUtopia coordinates and the open-door handle transform is invalid after direct child payload import. | | Lift2 official baseline | Not yet proven | Architecture is prepared to add/evaluate the lift2 candidate profile, but the official baseline smoke still needs to be run. | ## Evidence @@ -28,7 +30,8 @@ Latest isolated smoke: - Existing EOS/other-engineer port: `8087`, left untouched and still online after the run - Final status: `complete`, `3/3` episodes completed - Final result: all three tasks recorded `score=0.0`, `sr=0.0` -- Regression tests: `python -m pytest tests/labutopia_poc -q` -> `23 passed, 1 skipped` +- Regression tests: `python -m pytest tests/labutopia_poc -q` -> `25 passed, 1 skipped` +- Diagnostics contract: `python -m pytest tests/labutopia_poc/test_render_diagnostics_contract.py -q` -> `2 passed` - Package validator: `python standalone_tools/labutopia_poc/validate_task_package.py` -> `LabUtopia task package validation OK` Details are recorded in `docs/labutopia_lab_poc/franka_smoke.md`. @@ -46,8 +49,11 @@ Reason: it is already compatible with Isaac Sim 4.1, Ray, GenManip client, cuRob The next lane is planned in `docs/superpowers/plans/2026-06-22-ebench-labutopia-lift2-baseline-lane.md`. -1. P0 runtime asset preflight: build/verify a LabUtopia composite asset root before running lift2. Current observation: the scene overlay exists, but lift2 robot and cuRobo assets are not present under the scene-only overlay root. -2. P1 lift2 dry smoke: run `ebench/labutopia_lab_poc/lift2_candidate` on isolated port `18088`, require `complete` and `3/3` result files, keep `official_baseline_execution=false`. -3. P2 official baseline discovery: locate official EBench lift2/OpenPI runner files, retain paths and hashes, do not execute policy yet. -4. P3 official baseline local contrast: run one official-style online loop only after P2 passes, retain source terminal evidence, keep `standard_model_score=null` until a separate score-release gate. -5. Closure: write EOS-style dated records with artifact linkage, forbidden-claims, and path-leakage checks before changing PM-facing claims. +1. P0 render source fix: fix camera axes/pose handling and deterministic lighting until `camera2` readback is non-black in the normal eval path. +2. P1 asset/layout fix: rebuild or supplement the LabUtopia overlay so required objects and nested parts preserve valid transforms and sit in the robot workspace. +3. P2 evidence regeneration: rerun the three Franka tasks through eval-path capture, require visual QA PASS, and write a manifest before replacing report images. +4. P3 runtime asset preflight: build/verify a LabUtopia composite asset root before running lift2. Current observation: the scene overlay exists, but lift2 robot and cuRobo assets are not present under the scene-only overlay root. +5. P4 lift2 dry smoke: run `ebench/labutopia_lab_poc/lift2_candidate` on an isolated port, require `complete` and `3/3` result files, keep `official_baseline_execution=false`. +6. P5 official baseline discovery: locate official EBench lift2/OpenPI runner files, retain paths and hashes, do not execute policy yet. +7. P6 official baseline local contrast: run one official-style online loop only after P5 passes, retain source terminal evidence, keep `standard_model_score=null` until a separate score-release gate. +8. Closure: write EOS-style dated records with artifact linkage, forbidden-claims, and path-leakage checks before changing PM-facing claims. diff --git a/docs/superpowers/plans/2026-06-23-labutopia-ebench-render-layout-closure.md b/docs/superpowers/plans/2026-06-23-labutopia-ebench-render-layout-closure.md index bd1b844..1882f06 100644 --- a/docs/superpowers/plans/2026-06-23-labutopia-ebench-render-layout-closure.md +++ b/docs/superpowers/plans/2026-06-23-labutopia-ebench-render-layout-closure.md @@ -53,11 +53,24 @@ official policy quality claim ## P0: Camera Black-Frame Root Cause +Current status as of 2026-06-23: + +```text +Diagnostic helper and runtime capture script exist. +tests/labutopia_poc/test_render_diagnostics_contract.py: 2 passed +tests/labutopia_poc: 25 passed, 1 skipped +level1_pick: readback_black_before_recorder +level1_place: readback_black_before_recorder +level1_open_door: readback_black_before_recorder +``` + +Recorder writing is now ruled out as the primary black-frame source. P0 remains open because the source of the black readback still needs to be fixed and revalidated. + **Files:** - Create: `standalone_tools/labutopia_poc/capture_eval_render_diagnostics.py` - Create: `tests/labutopia_poc/test_render_diagnostics_contract.py` -- [ ] **Step 1: Write diagnostics contract test** +- [x] **Step 1: Write diagnostics contract test** Create `tests/labutopia_poc/test_render_diagnostics_contract.py` with this contract: @@ -98,7 +111,7 @@ def test_classify_visible_frame_as_pass(): assert classify_frame_stats(stats) == "visible_frame" ``` -- [ ] **Step 2: Run the test and confirm RED** +- [x] **Step 2: Run the test and confirm RED** Run: @@ -112,7 +125,7 @@ Expected: ImportError or missing function failure ``` -- [ ] **Step 3: Implement the pure-Python diagnostics helpers** +- [x] **Step 3: Implement the pure-Python diagnostics helpers** Create `standalone_tools/labutopia_poc/capture_eval_render_diagnostics.py` with: @@ -170,7 +183,7 @@ def classify_frame_stats(stats: dict[str, object]) -> Literal["black_frame_fail" return "visible_frame" ``` -- [ ] **Step 4: Run the test and confirm GREEN** +- [x] **Step 4: Run the test and confirm GREEN** Run: @@ -184,7 +197,7 @@ Expected: 2 passed ``` -- [ ] **Step 5: Add Isaac runtime capture mode** +- [x] **Step 5: Add Isaac runtime capture mode** Extend `capture_eval_render_diagnostics.py` with an `argparse` CLI that accepts: @@ -194,15 +207,15 @@ Extend `capture_eval_render_diagnostics.py` with an `argparse` CLI that accepts: --run-id labutopia_render_diag_YYYYMMDD_HHMMSS --port 18091 --output-dir saved/diagnostics/labutopia_render_diag_YYYYMMDD_HHMMSS ---save-one-step +--save-reset-frame ``` The CLI must write: ```text diagnostics.json -camera1/00000.png -camera2/00000.png +readback_after_get_eval_camera_data/camera2/00000.png +recorder_png/camera2/00000.png ``` `diagnostics.json` must include: @@ -226,7 +239,7 @@ camera2/00000.png } ``` -- [ ] **Step 6: Run isolated camera diagnostics** +- [x] **Step 6: Run isolated camera diagnostics** Use the conda Python: @@ -238,19 +251,19 @@ Use the conda Python: --run-id labutopia_render_diag_$(date +%Y%m%d_%H%M%S) \ --port 18091 \ --output-dir saved/diagnostics/labutopia_render_diag_pick \ - --save-one-step + --save-reset-frame ``` Expected: ```text diagnostics.json written -camera frame stats recorded for camera1 and camera2 -camera prim path, render product path, world pose, focal data, RGB stats, and nonzero count recorded immediately after get_eval_camera_data() +camera2 frame stats recorded for the normal eval path +camera prim path, render product path, world pose, RGB stats, and nonzero count recorded immediately after get_eval_camera_data() no process remains on port 18091 after completion ``` -- [ ] **Step 7: Repeat diagnostics for all tasks** +- [x] **Step 7: Repeat diagnostics for all tasks** Run the same command for: @@ -264,10 +277,46 @@ Expected: ```text Each task has camera frame stats and object pose diagnostics. -If camera2 is black, diagnostics identify whether camera1 is also black. -level1_open_door additionally records articulation names, drying-box pose, handle pose, and handle/object projection into camera2. +If camera2 is black, diagnostics classify whether the boundary is before or after recorder writing. +Normal eval removes camera1; camera1 capture, object extents, and object projections remain follow-up instrumentation. +``` + +Observed: + +```text +level1_pick: camera2 readback and recorder PNG are black, channel_max=[0,0,0], nonzero=0 +level1_place: camera2 readback and recorder PNG are black, channel_max=[0,0,0], nonzero=0 +level1_open_door: camera2 readback and recorder PNG are black, channel_max=[0,0,0], nonzero=0 ``` +Artifacts: + +```text +saved/diagnostics/labutopia_render_diag_pick_20260623_070712/level1_pick/diagnostics.json +saved/diagnostics/labutopia_render_diag_level1_place_20260623_070855/level1_place/diagnostics.json +saved/diagnostics/labutopia_render_diag_level1_open_door_20260623_070933/level1_open_door/diagnostics.json +docs/labutopia_lab_poc/evidence_manifests/render_diagnostics_20260623.json +``` + +### P0 Follow-Up: Source Fix Order + +Do the next source fixes in this order and keep one variable per run: + +1. Camera axes/pose: + - `configs/cameras/labutopia_franka_poc.yml` + - `genmanip/utils/loader/scene.py:create_camera_list` + - Add or reuse `camera_axes` handling so free cameras use the intended Isaac/USD convention. + - Acceptance: `camera2` readback is non-black in at least one controlled diagnostic run. +2. Deterministic lighting: + - Add or payload at least one light into the runtime overlay, or add explicit task-scene lighting. + - Acceptance: static USD validation confirms a light exists; eval readback is not dependent on report-only direct-render lighting. +3. Asset/layout normalization: + - Rebuild the overlay or add explicit local task assets so required objects are in the robot workspace and nested parts preserve composed transforms. + - Acceptance: object centers/scales/bounds pass static validation before Isaac runtime capture. +4. Eval-path regeneration: + - Rerun the three diagnostics and independent visual QA. + - Acceptance: all three task frames are non-black and show task-relevant objects. + ## P1: Reset-Time Task Layout Closure **Files:** @@ -355,14 +404,15 @@ Add to `level1_open_door.yml`: task_visual_goal: door_and_handle_visible ``` -- [ ] **Step 4: Decide camera fix from P0 diagnostics** +- [ ] **Step 4: Apply source fixes from P0 diagnostics** -Use the P0 diagnostics to choose one outcome: +Use the P0 diagnostics to handle these outcomes in order: ```text -Outcome A: render product/readback broken -> fix camera creation/readback before changing poses. -Outcome B: pose/lighting broken only -> update camera pose and lighting config. -Outcome C: task-specific view needed -> create task-specific camera config for open_door. +Outcome A: camera axes/pose likely wrong -> add camera_axes support and retest readback. +Outcome B: no deterministic lights -> add runtime overlay/task lighting and retest readback. +Outcome C: asset/layout invalid -> normalize required objects and nested parts before camera tuning. +Outcome D: task-specific view needed -> create task-specific camera config for open_door. ``` Do not change all variables at once. diff --git a/standalone_tools/labutopia_poc/capture_eval_render_diagnostics.py b/standalone_tools/labutopia_poc/capture_eval_render_diagnostics.py new file mode 100644 index 0000000..97fe249 --- /dev/null +++ b/standalone_tools/labutopia_poc/capture_eval_render_diagnostics.py @@ -0,0 +1,518 @@ +#!/usr/bin/env python3 +"""Capture LabUtopia eval-path render diagnostics for one reset frame.""" + +from __future__ import annotations + +import argparse +import copy +import json +import os +import sys +from dataclasses import asdict, dataclass +from pathlib import Path +from types import SimpleNamespace +from typing import Any, Literal + + +REPO_ROOT = Path(__file__).resolve().parents[2] +if str(REPO_ROOT) not in sys.path: + sys.path.insert(0, str(REPO_ROOT)) + +FrameClassification = Literal["black_frame_fail", "visible_frame"] +BoundaryClassification = Literal[ + "no_readback_frame", + "readback_black_before_recorder", + "recorder_write_black", + "readback_visible", +] + + +@dataclass(frozen=True) +class CameraFrameStats: + camera_name: str + frame_path: str + width: int + height: int + channel_min: list[float] + channel_max: list[float] + channel_mean: list[float] + nonzero_pixels: int + + +def build_camera_frame_stats( + *, + camera_name: str, + frame_path: str, + width: int, + height: int, + channel_min: list[float], + channel_max: list[float], + channel_mean: list[float], + nonzero_pixels: int, +) -> dict[str, object]: + return asdict( + CameraFrameStats( + camera_name=camera_name, + frame_path=frame_path, + width=width, + height=height, + channel_min=channel_min, + channel_max=channel_max, + channel_mean=channel_mean, + nonzero_pixels=nonzero_pixels, + ) + ) + + +def classify_frame_stats(stats: dict[str, object]) -> FrameClassification: + channel_max = stats["channel_max"] + nonzero_pixels = int(stats["nonzero_pixels"]) + if not isinstance(channel_max, list): + raise TypeError("channel_max must be a list") + max_value = max(float(value) for value in channel_max) + if max_value <= 0.0 or nonzero_pixels == 0: + return "black_frame_fail" + return "visible_frame" + + +def classify_boundary( + readback_stats: list[dict[str, object]], + recorder_stats: list[dict[str, object]], +) -> BoundaryClassification: + if not readback_stats: + return "no_readback_frame" + if any(classify_frame_stats(stats) == "black_frame_fail" for stats in readback_stats): + return "readback_black_before_recorder" + if recorder_stats and any( + classify_frame_stats(stats) == "black_frame_fail" for stats in recorder_stats + ): + return "recorder_write_black" + return "readback_visible" + + +def _jsonable(value: Any) -> Any: + if hasattr(value, "tolist"): + return value.tolist() + if isinstance(value, Path): + return str(value) + if isinstance(value, dict): + return {str(key): _jsonable(item) for key, item in value.items()} + if isinstance(value, (list, tuple)): + return [_jsonable(item) for item in value] + return value + + +def _normalize_rgb_array(rgb: Any) -> Any: + import numpy as np + + arr = np.asarray(rgb) + if arr.ndim != 3 or arr.shape[2] < 3: + raise ValueError(f"Expected RGB/RGBA image with shape HxWx3/4, got {arr.shape}") + arr = arr[:, :, :3] + if arr.dtype == np.uint8: + return np.ascontiguousarray(arr) + if np.issubdtype(arr.dtype, np.floating): + finite = arr[np.isfinite(arr)] + max_value = float(finite.max()) if finite.size else 0.0 + min_value = float(finite.min()) if finite.size else 0.0 + if min_value >= 0.0 and max_value <= 1.0: + arr = arr * 255.0 + return np.ascontiguousarray(np.clip(arr, 0, 255).astype(np.uint8)) + + +def frame_stats_from_rgb( + *, + camera_name: str, + frame_path: str | Path, + rgb: Any, +) -> dict[str, object]: + import numpy as np + + arr = _normalize_rgb_array(rgb) + flat = arr.reshape(-1, 3) + stats = build_camera_frame_stats( + camera_name=camera_name, + frame_path=str(frame_path), + width=int(arr.shape[1]), + height=int(arr.shape[0]), + channel_min=[float(value) for value in flat.min(axis=0)], + channel_max=[float(value) for value in flat.max(axis=0)], + channel_mean=[float(value) for value in flat.mean(axis=0)], + nonzero_pixels=int(np.count_nonzero(np.any(arr != 0, axis=2))), + ) + stats["classification"] = classify_frame_stats(stats) + return stats + + +def frame_stats_from_png( + *, + camera_name: str, + frame_path: str | Path, +) -> dict[str, object]: + import cv2 + + image_bgr = cv2.imread(str(frame_path), cv2.IMREAD_COLOR) + if image_bgr is None: + raise FileNotFoundError(frame_path) + image_rgb = cv2.cvtColor(image_bgr, cv2.COLOR_BGR2RGB) + return frame_stats_from_rgb( + camera_name=camera_name, + frame_path=frame_path, + rgb=image_rgb, + ) + + +def _save_rgb_png(rgb: Any, frame_path: Path) -> None: + from genmanip.utils.standalone.frame_utils import save_image + + frame_path.parent.mkdir(parents=True, exist_ok=True) + save_image(_normalize_rgb_array(rgb), str(frame_path)) + + +def _select_eval_config( + evaluation_configs: list[dict[str, Any]], + task_name: str, +) -> dict[str, Any]: + matches = [ + cfg + for cfg in evaluation_configs + if cfg.get("task_name") == task_name + or str(cfg.get("task_name", "")).endswith(f"/{task_name}") + ] + if not matches: + available = [str(cfg.get("task_name", "")) for cfg in evaluation_configs] + raise ValueError(f"Task {task_name!r} not found. Available: {available}") + if len(matches) > 1: + raise ValueError(f"Task {task_name!r} is ambiguous: {matches}") + return copy.deepcopy(matches[0]) + + +def _load_selected_config( + *, + config_ref: str, + task_name: str, + current_dir: Path, +) -> tuple[dict[str, Any], str]: + from genmanip.core.evaluator.utils import parse_configs_and_benchmark_id + from genmanip.utils.standalone.utils import parse_eval_config + from genmanip.utils.standalone.version_utils import process_archived_config + + raw_configs, benchmark_id, _is_genmanip_package = parse_configs_and_benchmark_id( + config_ref, + str(current_dir), + ) + parsed_configs: list[dict[str, Any]] = [] + for raw_config in raw_configs: + for cfg in parse_eval_config(raw_config): + parsed_configs.append(process_archived_config(copy.deepcopy(cfg))) + return _select_eval_config(parsed_configs, task_name), benchmark_id + + +def _apply_env_vars(eval_config: dict[str, Any], default_config: dict[str, Any]) -> dict[str, str]: + applied: dict[str, str] = {} + assets_dir = str(default_config.get("ASSETS_DIR", "")) + for key, value in (eval_config.get("env_vars") or {}).items(): + if not isinstance(key, str) or not isinstance(value, str): + continue + resolved = value.replace("{ASSETS_DIR}", assets_dir) + os.environ[key] = resolved + applied[key] = resolved + return applied + + +def _camera_render_product_path(camera: Any) -> str | None: + for attr_name in ("render_product_path", "_render_product_path"): + value = getattr(camera, attr_name, None) + if value: + return str(value) + render_product = getattr(camera, "_render_product", None) + value = getattr(render_product, "path", None) + return str(value) if value else None + + +def _camera_metadata(camera: Any) -> dict[str, Any]: + metadata: dict[str, Any] = { + "prim_path": str(getattr(camera, "prim_path", "")), + "render_product_path": _camera_render_product_path(camera), + } + if hasattr(camera, "get_world_pose"): + try: + position, orientation = camera.get_world_pose() + metadata["world_position"] = _jsonable(position) + metadata["world_orientation"] = _jsonable(orientation) + except Exception as exc: # pragma: no cover - Isaac-only diagnostics. + metadata["world_pose_error"] = repr(exc) + return metadata + + +def _render_product_binding(render_product_path: str | None) -> dict[str, Any]: + if not render_product_path: + return {} + try: + import omni.usd + + stage = omni.usd.get_context().get_stage() + prim = stage.GetPrimAtPath(render_product_path) + if not prim or not prim.IsValid(): + return {"render_product_path": render_product_path, "valid": False} + relation = prim.GetRelationship("camera") + targets = [str(target) for target in relation.GetTargets()] if relation else [] + attr = prim.GetAttribute("camera") + attr_value = attr.Get() if attr and attr.IsValid() else None + return { + "render_product_path": render_product_path, + "valid": True, + "camera_relationship_targets": targets, + "camera_attribute": str(attr_value) if attr_value is not None else None, + } + except Exception as exc: # pragma: no cover - Isaac-only diagnostics. + return {"render_product_path": render_product_path, "error": repr(exc)} + + +def _named_prim_metadata(named_prims: dict[str, Any]) -> dict[str, dict[str, Any]]: + metadata: dict[str, dict[str, Any]] = {} + for name, prim in sorted(named_prims.items()): + item: dict[str, Any] = { + "prim_path": str(getattr(prim, "prim_path", "")), + } + if hasattr(prim, "get_world_pose"): + try: + position, orientation = prim.get_world_pose() + item["world_position"] = _jsonable(position) + item["world_orientation"] = _jsonable(orientation) + except Exception as exc: # pragma: no cover - Isaac-only diagnostics. + item["world_pose_error"] = repr(exc) + if hasattr(prim, "get_local_scale"): + try: + item["local_scale"] = _jsonable(prim.get_local_scale()) + except Exception as exc: # pragma: no cover - Isaac-only diagnostics. + item["local_scale_error"] = repr(exc) + if hasattr(prim, "get_joint_positions"): + try: + item["joint_positions"] = _jsonable(prim.get_joint_positions()) + except Exception as exc: # pragma: no cover - Isaac-only diagnostics. + item["joint_positions_error"] = repr(exc) + metadata[str(name)] = item + return metadata + + +def _copy_recorder_frame_stats( + *, + traj_log_dir: str | None, + output_dir: Path, +) -> list[dict[str, object]]: + if traj_log_dir is None: + return [] + recorder_stats: list[dict[str, object]] = [] + traj_path = Path(traj_log_dir) + if not traj_path.exists(): + return recorder_stats + for frame_path in sorted(traj_path.glob("*/00000.png")): + camera_name = frame_path.parent.name + copied_path = output_dir / "recorder_png" / camera_name / frame_path.name + copied_path.parent.mkdir(parents=True, exist_ok=True) + copied_path.write_bytes(frame_path.read_bytes()) + stats = frame_stats_from_png(camera_name=camera_name, frame_path=copied_path) + stats["stage"] = "recorder_png" + stats["source_frame_path"] = str(frame_path) + recorder_stats.append(stats) + return recorder_stats + + +def _write_json(path: Path, payload: dict[str, Any]) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text( + json.dumps(_jsonable(payload), indent=2, sort_keys=True) + "\n", + encoding="utf-8", + ) + + +def run_runtime_diagnostics(args: argparse.Namespace) -> dict[str, Any]: + current_dir = REPO_ROOT + output_dir = Path(args.output_dir) + output_dir.mkdir(parents=True, exist_ok=True) + + from genmanip.core.evaluator.labutopia_assets import ( + resolve_labutopia_poc_assets_override, + ) + from genmanip.utils.standalone.file_utils import load_default_config + + default_config = load_default_config( + str(current_dir), + "__None__.json", + "local" if args.local else "default", + ) + assets_override = resolve_labutopia_poc_assets_override(current_dir, args.config) + if assets_override is not None: + default_config["ASSETS_DIR"] = assets_override.overlay_root + + eval_config, benchmark_id = _load_selected_config( + config_ref=args.config, + task_name=args.task, + current_dir=current_dir, + ) + applied_env_vars = _apply_env_vars(eval_config, default_config) + + from isaacsim import SimulationApp # type: ignore + + simulation_app = SimulationApp({"headless": not args.local, "multi_gpu": False}) + + env = None + diagnostics: dict[str, Any] = { + "run_id": args.run_id, + "task": args.task, + "task_name": eval_config["task_name"], + "seed": args.seed, + "config": args.config, + "benchmark_id": benchmark_id, + "output_dir": str(output_dir), + "port": args.port, + "reset_frame_capture": True, + "assets_override": asdict(assets_override) if assets_override is not None else None, + "applied_env_vars": applied_env_vars, + "camera_frames": [], + "camera_poses": {}, + "render_products": {}, + "render_product_binding": {}, + "object_world_poses": {}, + "object_extents": {}, + "projected_object_centers": {}, + "articulation_state": {}, + "claim_boundary": { + "task_render_accepted": False, + "official_baseline_evaluable": False, + }, + } + + try: + import genmanip.core.evaluator.env as env_module + from genmanip.core.evaluator.env import IsaacEvalEnvRay + + original_get_eval_camera_data = env_module.get_eval_camera_data + readback_stats: list[dict[str, object]] = [] + + def capture_get_eval_camera_data(camera_list: dict[str, Any]) -> dict[str, Any]: + camera_data = original_get_eval_camera_data(camera_list) + for camera_name, camera in sorted(camera_list.items()): + diagnostics["camera_poses"][camera_name] = _camera_metadata(camera) + render_product_path = _camera_render_product_path(camera) + diagnostics["render_products"][camera_name] = { + "render_product_path": render_product_path, + } + diagnostics["render_product_binding"][camera_name] = ( + _render_product_binding(render_product_path) + ) + for camera_name, item in sorted(camera_data.items()): + rgb = item.get("rgb") + frame_path = output_dir / "readback_after_get_eval_camera_data" / camera_name / "00000.png" + _save_rgb_png(rgb, frame_path) + stats = frame_stats_from_rgb( + camera_name=camera_name, + frame_path=frame_path, + rgb=rgb, + ) + stats["stage"] = "readback_after_get_eval_camera_data" + stats["source"] = "genmanip.core.evaluator.env.get_eval_camera_data" + readback_stats.append(stats) + return camera_data + + env_module.get_eval_camera_data = capture_get_eval_camera_data + try: + runtime_args = SimpleNamespace( + run_id=args.run_id, + num_steps=args.num_steps, + local=args.local, + without_render=False, + save_process=True, + episode_recorder_save_every=1, + random_randomization=False, + is_relative_action=False, + ) + env = IsaacEvalEnvRay( + runtime_args, + simulation_app, + default_config, + str(current_dir), + benchmark_id=benchmark_id, + ) + env.reset(args.seed, eval_config, default_config) + scene = env.scene + if scene is not None: + diagnostics["object_world_poses"] = _named_prim_metadata( + getattr(scene, "object_list", {}) or {} + ) + diagnostics["articulation_state"] = _named_prim_metadata( + getattr(scene, "articulation_list", {}) or {} + ) + recorder_stats = _copy_recorder_frame_stats( + traj_log_dir=env.traj_log_dir, + output_dir=output_dir, + ) + finally: + env_module.get_eval_camera_data = original_get_eval_camera_data + + diagnostics["camera_frames"] = readback_stats + recorder_stats + diagnostics["boundary_classification"] = classify_boundary( + readback_stats, + recorder_stats, + ) + diagnostics["recorder_traj_log_dir"] = env.traj_log_dir if env is not None else None + finally: + _write_json(output_dir / "diagnostics.json", diagnostics) + if env is not None: + try: + env.close() + except Exception: + simulation_app.close() + else: + simulation_app.close() + return diagnostics + + +def parse_args(argv: list[str] | None = None) -> argparse.Namespace: + parser = argparse.ArgumentParser( + description="Capture one LabUtopia eval-path reset render diagnostic." + ) + parser.add_argument( + "--config", + default="ebench/labutopia_lab_poc/franka_poc", + help="Task config group or YAML under configs/tasks.", + ) + parser.add_argument("--task", required=True, help="Task basename, e.g. level1_pick.") + parser.add_argument("--run-id", required=True, help="Unique eval run identifier.") + parser.add_argument("--seed", default="000", help="Episode seed. Default: 000.") + parser.add_argument( + "--output-dir", + required=True, + help="Directory for diagnostics.json and copied PNG frames.", + ) + parser.add_argument( + "--port", + type=int, + default=18091, + help="Recorded isolation port label for parity with server runs.", + ) + parser.add_argument( + "--num-steps", + type=int, + default=None, + help="Override scene num_steps. Default: use task config.", + ) + parser.add_argument( + "--save-reset-frame", + "--save-one-step", + dest="save_reset_frame", + action="store_true", + help="Compatibility flag. Reset-frame capture is always enabled and writes 00000.png.", + ) + parser.add_argument("--local", action="store_true", help="Run Isaac with GUI.") + return parser.parse_args(argv) + + +def main(argv: list[str] | None = None) -> None: + diagnostics = run_runtime_diagnostics(parse_args(argv)) + print(json.dumps(_jsonable(diagnostics), indent=2, sort_keys=True)) + + +if __name__ == "__main__": + main() diff --git a/tests/labutopia_poc/test_render_diagnostics_contract.py b/tests/labutopia_poc/test_render_diagnostics_contract.py new file mode 100644 index 0000000..aeb27e9 --- /dev/null +++ b/tests/labutopia_poc/test_render_diagnostics_contract.py @@ -0,0 +1,34 @@ +from standalone_tools.labutopia_poc.capture_eval_render_diagnostics import ( + build_camera_frame_stats, + classify_frame_stats, +) + + +def test_classify_black_frame_as_failed(): + stats = build_camera_frame_stats( + camera_name="camera2", + frame_path="camera2/00000.png", + width=256, + height=256, + channel_min=[0, 0, 0], + channel_max=[0, 0, 0], + channel_mean=[0.0, 0.0, 0.0], + nonzero_pixels=0, + ) + + assert classify_frame_stats(stats) == "black_frame_fail" + + +def test_classify_visible_frame_as_pass(): + stats = build_camera_frame_stats( + camera_name="camera2", + frame_path="camera2/00000.png", + width=256, + height=256, + channel_min=[0, 1, 0], + channel_max=[180, 190, 170], + channel_mean=[72.0, 80.0, 69.0], + nonzero_pixels=42000, + ) + + assert classify_frame_stats(stats) == "visible_frame" From 76e1da24e73f314808cadfe7e6ef36755a7a181f Mon Sep 17 00:00:00 2001 From: Codex Date: Tue, 23 Jun 2026 16:25:11 +0800 Subject: [PATCH 16/30] fix: close LabUtopia P0 readback gates --- configs/cameras/labutopia_franka_poc.yml | 4 +- .../common/assets_manifest.json | 8 + docs/index.html | 6 +- .../render_p0a_p0b_20260623.json | 91 +++++++++++ docs/labutopia_lab_poc/franka_render_smoke.md | 22 ++- .../render_visual_investigation_20260623.md | 46 +++++- ...26-06-22-labutopia-ebench-weekly-report.md | 15 +- .../index.html | 37 +++-- .../plans/2026-06-22-ebench-labutopia-poc.md | 6 +- ...-labutopia-ebench-render-layout-closure.md | 40 +++-- .../utils/standalone/camera_pose_utils.py | 33 ++++ genmanip/utils/usd_utils/camera_utils.py | 10 +- .../labutopia_poc/build_asset_overlay.py | 17 ++ .../labutopia_poc/validate_task_package.py | 56 +++++++ .../labutopia_poc/test_build_asset_overlay.py | 11 ++ tests/labutopia_poc/test_camera_pose_utils.py | 61 ++++++++ .../labutopia_poc/test_camera_utils_setup.py | 145 ++++++++++++++++++ .../test_validate_task_package.py | 103 +++++++++++++ 18 files changed, 665 insertions(+), 46 deletions(-) create mode 100644 docs/labutopia_lab_poc/evidence_manifests/render_p0a_p0b_20260623.json create mode 100644 genmanip/utils/standalone/camera_pose_utils.py create mode 100644 tests/labutopia_poc/test_camera_pose_utils.py create mode 100644 tests/labutopia_poc/test_camera_utils_setup.py diff --git a/configs/cameras/labutopia_franka_poc.yml b/configs/cameras/labutopia_franka_poc.yml index f7e9ae9..8d11f3a 100644 --- a/configs/cameras/labutopia_franka_poc.yml +++ b/configs/cameras/labutopia_franka_poc.yml @@ -4,6 +4,7 @@ camera1: prim_path: /LabUtopiaCamera1 position: [2.0, 0.0, 2.0] orientation: [0.61237, 0.35355, 0.35355, 0.61237] + camera_axes: usd resolution: [256, 256] image_type: rgb with_distance: false @@ -15,8 +16,9 @@ camera2: name: camera_2 exists: false prim_path: /LabUtopiaCamera2 - position: [0.1, 0.0, 2.5] + position: [9.6, 0.0, 2.5] orientation: [0.70711, 0.0, 0.0, -0.70711] + camera_axes: usd resolution: [256, 256] image_type: rgb with_distance: false diff --git a/configs/tasks/ebench/labutopia_lab_poc/common/assets_manifest.json b/configs/tasks/ebench/labutopia_lab_poc/common/assets_manifest.json index 08a55c1..9ee69a2 100644 --- a/configs/tasks/ebench/labutopia_lab_poc/common/assets_manifest.json +++ b/configs/tasks/ebench/labutopia_lab_poc/common/assets_manifest.json @@ -49,6 +49,13 @@ "obj_DryingBox_01_handle": "/World/labutopia_level1_poc/obj_obj_DryingBox_01_handle", "table": "/World/labutopia_level1_poc/obj_table" }, + "deterministic_lights": [ + { + "prim_path": "/World/labutopia_level1_poc/DeterministicDomeLight", + "type": "DomeLight", + "intensity": 1000 + } + ], "required_object_uids": [ "obj_conical_bottle02", "obj_beaker2", @@ -64,6 +71,7 @@ "runtime_notes": [ "scene.usda exposes a single scene uid under /World for GenManip discovery.", "Immediate obj_* wrapper prims payload the selected LabUtopia source prims.", + "A deterministic dome light is authored in the runtime wrapper scene.", "Runtime object keys strip one leading obj_ from wrapper prim names." ] } diff --git a/docs/index.html b/docs/index.html index 98d1c36..367c4ce 100644 --- a/docs/index.html +++ b/docs/index.html @@ -49,11 +49,11 @@

    GenManip LabUtopia Reports

    Zero-build static report surface for LabUtopia x EBench integration evidence.

    LabUtopia x EBench 接入周报 · 2026-06-22 -

    Franka POC end-to-end smoke has completed; render readback/layout closure and Lift2 official baseline readiness remain behind evidence gates.

    +

    Franka POC end-to-end smoke has completed; P0a/P0b now make pick/place eval readback non-black, while render layout acceptance and Lift2 official baseline readiness remain behind evidence gates.

    3/3 episodes complete - 25 passed, 1 skipped - readback black before recorder + 34 passed, 1 skipped + pick/place readback visible Franka POC only GitHub Pages ready
    diff --git a/docs/labutopia_lab_poc/evidence_manifests/render_p0a_p0b_20260623.json b/docs/labutopia_lab_poc/evidence_manifests/render_p0a_p0b_20260623.json new file mode 100644 index 0000000..06bbf2e --- /dev/null +++ b/docs/labutopia_lab_poc/evidence_manifests/render_p0a_p0b_20260623.json @@ -0,0 +1,91 @@ +{ + "date": "2026-06-23", + "base_commit": "9e60237cd088c279b64f1ab0e1d3c14f911d09cd", + "working_tree_changes_required": true, + "direct_render": false, + "official_baseline_execution": false, + "task_render_accepted": false, + "official_baseline_evaluable": false, + "summary": "P0a/P0b source fixes make LabUtopia Franka POC camera2 eval-path readback non-black for pick/place, but the resulting frames are nearly flat gray and are not accepted task render evidence.", + "environment": { + "conda_env": "/cpfs/shared/simulation/zhuzihou/dev/conda-managed/envs/embodied-eval-os-sim-isaacsim41-genmanip-py310", + "config": "ebench/labutopia_lab_poc/franka_poc", + "port_label": 18091, + "asset_root": "/cpfs/shared/simulation/zhuzihou/dev/_datasets/EBench-Assets-Overlay/labutopia_level1_poc/assets", + "camera_config": "configs/cameras/labutopia_franka_poc.yml" + }, + "source_fixes": { + "p0a_camera_axes_pose": { + "files": [ + "genmanip/utils/standalone/camera_pose_utils.py", + "genmanip/utils/usd_utils/camera_utils.py", + "configs/cameras/labutopia_franka_poc.yml" + ], + "camera2_world_position": [9.600000381469727, 0.0, 2.5], + "camera2_world_orientation": [ + 0.7071067811792221, + -0.0000032186508729419352, + 0.7071067811792221, + 0.0000032186508729419352 + ], + "camera_axes": "usd" + }, + "p0b_deterministic_lighting": { + "files": [ + "standalone_tools/labutopia_poc/build_asset_overlay.py", + "configs/tasks/ebench/labutopia_lab_poc/common/assets_manifest.json", + "standalone_tools/labutopia_poc/validate_task_package.py" + ], + "runtime_light": { + "prim_path": "/World/labutopia_level1_poc/DeterministicDomeLight", + "type": "DomeLight", + "intensity": 1000 + }, + "runtime_scene": "/cpfs/shared/simulation/zhuzihou/dev/_datasets/EBench-Assets-Overlay/labutopia_level1_poc/assets/scene_usds/labutopia/level1_poc/lab_001/scene.usda" + } + }, + "diagnostics": { + "level1_pick": { + "run_id": "labutopia_p0a_p0b_pick_20260623_155645", + "diagnostics_json": "saved/diagnostics/labutopia_p0a_p0b_pick_20260623_155645/level1_pick/diagnostics.json", + "boundary_classification": "readback_visible", + "readback_frame": "saved/diagnostics/labutopia_p0a_p0b_pick_20260623_155645/level1_pick/readback_after_get_eval_camera_data/camera2/00000.png", + "recorder_frame": "saved/diagnostics/labutopia_p0a_p0b_pick_20260623_155645/level1_pick/recorder_png/camera2/00000.png", + "sha256": "8b3c6da3ad4aafdfdc586e684e7dfd539bf4183fede5202d890da909c3fe16d4", + "channel_min": [139.0, 139.0, 139.0], + "channel_max": [228.0, 228.0, 228.0], + "channel_mean": [227.973, 227.973, 227.974], + "nonzero_pixels": 65536, + "unique_rgb_colors": 36, + "rgb_std": [1.307, 1.305, 1.301], + "visual_qa": "FAIL_LOW_TEXTURE" + }, + "level1_place": { + "run_id": "labutopia_p0a_p0b_place_20260623_155831", + "diagnostics_json": "saved/diagnostics/labutopia_p0a_p0b_place_20260623_155831/level1_place/diagnostics.json", + "boundary_classification": "readback_visible", + "readback_frame": "saved/diagnostics/labutopia_p0a_p0b_place_20260623_155831/level1_place/readback_after_get_eval_camera_data/camera2/00000.png", + "recorder_frame": "saved/diagnostics/labutopia_p0a_p0b_place_20260623_155831/level1_place/recorder_png/camera2/00000.png", + "sha256": "399c69cdf511cae443f7f9f45a02d2ea26bdadbb446b0c4832f0fbb56a96632a", + "channel_min": [136.0, 136.0, 136.0], + "channel_max": [228.0, 228.0, 228.0], + "channel_mean": [227.973, 227.973, 227.973], + "nonzero_pixels": 65536, + "unique_rgb_colors": 40, + "rgb_std": [1.347, 1.346, 1.342], + "visual_qa": "FAIL_LOW_TEXTURE" + } + }, + "claim_boundary": { + "allowed": [ + "P0a/P0b removed the pure-black camera2 readback failure for pick/place controlled diagnostics.", + "Deterministic runtime lighting is now authored in the LabUtopia POC overlay and statically validated." + ], + "blocked": [ + "PM-ready task render evidence.", + "All-three-task visual acceptance.", + "Reset-time task layout acceptance.", + "Official Lift2 baseline evaluability or score." + ] + } +} diff --git a/docs/labutopia_lab_poc/franka_render_smoke.md b/docs/labutopia_lab_poc/franka_render_smoke.md index 0d5bf8e..96fbad4 100644 --- a/docs/labutopia_lab_poc/franka_render_smoke.md +++ b/docs/labutopia_lab_poc/franka_render_smoke.md @@ -84,6 +84,25 @@ Evidence manifest: docs/labutopia_lab_poc/evidence_manifests/render_diagnostics_20260623.json ``` +P0a/P0b follow-up on 2026-06-23: + +```text +camera axes/pose: camera2 now uses camera_axes: usd and position [9.6, 0.0, 2.5] +deterministic lighting: runtime overlay scene authors /World/labutopia_level1_poc/DeterministicDomeLight +level1_pick: readback_visible +level1_place: readback_visible +``` + +Follow-up evidence: + +```text +saved/diagnostics/labutopia_p0a_p0b_pick_20260623_155645/level1_pick/diagnostics.json +saved/diagnostics/labutopia_p0a_p0b_place_20260623_155831/level1_place/diagnostics.json +docs/labutopia_lab_poc/evidence_manifests/render_p0a_p0b_20260623.json +``` + +This fixes the pure-black readback failure for controlled pick/place P0 diagnostics. It still does not provide accepted task render evidence: the regenerated frames are nearly flat gray and show only a tiny mark, so report images must not be replaced yet. + The initial weekly report used static direct-render screenshots from the same EBench/GenManip-loaded LabUtopia stage and the same overlay asset root. Follow-up visual QA on 2026-06-23 found those screenshots are not acceptable as task-scene evidence. The direct render changed report lighting and camera viewpoint, and it did not prove task configuration, evaluator logic, result scores, or reset-time visual correctness. ## Report images @@ -104,11 +123,10 @@ Follow-up visual QA: ## Open risks -- Fix eval recorder `camera2` black frames at the source. Readback is now proven black before recorder writing. +- Keep P0a/P0b evidence scoped correctly: camera2 readback is no longer pure black for pick/place, but frames are still not task-accepted. - Close the gap between visible mesh bbox, wrapper pose, and task semantic coordinates. - Make LabUtopia reset-time task layout explicit instead of relying on fallback live-scene metadata. - Fix LabUtopia asset import/layout red flags: selected objects are still in source-lab coordinates, and the open-door handle is imported as an invalid independent child transform. -- Add deterministic lighting to the runtime overlay before accepting any render evidence. - Capture reset-time keyframes for all three tasks through the normal eval recording path. - Replace the current report images only after independent visual QA passes. - Keep official Lift2 baseline claims blocked until Lift2 composite assets and official runner discovery are complete. diff --git a/docs/labutopia_lab_poc/render_visual_investigation_20260623.md b/docs/labutopia_lab_poc/render_visual_investigation_20260623.md index dc14461..ce13187 100644 --- a/docs/labutopia_lab_poc/render_visual_investigation_20260623.md +++ b/docs/labutopia_lab_poc/render_visual_investigation_20260623.md @@ -114,6 +114,42 @@ camera2 orientation in diagnostics: [-0.7071, ~0, ~0, 0.7071] The render product binding appears valid, so the most likely immediate camera-side problem is camera pose/axes or lighting/readback timing, not recorder file writing. +## 2026-06-23 P0a/P0b Follow-Up + +P0a camera axes/pose and P0b deterministic lighting were applied after the black-frame diagnostics: + +```text +camera_axes support: genmanip/utils/standalone/camera_pose_utils.py +camera setup call site: genmanip/utils/usd_utils/camera_utils.py +camera2 retarget: configs/cameras/labutopia_franka_poc.yml -> position [9.6, 0.0, 2.5], camera_axes: usd +deterministic light: /World/labutopia_level1_poc/DeterministicDomeLight, DomeLight intensity 1000 +runtime overlay: /cpfs/shared/simulation/zhuzihou/dev/_datasets/EBench-Assets-Overlay/labutopia_level1_poc/assets/scene_usds/labutopia/level1_poc/lab_001/scene.usda +``` + +New controlled eval-path diagnostics: + +```text +level1_pick: saved/diagnostics/labutopia_p0a_p0b_pick_20260623_155645/level1_pick/diagnostics.json +level1_place: saved/diagnostics/labutopia_p0a_p0b_place_20260623_155831/level1_place/diagnostics.json +manifest: docs/labutopia_lab_poc/evidence_manifests/render_p0a_p0b_20260623.json +``` + +Observed boundary: + +```text +level1_pick: readback_visible, camera2 nonzero_pixels=65536, channel_max=[228,228,228] +level1_place: readback_visible, camera2 nonzero_pixels=65536, channel_max=[228,228,228] +``` + +This closes the pure-black readback failure for the pick/place controlled P0 diagnostics. It does not close task visual acceptance. Both frames are still nearly flat gray: + +```text +level1_pick: unique RGB colors=36, RGB std ~= [1.31, 1.31, 1.30] +level1_place: unique RGB colors=40, RGB std ~= [1.35, 1.35, 1.34] +``` + +Visual inspection shows a tiny gray mark on a mostly gray frame, not task-relevant objects. Therefore the next blocker remains asset/layout normalization, not recorder writing. + Layout evidence: ```text @@ -146,8 +182,9 @@ genmanip/utils/loader/scene.py - The open-door handle is imported as a direct child payload and appears to lose the parent drying-box transform/scale. - This is a baseline-evaluability blocker, not just a visual defect. 2. Eval camera readback is black before recorder writing. - - The diagnostics prove the black frame exists immediately after `get_eval_camera_data()`. - - Candidate causes: camera axes/pose points at empty/dark view, missing deterministic lighting, or readback before valid render accumulation. + - The first diagnostics proved the black frame existed immediately after `get_eval_camera_data()`. + - P0a/P0b follow-up changed pick/place from `readback_black_before_recorder` to `readback_visible`. + - Remaining visual failure is near-flat, low-texture output caused by asset/layout scale and placement defects. - Render product binding to `/Camera/LabUtopiaCamera2` appears valid in diagnostics. 3. Task layout is not yet a real LabUtopia reset layout. - LabUtopia source position ranges are in `task_semantics.yml`, but the runtime task YAMLs have empty `object_config` and null layout. @@ -166,6 +203,7 @@ Allowed now: ```text LabUtopia Franka POC can run through the local GenManip/EBench server-client smoke path and finalize 3/3 tasks with result files. +P0a/P0b controlled diagnostics prove camera2 readback is no longer pure black for pick/place after camera_axes/pose and deterministic lighting fixes. ``` Not allowed now: @@ -182,8 +220,8 @@ The current screenshots prove task correctness. Run these in an isolated port/run_id so EOS or another engineer's run is not confused with this work: -1. Fix/validate camera axes handling in the free-camera loader path, then rerun diagnostics with only that variable changed. -2. Add deterministic lighting to the runtime overlay, then rerun diagnostics with only lighting changed. +1. Fix/validate camera axes handling in the free-camera loader path, then rerun diagnostics with only that variable changed. Done for P0a, with pick/place `readback_visible`. +2. Add deterministic lighting to the runtime overlay, then rerun diagnostics with only lighting changed. Done for P0b, with static validation and pick/place `readback_visible`. 3. Rebuild or supplement the overlay so nested LabUtopia parts preserve composed transforms, especially `DryingBox_01/handle`. 4. Normalize selected task objects into the GenManip/Franka workspace and record explicit task reset layouts instead of relying on fallback live-scene metadata. 5. Add static USD validation for object centers, bounds, scales, nested-part transform preservation, and at least one light. diff --git a/docs/records/2026-06-22-labutopia-ebench-weekly-report.md b/docs/records/2026-06-22-labutopia-ebench-weekly-report.md index 58ea76f..d8f4f31 100644 --- a/docs/records/2026-06-22-labutopia-ebench-weekly-report.md +++ b/docs/records/2026-06-22-labutopia-ebench-weekly-report.md @@ -7,7 +7,7 @@ HTML 版产品汇报页: 本周已经把 LabUtopia 的 Franka POC 跑通到端到端 smoke 阶段:任务可以提交、场景可以加载、三个 level-1 任务可以 reset/step、结果可以落盘、最终状态可以正常 complete。 -这说明当前最关键的“接入链路”已经打通。需要注意的是,这还不是任务求解成功,也不是官方 baseline 成绩;当前 smoke 使用默认动作,所以三个任务分数都是 `0.0`。2026-06-23 复核后还要补充一点:当前渲染/布局验收是 blocked,三张静态渲染图未通过视觉 QA,不能继续作为 PM-ready 渲染证据。 +这说明当前最关键的“接入链路”已经打通。需要注意的是,这还不是任务求解成功,也不是官方 baseline 成绩;当前 smoke 使用默认动作,所以三个任务分数都是 `0.0`。2026-06-23 复核后还要补充一点:P0a/P0b 已把 pick/place 的 eval `camera2` 从纯黑修到非黑,但当前渲染/布局验收仍是 blocked,三张静态渲染图未通过视觉 QA,不能继续作为 PM-ready 渲染证据。 ## 本周完成了什么 @@ -111,6 +111,7 @@ docs/records/evidence/2026-06-22-labutopia-ebench-weekly-report/assets/labutopia - `run_id=labutopia_franka_render_smoke_20260622_150819` 跑过保存帧 smoke,但 eval recorder 的 `camera2` 帧当前是黑屏。 - 三个任务各 32 帧的 eval `camera2` 抽样统计均为纯黑:RGB min/max/mean 都是 0。 - 2026-06-23 新增 runtime 诊断后,三个任务都定位为 `readback_black_before_recorder`:黑帧在 `get_eval_camera_data()` 后就已经存在,recorder 写盘不是根因。 +- 2026-06-23 P0a/P0b 修复后,`level1_pick` 和 `level1_place` 已变为 `readback_visible`,说明 camera axes/pose 和 deterministic lighting 对 pick/place 的黑帧源头有效;但 `level1_open_door` 尚未复验通过,新帧也几乎是纯灰低纹理,仍不能作为任务渲染验收图。 - 进一步检查发现资产/layout 也有硬问题:瓶子/烧杯/托盘仍在源 LabUtopia 坐标的 x≈8-10,干燥箱在 x≈45,把手甚至出现在百米量级坐标。这说明不能只调相机,必须同步修复导入布局。 - 当前 task YAML 仍是 `object_config: {}`、`layout_config.type: null`,还没有把 LabUtopia 原始 position_range 真正落成 GenManip reset-time 布局。 - 因此下一步必须先修复 eval camera/readback、任务布局和可复现截图脚本,再做 reset 后关键帧验收。 @@ -122,7 +123,7 @@ docs/records/evidence/2026-06-22-labutopia-ebench-weekly-report/assets/labutopia | 资产根目录不对 | 系统一开始会去错误目录找 LabUtopia 场景 | 增加 LabUtopia manifest 识别逻辑,运行时切换到 overlay asset root | 已解决 | | 缺少 `meta_info.pkl` | 传统 GenManip 任务依赖采集包里的元信息,LabUtopia POC 没有这份文件 | 针对 LabUtopia POC,从实时场景里自动生成最小可用元信息 | 已解决 | | camera cleanup 字段缺失 | 切换任务时 camera 清理报字段错误 | 补齐 camera 配置里的 cleanup flags | 已解决 | -| eval recorder `camera2` 黑屏 | 保存过程帧时 `camera2` 输出纯黑 | 已定位到 `readback_black_before_recorder`;recorder 写盘被排除,下一步修 camera axes/pose、lighting 和 readback 源头 | 待修复 | +| eval recorder `camera2` 黑屏 | 保存过程帧时 `camera2` 输出纯黑 | P0a/P0b 已修 camera axes/pose 和 deterministic lighting;pick/place 已从纯黑变为非黑 readback,open_door 尚未复验通过 | pick/place 部分解除,整体仍 blocked | | 当前三张渲染图未通过视觉验收 | `pick` 目标不清,`place` 缺 beaker,`open_door` 缺门/把手/开门状态 | 降级为历史失败样例,新增调研记录和 P0-P2 修复计划 | 待重拍 | | 资产导入/layout 坐标不闭环 | 任务物体仍在源 lab 坐标,open-door handle 导入后坐标异常放大 | 需要重建或补充 overlay,保留 nested part composed transform,并把任务物体归一到机器人工作空间 | 待闭环 | | 最终进度不 complete | 任务已跑完,但进度没有写入,导致客户端最后等待超时 | 任务结束时写最小 `result_info.json`,并修复进度统计 | 已解决 | @@ -136,7 +137,7 @@ docs/records/evidence/2026-06-22-labutopia-ebench-weekly-report/assets/labutopia | LabUtopia 场景接入 | 已跑通 | 场景资产能加载,任务链路能跑完 | | Franka POC smoke | 已完成 | 3 个任务全部 complete | | 结果落盘 | 已完成 | per-task 和 final result 都能写出 | -| 渲染图/视频验收 | 未通过 | 当前三张直渲图视觉 QA 不合格;`camera2`、视频和 reset 后任务布局仍需闭环 | +| 渲染图/视频验收 | 未通过 | P0a/P0b 已让 pick/place eval readback 非黑;但画面仍近似纯灰,三张直渲图视觉 QA 不合格,视频和 reset 后任务布局仍需闭环 | | 任务求解能力 | 未验证 | 当前默认动作得分 0.0,不代表策略能力 | | 官方 baseline | 未进入本周结论 | 后续单独讨论和规划 | @@ -155,7 +156,7 @@ total_episodes=3 ```text python -m pytest tests/labutopia_poc -q -25 passed, 1 skipped +34 passed, 1 skipped python -m pytest tests/labutopia_poc/test_render_diagnostics_contract.py -q 2 passed @@ -179,6 +180,8 @@ saved/eval_results/ebench/labutopia_franka_smoke_clean8_20260622_100208/.../leve - eval recorder `camera2` frames: black, sampled RGB min/max/mean all 0 - runtime diagnostics: `level1_pick/place/open_door` all `readback_black_before_recorder` - evidence manifest: [docs/labutopia_lab_poc/evidence_manifests/render_diagnostics_20260623.json](../labutopia_lab_poc/evidence_manifests/render_diagnostics_20260623.json) +- P0a/P0b follow-up: `level1_pick/place` now `readback_visible`, but low-texture visual QA is still failed +- P0a/P0b evidence manifest: [docs/labutopia_lab_poc/evidence_manifests/render_p0a_p0b_20260623.json](../labutopia_lab_poc/evidence_manifests/render_p0a_p0b_20260623.json) - static direct-render evidence: visual QA failed on 2026-06-23 - investigation: [docs/labutopia_lab_poc/render_visual_investigation_20260623.md](../labutopia_lab_poc/render_visual_investigation_20260623.md) - plan: [docs/superpowers/plans/2026-06-23-labutopia-ebench-render-layout-closure.md](../superpowers/plans/2026-06-23-labutopia-ebench-render-layout-closure.md) @@ -189,8 +192,8 @@ saved/eval_results/ebench/labutopia_franka_smoke_clean8_20260622_100208/.../leve 建议顺序: -1. P0a:修相机 axes/pose 支持,让 `camera2` 从 eval readback 直接产出非黑帧。 -2. P0b:补 deterministic lighting,避免只靠 direct-render 临时灯光。 +1. P0a:已修相机 axes/pose 支持,让 pick/place 的 `camera2` 从 eval readback 直接产出非黑帧。 +2. P0b:已补 deterministic lighting,避免只靠 direct-render 临时灯光。 3. P1:重建或补充 LabUtopia overlay,把任务对象和 nested handle 保持正确 transform,并放到 Franka 工作空间。 4. P2:用正常 eval-path 重新抓三任务关键帧,写 evidence manifest,并通过独立视觉 QA。 5. P3:图像验收通过后再替换 HTML 里的三张图,并恢复 PM 可见性汇报。 diff --git a/docs/records/evidence/2026-06-22-labutopia-ebench-weekly-report/index.html b/docs/records/evidence/2026-06-22-labutopia-ebench-weekly-report/index.html index 54959ea..69b6920 100644 --- a/docs/records/evidence/2026-06-22-labutopia-ebench-weekly-report/index.html +++ b/docs/records/evidence/2026-06-22-labutopia-ebench-weekly-report/index.html @@ -392,7 +392,7 @@

    Weekly report · 2026-06-22 · PM-facing evidence page

    LabUtopia x EBench接入周报

    -

    本周 Franka POC 已经跑通端到端 smoke:任务能提交,LabUtopia 场景能加载,三个 level-1 任务能 reset/step,结果能落盘并正常 complete。当前结论是“接入链路跑通”;渲染/布局验收仍是 blocked,当前三张渲染图未通过视觉验收。

    +

    本周 Franka POC 已经跑通端到端 smoke:任务能提交,LabUtopia 场景能加载,三个 level-1 任务能 reset/step,结果能落盘并正常 complete。P0a/P0b 已把 pick/place 的 eval camera2 从纯黑修到非黑;渲染/布局验收仍是 blocked,当前三张渲染图未通过视觉验收。

    wiring smoke complete 3/3 episodes finalized @@ -402,7 +402,7 @@

    LabUtopia x EBench接入周报

    @@ -428,13 +428,13 @@

    当前进展总览

    测试
    -
    25 passed
    +
    34 passed
    1 skipped,诊断合同 2 passed,validator 通过。
    渲染证据
    未通过
    -
    三张直渲图视觉 QA 不合格;camera2 已定位为 readback 后、recorder 前黑屏。
    +
    P0a/P0b 后 pick/place 非黑;画面仍近似纯灰,三张直渲图视觉 QA 不合格。
    官方 Lift2 baseline
    @@ -574,7 +574,7 @@

    EBench 加载后的三任务渲染图

    2026-06-23 复核结论:当前三张渲染图未通过视觉验收,不能作为任务渲染已闭环的证据。下面保留为历史失败样例,方便说明 P0 修复范围。

    渲染证据状态

    -

    自动 eval recorder 保存的 camera2 帧为纯黑;2026-06-23 runtime 诊断已确认三个任务都是 readback_black_before_recorder,recorder 写盘不是根因。三张 direct-render 静态图不是正常 eval recorder 产物,也没有可复现生产脚本。视觉 QA 结果为 level1_pick=WARNlevel1_place=FAILlevel1_open_door=FAIL

    +

    早期 eval recorder 保存的 camera2 帧为纯黑;2026-06-23 runtime 诊断确认三个任务都是 readback_black_before_recorder,recorder 写盘不是根因。P0a/P0b 后 level1_pick/level1_place 已变为 readback_visible,但新帧几乎纯灰、低纹理,仍不能作为任务验收图。三张 direct-render 静态图不是正常 eval recorder 产物,也没有可复现生产脚本。视觉 QA 结果为 level1_pick=WARNlevel1_place=FAILlevel1_open_door=FAIL

    @@ -626,8 +626,13 @@

    验证证据

    render diagnostics - readback_black_before_recorder - 三个任务均为 readback 后全黑:channel_max=[0,0,0]nonzero=0;manifest 已记录。 + P0a/P0b advanced + 旧诊断:三个任务均为 readback_black_before_recorder。新诊断:pick/placereadback_visible,但低纹理未过视觉验收。 + + + P0a/P0b manifest + pick/place visible + render_p0a_p0b_20260623.json 记录 camera axes/pose、deterministic light 和 pick/place 非黑 readback。 visual QA @@ -636,7 +641,7 @@

    验证证据

    pytest - 25 passed, 1 skipped + 34 passed, 1 skipped 覆盖资产 override、fallback metadata、结果落盘、后处理 fail-fast 和 render diagnostic 合同等回归点。 @@ -693,8 +698,8 @@

    问题和处理结果

    eval recorder camera2 黑屏 保存过程帧时 camera2 输出为黑屏。 - 已定位为 readback_black_before_recorder;recorder 写盘被排除,下一步修 camera axes/pose、lighting 和 readback 源头。 - P0 阻塞 + P0a/P0b 已修 camera axes/pose 和 deterministic lighting;pick/place 已从纯黑变为非黑 readback,open_door 尚未复验通过。 + pick/place 部分解除,整体仍 blocked 当前三张渲染图未通过视觉验收 @@ -736,6 +741,7 @@

    现在可以说

  • Franka POC 的 LabUtopia scene overlay 加载路径已经验证。
  • 当前三张渲染图已经完成复核,并明确判定为不合格历史样例。
  • 黑帧边界已经定位到 camera readback 之后、recorder 写盘之前。
  • +
  • P0a/P0b 后,pick/place 的 eval camera2 已从纯黑变为非黑 readback。
  • @@ -743,7 +749,7 @@

    现在不能说

    • 不能说任务已经求解成功,当前默认动作得分是 0.0
    • 不能说官方 Lift2 baseline 已经可评,Lift2 复合资产和官方 runner 还没闭环。
    • -
    • 不能说画面/视频/任务布局已经验收;当前三张图视觉 QA 未通过,eval camera2 的 readback 仍是黑屏。
    • +
    • 不能说画面/视频/任务布局已经验收;当前三张图视觉 QA 未通过,新 eval readback 仍接近纯灰。
    • 不能说资产导入布局已闭环;当前对象坐标和 open-door handle transform 仍需修复。
    @@ -756,7 +762,7 @@

    接下来几步

    P0

    Camera readback 源头修复

    -

    已定位为 readback_black_before_recorder;下一步逐项修 camera axes/pose、deterministic lighting 和 readback 源头,直到 eval readback 非黑。

    +

    已完成 camera axes/pose 和 deterministic lighting 的 P0a/P0b 修复;pick/place 已经非黑。当前剩余问题是低纹理、对象不可识别,需要进入 P1 资产/layout。

    P1 @@ -820,7 +826,7 @@

    相关文档

    Render visual investigation - 6/23 多角度复核:当前三张图未通过视觉验收,camera2 为 readback 后、recorder 前黑屏。 + 6/23 多角度复核:三张旧图未通过视觉验收;后续 P0a/P0b 让 pick/place readback 非黑,但仍低纹理,open_door 仍待闭环。 打开 @@ -828,6 +834,11 @@

    相关文档

    三任务 runtime 诊断、帧 hash、claim boundary 和 layout red flags。 打开 + + P0a/P0b render manifest + camera axes/pose、deterministic light 和 pick/place 非黑 readback 证据。 + 打开 + Render/layout closure plan P0-P2 修复计划:camera 诊断、reset 布局、eval-path 重拍和视觉 QA。 diff --git a/docs/superpowers/plans/2026-06-22-ebench-labutopia-poc.md b/docs/superpowers/plans/2026-06-22-ebench-labutopia-poc.md index 08d13d0..c9ccc89 100644 --- a/docs/superpowers/plans/2026-06-22-ebench-labutopia-poc.md +++ b/docs/superpowers/plans/2026-06-22-ebench-labutopia-poc.md @@ -17,7 +17,7 @@ This is a wiring and platform-readiness milestone, not a task-solving milestone. | Scene metadata | Ready for smoke | Missing collected-package `meta_info.pkl` is synthesized from the live LabUtopia scene for POC tasks. | | Camera cleanup | Ready for smoke | POC camera configs now include cleanup flags required by GenManip camera reset. | | Result lifecycle | Ready for smoke | Episodes that terminate before recorder finalize persist minimal `result_info.json`; real post-processing exceptions now fail fast instead of being hidden as completed results. | -| Render/readback evidence | Blocked | 2026-06-23 diagnostics show all three tasks are `readback_black_before_recorder`; recorder writing is not the root cause. | +| Render/readback evidence | Blocked | 2026-06-23 pre-fix diagnostics showed all three tasks as `readback_black_before_recorder`; P0a/P0b follow-up moved pick/place to `readback_visible`, but the frames are still low-texture and open_door is not revalidated. | | Asset/layout acceptance | Blocked | Selected objects remain in source LabUtopia coordinates and the open-door handle transform is invalid after direct child payload import. | | Lift2 official baseline | Not yet proven | Architecture is prepared to add/evaluate the lift2 candidate profile, but the official baseline smoke still needs to be run. | @@ -30,7 +30,7 @@ Latest isolated smoke: - Existing EOS/other-engineer port: `8087`, left untouched and still online after the run - Final status: `complete`, `3/3` episodes completed - Final result: all three tasks recorded `score=0.0`, `sr=0.0` -- Regression tests: `python -m pytest tests/labutopia_poc -q` -> `25 passed, 1 skipped` +- Regression tests: `python -m pytest tests/labutopia_poc -q` -> `34 passed, 1 skipped` - Diagnostics contract: `python -m pytest tests/labutopia_poc/test_render_diagnostics_contract.py -q` -> `2 passed` - Package validator: `python standalone_tools/labutopia_poc/validate_task_package.py` -> `LabUtopia task package validation OK` @@ -49,7 +49,7 @@ Reason: it is already compatible with Isaac Sim 4.1, Ray, GenManip client, cuRob The next lane is planned in `docs/superpowers/plans/2026-06-22-ebench-labutopia-lift2-baseline-lane.md`. -1. P0 render source fix: fix camera axes/pose handling and deterministic lighting until `camera2` readback is non-black in the normal eval path. +1. P0 render source fix: camera axes/pose handling and deterministic lighting now make pick/place `camera2` readback non-black; keep open_door and visual acceptance scoped to P1/P2. 2. P1 asset/layout fix: rebuild or supplement the LabUtopia overlay so required objects and nested parts preserve valid transforms and sit in the robot workspace. 3. P2 evidence regeneration: rerun the three Franka tasks through eval-path capture, require visual QA PASS, and write a manifest before replacing report images. 4. P3 runtime asset preflight: build/verify a LabUtopia composite asset root before running lift2. Current observation: the scene overlay exists, but lift2 robot and cuRobo assets are not present under the scene-only overlay root. diff --git a/docs/superpowers/plans/2026-06-23-labutopia-ebench-render-layout-closure.md b/docs/superpowers/plans/2026-06-23-labutopia-ebench-render-layout-closure.md index 1882f06..30546d3 100644 --- a/docs/superpowers/plans/2026-06-23-labutopia-ebench-render-layout-closure.md +++ b/docs/superpowers/plans/2026-06-23-labutopia-ebench-render-layout-closure.md @@ -58,13 +58,17 @@ Current status as of 2026-06-23: ```text Diagnostic helper and runtime capture script exist. tests/labutopia_poc/test_render_diagnostics_contract.py: 2 passed -tests/labutopia_poc: 25 passed, 1 skipped +LabUtopia POC regression tests: 34 passed, 1 skipped level1_pick: readback_black_before_recorder level1_place: readback_black_before_recorder level1_open_door: readback_black_before_recorder +after P0a/P0b: +level1_pick: readback_visible, low-texture frame, not task accepted +level1_place: readback_visible, low-texture frame, not task accepted +level1_open_door: not revalidated; remains blocked by asset/layout work ``` -Recorder writing is now ruled out as the primary black-frame source. P0 remains open because the source of the black readback still needs to be fixed and revalidated. +Recorder writing is now ruled out as the primary black-frame source. P0a/P0b source fixes remove the pure-black readback failure for controlled pick/place diagnostics, but task render acceptance remains blocked by asset/layout defects. **Files:** - Create: `standalone_tools/labutopia_poc/capture_eval_render_diagnostics.py` @@ -302,14 +306,32 @@ docs/labutopia_lab_poc/evidence_manifests/render_diagnostics_20260623.json Do the next source fixes in this order and keep one variable per run: -1. Camera axes/pose: +1. Camera axes/pose: done for controlled pick/place P0 diagnostics. - `configs/cameras/labutopia_franka_poc.yml` - - `genmanip/utils/loader/scene.py:create_camera_list` - - Add or reuse `camera_axes` handling so free cameras use the intended Isaac/USD convention. - - Acceptance: `camera2` readback is non-black in at least one controlled diagnostic run. -2. Deterministic lighting: - - Add or payload at least one light into the runtime overlay, or add explicit task-scene lighting. - - Acceptance: static USD validation confirms a light exists; eval readback is not dependent on report-only direct-render lighting. + - `genmanip/utils/usd_utils/camera_utils.py` + - `genmanip/utils/standalone/camera_pose_utils.py` + - `camera_axes: usd` is honored for GenManip-style/free cameras. + - `camera2` retargeted to `[9.6, 0.0, 2.5]`. + - Evidence: + +```text +saved/diagnostics/labutopia_p0a_p0b_pick_20260623_155645/level1_pick/diagnostics.json +saved/diagnostics/labutopia_p0a_p0b_place_20260623_155831/level1_place/diagnostics.json +``` + +2. Deterministic lighting: done for the runtime overlay. + - `standalone_tools/labutopia_poc/build_asset_overlay.py` + - `configs/tasks/ebench/labutopia_lab_poc/common/assets_manifest.json` + - `standalone_tools/labutopia_poc/validate_task_package.py` + - Runtime wrapper authors `/World/labutopia_level1_poc/DeterministicDomeLight` with intensity `1000`. + - Acceptance: static validation confirms the light exists and pick/place eval readback is no longer pure black. + - Evidence: + +```text +docs/labutopia_lab_poc/evidence_manifests/render_p0a_p0b_20260623.json +python standalone_tools/labutopia_poc/validate_task_package.py -> OK +``` + 3. Asset/layout normalization: - Rebuild the overlay or add explicit local task assets so required objects are in the robot workspace and nested parts preserve composed transforms. - Acceptance: object centers/scales/bounds pass static validation before Isaac runtime capture. diff --git a/genmanip/utils/standalone/camera_pose_utils.py b/genmanip/utils/standalone/camera_pose_utils.py new file mode 100644 index 0000000..b9d5e5c --- /dev/null +++ b/genmanip/utils/standalone/camera_pose_utils.py @@ -0,0 +1,33 @@ +from __future__ import annotations + +from typing import Any + + +VALID_CAMERA_AXES = {"world", "ros", "usd"} + + +def _camera_axes_from_config( + camera_cfg: dict[str, Any], default_camera_axes: str +) -> str: + camera_axes = camera_cfg.get("camera_axes", default_camera_axes) + if camera_axes not in VALID_CAMERA_AXES: + raise ValueError( + f"camera_axes must be one of {sorted(VALID_CAMERA_AXES)}, got {camera_axes!r}" + ) + return str(camera_axes) + + +def set_camera_local_pose_from_config( + camera: Any, + camera_cfg: dict[str, Any], + *, + default_camera_axes: str = "world", +) -> bool: + if "position" not in camera_cfg or "orientation" not in camera_cfg: + return False + camera.set_local_pose( + translation=camera_cfg["position"], + orientation=camera_cfg["orientation"], + camera_axes=_camera_axes_from_config(camera_cfg, default_camera_axes), + ) + return True diff --git a/genmanip/utils/usd_utils/camera_utils.py b/genmanip/utils/usd_utils/camera_utils.py index e5da6ca..228e4f4 100644 --- a/genmanip/utils/usd_utils/camera_utils.py +++ b/genmanip/utils/usd_utils/camera_utils.py @@ -15,6 +15,9 @@ from genmanip.utils.standalone.pc_utils import get_world_corners_from_bbox3d from genmanip.utils.standalone.transform_utils import pose_to_transform +from genmanip.utils.standalone.camera_pose_utils import ( + set_camera_local_pose_from_config, +) def get_tcp_3d_trace(tcp_xform_list: list[XFormPrim]) -> list[np.ndarray]: @@ -387,11 +390,7 @@ def setup_camera( camera.is_camera_matrix = np.array( [[fx, 0.0, cx], [0.0, fy, cy], [0.0, 0.0, 1.0]] ) - camera.set_local_pose( - translation=camera_cfg.get("position"), - orientation=camera_cfg.get("orientation"), - camera_axes=camera_cfg.get("camera_axes", "usd"), - ) + set_camera_local_pose_from_config(camera, camera_cfg, default_camera_axes="usd") # GenManip Style else: camera.set_focal_length(camera_cfg.get("focal_length", 4.5)) @@ -404,6 +403,7 @@ def setup_camera( camera_params = camera_cfg.get("camera_params", None) if camera_params is not None: set_camera_rational_polynomial(camera, *camera_params) + set_camera_local_pose_from_config(camera, camera_cfg) # add custom annotators if not only_color_rep_for_camera: diff --git a/standalone_tools/labutopia_poc/build_asset_overlay.py b/standalone_tools/labutopia_poc/build_asset_overlay.py index 338c6a2..c0885e9 100644 --- a/standalone_tools/labutopia_poc/build_asset_overlay.py +++ b/standalone_tools/labutopia_poc/build_asset_overlay.py @@ -47,6 +47,13 @@ "obj_table", ] TABLE_UID = "table" +DETERMINISTIC_LIGHTS = [ + { + "prim_path": f"/World/{SCENE_UID}/DeterministicDomeLight", + "type": "DomeLight", + "intensity": 1000, + } +] def _wrapper_name(runtime_object_key: str) -> str: @@ -81,6 +88,13 @@ def _write_scene_wrapper(path: Path) -> None: }}""" ) + deterministic_light_defs = """ + def DomeLight "DeterministicDomeLight" + { + color3f inputs:color = (1, 1, 1) + float inputs:intensity = 1000 + }""" + scene_text = ( """#usda 1.0 ( @@ -93,6 +107,7 @@ def Xform "World" + f' def Xform "{SCENE_UID}"\n' + " {\n" + "\n".join(wrapper_defs) + + deterministic_light_defs + "\n }\n}\n" ) path.write_text( @@ -170,10 +185,12 @@ def build_asset_overlay( "wrapper_prim_paths": _wrapper_prim_paths(), "table_uid": TABLE_UID, "required_genmanip_object_uids": REQUIRED_GENMANIP_OBJECT_UIDS, + "deterministic_lights": DETERMINISTIC_LIGHTS, "copied_files": _copied_files(overlay_root, copied_paths), "notes": [ "scene.usda exposes a single scene uid under /World for GenManip discovery.", "Immediate obj_* wrapper prims payload the selected LabUtopia source prims.", + "A deterministic dome light is authored in the runtime wrapper scene.", "Runtime object keys strip one leading obj_ from wrapper prim names.", ], } diff --git a/standalone_tools/labutopia_poc/validate_task_package.py b/standalone_tools/labutopia_poc/validate_task_package.py index d03a71a..d951302 100644 --- a/standalone_tools/labutopia_poc/validate_task_package.py +++ b/standalone_tools/labutopia_poc/validate_task_package.py @@ -41,6 +41,18 @@ "obj_DryingBox_01_handle": "/World/labutopia_level1_poc/obj_obj_DryingBox_01_handle", "table": "/World/labutopia_level1_poc/obj_table", } +EXPECTED_DETERMINISTIC_LIGHTS = [ + { + "prim_path": "/World/labutopia_level1_poc/DeterministicDomeLight", + "type": "DomeLight", + "intensity": 1000, + } +] +EXPECTED_FRANKA_CAMERA_AXES = { + "camera1": "usd", + "camera2": "usd", +} +EXPECTED_FRANKA_CAMERA2_POSITION = [9.6, 0.0, 2.5] PROFILE_EXPECTATIONS = { "franka_poc": { "robot_type": "manip/franka/panda_hand", @@ -183,6 +195,23 @@ def _validate_assets_manifest() -> None: manifest.get("wrapper_prim_paths") == EXPECTED_WRAPPER_PRIM_PATHS, f"{path}: wrapper_prim_paths must preserve GenManip key stripping", ) + _assert( + manifest.get("deterministic_lights") == EXPECTED_DETERMINISTIC_LIGHTS, + f"{path}: deterministic_lights must declare the runtime wrapper light", + ) + runtime_scene_text = runtime_scene.read_text(encoding="utf-8") + _assert( + 'def DomeLight "DeterministicDomeLight"' in runtime_scene_text, + f"{runtime_scene}: missing DeterministicDomeLight", + ) + _assert( + "float inputs:intensity = 1000" in runtime_scene_text, + f"{runtime_scene}: DeterministicDomeLight must have positive fixed intensity", + ) + _assert( + "inputs:texture:file" not in runtime_scene_text, + f"{runtime_scene}: DeterministicDomeLight must not depend on HDR texture", + ) generated_manifest = manifest.get("generated_manifest") _assert( @@ -201,6 +230,7 @@ def _validate_assets_manifest() -> None: "runtime_object_keys": "runtime_object_keys", "wrapper_prim_paths": "wrapper_prim_paths", "source_to_runtime_object_key": "source_to_runtime_object_key", + "deterministic_lights": "deterministic_lights", }.items(): _assert( manifest.get(common_key) == generated.get(generated_key), @@ -236,6 +266,11 @@ def _validate_camera_configs() -> None: path = ROOT / expected["camera_config"] data = _load_yaml(path) _assert(isinstance(data, dict), f"{path}: expected camera mapping") + if profile == "franka_poc": + _assert( + set(EXPECTED_FRANKA_CAMERA_AXES).issubset(data), + f"{path}: franka_poc cameras must include {sorted(EXPECTED_FRANKA_CAMERA_AXES)}", + ) for camera_name, camera in data.items(): _assert( isinstance(camera, dict), @@ -246,6 +281,27 @@ def _validate_camera_configs() -> None: not missing, f"{path}:{camera_name}: {profile} camera missing cleanup flags {missing}", ) + if profile == "franka_poc": + expected_axes = EXPECTED_FRANKA_CAMERA_AXES.get(camera_name) + if expected_axes is not None: + _assert( + camera.get("camera_axes") == expected_axes, + f"{path}:{camera_name}: camera_axes must remain {expected_axes!r}", + ) + if profile == "franka_poc": + camera2 = data.get("camera2", {}) + position = camera2.get("position") + _assert( + isinstance(position, list) and len(position) == 3, + f"{path}:camera2 position must be a 3-vector", + ) + _assert( + all( + abs(float(actual) - expected) < 1e-6 + for actual, expected in zip(position, EXPECTED_FRANKA_CAMERA2_POSITION) + ), + f"{path}:camera2 position must remain {EXPECTED_FRANKA_CAMERA2_POSITION!r}", + ) def _walk_goal_dicts(value: Any, path: Path) -> list[dict[str, Any]]: diff --git a/tests/labutopia_poc/test_build_asset_overlay.py b/tests/labutopia_poc/test_build_asset_overlay.py index 4be477f..71869b5 100644 --- a/tests/labutopia_poc/test_build_asset_overlay.py +++ b/tests/labutopia_poc/test_build_asset_overlay.py @@ -44,6 +44,9 @@ def test_build_asset_overlay_writes_scene_wrapper_manifest_and_cleans_reruns( 'def Xform "obj_table" (\n' ' prepend payload = @scene.usd@' ) in scene_usda + assert 'def DomeLight "DeterministicDomeLight"' in scene_usda + assert "float inputs:intensity = 1000" in scene_usda + assert "inputs:texture:file" not in scene_usda manifest_path = overlay_root / "manifests" / "labutopia_level1_poc.json" manifest = json.loads(manifest_path.read_text(encoding="utf-8")) @@ -94,9 +97,17 @@ def test_build_asset_overlay_writes_scene_wrapper_manifest_and_cleans_reruns( "obj_table", ] assert manifest["table_uid"] == "table" + assert manifest["deterministic_lights"] == [ + { + "prim_path": "/World/labutopia_level1_poc/DeterministicDomeLight", + "type": "DomeLight", + "intensity": 1000, + } + ] assert manifest["notes"] == [ "scene.usda exposes a single scene uid under /World for GenManip discovery.", "Immediate obj_* wrapper prims payload the selected LabUtopia source prims.", + "A deterministic dome light is authored in the runtime wrapper scene.", "Runtime object keys strip one leading obj_ from wrapper prim names.", ] diff --git a/tests/labutopia_poc/test_camera_pose_utils.py b/tests/labutopia_poc/test_camera_pose_utils.py new file mode 100644 index 0000000..73595eb --- /dev/null +++ b/tests/labutopia_poc/test_camera_pose_utils.py @@ -0,0 +1,61 @@ +from genmanip.utils.standalone.camera_pose_utils import ( + set_camera_local_pose_from_config, +) + + +class _FakeCamera: + def __init__(self): + self.calls = [] + + def set_local_pose(self, **kwargs): + self.calls.append(kwargs) + + +def test_genmanip_style_camera_pose_passes_explicit_camera_axes(): + camera = _FakeCamera() + + applied = set_camera_local_pose_from_config( + camera, + { + "position": [0.1, 0.0, 2.5], + "orientation": [0.70711, 0.0, 0.0, -0.70711], + "camera_axes": "usd", + }, + ) + + assert applied is True + assert camera.calls == [ + { + "translation": [0.1, 0.0, 2.5], + "orientation": [0.70711, 0.0, 0.0, -0.70711], + "camera_axes": "usd", + } + ] + + +def test_simbox_camera_pose_preserves_default_usd_axes(): + camera = _FakeCamera() + + applied = set_camera_local_pose_from_config( + camera, + { + "position": [0.07, 0.01, 0.08], + "orientation": [0.62, 0.33, -0.33, -0.62], + }, + default_camera_axes="usd", + ) + + assert applied is True + assert camera.calls[0]["camera_axes"] == "usd" + + +def test_camera_pose_is_not_reapplied_without_complete_pose(): + camera = _FakeCamera() + + applied = set_camera_local_pose_from_config( + camera, + {"position": [0.1, 0.0, 2.5], "camera_axes": "usd"}, + ) + + assert applied is False + assert camera.calls == [] diff --git a/tests/labutopia_poc/test_camera_utils_setup.py b/tests/labutopia_poc/test_camera_utils_setup.py new file mode 100644 index 0000000..8dc0703 --- /dev/null +++ b/tests/labutopia_poc/test_camera_utils_setup.py @@ -0,0 +1,145 @@ +import ast +import importlib +import importlib.util +import sys +import types +from pathlib import Path + + +class _FakeCamera: + def __init__(self): + self.calls = [] + + def initialize(self): + self.calls.append(("initialize",)) + + def set_focal_length(self, value): + self.calls.append(("set_focal_length", value)) + + def set_clipping_range(self, minimum, maximum): + self.calls.append(("set_clipping_range", minimum, maximum)) + + def set_vertical_aperture(self, value): + self.calls.append(("set_vertical_aperture", value)) + + def set_horizontal_aperture(self, value): + self.calls.append(("set_horizontal_aperture", value)) + + def set_local_pose(self, **kwargs): + self.calls.append(("set_local_pose", kwargs)) + + +def _ensure_module(name): + module = sys.modules.setdefault(name, types.ModuleType(name)) + if "." in name: + parent_name, child_name = name.rsplit(".", 1) + parent = _ensure_module(parent_name) + if not hasattr(parent, child_name): + setattr(parent, child_name, module) + return module + + +def _install_omni_camera_stubs(): + prims = _ensure_module("omni.isaac.core.prims") + sensor = _ensure_module("omni.isaac.sensor") + setattr(prims, "XFormPrim", object) + setattr(sensor, "Camera", object) + + +def _install_camera_utils_dependency_stubs(): + importlib.import_module("genmanip.utils.standalone.camera_pose_utils") + pc_utils = _ensure_module("genmanip.utils.standalone.pc_utils") + transform_utils = _ensure_module("genmanip.utils.standalone.transform_utils") + setattr(pc_utils, "get_world_corners_from_bbox3d", lambda *args, **kwargs: None) + setattr(transform_utils, "pose_to_transform", lambda *args, **kwargs: None) + + +def _load_camera_utils(): + _install_omni_camera_stubs() + _install_camera_utils_dependency_stubs() + module_path = ( + Path(__file__).resolve().parents[2] / "genmanip/utils/usd_utils/camera_utils.py" + ) + module_name = "_labutopia_camera_utils_under_test" + spec = importlib.util.spec_from_file_location(module_name, module_path) + module = importlib.util.module_from_spec(spec) + sys.modules[module_name] = module + assert spec.loader is not None + spec.loader.exec_module(module) + return module + + +def test_setup_camera_genmanip_branch_applies_configured_pose_and_axes(): + camera_utils = _load_camera_utils() + camera = _FakeCamera() + + camera_utils.setup_camera( + camera, + { + "position": [9.6, 0.0, 2.5], + "orientation": [0.70711, 0.0, 0.0, -0.70711], + "camera_axes": "usd", + "with_distance": False, + "with_semantic": False, + "with_bbox2d": False, + "with_bbox3d": False, + "with_motion_vector": False, + }, + only_color_rep_for_camera=True, + ) + + local_pose_calls = [call for call in camera.calls if call[0] == "set_local_pose"] + assert local_pose_calls == [ + ( + "set_local_pose", + { + "translation": [9.6, 0.0, 2.5], + "orientation": [0.70711, 0.0, 0.0, -0.70711], + "camera_axes": "usd", + }, + ) + ] + + +def test_loader_free_camera_path_passes_full_config_to_setup_camera(): + scene_path = Path(__file__).resolve().parents[2] / "genmanip/utils/loader/scene.py" + tree = ast.parse(scene_path.read_text(encoding="utf-8")) + create_camera_list = next( + node + for node in tree.body + if isinstance(node, ast.FunctionDef) and node.name == "create_camera_list" + ) + + setup_camera_calls = [ + node + for node in ast.walk(create_camera_list) + if isinstance(node, ast.Call) + and isinstance(node.func, ast.Name) + and node.func.id == "setup_camera" + ] + assert len(setup_camera_calls) == 1 + assert ast.unparse(setup_camera_calls[0].args[0]) == "camera_list[key]" + setup_keywords = { + keyword.arg: ast.unparse(keyword.value) + for keyword in setup_camera_calls[0].keywords + } + assert setup_keywords["camera_cfg"] == "camera_data[key]" + + exists_branch = next( + node + for node in ast.walk(create_camera_list) + if isinstance(node, ast.If) and ast.unparse(node.test) == "camera_data[key]['exists']" + ) + free_camera_call = next( + node + for node in ast.walk(ast.Module(body=exists_branch.orelse, type_ignores=[])) + if isinstance(node, ast.Call) + and isinstance(node.func, ast.Name) + and node.func.id == "Camera" + ) + free_camera_keywords = { + keyword.arg: ast.unparse(keyword.value) + for keyword in free_camera_call.keywords + } + assert free_camera_keywords["position"] == "camera_data[key]['position']" + assert free_camera_keywords["orientation"] == "camera_data[key]['orientation']" diff --git a/tests/labutopia_poc/test_validate_task_package.py b/tests/labutopia_poc/test_validate_task_package.py index 28127e6..273660b 100644 --- a/tests/labutopia_poc/test_validate_task_package.py +++ b/tests/labutopia_poc/test_validate_task_package.py @@ -4,6 +4,7 @@ import types import pytest +import yaml from standalone_tools.labutopia_poc import validate_task_package @@ -20,6 +21,27 @@ "with_semantic", "with_distance", } +BASE_FRANKA_CAMERAS = { + "camera1": { + "position": [2.0, 0.0, 2.0], + "orientation": [0.61237, 0.35355, 0.35355, 0.61237], + "camera_axes": "usd", + **{flag: False for flag in CAMERA_CLEANUP_FLAGS}, + }, + "camera2": { + "position": [9.6, 0.0, 2.5], + "orientation": [0.70711, 0.0, 0.0, -0.70711], + "camera_axes": "usd", + **{flag: False for flag in CAMERA_CLEANUP_FLAGS}, + }, +} +BASE_LIFT2_CAMERAS = { + "camera1": { + "position": [0.0, 0.0, 0.0], + "orientation": [1.0, 0.0, 0.0, 0.0], + **{flag: False for flag in CAMERA_CLEANUP_FLAGS}, + } +} def _write_json(path, data): @@ -27,6 +49,11 @@ def _write_json(path, data): path.write_text(json.dumps(data), encoding="utf-8") +def _write_yaml(path, data): + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(yaml.safe_dump(data), encoding="utf-8") + + def _write_task_files(task_root): for profile in ("franka_poc", "lift2_candidate"): profile_root = task_root / "ebench/labutopia_lab_poc" / profile @@ -48,6 +75,14 @@ def _write_valid_indexes(task_root): ) +def _write_camera_config_fixture(tmp_root, franka_cameras): + _write_yaml(tmp_root / "configs/cameras/labutopia_franka_poc.yml", franka_cameras) + _write_yaml( + tmp_root / "configs/cameras/fixed_camera_lift2_simbox.yml", + BASE_LIFT2_CAMERAS, + ) + + def test_indexed_task_yaml_paths_rejects_duplicate_profile_entries(tmp_path, monkeypatch): task_root = tmp_path / "tasks" _write_task_files(task_root) @@ -145,3 +180,71 @@ def test_labutopia_camera_configs_define_cleanup_flags(): for camera_name, camera in cameras.items(): missing = CAMERA_CLEANUP_FLAGS - set(camera) assert not missing, f"{camera_path}:{camera_name} missing {missing}" + + +def test_labutopia_franka_camera_config_declares_axes_and_task_view(): + camera_path = validate_task_package.ROOT / "configs/cameras/labutopia_franka_poc.yml" + cameras = validate_task_package._load_yaml(camera_path) + + for camera_name, expected_axes in ( + validate_task_package.EXPECTED_FRANKA_CAMERA_AXES.items() + ): + assert cameras[camera_name]["camera_axes"] == expected_axes + + assert ( + cameras["camera2"]["position"] + == validate_task_package.EXPECTED_FRANKA_CAMERA2_POSITION + ) + + +def test_validate_camera_configs_rejects_franka_camera_axes_regression( + tmp_path, monkeypatch +): + franka_cameras = { + camera_name: dict(camera) + for camera_name, camera in BASE_FRANKA_CAMERAS.items() + } + franka_cameras["camera2"]["camera_axes"] = "world" + _write_camera_config_fixture(tmp_path, franka_cameras) + monkeypatch.setattr(validate_task_package, "ROOT", tmp_path) + + with pytest.raises(AssertionError, match="camera2: camera_axes must remain 'usd'"): + validate_task_package._validate_camera_configs() + + +def test_validate_camera_configs_rejects_franka_camera2_position_regression( + tmp_path, monkeypatch +): + franka_cameras = { + camera_name: dict(camera) + for camera_name, camera in BASE_FRANKA_CAMERAS.items() + } + franka_cameras["camera2"]["position"] = [0.1, 0.0, 2.5] + _write_camera_config_fixture(tmp_path, franka_cameras) + monkeypatch.setattr(validate_task_package, "ROOT", tmp_path) + + with pytest.raises(AssertionError, match="camera2 position must remain"): + validate_task_package._validate_camera_configs() + + +def test_assets_manifest_declares_deterministic_runtime_light(): + manifest_path = ( + validate_task_package.PACKAGE_ROOT / "common/assets_manifest.json" + ) + manifest = validate_task_package._load_json(manifest_path) + + assert manifest["deterministic_lights"] == [ + { + "prim_path": "/World/labutopia_level1_poc/DeterministicDomeLight", + "type": "DomeLight", + "intensity": 1000, + } + ] + + runtime_scene = ( + validate_task_package.Path(manifest["overlay_root"]) + / f"{manifest['runtime_usd_name']}.usda" + ) + scene_text = runtime_scene.read_text(encoding="utf-8") + assert 'def DomeLight "DeterministicDomeLight"' in scene_text + assert "float inputs:intensity = 1000" in scene_text From f96577dfd546761973467b7b5f264c1392a92af4 Mon Sep 17 00:00:00 2001 From: Codex Date: Wed, 24 Jun 2026 00:39:31 +0800 Subject: [PATCH 17/30] feat: close LabUtopia P1 render diagnostics --- configs/cameras/labutopia_franka_poc.yml | 4 +- .../labutopia_franka_poc_open_door.yml | 31 + configs/cameras/labutopia_franka_poc_pick.yml | 29 + .../cameras/labutopia_franka_poc_place.yml | 29 + .../common/assets_manifest.json | 221 ++++++- .../franka_poc/level1_open_door.yml | 61 +- .../franka_poc/level1_pick.yml | 37 +- .../franka_poc/level1_place.yml | 40 +- .../lift2_candidate/level1_open_door.yml | 19 +- .../render_p1_asset_layout_20260623.json | 197 ++++++ docs/labutopia_lab_poc/franka_render_smoke.md | 10 +- .../render_visual_investigation_20260623.md | 271 +++++++- ...26-06-22-labutopia-ebench-weekly-report.md | 69 +- ...anka-level1-open-door-eval-readback-p1.png | Bin 0 -> 98720 bytes ...ia-franka-level1-pick-eval-readback-p1.png | Bin 0 -> 60884 bytes ...a-franka-level1-place-eval-readback-p1.png | Bin 0 -> 62724 bytes .../index.html | 256 ++++++-- .../plans/2026-06-22-ebench-labutopia-poc.md | 25 +- ...-labutopia-ebench-render-layout-closure.md | 113 +++- genmanip/core/evaluator/env.py | 2 + genmanip/core/evaluator/labutopia_layout.py | 13 +- genmanip/core/scene/scene.py | 14 +- genmanip/utils/loader/preprocess_rules.py | 124 ++++ genmanip/utils/loader/scene.py | 4 + .../labutopia_poc/build_asset_overlay.py | 562 +++++++++++++++- .../capture_eval_render_diagnostics.py | 252 +++++++- .../labutopia_poc/validate_task_package.py | 611 +++++++++++++++++- .../labutopia_poc/test_build_asset_overlay.py | 126 +++- .../test_labutopia_layout_fallback.py | 39 ++ .../test_render_diagnostics_contract.py | 104 +++ .../test_scene_preprocess_rules.py | 121 ++++ .../test_validate_task_package.py | 427 +++++++++++- 32 files changed, 3600 insertions(+), 211 deletions(-) create mode 100644 configs/cameras/labutopia_franka_poc_open_door.yml create mode 100644 configs/cameras/labutopia_franka_poc_pick.yml create mode 100644 configs/cameras/labutopia_franka_poc_place.yml create mode 100644 docs/labutopia_lab_poc/evidence_manifests/render_p1_asset_layout_20260623.json create mode 100644 docs/records/evidence/2026-06-22-labutopia-ebench-weekly-report/assets/labutopia-franka-level1-open-door-eval-readback-p1.png create mode 100644 docs/records/evidence/2026-06-22-labutopia-ebench-weekly-report/assets/labutopia-franka-level1-pick-eval-readback-p1.png create mode 100644 docs/records/evidence/2026-06-22-labutopia-ebench-weekly-report/assets/labutopia-franka-level1-place-eval-readback-p1.png create mode 100644 genmanip/utils/loader/preprocess_rules.py create mode 100644 tests/labutopia_poc/test_scene_preprocess_rules.py diff --git a/configs/cameras/labutopia_franka_poc.yml b/configs/cameras/labutopia_franka_poc.yml index 8d11f3a..0f0ed26 100644 --- a/configs/cameras/labutopia_franka_poc.yml +++ b/configs/cameras/labutopia_franka_poc.yml @@ -16,8 +16,8 @@ camera2: name: camera_2 exists: false prim_path: /LabUtopiaCamera2 - position: [9.6, 0.0, 2.5] - orientation: [0.70711, 0.0, 0.0, -0.70711] + position: [0.45, -1.1, 1.55] + orientation: [0.87184, 0.4898, 0.0, 0.0] camera_axes: usd resolution: [256, 256] image_type: rgb diff --git a/configs/cameras/labutopia_franka_poc_open_door.yml b/configs/cameras/labutopia_franka_poc_open_door.yml new file mode 100644 index 0000000..057752f --- /dev/null +++ b/configs/cameras/labutopia_franka_poc_open_door.yml @@ -0,0 +1,31 @@ +camera1: + name: camera_1 + exists: false + prim_path: /LabUtopiaOpenDoorCamera1 + position: [2.0, 0.0, 2.0] + orientation: [0.61237, 0.35355, 0.35355, 0.61237] + camera_axes: usd + resolution: [256, 256] + image_type: rgb + with_distance: false + with_semantic: false + with_bbox2d: false + with_bbox3d: false + with_motion_vector: false +camera2: + name: camera_2 + exists: false + prim_path: /LabUtopiaOpenDoorCamera2 + position: [0.75, -0.56, 1.02] + orientation: [0.87184, 0.4898, 0.0, 0.0] + camera_axes: usd + resolution: [512, 512] + focal_length: 5.0 + horizontal_aperture: 10.0 + image_type: rgb + task_view: level1_open_door + with_distance: false + with_semantic: false + with_bbox2d: false + with_bbox3d: false + with_motion_vector: false diff --git a/configs/cameras/labutopia_franka_poc_pick.yml b/configs/cameras/labutopia_franka_poc_pick.yml new file mode 100644 index 0000000..67ab832 --- /dev/null +++ b/configs/cameras/labutopia_franka_poc_pick.yml @@ -0,0 +1,29 @@ +camera1: + name: camera_1 + exists: false + prim_path: /LabUtopiaPickCamera1 + position: [2.0, 0.0, 2.0] + orientation: [0.61237, 0.35355, 0.35355, 0.61237] + camera_axes: usd + resolution: [256, 256] + image_type: rgb + with_distance: false + with_semantic: false + with_bbox2d: false + with_bbox3d: false + with_motion_vector: false +camera2: + name: camera_2 + exists: false + prim_path: /LabUtopiaPickCamera2 + position: [0.28, -0.55, 1.2] + orientation: [0.87184, 0.4898, 0.0, 0.0] + camera_axes: usd + resolution: [512, 512] + image_type: rgb + task_view: level1_pick + with_distance: false + with_semantic: false + with_bbox2d: false + with_bbox3d: false + with_motion_vector: false diff --git a/configs/cameras/labutopia_franka_poc_place.yml b/configs/cameras/labutopia_franka_poc_place.yml new file mode 100644 index 0000000..e4cfcf7 --- /dev/null +++ b/configs/cameras/labutopia_franka_poc_place.yml @@ -0,0 +1,29 @@ +camera1: + name: camera_1 + exists: false + prim_path: /LabUtopiaPlaceCamera1 + position: [2.0, 0.0, 2.0] + orientation: [0.61237, 0.35355, 0.35355, 0.61237] + camera_axes: usd + resolution: [256, 256] + image_type: rgb + with_distance: false + with_semantic: false + with_bbox2d: false + with_bbox3d: false + with_motion_vector: false +camera2: + name: camera_2 + exists: false + prim_path: /LabUtopiaPlaceCamera2 + position: [0.26, -0.7, 1.32] + orientation: [0.87184, 0.4898, 0.0, 0.0] + camera_axes: usd + resolution: [512, 512] + image_type: rgb + task_view: level1_place + with_distance: false + with_semantic: false + with_bbox2d: false + with_bbox3d: false + with_motion_vector: false diff --git a/configs/tasks/ebench/labutopia_lab_poc/common/assets_manifest.json b/configs/tasks/ebench/labutopia_lab_poc/common/assets_manifest.json index 9ee69a2..74cb02e 100644 --- a/configs/tasks/ebench/labutopia_lab_poc/common/assets_manifest.json +++ b/configs/tasks/ebench/labutopia_lab_poc/common/assets_manifest.json @@ -12,26 +12,26 @@ "/World/table" ], "source_task_prims": { + "level1_open_door": [ + "/World/DryingBox_01", + "/World/DryingBox_01/handle", + "/World/DryingBox_01/RevoluteJoint" + ], "level1_pick": [ "/World/conical_bottle02" ], "level1_place": [ "/World/beaker2", "/World/target_plat" - ], - "level1_open_door": [ - "/World/DryingBox_01", - "/World/DryingBox_01/handle", - "/World/DryingBox_01/RevoluteJoint" ] }, "source_to_runtime_object_key": { - "/World/conical_bottle02": "obj_conical_bottle02", - "/World/beaker2": "obj_beaker2", - "/World/target_plat": "obj_target_plat", "/World/DryingBox_01": "obj_DryingBox_01", "/World/DryingBox_01/handle": "obj_DryingBox_01_handle", - "/World/table": "table" + "/World/beaker2": "obj_beaker2", + "/World/conical_bottle02": "obj_conical_bottle02", + "/World/table": "table", + "/World/target_plat": "obj_target_plat" }, "runtime_object_keys": [ "obj_conical_bottle02", @@ -42,20 +42,208 @@ "table" ], "wrapper_prim_paths": { - "obj_conical_bottle02": "/World/labutopia_level1_poc/obj_obj_conical_bottle02", + "obj_DryingBox_01": "/World/labutopia_level1_poc/obj_obj_DryingBox_01", + "obj_DryingBox_01_handle": "/World/labutopia_level1_poc/obj_obj_DryingBox_01/handle", "obj_beaker2": "/World/labutopia_level1_poc/obj_obj_beaker2", + "obj_conical_bottle02": "/World/labutopia_level1_poc/obj_obj_conical_bottle02", "obj_target_plat": "/World/labutopia_level1_poc/obj_obj_target_plat", - "obj_DryingBox_01": "/World/labutopia_level1_poc/obj_obj_DryingBox_01", - "obj_DryingBox_01_handle": "/World/labutopia_level1_poc/obj_obj_DryingBox_01_handle", "table": "/World/labutopia_level1_poc/obj_table" }, + "articulation_part_paths": { + "obj_DryingBox_01_handle": "/World/labutopia_level1_poc/obj_obj_DryingBox_01/handle" + }, + "render_object_contracts": { + "obj_DryingBox_01": { + "desired_runtime_translation": [ + 0.75, + 0.1, + 0.78 + ], + "display_color": [ + 0.82, + 0.84, + 0.88 + ], + "expected_world_bbox_lwh_m": { + "max": [ + 0.75, + 0.9, + 0.8 + ], + "min": [ + 0.45, + 0.5, + 0.45 + ] + }, + "role": "articulated_drying_box", + "source_prim_path": "/World/DryingBox_01", + "wrapper_prim_path": "/World/labutopia_level1_poc/obj_obj_DryingBox_01" + }, + "obj_DryingBox_01_handle": { + "compose_nested_transform_with_parent": "obj_DryingBox_01", + "display_color": [ + 1.0, + 0.18, + 0.04 + ], + "expected_world_bbox_lwh_m": { + "max": [ + 0.08, + 0.08, + 0.26 + ], + "min": [ + 0.03, + 0.03, + 0.14 + ] + }, + "role": "door_handle", + "source_prim_path": "/World/DryingBox_01/handle", + "wrapper_prim_path": "/World/labutopia_level1_poc/obj_obj_DryingBox_01/handle" + }, + "obj_beaker2": { + "desired_runtime_translation": [ + 0.27, + 0.18, + 0.84 + ], + "display_color": [ + 0.1, + 0.72, + 0.54 + ], + "expected_world_bbox_lwh_m": { + "max": [ + 0.16, + 0.16, + 0.14 + ], + "min": [ + 0.07, + 0.07, + 0.05 + ] + }, + "role": "place_object", + "source_prim_path": "/World/beaker2", + "wrapper_prim_path": "/World/labutopia_level1_poc/obj_obj_beaker2" + }, + "obj_conical_bottle02": { + "desired_runtime_translation": [ + 0.28, + 0.0, + 0.8 + ], + "display_color": [ + 0.1, + 0.48, + 0.95 + ], + "expected_world_bbox_lwh_m": { + "max": [ + 0.14, + 0.14, + 0.22 + ], + "min": [ + 0.05, + 0.05, + 0.1 + ] + }, + "role": "pick_target", + "source_prim_path": "/World/conical_bottle02", + "wrapper_prim_path": "/World/labutopia_level1_poc/obj_obj_conical_bottle02" + }, + "obj_target_plat": { + "desired_runtime_translation": [ + 0.26, + -0.24, + 0.776 + ], + "display_color": [ + 0.95, + 0.78, + 0.12 + ], + "expected_world_bbox_lwh_m": { + "max": [ + 0.14, + 0.14, + 0.02 + ], + "min": [ + 0.08, + 0.08, + 5e-05 + ] + }, + "role": "place_target", + "source_prim_path": "/World/target_plat", + "wrapper_prim_path": "/World/labutopia_level1_poc/obj_obj_target_plat" + } + }, "deterministic_lights": [ { + "intensity": 1000, "prim_path": "/World/labutopia_level1_poc/DeterministicDomeLight", - "type": "DomeLight", - "intensity": 1000 + "type": "DomeLight" } ], + "drying_box_runtime_asset": { + "base_joint_name": "BaseFixedJoint", + "joint_name": "RevoluteJoint", + "removed_source_joint_types": [ + "PhysicsPrismaticJoint" + ], + "source_payload_used": false, + "strategy": "sanitized_surrogate", + "visual_affordances": [ + { + "display_color": [ + 0.28, + 0.34, + 0.42 + ], + "name": "high_contrast_door_panel" + }, + { + "display_color": [ + 0.04, + 0.05, + 0.06 + ], + "name": "door_outline_seams" + }, + { + "display_color": [ + 0.05, + 0.07, + 0.09 + ], + "name": "handle_mount_backplate" + }, + { + "display_color": [ + 1.0, + 0.18, + 0.04 + ], + "name": "high_contrast_handle" + } + ], + "wrapper_prim_path": "/World/labutopia_level1_poc/obj_obj_DryingBox_01" + }, + "required_genmanip_object_uids": [ + "obj_conical_bottle02", + "obj_beaker2", + "obj_target_plat", + "obj_DryingBox_01", + "obj_DryingBox_01_handle", + "obj_table" + ], "required_object_uids": [ "obj_conical_bottle02", "obj_beaker2", @@ -70,7 +258,10 @@ }, "runtime_notes": [ "scene.usda exposes a single scene uid under /World for GenManip discovery.", - "Immediate obj_* wrapper prims payload the selected LabUtopia source prims.", + "Immediate obj_* wrapper prims payload top-level LabUtopia source prims except DryingBox_01.", + "DryingBox_01 uses a sanitized runtime surrogate with identity root scale and finite inertial attributes.", + "The drying-box handle is exposed as an articulation part, not an independent payload.", + "Task object wrapper translations normalize LabUtopia source coordinates into the robot/table workspace.", "A deterministic dome light is authored in the runtime wrapper scene.", "Runtime object keys strip one leading obj_ from wrapper prim names." ] diff --git a/configs/tasks/ebench/labutopia_lab_poc/franka_poc/level1_open_door.yml b/configs/tasks/ebench/labutopia_lab_poc/franka_poc/level1_open_door.yml index 9d2a0ae..25b82af 100644 --- a/configs/tasks/ebench/labutopia_lab_poc/franka_poc/level1_open_door.yml +++ b/configs/tasks/ebench/labutopia_lab_poc/franka_poc/level1_open_door.yml @@ -13,7 +13,7 @@ evaluation_configs: position: [-0.4, 0.0, 0.71] domain_randomization: cameras: - config_path: configs/cameras/labutopia_franka_poc.yml + config_path: configs/cameras/labutopia_franka_poc_open_door.yml type: fixed random_environment: has_wall: false @@ -30,17 +30,66 @@ evaluation_configs: robot: 0 articulation: [] goal: - - - - type: manip/labutopia/handle_displacement - obj_uid: obj_DryingBox_01_handle - min_distance: 0.12 + - - - type: manip/default/check_joint_angle + articulation_obj_uid: obj_DryingBox_01 + joint_name: RevoluteJoint + angle_deg_range: [30, 120] skip_steps: 1 succ_cnts: 59 mode: manual planner: curobo - object_config: {} - preprocess_config: [] + object_config: + obj_DryingBox_01: + type: existed_object + uid_list: + - obj_DryingBox_01 + is_articulated: true + target_positions: [0.0] + articulation_info: + is_articulated: true + part: + handle: /handle + preprocess_config: + - type: set_object_active + config: + active: false + uids: + - obj_conical_bottle02 + - obj_beaker2 + - obj_target_plat layout_config: ignored_objects: [] type: null + labutopia_render_validation: + schema_version: 1 + primary_camera: camera2 + evidence_camera_config: configs/cameras/labutopia_franka_poc_open_door.yml + required_camera_names: + - camera1 + - camera2 + required_visible_objects: + - obj_DryingBox_01 + - obj_DryingBox_01_handle + hidden_non_task_objects: + - obj_conical_bottle02 + - obj_beaker2 + - obj_target_plat + object_pixel_thresholds: + obj_DryingBox_01: + min_width_px: 160 + min_height_px: 150 + min_area_fraction: 0.12 + obj_DryingBox_01_handle: + min_width_px: 18 + min_height_px: 64 + min_area_fraction: 0.004 + task_visual_goal: door_and_handle_visible + reject_frame_if: + - black_frame + - low_texture + - required_object_missing + - severe_clipping + evidence_policy: + direct_render: false env_vars: MDL_SYSTEM_PATH: "/isaac-sim/materials/:{ASSETS_DIR}/scene_usds/labutopia/level1_poc/lab_001/SubUSDs/materials:{ASSETS_DIR}/miscs/mdl/labutopia/mdl" diff --git a/configs/tasks/ebench/labutopia_lab_poc/franka_poc/level1_pick.yml b/configs/tasks/ebench/labutopia_lab_poc/franka_poc/level1_pick.yml index 66d24a9..38f8b92 100644 --- a/configs/tasks/ebench/labutopia_lab_poc/franka_poc/level1_pick.yml +++ b/configs/tasks/ebench/labutopia_lab_poc/franka_poc/level1_pick.yml @@ -13,7 +13,7 @@ evaluation_configs: position: [-0.4, 0.0, 0.71] domain_randomization: cameras: - config_path: configs/cameras/labutopia_franka_poc.yml + config_path: configs/cameras/labutopia_franka_poc_pick.yml type: fixed random_environment: has_wall: false @@ -39,9 +39,42 @@ evaluation_configs: mode: manual planner: curobo object_config: {} - preprocess_config: [] + preprocess_config: + - type: set_object_active + config: + active: false + uids: + - obj_beaker2 + - obj_target_plat + - obj_DryingBox_01 layout_config: ignored_objects: [] type: null + labutopia_render_validation: + schema_version: 1 + primary_camera: camera2 + evidence_camera_config: configs/cameras/labutopia_franka_poc_pick.yml + required_camera_names: + - camera1 + - camera2 + required_visible_objects: + - obj_conical_bottle02 + hidden_non_task_objects: + - obj_beaker2 + - obj_target_plat + - obj_DryingBox_01 + object_pixel_thresholds: + obj_conical_bottle02: + min_width_px: 36 + min_height_px: 48 + min_area_fraction: 0.01 + task_visual_goal: pick_target_visible + reject_frame_if: + - black_frame + - low_texture + - required_object_missing + - severe_clipping + evidence_policy: + direct_render: false env_vars: MDL_SYSTEM_PATH: "/isaac-sim/materials/:{ASSETS_DIR}/scene_usds/labutopia/level1_poc/lab_001/SubUSDs/materials:{ASSETS_DIR}/miscs/mdl/labutopia/mdl" diff --git a/configs/tasks/ebench/labutopia_lab_poc/franka_poc/level1_place.yml b/configs/tasks/ebench/labutopia_lab_poc/franka_poc/level1_place.yml index bd7dbe1..7479a59 100644 --- a/configs/tasks/ebench/labutopia_lab_poc/franka_poc/level1_place.yml +++ b/configs/tasks/ebench/labutopia_lab_poc/franka_poc/level1_place.yml @@ -13,7 +13,7 @@ evaluation_configs: position: [-0.4, 0.0, 0.71] domain_randomization: cameras: - config_path: configs/cameras/labutopia_franka_poc.yml + config_path: configs/cameras/labutopia_franka_poc_place.yml type: fixed random_environment: has_wall: false @@ -40,9 +40,45 @@ evaluation_configs: mode: manual planner: curobo object_config: {} - preprocess_config: [] + preprocess_config: + - type: set_object_active + config: + active: false + uids: + - obj_conical_bottle02 + - obj_DryingBox_01 layout_config: ignored_objects: [] type: null + labutopia_render_validation: + schema_version: 1 + primary_camera: camera2 + evidence_camera_config: configs/cameras/labutopia_franka_poc_place.yml + required_camera_names: + - camera1 + - camera2 + required_visible_objects: + - obj_beaker2 + - obj_target_plat + hidden_non_task_objects: + - obj_conical_bottle02 + - obj_DryingBox_01 + object_pixel_thresholds: + obj_beaker2: + min_width_px: 34 + min_height_px: 34 + min_area_fraction: 0.008 + obj_target_plat: + min_width_px: 42 + min_height_px: 24 + min_area_fraction: 0.006 + task_visual_goal: beaker_and_target_visible + reject_frame_if: + - black_frame + - low_texture + - required_object_missing + - severe_clipping + evidence_policy: + direct_render: false env_vars: MDL_SYSTEM_PATH: "/isaac-sim/materials/:{ASSETS_DIR}/scene_usds/labutopia/level1_poc/lab_001/SubUSDs/materials:{ASSETS_DIR}/miscs/mdl/labutopia/mdl" diff --git a/configs/tasks/ebench/labutopia_lab_poc/lift2_candidate/level1_open_door.yml b/configs/tasks/ebench/labutopia_lab_poc/lift2_candidate/level1_open_door.yml index aaa85df..aa3a9df 100644 --- a/configs/tasks/ebench/labutopia_lab_poc/lift2_candidate/level1_open_door.yml +++ b/configs/tasks/ebench/labutopia_lab_poc/lift2_candidate/level1_open_door.yml @@ -30,14 +30,25 @@ evaluation_configs: robot: 0 articulation: [] goal: - - - - type: manip/labutopia/handle_displacement - obj_uid: obj_DryingBox_01_handle - min_distance: 0.12 + - - - type: manip/default/check_joint_angle + articulation_obj_uid: obj_DryingBox_01 + joint_name: RevoluteJoint + angle_deg_range: [30, 120] skip_steps: 1 succ_cnts: 59 mode: manual planner: curobo - object_config: {} + object_config: + obj_DryingBox_01: + type: existed_object + uid_list: + - obj_DryingBox_01 + is_articulated: true + target_positions: [0.0] + articulation_info: + is_articulated: true + part: + handle: /handle preprocess_config: [] layout_config: ignored_objects: [] diff --git a/docs/labutopia_lab_poc/evidence_manifests/render_p1_asset_layout_20260623.json b/docs/labutopia_lab_poc/evidence_manifests/render_p1_asset_layout_20260623.json new file mode 100644 index 0000000..ae30767 --- /dev/null +++ b/docs/labutopia_lab_poc/evidence_manifests/render_p1_asset_layout_20260623.json @@ -0,0 +1,197 @@ +{ + "date": "2026-06-23", + "base_commit": "76e1da24e73f314808cadfe7e6ef36755a7a181f", + "working_tree_changes_required": true, + "direct_render": false, + "official_baseline_execution": false, + "task_render_accepted": false, + "official_baseline_evaluable": false, + "summary": "P1 moved the LabUtopia POC from black or cluttered frames to three evaluator readback-visible frames. Static USD placement and nested DryingBox handle discovery are sane. Task-level hiding makes pick readable and place basically readable for PM diagnosis. open_door runtime physics now starts at the expected closed joint target, and the latest formal single-handle front-camera retake makes the DryingBox frame, door panel, and one orange handle/action point visible. Independent image-only QA rates the old-vs-current PM comparison PASS, with open_door PASS/WARN as diagnostic evidence only. render_validation_not_passed remains the release blocker, so task_render_accepted and official_baseline_evaluable stay false.", + "environment": { + "conda_env": "/cpfs/shared/simulation/zhuzihou/dev/conda-managed/envs/embodied-eval-os-sim-isaacsim41-genmanip-py310", + "config": "ebench/labutopia_lab_poc/franka_poc", + "asset_root": "/cpfs/shared/simulation/zhuzihou/dev/_datasets/EBench-Assets-Overlay/labutopia_level1_poc/assets", + "base_camera_config": "configs/cameras/labutopia_franka_poc.yml", + "task_camera_configs": { + "level1_pick": "configs/cameras/labutopia_franka_poc_pick.yml", + "level1_place": "configs/cameras/labutopia_franka_poc_place.yml", + "level1_open_door": "configs/cameras/labutopia_franka_poc_open_door.yml" + }, + "runtime_isolation": { + "ports_used": [18102, 18103, 18104, 18131, 18132, 18133, 18134, 18135], + "eos_port_8087_touched": false + } + }, + "static_usd_readback": { + "scene_opens": true, + "obj_conical_bottle02": { + "world_translate": [0.28, 0.0, 0.8], + "bbox_size": [0.092574, 0.092573, 0.164524] + }, + "obj_beaker2": { + "world_translate": [0.27, 0.18, 0.84], + "bbox_size": [0.110653, 0.110653, 0.0904] + }, + "obj_target_plat": { + "world_translate": [0.26, -0.24, 0.776], + "bbox_size": [0.1, 0.1, 0.0001] + }, + "obj_DryingBox_01": { + "world_translate": [0.75, 0.1, 0.78], + "bbox_size": [0.57642, 0.740892, 0.63] + }, + "obj_DryingBox_01_handle": { + "wrapper_prim_path": "/World/labutopia_level1_poc/obj_obj_DryingBox_01/handle", + "top_level_duplicate_removed": true, + "world_translate": [0.455606, 0.248763, 1.108592], + "bbox_size": [0.048367, 0.047912, 0.201624] + } + }, + "task_level_visibility_isolation": { + "level1_pick": { + "visible_task_objects": ["obj_conical_bottle02"], + "hidden_non_task_objects": ["obj_beaker2", "obj_target_plat", "obj_DryingBox_01"] + }, + "level1_place": { + "visible_task_objects": ["obj_beaker2", "obj_target_plat"], + "hidden_non_task_objects": ["obj_conical_bottle02", "obj_DryingBox_01"] + }, + "level1_open_door": { + "visible_task_objects": ["obj_DryingBox_01", "obj_DryingBox_01_handle"], + "hidden_non_task_objects": ["obj_conical_bottle02", "obj_beaker2", "obj_target_plat"] + } + }, + "diagnostics": { + "level1_pick": { + "run_id": "labutopia_p1_visibility_pick_20260623_175050", + "diagnostics_json": "saved/diagnostics/labutopia_p1_visibility_pick_20260623_175050/diagnostics.json", + "boundary_classification": "readback_visible", + "readback_frame": "saved/diagnostics/labutopia_p1_visibility_pick_20260623_175050/readback_after_get_eval_camera_data/camera2/00000.png", + "report_image": "docs/records/evidence/2026-06-22-labutopia-ebench-weekly-report/assets/labutopia-franka-level1-pick-eval-readback-p1.png", + "sha256": "78e193beb7bc469c5f8dd40eecb9e532c6cfe3b836b1db4a12c3275253e7755d", + "channel_min": [65.0, 145.0, 146.0], + "channel_max": [228.0, 228.0, 228.0], + "channel_mean": [179.716, 180.23, 180.45], + "nonzero_pixels": 262144, + "unique_rgb_colors": 1075, + "runtime_physics_stable": true, + "visual_review": "READABLE_PENDING_FORMAL_QA", + "visual_reason": "Task-level hiding leaves the target bottle clearly visible on the table. This is PM-readable diagnostic evidence, but formal visual/pixel QA has not yet been signed off." + }, + "level1_place": { + "run_id": "labutopia_p1_visibility_place_20260623_175232", + "diagnostics_json": "saved/diagnostics/labutopia_p1_visibility_place_20260623_175232/diagnostics.json", + "boundary_classification": "readback_visible", + "readback_frame": "saved/diagnostics/labutopia_p1_visibility_place_20260623_175232/readback_after_get_eval_camera_data/camera2/00000.png", + "report_image": "docs/records/evidence/2026-06-22-labutopia-ebench-weekly-report/assets/labutopia-franka-level1-place-eval-readback-p1.png", + "sha256": "4a5603401f35f18c0039f31ebab2efba6cb895ff606842a709febe2a2402ecac", + "channel_min": [62.0, 145.0, 116.0], + "channel_max": [228.0, 228.0, 228.0], + "channel_mean": [182.224, 182.371, 181.986], + "nonzero_pixels": 262144, + "unique_rgb_colors": 891, + "runtime_physics_stable": true, + "visual_review": "READABLE_PENDING_FORMAL_QA", + "visual_reason": "The beaker and yellow target platform are visible in the same evaluator frame. The relation is understandable, though a closer camera would improve final PM polish." + }, + "level1_open_door": { + "run_id": "labutopia_p1_open_door_single_handle_formal_20260624_0001", + "diagnostics_json": "saved/diagnostics/labutopia_p1_open_door_single_handle_formal_20260624_0001/diagnostics.json", + "boundary_classification": "readback_visible", + "readback_frame": "saved/diagnostics/labutopia_p1_open_door_single_handle_formal_20260624_0001/readback_after_get_eval_camera_data/camera2/00000.png", + "report_image": "docs/records/evidence/2026-06-22-labutopia-ebench-weekly-report/assets/labutopia-franka-level1-open-door-eval-readback-p1.png", + "sha256": "f9b6c1ee41ac0a51a2377e4eed372b50a51ac4c793e5c0a52f7ef4f27a9e3e86", + "channel_min": [27.0, 29.0, 31.0], + "channel_max": [228.0, 228.0, 228.0], + "channel_mean": [165.751, 165.583, 166.099], + "nonzero_pixels": 262144, + "unique_rgb_colors": 3860, + "pixel_stats": { + "orange_red_pixels": 2195, + "orange_red_bbox_xyxy": [241, 157, 276, 233], + "non_floor_dark_mid_pixels": 7607, + "non_floor_dark_mid_bbox_xyxy": [35, 107, 413, 245], + "bluegrey_door_panel_pixels": 163648, + "bluegrey_door_panel_bbox_xyxy": [0, 106, 511, 511] + }, + "visual_review": "WARN_DIAGNOSABLE_BUT_NOT_ACCEPTED", + "visual_reason": "The formal single-handle front-camera retake shows the DryingBox/cabinet frame, door panel, and one orange handle/action point in one evaluator readback frame. Independent image-only QA rates it PASS/WARN as PM-facing diagnostic evidence. It is not official task acceptance because diagnostics still report render_validation_not_passed.", + "runtime_physics": { + "articulation_joint_positions": [0.0], + "expected_articulation_joint_positions": [0.0], + "dof_names": ["RevoluteJoint"], + "world_position": [0.75, 0.18000000715255737, 0.7799999713897705], + "runtime_physics_stable": true, + "status": "CLOSED_START_FIXED_SINGLE_HANDLE_FRONT_CAMERA_DIAGNOSTIC_ONLY" + } + } + }, + "open_door_root_cause_review": { + "summary": "GenManip task UID/articulation discovery is working; the instability is downstream of malformed DryingBox USD/PhysX topology.", + "findings": [ + "The composed runtime prim still has root scale 0.001 instead of a baked identity-scale articulation.", + "Multiple rigid links use the basename mesh, matching duplicate link-name warnings.", + "Some rigid links retain non-finite centerOfMass or invalid principalAxes.", + "At least one fixed-joint body target resolves to a collision prim without PhysicsRigidBodyAPI.", + "The button prismatic joint adds an extra DOF, so runtime reports RevoluteJoint and PrismaticJoint." + ], + "recommended_fix_sequence": [ + "Extend static validation to fail on non-identity articulation root scale, duplicate rigid-link basenames, non-finite COM, invalid principal axes, joint targets without rigid bodies, and unexpected extra DOFs.", + "Replace incremental over fixes with a sanitized DryingBox runtime asset: baked scale, identity root scale, unique rigid link names, required links/joints only, and stable handle placement.", + "Regenerate the LabUtopia overlay scene.usda and rerun open_door diagnostics.", + "Accept open_door only after finite joint positions, no duplicate-link warnings, stable runtime_sanity, and a visually plausible door/handle frame." + ], + "latest_resolution": { + "strategy": "sanitized DryingBox surrogate with fixed base and one aligned revolute door joint", + "static_topology_ready": true, + "runtime_joint_stable": true, + "runtime_joint_target_matches": true, + "visible_high_contrast_marker": false, + "single_physical_handle_visible": true, + "duplicate_marker_removed": true, + "remaining_blocker": "diagnostics still report render_validation_not_passed, so official task render acceptance and baseline evaluability remain blocked" + } + }, + "review": { + "visual_reviewer": { + "level1_pick": "READABLE_PENDING_FORMAL_QA", + "level1_place": "READABLE_PENDING_FORMAL_QA", + "level1_open_door": "PASS_WARN_DIAGNOSTIC_NOT_ACCEPTED" + }, + "baseline_evaluability_reviewer": { + "official_baseline_evaluable": false, + "primary_blocker": "formal task render acceptance is not signed off", + "secondary_blocker": "Lift2 composite assets and official runner execution are not closed" + } + }, + "browser_visual_review": { + "tool": "Chromium headless CLI from ~/.cache/ms-playwright/chromium-1228", + "desktop_screenshot": "/tmp/labutopia_weekly_old_new_review_20260624/desktop_full_report.png", + "mobile_screenshot": "/tmp/labutopia_weekly_old_new_review_20260624/mobile_full_report_long.png", + "audit_json": "/tmp/labutopia_weekly_old_new_review_20260624/audit.json", + "image_requests": "static HTML parse found six report images and all six files exist; DOM dump contains required old/new/open_door/render_validation text", + "result": "PASS_FOR_REPORT_DISPLAY", + "notes": "Desktop and mobile long screenshots show old JPGs and new square evaluator PNGs, including the latest single-handle open_door eval PNG. Chromium emitted expected headless DBus warnings only. This display audit does not mean task visual acceptance." + }, + "claim_boundary": { + "allowed": [ + "All three Franka POC tasks now produce non-black evaluator camera2 readback frames.", + "Static USD readback shows task objects and the nested DryingBox handle in plausible workspace coordinates.", + "The old top-level duplicated handle payload is removed; the handle is represented as part of the DryingBox assembly.", + "Task-level hiding makes pick and place readable as PM-facing diagnostic frames.", + "The latest open_door runtime diagnostic has stable DryingBox joint positions, matches the expected closed target [0.0], only exposes RevoluteJoint, and shows the DryingBox frame, door panel, and one orange handle/action point in the same eval readback frame." + ], + "blocked": [ + "task_render_accepted=true.", + "Official Lift2 baseline evaluability.", + "Any claim that open_door visual evidence is reliable for baseline evaluation.", + "Any claim that the current report display QA equals task visual acceptance." + ] + }, + "next_steps": [ + "Run formal visual QA for pick/place and keep task_render_accepted=false until signed off.", + "Keep diagnostics claim_boundary as the release gate; do not upgrade claims from readback_visible alone.", + "Use the latest single-handle front-camera open_door frame as PM-facing diagnostic evidence only.", + "Refresh browser display QA and formal task visual QA; keep task_render_accepted=false until render_validation passes." + ] +} diff --git a/docs/labutopia_lab_poc/franka_render_smoke.md b/docs/labutopia_lab_poc/franka_render_smoke.md index 96fbad4..94daa46 100644 --- a/docs/labutopia_lab_poc/franka_render_smoke.md +++ b/docs/labutopia_lab_poc/franka_render_smoke.md @@ -105,6 +105,14 @@ This fixes the pure-black readback failure for controlled pick/place P0 diagnost The initial weekly report used static direct-render screenshots from the same EBench/GenManip-loaded LabUtopia stage and the same overlay asset root. Follow-up visual QA on 2026-06-23 found those screenshots are not acceptable as task-scene evidence. The direct render changed report lighting and camera viewpoint, and it did not prove task configuration, evaluator logic, result scores, or reset-time visual correctness. +Superseded update, 2026-06-23: + +- The "report images must not be replaced yet" note above applied to the P0a/P0b nearly flat gray frames only. +- The weekly report now keeps the old JPGs as historical failed evidence and also includes evaluator readback PNGs. +- `level1_pick` and `level1_place` are PM-readable diagnostic frames, pending formal visual QA. +- `level1_open_door` runtime physics has been stabilized with a sanitized DryingBox surrogate, aligned hinge, and target replay; the latest diagnostic starts closed and shows a high-contrast handle marker, but visual QA is still WARN/not accepted because the interaction point is small and edge-adjacent. +- The current claim boundary remains `task_render_accepted=false` and `official_baseline_evaluable=false`. + ## Report images Committed report assets: @@ -126,7 +134,7 @@ Follow-up visual QA: - Keep P0a/P0b evidence scoped correctly: camera2 readback is no longer pure black for pick/place, but frames are still not task-accepted. - Close the gap between visible mesh bbox, wrapper pose, and task semantic coordinates. - Make LabUtopia reset-time task layout explicit instead of relying on fallback live-scene metadata. -- Fix LabUtopia asset import/layout red flags: selected objects are still in source-lab coordinates, and the open-door handle is imported as an invalid independent child transform. +- Continue from the superseded asset/layout red flags: selected objects have since been normalized into the Franka workspace, and the open-door handle is nested under the DryingBox runtime assembly. The remaining render risk is `open_door` camera framing and handle/interaction-point clarity. - Capture reset-time keyframes for all three tasks through the normal eval recording path. - Replace the current report images only after independent visual QA passes. - Keep official Lift2 baseline claims blocked until Lift2 composite assets and official runner discovery are complete. diff --git a/docs/labutopia_lab_poc/render_visual_investigation_20260623.md b/docs/labutopia_lab_poc/render_visual_investigation_20260623.md index ce13187..f9828e1 100644 --- a/docs/labutopia_lab_poc/render_visual_investigation_20260623.md +++ b/docs/labutopia_lab_poc/render_visual_investigation_20260623.md @@ -4,9 +4,9 @@ Date: 2026-06-23 ## Scope -This record summarizes the visual and technical investigation for the three LabUtopia Franka POC render images that were added to the 2026-06-22 weekly report. +This record summarizes the visual and technical investigation for the LabUtopia Franka POC render images that were added to the 2026-06-22 weekly report. -The conclusion is that the current three images are not acceptable as task-scene render evidence. They should be treated as failed visual evidence until the normal eval recorder path produces task-relevant reset-time frames. +The original JPG images are historical failed evidence. The latest evaluator readback PNGs are now non-black and task-level visibility isolation makes `level1_pick` readable and `level1_place` basically readable for PM diagnosis. `level1_open_door` runtime physics has been stabilized with a sanitized DryingBox surrogate and now starts at the expected closed joint target. The 2026-06-24 formal single-handle front-camera retake makes the DryingBox frame, door panel, and one orange handle/action point visible. Independent image-only QA rates the old-vs-current comparison PASS for PM diagnostic reporting, with `open_door` PASS/WARN as diagnostic evidence only. Therefore `task_render_accepted=false` and `official_baseline_evaluable=false` remain the correct claim boundary until `render_validation` and formal task visual QA pass. ## Reviewed Images @@ -162,6 +162,126 @@ obj_DryingBox_01_handle position ~= [-148.763, -294.393, 328.592] This means the asset import/layout is also faulty. A camera-only fix may make a frame non-black, but it would not make the Franka task or official Lift2 baseline evaluable because task objects are not normalized into a valid robot workspace. +## 2026-06-23 P1 Asset/Layout Follow-Up + +P1 changed the asset composition strategy and regenerated the LabUtopia overlay. The main corrections are: + +- The task objects are normalized into the Franka/table workspace instead of staying in source LabUtopia coordinates. +- `DryingBox_01/handle` is no longer duplicated as an independent top-level payload. +- The runtime handle object resolves to the nested path `/World/labutopia_level1_poc/obj_obj_DryingBox_01/handle`. +- The generated overlay authors deterministic light and display-color fallbacks. + +Static USD readback after rebuilding the overlay: + +```text +obj_conical_bottle02: world_translate ~= [0.28, 0.00, 0.80], bbox ~= [0.093, 0.093, 0.165] +obj_beaker2: world_translate ~= [0.27, 0.18, 0.84], bbox ~= [0.111, 0.111, 0.090] +obj_target_plat: world_translate ~= [0.26,-0.24, 0.776], bbox ~= [0.100, 0.100, 0.0001] +obj_DryingBox_01: world_translate ~= [0.75, 0.10, 0.78], bbox ~= [0.576, 0.741, 0.630] +obj_DryingBox_01_handle nested path: + /World/labutopia_level1_poc/obj_obj_DryingBox_01/handle + world_translate ~= [0.456, 0.249, 1.109], bbox ~= [0.048, 0.048, 0.202] +top-level /World/labutopia_level1_poc/obj_obj_DryingBox_01_handle: invalid, as intended +``` + +New eval-path diagnostics with the shared oblique camera: + +| Task | Run ID | Boundary | Visual QA | Notes | +| --- | --- | --- | --- | --- | +| `level1_pick` | `labutopia_p1_layout_pick_20260623_170924` | `readback_visible` | BLOCKER | Non-black and spatially plausible, but the yellow bottle is too small and visually confused with the drying box/door. | +| `level1_place` | `labutopia_p1_layout_place_20260623_171047` | `readback_visible` | BLOCKER | Non-black, but source object and yellow target platform are not readable as a place relation. | +| `level1_open_door` | `labutopia_p1_layout_open_door_20260623_171256` | `readback_visible` | WARN | Door is readable, but handle/action target is weak; runtime articulation is unstable. | + +Evidence manifest: + +```text +docs/labutopia_lab_poc/evidence_manifests/render_p1_asset_layout_20260623.json +``` + +Independent visual QA conclusion: + +```text +level1_pick: BLOCKER +level1_place: BLOCKER +level1_open_door: WARN +``` + +Baseline evaluability review conclusion at this stage: + +```text +official_baseline_evaluable=false +task_render_accepted=false +primary blocker at this stage: open_door runtime articulation/PhysX instability +secondary blocker: pick/place task-object visibility +``` + +At this P1 stage, the `open_door` runtime diagnostic became the most important blocker. Although static USD readback was sane, runtime articulation reported: + +```text +articulation_state.obj_DryingBox_01.joint_positions ~= [3.888585221393613e+16, 0.0] +logs include Invalid PhysX transform warnings +logs include huge bbox warnings for task objects during reset +``` + +Therefore P1 is only partially closed: + +```text +static_usd_ok=true +camera_readback_visible=true +runtime_physics_stable=false +task_render_accepted=false +official_baseline_evaluable=false +``` + +## 2026-06-23 P1 Visibility Isolation Follow-Up + +After the first P1 asset/layout run, task-level visibility isolation was added through `preprocess_config` so each task hides non-task objects before capture. This makes the diagnostic images easier for PM review without changing the baseline claim boundary. + +Visibility-isolation eval-path diagnostics at this stage: + +| Task | Run ID | Boundary | Visual status | Notes | +| --- | --- | --- | --- | --- | +| `level1_pick` | `labutopia_p1_visibility_pick_20260623_175050` | `readback_visible` | readable, pending formal QA | Only the tabletop and blue bottle remain visible; the pick target is clear. | +| `level1_place` | `labutopia_p1_visibility_place_20260623_175232` | `readback_visible` | basically readable, pending formal QA | The beaker and yellow target platform are visible together; a closer final camera would improve polish. | +| `level1_open_door` | `labutopia_p1_visibility_open_door_20260623_175404` | `readback_visible` | superseded runtime blocker | The box/door are visible, but this run had invalid articulation state and has been superseded by the later aligned-hinge run below. | + +Report assets originally copied from these diagnostics: + +```text +docs/records/evidence/2026-06-22-labutopia-ebench-weekly-report/assets/labutopia-franka-level1-pick-eval-readback-p1.png +docs/records/evidence/2026-06-22-labutopia-ebench-weekly-report/assets/labutopia-franka-level1-place-eval-readback-p1.png +docs/records/evidence/2026-06-22-labutopia-ebench-weekly-report/assets/labutopia-franka-level1-open-door-eval-readback-p1.png +``` + +The `open_door` report PNG has since been replaced by the aligned-hinge stable runtime diagnostic recorded below. + +The original copied report PNGs were 512x512 evaluator readback frames with these hashes: + +```text +level1_pick: 78e193beb7bc469c5f8dd40eecb9e532c6cfe3b836b1db4a12c3275253e7755d +level1_place: 4a5603401f35f18c0039f31ebab2efba6cb895ff606842a709febe2a2402ecac +level1_open_door: 89dfb510ed0de0ee1b989953ed62f467fd31e9cc1857dc6a6060db8d4bebb1e4 +``` + +Superseded open-door runtime blocker: + +```text +runtime_sanity.runtime_physics_stable=false +joint_positions ~= [15733351251968.0, 0.0] +dof_names = [RevoluteJoint, PrismaticJoint] +claim_boundary.blockers includes runtime_physics_unstable +``` + +Additional read-only articulation review found that this is not a UID lookup problem. GenManip discovers `obj_DryingBox_01` as an articulation, but the underlying USD/PhysX topology remains unsafe: + +- the composed articulation root still has scale `0.001`; +- several rigid links are named `mesh`, matching duplicate link-name warnings; +- some rigid links retain non-finite center of mass or invalid principal axes; +- at least one joint body target resolves to a collision prim without `PhysicsRigidBodyAPI`; +- the button prismatic joint exposes an extra DOF, so the runtime reports both `RevoluteJoint` and `PrismaticJoint`. + +Recommended next fix is to strengthen static validation first, then replace the incremental DryingBox overrides with a sanitized runtime asset that bakes scale, uses unique rigid link names, keeps only required links/joints, and produces finite articulation state. + Relevant configuration: ```text @@ -175,27 +295,141 @@ genmanip/core/evaluator/labutopia_layout.py genmanip/utils/loader/scene.py ``` +## 2026-06-23 P1 DryingBox Surrogate / Aligned Hinge Follow-Up + +The runtime physics blocker above has been partially closed for the POC path. The overlay now uses a sanitized DryingBox runtime surrogate instead of relying on the malformed source articulation directly. + +Key changes: + +- The source `DryingBox_01` payload is not used for the runtime articulation. +- The surrogate articulation has no non-identity root scale. +- It exposes three stable rigid links: `body_link`, `door_link`, and `handle`. +- It uses a world fixed-base joint plus one `RevoluteJoint` for the door. +- The extra source `PrismaticJoint` is removed from the runtime articulation. +- Rigid links have finite mass, inertia, center of mass, and principal axes. +- The door hinge anchors are aligned after the body/door geometry update. + +Static validation now reports: + +```text +world_fixed_base_joint_paths = [/World/labutopia_level1_poc/obj_obj_DryingBox_01/BaseFixedJoint] +unexpected_joint_types = [] +invalid_joint_body_targets = [] +zero_mass_links = [] +zero_inertia_links = [] +invalid_center_of_mass_links = [] +invalid_principal_axes_links = [] +runtime_topology_ready = true +sanitized_for_physx = true +``` + +Superseded runtime diagnostic: + +| Task | Run ID | Boundary | Runtime status | Visual QA | +| --- | --- | --- | --- | --- | +| `level1_open_door` | `labutopia_p1_open_door_skip_artpart_reset_trial_20260623_194939` | `readback_visible` | diagnosable, but closed start still wrong | FAIL | + +Runtime evidence: + +```text +diagnostic_error = None +runtime_physics_stable = true +dof_names = [RevoluteJoint] +joint_positions ~= [0.7112835049629211] +world_position ~= [0.75, 0.180000007, 0.779999971] +expected_closed_start = [0.0] +claim_boundary.blockers = [render_validation_not_passed] +official_baseline_evaluable = false +task_render_accepted = false +``` + +This run proved the sanitized topology was finite, but it was superseded because the closed-start target was not replayed after reset/warmup. + +Intermediate camera retake that was rejected: + +| Task | Run ID | Boundary | Visual QA | Reason rejected | +| --- | --- | --- | --- | --- | +| `level1_open_door` | `labutopia_p1_open_door_close_camera_20260623_123833` | `readback_visible` | FAIL | The orange handle/interaction point was clear, but the camera was too close: the drying-box body became an unidentifiable white/gray fragment, so a reviewer could not understand the open-door task context. | + +Current runtime diagnostic after target replay, duplicate marker removal, handle-side correction, and formal front-camera retake: + +| Task | Run ID | Boundary | Runtime status | Visual QA | +| --- | --- | --- | --- | --- | +| `level1_open_door` | `labutopia_p1_open_door_single_handle_formal_20260624_0001` | `readback_visible` | closed start fixed, door/frame/single handle visible | PASS/WARN diagnostic / not accepted | + +Runtime evidence: + +```text +diagnostic_error = None +runtime_physics_stable = true +dof_names = [RevoluteJoint] +joint_positions = [0.0] +expected_closed_start = [0.0] +orange_red_pixels = 2195 +orange_red_bbox_xyxy = [241, 157, 276, 233] +non_floor_dark_mid_pixels = 7607 +bluegrey_door_panel_pixels = 163648 +claim_boundary.blockers = [render_validation_not_passed] +official_baseline_evaluable = false +task_render_accepted = false +``` + +Report asset now copied from: + +```text +saved/diagnostics/labutopia_p1_open_door_single_handle_formal_20260624_0001/readback_after_get_eval_camera_data/camera2/00000.png +docs/records/evidence/2026-06-22-labutopia-ebench-weekly-report/assets/labutopia-franka-level1-open-door-eval-readback-p1.png +sha256 = f9b6c1ee41ac0a51a2377e4eed372b50a51ac4c793e5c0a52f7ef4f27a9e3e86 +``` + +Independent image-only QA verdict for the old-vs-current report set: + +```text +Overall verdict: PASS for PM-facing old-vs-current comparison evidence, with careful wording that the current images are evaluator readback diagnostics, not official baseline acceptance proof. +Old pick: FAIL; no identifiable target bottle, supports "target unclear". +Old place: FAIL; only a white plane on black background, supports "placement relation missing". +Old open_door: FAIL; close-up of dark/white box corner, no readable door, handle, or action point. +Current pick: PASS; centered blue bottle/flask-like target on tabletop. +Current place: PASS; teal object and yellow placement square are both visible. +Current open_door: PASS/WARN; cabinet/drying-box-like structure, door panel, and orange handle/action point are clearly in frame, but this remains diagnostic evidence rather than official baseline acceptance. +``` + +Updated interpretation: + +```text +runtime_physics_stable=true +runtime_joint_target_matches=true +camera_readback_visible=true +single_handle_door_frame_visible=true +task_render_accepted=false +official_baseline_evaluable=false +primary remaining blocker: render_validation_not_passed / formal task visual QA not signed off +``` + ## Ranked Root-Cause Hypotheses 1. Asset import/layout is not valid for GenManip/Franka task execution. - - The selected objects remain in source LabUtopia lab coordinates, far from the Franka/table workspace. - - The open-door handle is imported as a direct child payload and appears to lose the parent drying-box transform/scale. - - This is a baseline-evaluability blocker, not just a visual defect. + - Static object placement and nested handle discovery have been repaired for the POC. + - The old DryingBox USD/PhysX topology was a baseline-evaluability blocker: non-identity root scale, duplicate rigid link names, invalid inertial attributes, invalid joint body targets, and an extra prismatic DOF. + - The POC runtime path now uses a sanitized surrogate that passes static topology validation and latest runtime joint sanity. + - The latest formal front-camera image makes the DryingBox frame, door panel, and single orange handle/action point visible for PM diagnosis. + - The remaining blocker is the formal gate: diagnostics still report `render_validation_not_passed`, so this cannot be upgraded to task render acceptance or baseline evaluability. 2. Eval camera readback is black before recorder writing. - The first diagnostics proved the black frame existed immediately after `get_eval_camera_data()`. - P0a/P0b follow-up changed pick/place from `readback_black_before_recorder` to `readback_visible`. - Remaining visual failure is near-flat, low-texture output caused by asset/layout scale and placement defects. - Render product binding to `/Camera/LabUtopiaCamera2` appears valid in diagnostics. 3. Task layout is not yet a real LabUtopia reset layout. - - LabUtopia source position ranges are in `task_semantics.yml`, but the runtime task YAMLs have empty `object_config` and null layout. - - The fallback metadata captures the live loaded scene rather than sampling or enforcing LabUtopia task-specific reset poses. + - LabUtopia source position ranges are in `task_semantics.yml`, and task-level visibility isolation now removes non-task objects for POC diagnosis. + - Full reset sampling and formal task render acceptance are still not signed off. 4. Direct-render report screenshots used non-eval camera choices. - The report images were captured with changed lighting and viewpoint and no committed producer script. - This makes them unsuitable for proving task-scene correctness. 5. Overlay wrapper and task semantics are not yet aligned enough for visual or metric claims. - The wrapper payload exposes prims under GenManip-friendly names, but visible mesh bbox, wrapper pose, and semantic coordinates still need a measured consistency check. -6. The `open_door` camera is using a shared POC view instead of the source task's task-specific view. - - Original LabUtopia `level1_open_door.yaml` uses different camera positions from the shared POC camera config. +6. The `open_door` PM diagnostic image is now usable, but the acceptance gate is still closed. + - Articulation state now matches the expected closed target and the formal camera shows the door/frame/single handle. + - The next step is not to overclaim the screenshot; it is to keep `render_validation_not_passed` as the hard blocker and run formal visual QA before any baseline statement. ## Claim Boundary @@ -204,6 +438,8 @@ Allowed now: ```text LabUtopia Franka POC can run through the local GenManip/EBench server-client smoke path and finalize 3/3 tasks with result files. P0a/P0b controlled diagnostics prove camera2 readback is no longer pure black for pick/place after camera_axes/pose and deterministic lighting fixes. +P1 visibility diagnostics make pick readable and place basically readable as PM-facing diagnostic frames. +The latest open_door runtime diagnostic has stable DryingBox joint positions, matches the expected closed target [0.0], only exposes RevoluteJoint, and shows the DryingBox frame, door panel, and one orange handle/action point in the same evaluator readback frame. ``` Not allowed now: @@ -213,7 +449,8 @@ The three task render images are accepted. The eval video path works. Task reset layouts are visually verified. The official Lift2 baseline is evaluable. -The current screenshots prove task correctness. +The current open_door screenshot proves visual task correctness. +The current report display QA proves task visual acceptance. ``` ## Required Next Diagnostics @@ -222,12 +459,14 @@ Run these in an isolated port/run_id so EOS or another engineer's run is not con 1. Fix/validate camera axes handling in the free-camera loader path, then rerun diagnostics with only that variable changed. Done for P0a, with pick/place `readback_visible`. 2. Add deterministic lighting to the runtime overlay, then rerun diagnostics with only lighting changed. Done for P0b, with static validation and pick/place `readback_visible`. -3. Rebuild or supplement the overlay so nested LabUtopia parts preserve composed transforms, especially `DryingBox_01/handle`. -4. Normalize selected task objects into the GenManip/Franka workspace and record explicit task reset layouts instead of relying on fallback live-scene metadata. -5. Add static USD validation for object centers, bounds, scales, nested-part transform preservation, and at least one light. -6. After layout and camera/lighting are fixed, regenerate all three eval-path reset frames and run independent visual QA. -7. Write an evidence manifest for any future PM image. It must include `run_id`, task name, source eval frame path, report image path, sha256, camera config, asset root, commit hash, and `direct_render=false`. +3. Done for static POC layer: rebuild the overlay so selected task objects are in the Franka workspace and `DryingBox_01/handle` remains nested under the DryingBox. +4. Done for POC diagnosis: add task-level visibility isolation so pick/place are PM-readable. +5. Done for POC runtime layer: extend static USD/PhysX validation for DryingBox articulation root scale, duplicate rigid-link basenames, non-finite COM, invalid principal axes, invalid joint body targets, and unexpected extra DOFs. +6. Done for POC runtime layer: build a sanitized DryingBox runtime asset and rerun `level1_open_door` diagnostics until runtime joint positions are finite. +7. Done for POC runtime layer: fix `open_door` closed-start joint initialization, move the handle to the non-hinge side, remove the duplicate orange marker, and formalize the front camera. +8. Next: run browser display QA and formal task visual QA on the updated report set; keep `task_render_accepted=false` until `render_validation` passes. +9. Write an evidence manifest for any future PM image. It must include `run_id`, task name, source eval frame path, report image path, sha256, camera config, asset root, commit hash, and `direct_render=false`. ## Documentation Decision -The weekly report must say the current images failed visual review. The images may remain as historical failed evidence only if clearly marked. They must not be presented as accepted render evidence until P0 render/layout closure passes. +The weekly report must keep the old JPGs as historical failed evidence and present the new PNGs as evaluator readback diagnostics. It may say `pick` is readable and `place` is basically readable for PM diagnosis. It may also say the latest `open_door` DryingBox articulation is diagnosable after removing the earlier joint explosion, that the closed-start target now matches `[0.0]`, and that the DryingBox frame, door panel, and one orange handle/action point are visible in the same evaluator frame. It must also say this is PM-facing diagnostic evidence only. It must not claim `task_render_accepted=true`, official Lift2 baseline evaluability, or open-door visual acceptance until formal task visual QA and `render_validation` pass. diff --git a/docs/records/2026-06-22-labutopia-ebench-weekly-report.md b/docs/records/2026-06-22-labutopia-ebench-weekly-report.md index d8f4f31..9d88555 100644 --- a/docs/records/2026-06-22-labutopia-ebench-weekly-report.md +++ b/docs/records/2026-06-22-labutopia-ebench-weekly-report.md @@ -7,7 +7,7 @@ HTML 版产品汇报页: 本周已经把 LabUtopia 的 Franka POC 跑通到端到端 smoke 阶段:任务可以提交、场景可以加载、三个 level-1 任务可以 reset/step、结果可以落盘、最终状态可以正常 complete。 -这说明当前最关键的“接入链路”已经打通。需要注意的是,这还不是任务求解成功,也不是官方 baseline 成绩;当前 smoke 使用默认动作,所以三个任务分数都是 `0.0`。2026-06-23 复核后还要补充一点:P0a/P0b 已把 pick/place 的 eval `camera2` 从纯黑修到非黑,但当前渲染/布局验收仍是 blocked,三张静态渲染图未通过视觉 QA,不能继续作为 PM-ready 渲染证据。 +这说明当前最关键的“接入链路”已经打通。需要注意的是,这还不是任务求解成功,也不是官方 baseline 成绩;当前 smoke 使用默认动作,所以三个任务分数都是 `0.0`。2026-06-23 到 2026-06-24 最新复核补充:三任务现在都能通过 EBench/evaluator 正常读回非黑渲染图,资产静态坐标也已经从“明显导错”修到合理工作区;最新任务级隐藏和构图后,`pick` 已清楚,`place` 基本可读但还可再放大,`open_door` 已从关节爆值和黑箱角推进到关闭位正确、门板/框架/单个橙色把手可见的诊断状态,但自动 `render_validation` 仍未放行,所以仍不能说官方 baseline 可评已经闭环。 ## 本周完成了什么 @@ -32,7 +32,7 @@ score=0.0 for all tasks ### 2. LabUtopia 场景资产加载已跑通 -当前已经能正确加载 LabUtopia 的实验室场景资产,包括桌面、瓶子、烧杯、托盘、干燥箱、门把手等对象。 +当前已经能加载 LabUtopia 的实验室场景资产,包括桌面、瓶子、烧杯、托盘、干燥箱、门把手等对象。2026-06-23 P1 复核后,瓶子、烧杯、托盘、干燥箱和门把手的静态 USD 坐标/尺寸已经落在 Franka 工作区附近,门把手也不再作为一个独立飞走的顶层物体,而是挂在干燥箱内部路径上。 简单理解,资产加载现在是这样做的: @@ -65,7 +65,7 @@ DryingBox_01 -> obj_DryingBox_01 table -> table ``` -这部分已经通过实际 smoke 验证。这里的“资产加载”只表示 scene overlay 和对象名映射能进入 runtime,不表示 reset 后任务布局、相机画面或可评测视频已经验收。 +这部分已经通过实际 smoke、静态 USD readback 和三任务 evaluator camera readback 验证。这里的“资产加载”仍然只表示 scene overlay、对象名映射和当前相机读回链路能进入 runtime;`pick/place` 当前诊断图已经可读,但还没有正式视觉 QA 签收,`open_door` 的底层物理读数已稳定且回到关闭位,当前正式相机图也能看出门板、框架和单个橙色把手,不过自动 `render_validation` 仍未通过。 ### 3. 结果记录链路已修复 @@ -96,25 +96,37 @@ EOS/其他工程师任务: 8087 ## 有没有渲染图验证? -2026-06-23 复核结论:当前三张静态渲染图未通过视觉验收,不能继续作为“任务渲染已闭环”的证据。它们保留为历史问题样例,用来说明下一步必须修复 eval camera、任务布局和可复现截图链路。 +2026-06-23 到 2026-06-24 最新复核结论:现在已经有三张从 EBench/evaluator 路径读回的非黑图,说明“能不能通过评测链路拍到东西”这个问题已经推进了一大步。旧 JPG 保留为历史失败样例,新 PNG 是更可信的 eval-path 诊断图:`pick` 已清楚,`place` 基本可读,`open_door` 已从物理爆值和黑箱角推进到关闭位正确、门板/框架/单个橙色把手可识别,但自动 `render_validation` 仍未签收。 ```text docs/records/evidence/2026-06-22-labutopia-ebench-weekly-report/assets/labutopia-franka-level1-pick.jpg docs/records/evidence/2026-06-22-labutopia-ebench-weekly-report/assets/labutopia-franka-level1-place.jpg docs/records/evidence/2026-06-22-labutopia-ebench-weekly-report/assets/labutopia-franka-level1-open-door.jpg +docs/records/evidence/2026-06-22-labutopia-ebench-weekly-report/assets/labutopia-franka-level1-pick-eval-readback-p1.png +docs/records/evidence/2026-06-22-labutopia-ebench-weekly-report/assets/labutopia-franka-level1-place-eval-readback-p1.png +docs/records/evidence/2026-06-22-labutopia-ebench-weekly-report/assets/labutopia-franka-level1-open-door-eval-readback-p1.png ``` -这三张图来自同一个 LabUtopia Franka POC 任务包和同一个 LabUtopia overlay 资产根目录,但它们不是正常 eval recorder 产物,而是后补 direct-render 静态图。视觉复核后判定:`level1_pick` 只能算 WARN,`level1_place` 和 `level1_open_door` 是 FAIL。 +旧的三张 JPG 保留为历史失败样例。新的三张 PNG 来自正常 evaluator camera readback,不是 direct-render 截图;它们更适合说明今天的真实状态:链路能拍到场景,pick/place 通过任务级隐藏后已经能让 PM 看懂任务目标,open_door 已从“只看到黑箱角/看不见把手”推进到“关闭位正确、门板/框架/单把手可识别但仍未验收”。 + +给产品经理看的前后对照: + +| 任务 | 旧图问题 | 已经做的修复 | 现在还差什么 | +| --- | --- | --- | --- | +| `level1_pick` | 抓取目标不明显,只看图无法判断“要抓哪个瓶子” | 修正 eval camera readback、相机朝向、光照,把瓶子归一到 Franka 工作区,并在 pick 任务里隐藏烧杯、托盘、干燥箱等非目标物体 | 当前新图已经能让 PM 看懂“抓这个蓝色瓶子”;还差正式 pixel/视觉 QA 签收 | +| `level1_place` | 看不出源物体和目标托盘的关系,不像一个放置任务 | 修正托盘、烧杯、瓶子坐标和颜色标记,并在 place 任务里隐藏瓶子和干燥箱,只保留烧杯与目标托盘 | 当前新图能看懂“把烧杯放到黄色托盘附近”;后续可以再拉近相机 | +| `level1_open_door` | 几乎只拍到黑色箱体角,门板、把手、铰链和动作目标都不清楚 | 把门把手恢复为干燥箱内部子部件,不再作为独立物体飞走;生成 DryingBox runtime surrogate,固定底座并只保留一个门关节;再补关节初始目标回放、把手位置修正、删除重复橙色块和任务专用正面相机 | 最新图中关节已回到期望关闭位 `0.0`,门板、框架和单个橙色把手可识别;但自动 `render_validation` 仍未通过,下一步要把视觉 QA 和 Lift2 baseline gate 接上 | 需要特别说明: -- `run_id=labutopia_franka_render_smoke_20260622_150819` 跑过保存帧 smoke,但 eval recorder 的 `camera2` 帧当前是黑屏。 -- 三个任务各 32 帧的 eval `camera2` 抽样统计均为纯黑:RGB min/max/mean 都是 0。 -- 2026-06-23 新增 runtime 诊断后,三个任务都定位为 `readback_black_before_recorder`:黑帧在 `get_eval_camera_data()` 后就已经存在,recorder 写盘不是根因。 -- 2026-06-23 P0a/P0b 修复后,`level1_pick` 和 `level1_place` 已变为 `readback_visible`,说明 camera axes/pose 和 deterministic lighting 对 pick/place 的黑帧源头有效;但 `level1_open_door` 尚未复验通过,新帧也几乎是纯灰低纹理,仍不能作为任务渲染验收图。 -- 进一步检查发现资产/layout 也有硬问题:瓶子/烧杯/托盘仍在源 LabUtopia 坐标的 x≈8-10,干燥箱在 x≈45,把手甚至出现在百米量级坐标。这说明不能只调相机,必须同步修复导入布局。 -- 当前 task YAML 仍是 `object_config: {}`、`layout_config.type: null`,还没有把 LabUtopia 原始 position_range 真正落成 GenManip reset-time 布局。 -- 因此下一步必须先修复 eval camera/readback、任务布局和可复现截图脚本,再做 reset 后关键帧验收。 +- 旧问题:早期 eval recorder `camera2` 纯黑,根因在 recorder 写盘之前的 readback 阶段。 +- 已推进:P0/P1 后,`level1_pick`、`level1_place`、`level1_open_door` 都是 `readback_visible`。 +- 已推进:静态 USD readback 显示瓶子、烧杯、托盘、干燥箱、门把手的坐标和尺寸合理;门把手路径是 `/World/labutopia_level1_poc/obj_obj_DryingBox_01/handle`,不是独立飞走物体。 +- 已推进:任务级隐藏后,`level1_pick` 当前图只保留目标瓶,`level1_place` 当前图只保留烧杯和目标托盘,PM 可以看懂任务目标和关系。 +- 已推进:`level1_open_door` 旧版本运行期关节读数曾出现 `1.573e13` 量级;最新诊断只暴露 `RevoluteJoint`,关节读数为 `0.0`,仿真能完成 readback,问题已经从“爆掉不可看/只看到黑箱角”收敛到“门板、框架和单个橙色把手可识别”。 +- 已复核:独立视觉审阅认为当前 old-vs-current 对照可用于 PM 汇报;旧图确实支持“目标不清、放置关系缺失、看不到门把手”的问题描述,当前 open_door 候选图可以作为诊断证据。 +- 仍 blocked:`level1_open_door` 当前图只能作为 PM 诊断图,不能作为官方 baseline 可评证据,因为 diagnostics claim boundary 仍是 `render_validation_not_passed`、`task_render_accepted=false`、`official_baseline_evaluable=false`。 +- 因此下一步不是继续宣传截图,而是给三任务补正式视觉 QA,并把 Lift2 baseline 所需的复合资产和官方 runner gate 接上。 ## 本周遇到的问题和解决方式 @@ -123,9 +135,10 @@ docs/records/evidence/2026-06-22-labutopia-ebench-weekly-report/assets/labutopia | 资产根目录不对 | 系统一开始会去错误目录找 LabUtopia 场景 | 增加 LabUtopia manifest 识别逻辑,运行时切换到 overlay asset root | 已解决 | | 缺少 `meta_info.pkl` | 传统 GenManip 任务依赖采集包里的元信息,LabUtopia POC 没有这份文件 | 针对 LabUtopia POC,从实时场景里自动生成最小可用元信息 | 已解决 | | camera cleanup 字段缺失 | 切换任务时 camera 清理报字段错误 | 补齐 camera 配置里的 cleanup flags | 已解决 | -| eval recorder `camera2` 黑屏 | 保存过程帧时 `camera2` 输出纯黑 | P0a/P0b 已修 camera axes/pose 和 deterministic lighting;pick/place 已从纯黑变为非黑 readback,open_door 尚未复验通过 | pick/place 部分解除,整体仍 blocked | -| 当前三张渲染图未通过视觉验收 | `pick` 目标不清,`place` 缺 beaker,`open_door` 缺门/把手/开门状态 | 降级为历史失败样例,新增调研记录和 P0-P2 修复计划 | 待重拍 | -| 资产导入/layout 坐标不闭环 | 任务物体仍在源 lab 坐标,open-door handle 导入后坐标异常放大 | 需要重建或补充 overlay,保留 nested part composed transform,并把任务物体归一到机器人工作空间 | 待闭环 | +| eval recorder `camera2` 黑屏 | 保存过程帧时 `camera2` 输出纯黑 | 已修 camera axes/pose、deterministic lighting 和工作区相机;三任务当前 eval readback 均非黑 | 黑屏解除 | +| 当前新图仍未全部验收 | `pick` 已清楚,`place` 基本可读但还可放大;`open_door` 已能读回且关闭位正确,门板/框架/单把手可识别,但自动 `render_validation` 仍未放行 | pick/place 补正式视觉 QA;open_door 把当前正面图纳入同一验收门槛,并继续接 Lift2 baseline 所需的复合资产和官方 runner gate | 部分通过诊断 | +| `open_door` 运行期物理不稳定 | 旧版本 runtime articulation joint position 爆到 `1.573e13`,并伴随 PhysX transform warning | 已用 DryingBox runtime surrogate、固定底座、对齐后的门铰链和关节目标回放修复;最新读数为 `0.0 rad` 且只暴露 `RevoluteJoint` | 物理层已解决 | +| 资产导入/layout 静态坐标 | 之前任务物体在源 lab 坐标,handle 变成异常放大的独立物体 | P1 已把对象归一到 Franka 工作区,并把 handle 保留为 DryingBox 内部子路径 | 静态层已推进 | | 最终进度不 complete | 任务已跑完,但进度没有写入,导致客户端最后等待超时 | 任务结束时写最小 `result_info.json`,并修复进度统计 | 已解决 | | 后处理异常可能被误记为完成 | 如果后处理报错,不能用 0 分兜底伪装成成功 | 增加 fail-fast 逻辑和回归测试 | 已解决 | | 端口/结果混淆风险 | EOS 侧也有人在跑任务,担心互相影响 | 使用独立 worktree、独立 run_id、独立端口 18088,并复核 8087 状态 | 已解决 | @@ -137,9 +150,9 @@ docs/records/evidence/2026-06-22-labutopia-ebench-weekly-report/assets/labutopia | LabUtopia 场景接入 | 已跑通 | 场景资产能加载,任务链路能跑完 | | Franka POC smoke | 已完成 | 3 个任务全部 complete | | 结果落盘 | 已完成 | per-task 和 final result 都能写出 | -| 渲染图/视频验收 | 未通过 | P0a/P0b 已让 pick/place eval readback 非黑;但画面仍近似纯灰,三张直渲图视觉 QA 不合格,视频和 reset 后任务布局仍需闭环 | +| 渲染图/视频验收 | 部分通过诊断 | 三任务 eval readback 已非黑;pick 已清楚,place 基本可读,open_door 已可诊断且关闭位正确、门板/框架/单把手可识别,但自动 `render_validation` 仍未签收 | | 任务求解能力 | 未验证 | 当前默认动作得分 0.0,不代表策略能力 | -| 官方 baseline | 未进入本周结论 | 后续单独讨论和规划 | +| 官方 baseline | 不能宣称可评 | 三任务正式视觉验收、Lift2 复合资产和官方 runner 还没闭环,diagnostics claim boundary 仍是 `official_baseline_evaluable=false` | ## 验证证据 @@ -156,13 +169,16 @@ total_episodes=3 ```text python -m pytest tests/labutopia_poc -q -34 passed, 1 skipped +56 passed, 1 skipped python -m pytest tests/labutopia_poc/test_render_diagnostics_contract.py -q 2 passed python standalone_tools/labutopia_poc/validate_task_package.py LabUtopia task package validation OK + +report display QA +Chromium headless desktop/mobile long screenshots passed; six report images exist and the DOM contains the old-image section, new eval readback section, latest single-handle `open_door`, and `render_validation_not_passed` boundary text. Evidence: `/tmp/labutopia_weekly_old_new_review_20260624/desktop_full_report.png`, `/tmp/labutopia_weekly_old_new_review_20260624/mobile_full_report_long.png`, `/tmp/labutopia_weekly_old_new_review_20260624/audit.json`. This checks report display only, not task visual acceptance. ``` 结果文件: @@ -180,8 +196,10 @@ saved/eval_results/ebench/labutopia_franka_smoke_clean8_20260622_100208/.../leve - eval recorder `camera2` frames: black, sampled RGB min/max/mean all 0 - runtime diagnostics: `level1_pick/place/open_door` all `readback_black_before_recorder` - evidence manifest: [docs/labutopia_lab_poc/evidence_manifests/render_diagnostics_20260623.json](../labutopia_lab_poc/evidence_manifests/render_diagnostics_20260623.json) -- P0a/P0b follow-up: `level1_pick/place` now `readback_visible`, but low-texture visual QA is still failed +- P1 follow-up: `level1_pick/place/open_door` now `readback_visible`; latest task-level hiding makes pick readable and place basically readable, while open_door is diagnosable with closed joint target `[0.0]` and visible door/frame/single orange handle, but diagnostics still report `render_validation_not_passed` +- independent image-only review: old-vs-current comparison is PASS for PM-facing diagnostic evidence; open_door is PASS/WARN as diagnostic evidence and must not be described as official baseline acceptance - P0a/P0b evidence manifest: [docs/labutopia_lab_poc/evidence_manifests/render_p0a_p0b_20260623.json](../labutopia_lab_poc/evidence_manifests/render_p0a_p0b_20260623.json) +- P1 evidence manifest: [docs/labutopia_lab_poc/evidence_manifests/render_p1_asset_layout_20260623.json](../labutopia_lab_poc/evidence_manifests/render_p1_asset_layout_20260623.json) - static direct-render evidence: visual QA failed on 2026-06-23 - investigation: [docs/labutopia_lab_poc/render_visual_investigation_20260623.md](../labutopia_lab_poc/render_visual_investigation_20260623.md) - plan: [docs/superpowers/plans/2026-06-23-labutopia-ebench-render-layout-closure.md](../superpowers/plans/2026-06-23-labutopia-ebench-render-layout-closure.md) @@ -192,14 +210,15 @@ saved/eval_results/ebench/labutopia_franka_smoke_clean8_20260622_100208/.../leve 建议顺序: -1. P0a:已修相机 axes/pose 支持,让 pick/place 的 `camera2` 从 eval readback 直接产出非黑帧。 -2. P0b:已补 deterministic lighting,避免只靠 direct-render 临时灯光。 -3. P1:重建或补充 LabUtopia overlay,把任务对象和 nested handle 保持正确 transform,并放到 Franka 工作空间。 -4. P2:用正常 eval-path 重新抓三任务关键帧,写 evidence manifest,并通过独立视觉 QA。 -5. P3:图像验收通过后再替换 HTML 里的三张图,并恢复 PM 可见性汇报。 -6. P4:再进入 Lift2 复合资产预检和官方 baseline 路线。 +1. P0a/P0b:已修相机 readback 和 deterministic lighting,让 eval path 不再黑屏。 +2. P1a:已把对象静态坐标和 nested handle 归一到 Franka 工作区。 +3. P1b:已用 DryingBox surrogate 和关节目标回放修复 `open_door` runtime articulation/PhysX 爆值与关闭位问题。 +4. P1c:pick/place 补正式视觉 QA;open_door 使用最新单把手正式相机图继续验收,让门板、门面、把手和任务动作点在 evaluator 图里清楚可见。 +5. P2:用正常 eval-path 重新抓三任务关键帧,写 evidence manifest,并通过独立视觉 QA。 +6. P3:视觉和物理验收通过后,再进入 Lift2 复合资产根目录和官方 runner 发现。 ## 新增调研和计划文档 - [docs/labutopia_lab_poc/render_visual_investigation_20260623.md](../labutopia_lab_poc/render_visual_investigation_20260623.md) - [docs/superpowers/plans/2026-06-23-labutopia-ebench-render-layout-closure.md](../superpowers/plans/2026-06-23-labutopia-ebench-render-layout-closure.md) +- [docs/labutopia_lab_poc/evidence_manifests/render_p1_asset_layout_20260623.json](../labutopia_lab_poc/evidence_manifests/render_p1_asset_layout_20260623.json) diff --git a/docs/records/evidence/2026-06-22-labutopia-ebench-weekly-report/assets/labutopia-franka-level1-open-door-eval-readback-p1.png b/docs/records/evidence/2026-06-22-labutopia-ebench-weekly-report/assets/labutopia-franka-level1-open-door-eval-readback-p1.png new file mode 100644 index 0000000000000000000000000000000000000000..3c5cbd66ea307dc7452b927ca878e4f8aa7c0127 GIT binary patch literal 98720 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

    }QB$Mj#|=H~Lhs!pbni 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≫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 literal 0 HcmV?d00001 diff --git a/docs/records/evidence/2026-06-22-labutopia-ebench-weekly-report/assets/labutopia-franka-level1-pick-eval-readback-p1.png b/docs/records/evidence/2026-06-22-labutopia-ebench-weekly-report/assets/labutopia-franka-level1-pick-eval-readback-p1.png new file mode 100644 index 0000000000000000000000000000000000000000..ea9ce2c2ca3ea9da50ed5e327e663bb7a6501e1d GIT binary patch literal 60884 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Π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 literal 0 HcmV?d00001 diff --git a/docs/records/evidence/2026-06-22-labutopia-ebench-weekly-report/index.html b/docs/records/evidence/2026-06-22-labutopia-ebench-weekly-report/index.html index 69b6920..78eaceb 100644 --- a/docs/records/evidence/2026-06-22-labutopia-ebench-weekly-report/index.html +++ b/docs/records/evidence/2026-06-22-labutopia-ebench-weekly-report/index.html @@ -51,6 +51,11 @@ padding: 2px 5px; overflow-wrap: anywhere; } + td code, + th code { + white-space: nowrap; + overflow-wrap: normal; + } .topbar { position: sticky; top: 0; @@ -298,13 +303,13 @@ background: #ffffff; overflow: hidden; } - .render-card img { - display: block; - width: 100%; - aspect-ratio: 16 / 9; - object-fit: cover; - background: #101820; - } + .render-card img { + display: block; + width: 100%; + aspect-ratio: 1 / 1; + object-fit: contain; + background: #eef2f6; + } .render-card figcaption { min-height: 116px; padding: 12px; @@ -312,12 +317,22 @@ font-size: 13px; line-height: 1.45; } - .render-card figcaption b { - display: block; - margin-bottom: 5px; - color: var(--ink); - font-size: 15px; - } + .render-card figcaption b { + display: block; + margin-bottom: 5px; + color: var(--ink); + font-size: 15px; + } + .source-path { + display: block; + margin-top: 8px; + padding-top: 8px; + border-top: 1px solid var(--border); + color: #66768a; + font-size: 11px; + line-height: 1.35; + overflow-wrap: anywhere; + } .timeline { display: grid; grid-template-columns: repeat(5, minmax(150px, 1fr)); @@ -392,17 +407,18 @@

    Weekly report · 2026-06-22 · PM-facing evidence page

    LabUtopia x EBench接入周报

    -

    本周 Franka POC 已经跑通端到端 smoke:任务能提交,LabUtopia 场景能加载,三个 level-1 任务能 reset/step,结果能落盘并正常 complete。P0a/P0b 已把 pick/place 的 eval camera2 从纯黑修到非黑;渲染/布局验收仍是 blocked,当前三张渲染图未通过视觉验收。

    +

    本周 Franka POC 已经跑通端到端 smoke:任务能提交,LabUtopia 场景能加载,三个 level-1 任务能 reset/step,结果能落盘并正常 complete。2026-06-23 到 2026-06-24 的 P1 复核后,三任务 eval camera2 都能读回非黑画面,静态资产坐标也已归一到 Franka 工作区;最新复核显示 pick 已经清楚,place 基本可读但还可再放大,open_door 已从关节爆值和黑箱角推进到“门板、框架、单个橙色把手在同一张图里可见”的可汇报诊断状态,但自动 render_validation 仍未放行,不能宣称官方 baseline 可评。

    wiring smoke complete 3/3 episodes finalized + 3/3 readback visible score=0.0 expected - render/layout blocked + open_door visual QA warn
    @@ -428,18 +444,18 @@

    当前进展总览

    测试
    -
    34 passed
    -
    1 skipped,诊断合同 2 passed,validator 通过。
    +
    56 passed
    +
    1 skipped,validator 通过。
    渲染证据
    -
    未通过
    -
    P0a/P0b 后 pick/place 非黑;画面仍近似纯灰,三张直渲图视觉 QA 不合格。
    +
    3/3 可诊断
    +
    pick 已清楚,place 基本可读;open_door 关闭位正确且门板/单把手可见,但自动验收仍未签收。
    官方 Lift2 baseline
    -
    下一阶段
    -
    需要先完成复合资产根目录和官方 runner 发现。
    +
    不可宣称
    +
    视觉验收、Lift2 复合资产和官方 runner 还没闭环。
    @@ -483,9 +499,9 @@

    本周完成了什么

    渲染图验收 - 未通过视觉 QA - 当前三张图保留为失败样例:pick WARN,place/open_door FAIL。 - 不能作为 PM-ready 渲染证据;需要 eval-path reset 帧重拍。 + 部分可读 + 当前三张 eval readback 图为非黑:pick 已清楚,place 基本可读但可再放大,open_door 关闭位已回到 0.0,门板、框架和单个橙色把手已可识别。 + 三张都可作为当前诊断图;正式验收仍要等自动 render_validation、三任务视觉 QA 和 Lift2 baseline gate 通过。 @@ -560,34 +576,109 @@

    资产加载是怎么做的

    已经闭环的部分

    -

    LabUtopia 场景 overlay 能被 Franka POC 加载,相关对象名能通过运行时对象映射进入 evaluator。这只证明接入路径,不证明视觉截图、reset 布局或可评测视频。

    +

    LabUtopia 场景 overlay 能被 Franka POC 加载,相关对象名能通过运行时对象映射进入 evaluator。P1 后,静态 USD 坐标也已经归一到工作区,门把手保留为干燥箱内部子路径。这证明接入路径和静态资产组织已经推进,不证明任务图视觉验收。

    还没闭环的部分

    -

    Lift2 官方 baseline 不能只用 scene overlay,它还需要默认资产里的 robot_usds/lift2miscs/curobo。这一步排在 camera readback、资产/layout 和 P2 渲染证据闭环之后,再做“复合资产根目录”。

    +

    pick/place 的任务级构图已经明显推进,但还没做正式 PASS 签收;open_door 的运行期 articulation 已从爆值推进到稳定关闭位,最新正面图里门板、外框和单个橙色把手已可识别,但自动 render_validation 仍保持阻断。Lift2 官方 baseline 还需要默认资产里的 robot_usds/lift2miscs/curobo,这一步必须排在视觉 gate 之后。

    EBench 加载后的三任务渲染图

    -

    2026-06-23 复核结论:当前三张渲染图未通过视觉验收,不能作为任务渲染已闭环的证据。下面保留为历史失败样例,方便说明 P0 修复范围。

    -
    -

    渲染证据状态

    -

    早期 eval recorder 保存的 camera2 帧为纯黑;2026-06-23 runtime 诊断确认三个任务都是 readback_black_before_recorder,recorder 写盘不是根因。P0a/P0b 后 level1_pick/level1_place 已变为 readback_visible,但新帧几乎纯灰、低纹理,仍不能作为任务验收图。三张 direct-render 静态图不是正常 eval recorder 产物,也没有可复现生产脚本。视觉 QA 结果为 level1_pick=WARNlevel1_place=FAILlevel1_open_door=FAIL

    +

    2026-06-23 到 2026-06-24 P1 复核结论:旧图和新图都放在这里,方便做前后对照。旧图是历史失败样例,用来说明最开始“相机拍不到、资产位置不对、任务目标看不懂”的问题;新图来自正常 EBench/evaluator camera readback,用来说明我们已经把底层链路推进到“能拍到真实场景”。当前结论是 pick 已清楚、place 基本可读、open_door 已从黑箱角和物理爆值推进到关闭位正确,且门板、框架、单个橙色把手可识别;但自动验收仍未通过。

    +
    +
    +

    旧图要表达的问题

    +

    旧图的核心问题不是“画面不够漂亮”,而是“看图不能判断任务是否成立”:抓取图像白色设备局部,不像要抓的蓝色瓶;放置图只剩黑背景和白色平面,看不到要放的物体;开门图只拍到黑色箱体角,看不到门、把手和动作点。这类图只能说明我们曾经拍到过某个局部,不能说明 EBench 评测链路已经可用。

    +
    +
    +

    我们是怎么解决的

    +

    先修相机和光照,让评测相机真的能读回画面;再把瓶子、烧杯、托盘、干燥箱从原始实验室坐标搬回 Franka 工作区;然后按任务隐藏无关物体。对 open_door 还额外做了干燥箱运行期替身资产:固定底座、只保留一个门关节、对齐铰链;随后补关节初始目标回放,把把手移到非铰链侧,删除额外橙色块,并换成正面任务相机,让门板、框架和单把手能同时看见。

    +
    +
    +
    +

    渲染证据状态

    +

    当前三任务均为 readback_visible。最新任务级隐藏后,level1_pick 只保留抓取瓶子,目标清楚;level1_place 同时保留烧杯和黄色目标托盘,任务关系基本可读;level1_open_door 能完成 readback,关节位置已回到期望关闭位 0.0,正面图里箱体框架、门板和单个橙色把手已可识别;但诊断输出仍显示 render_validation_not_passed,正式视觉 QA 仍未通过。

    + +

    旧图:历史失败样例

    +

    这三张旧图不是最终验收图。它们的问题很直观:像是在看一个没摆好的舞台,有的只拍到局部,有的看不到任务目标,有的看不出机器人到底要抓、放还是开门。更关键的是,旧图不是稳定的 eval-path 证据,不能代表模型评测时真实看到的画面。

    - level1_pick task render after EBench LabUtopia asset loading -
    level1_pick · WARN目标物不够清晰,材质和裁切让 pick object 难以确认;只能作为失败样例,不能作为验收图。
    + old level1_pick failed render sample +
    旧 level1_pick · 失败样例画面主要是白色设备局部和大面积灰面,目标瓶不清楚,产品无法一眼判断“机器人要抓哪个瓶子”。
    - level1_place target platform render after EBench LabUtopia asset loading -
    level1_place · FAIL只看到 target platform,看不到 beaker,也看不到放置关系;必须通过 eval-path reset 帧重拍。
    + old level1_place failed render sample +
    旧 level1_place · 失败样例画面只剩黑背景和一个白色平面,看不到要放的物体,也看不到目标托盘关系。
    - level1_open_door drying box render after EBench LabUtopia asset loading -
    level1_open_door · FAIL只看到箱体角落,看不到门、把手、铰链或开门状态;必须使用任务相关视角重拍。
    + old level1_open_door failed render sample +
    旧 level1_open_door · 失败样例几乎只拍到黑色箱体角,门板、把手、铰链和开门动作点都不明确。
    +
    +
    + +

    旧图问题怎么解决

    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    任务旧图问题已经做的修复现在还差什么
    level1_pick旧图里抓取目标不明显,PM 只看图无法判断“要抓哪个瓶子”。修正 eval camera readback、相机朝向、光照,把瓶子归一到 Franka 工作区,并在 pick 任务里隐藏烧杯、托盘、干燥箱等非目标物体。当前新图已经能让 PM 看懂“抓这个蓝色瓶子”;还差正式 pixel/视觉 QA 签收,才能把 task_render_accepted 改成 true。
    level1_place旧图看不出源物体和目标托盘的关系,不像一个“把东西放到哪里”的任务。修正托盘、烧杯、瓶子坐标和颜色标记,并在 place 任务里隐藏瓶子和干燥箱,只保留烧杯与目标托盘。当前新图能看懂“把烧杯放到黄色托盘附近”;后续可以再拉近相机,让目标关系更大、更稳定。
    level1_open_door旧图看不清干燥箱门和门把手,无法判断开门动作目标。把门把手恢复为干燥箱内部子部件,不再作为独立物体飞走;同时生成运行期干燥箱替身资产,固定底座并只保留一个门关节;再补关节初始目标回放、把手位置修正、删除重复橙色块和任务专用正面相机。最新图中关节已回到期望关闭位 0.0,门板、框架和单个橙色把手可见;但自动 render_validation 仍未通过,下一步要把视觉 QA 和 Lift2 baseline gate 接上。
    +
    + +
    +
    +

    已经解决了什么

    +

    先把黑屏定位到相机 readback,不是 recorder 写盘;然后修了 camera axes/pose 和确定性光照。接着把 LabUtopia 原始坐标里的瓶子、烧杯、托盘、干燥箱归一到 Franka 工作区,并把门把手恢复成干燥箱内部子部件。最后按任务隐藏非目标资产,让 pick/place 的图不再被无关物体干扰。

    +
    +
    +

    还没解决什么

    +

    pick/place 已经从“看不懂任务”推进到“PM 可以看懂当前诊断图”;但还没做正式验收签收。open_door 已从爆值和黑箱角推进到关闭位正确、门板/框架/单把手可见,不过自动验收仍未放行,所以仍不能说官方 baseline 可评。

    +
    +
    + +

    新图:当前 EBench eval readback

    +

    这三张是当前更可信的诊断图,因为它们来自 evaluator camera readback。它们证明底层加载和相机读回已经推进,但也暴露了下一步要解决的正式视觉验收问题。

    +
    +
    + level1_pick eval readback after EBench LabUtopia asset loading +
    level1_pick · 当前可读任务级隐藏后只保留桌面和蓝色瓶子,抓取目标已经清楚;下一步补正式视觉 QA 后才能签收。source: saved/diagnostics/labutopia_p1_visibility_pick_20260623_175050/readback_after_get_eval_camera_data/camera2/00000.png
    +
    +
    + level1_place eval readback after EBench LabUtopia asset loading +
    level1_place · 基本可读烧杯和黄色目标托盘同时可见,放置关系可以解释;后续可把相机再拉近,提升 PM 截图质感。source: saved/diagnostics/labutopia_p1_visibility_place_20260623_175232/readback_after_get_eval_camera_data/camera2/00000.png
    +
    +
    + level1_open_door eval readback after EBench LabUtopia asset loading +
    level1_open_door · 可诊断 / 未验收最新正式配置诊断中只暴露 RevoluteJoint,关节位置已回到期望关闭位 0.0;门板、框架和单个橙色把手在同一 evaluator readback 图里可见,但诊断仍标记 render_validation_not_passed,不能作为官方 baseline 可评证据。source: saved/diagnostics/labutopia_p1_open_door_single_handle_formal_20260624_0001/readback_after_get_eval_camera_data/camera2/00000.png
    @@ -626,22 +717,32 @@

    验证证据

    render diagnostics - P0a/P0b advanced - 旧诊断:三个任务均为 readback_black_before_recorder。新诊断:pick/placereadback_visible,但低纹理未过视觉验收。 + P1 advanced + 旧诊断:三个任务均为 readback_black_before_recorder。P1 诊断:三个任务均为 readback_visible;最新 open_door 运行期诊断已从爆值推进到关闭位正确、门板/框架/单把手可见,但 render_validation 仍未通过。 P0a/P0b manifest pick/place visible render_p0a_p0b_20260623.json 记录 camera axes/pose、deterministic light 和 pick/place 非黑 readback。 + + P1 asset/layout manifest + 3/3 visible, 2 readable + 1 warn + render_p1_asset_layout_20260623.json 记录三任务 eval readback 图、静态 USD 坐标、任务级隐藏和 open_door 最新关闭位/单把手正面诊断状态。 + visual QA - WARN / FAIL / FAIL - pick WARN,place FAIL,open_door FAIL;详见 2026-06-23 调研记录。 + READABLE / READABLE / WARN + pick 已清楚,place 基本可读;open_door 可诊断且关闭位正确,门板/框架/单把手可识别,但自动验收仍未签收。 + + + report display QA + passed + Chromium headless 桌面/移动端长截图已检查;6 张图片文件均存在,DOM 包含旧图、新图、单把手 open_door 和 render_validation_not_passed 说明。这个检查只证明页面展示没坏,不等于任务视觉验收通过。 pytest - 34 passed, 1 skipped + 56 passed, 1 skipped 覆盖资产 override、fallback metadata、结果落盘、后处理 fail-fast 和 render diagnostic 合同等回归点。 @@ -698,20 +799,32 @@

    问题和处理结果

    eval recorder camera2 黑屏 保存过程帧时 camera2 输出为黑屏。 - P0a/P0b 已修 camera axes/pose 和 deterministic lighting;pick/place 已从纯黑变为非黑 readback,open_door 尚未复验通过。 - pick/place 部分解除,整体仍 blocked + 已修 camera axes/pose、deterministic lighting 和工作区相机;P1 三任务均能从 eval camera 读回非黑图。 + 黑屏解除 + + + 旧图不可作为验收证据 + 旧图看不清任务目标,也不是稳定 eval-path 证据。 + 旧图保留为历史失败样例;新图改用正常 evaluator readback,并清楚标注哪些已可读、哪些仍 blocked。 + 已解释清楚 + + + 当前新图仍未全部验收 + pick 已清楚,place 基本可读但还可放大;open_door 已能读回且关闭位正确,门板/框架/单把手可识别,但自动 render_validation 仍未放行。 + pick/place 补正式视觉 QA;open_door 把当前正面图纳入同一验收门槛,并继续接 Lift2 baseline 所需的复合资产和官方 runner gate。 + 部分通过诊断 - 当前三张渲染图未通过视觉验收 - pick 目标不清,place 缺 beaker,open_door 缺门/把手/开门状态。 - 降级为历史失败样例;新增调研记录和 P0-P2 render/layout closure 计划。 - 待重拍 + 资产导入/layout 静态坐标 + 早期对象在源 lab 坐标,门把手会像独立物体一样飞到远处。 + P1 已把对象归一到 Franka 工作区,并把 handle 保留为 DryingBox 内部子路径。 + 静态层已推进 - 资产导入/layout 坐标不闭环 - 任务物体仍在源 lab 坐标,open-door handle 导入后坐标异常放大。 - 需要重建或补充 overlay,保留 nested part composed transform,并把任务物体归一到机器人工作空间。 - 待闭环 + open_door runtime 物理不稳定 + 旧版本 runtime articulation joint position 爆到 1.573e13,并伴随 PhysX transform warning。 + 已用稳定的 DryingBox runtime surrogate、固定底座、对齐后的门铰链和关节目标回放修复;最新读数为 0.0 rad 且只暴露 RevoluteJoint。 + 物理层已解决 最终进度不 complete @@ -739,9 +852,11 @@

    现在可以说

  • LabUtopia Franka POC 已经能通过 GenManip/EBench server-client 链路本地跑完。
  • 三个 level-1 任务能完成 reset/step/finalize,并生成 per-task 和 final result。
  • Franka POC 的 LabUtopia scene overlay 加载路径已经验证。
  • -
  • 当前三张渲染图已经完成复核,并明确判定为不合格历史样例。
  • -
  • 黑帧边界已经定位到 camera readback 之后、recorder 写盘之前。
  • -
  • P0a/P0b 后,pick/place 的 eval camera2 已从纯黑变为非黑 readback。
  • +
  • 旧图问题已经明确:看不清任务目标,也不是稳定 eval-path 证据。
  • +
  • 黑帧边界已经定位到 camera readback 之后、recorder 写盘之前。
  • +
  • P1 后,三任务 eval camera2 都已从纯黑变为非黑 readback。
  • +
  • 静态 USD 坐标和门把手层级已经明显推进,handle 不再作为独立飞走物体。
  • +
  • 任务级隐藏后,pickplace 的当前诊断图已经能让 PM 看懂目标和关系。
  • @@ -749,8 +864,8 @@

    现在不能说

    • 不能说任务已经求解成功,当前默认动作得分是 0.0
    • 不能说官方 Lift2 baseline 已经可评,Lift2 复合资产和官方 runner 还没闭环。
    • -
    • 不能说画面/视频/任务布局已经验收;当前三张图视觉 QA 未通过,新 eval readback 仍接近纯灰。
    • -
    • 不能说资产导入布局已闭环;当前对象坐标和 open-door handle transform 仍需修复。
    • +
    • 不能说三任务画面/视频/任务布局已经全部验收;open_door 仍未通过。
    • +
    • 不能说 open_door 已经视觉可评;当前图可以做 PM 诊断说明,但诊断仍标记 render_validation_not_passed
    @@ -762,17 +877,27 @@

    接下来几步

    P0

    Camera readback 源头修复

    -

    已完成 camera axes/pose 和 deterministic lighting 的 P0a/P0b 修复;pick/place 已经非黑。当前剩余问题是低纹理、对象不可识别,需要进入 P1 资产/layout。

    +

    已完成 camera axes/pose、deterministic lighting 和工作区相机修复;三任务已经非黑。

    +
    +
    + P1a +

    静态资产/layout 正规化

    +

    已把任务对象归一到 Franka 工作区,保留 nested handle transform,并用静态 USD 校验 object center/bounds/scale/light。

    - P1 -

    资产/layout 正规化

    -

    重建或补充 LabUtopia overlay,保留 nested part transform,把任务对象归一到 Franka 工作空间,并用静态 USD 校验 object center/bounds/scale/light。

    + P1b +

    open_door 视角和门缝增强

    +

    物理爆值和关闭位问题已通过 DryingBox surrogate 与关节目标回放推进到可诊断;最新正面图已经让门板、框架和单个橙色把手可识别。下一步把这张图纳入正式视觉 QA,并继续让自动 render_validation 成为是否可评的硬门槛。

    +
    +
    + P1c +

    任务级相机/构图

    +

    pick/place 先补正式视觉 QA;open_door 使用最新单把手正式相机图继续验收,要求门板、门面、把手和任务动作点在 eval 图中清楚可见。

    P2

    Eval-path 重拍验收

    -

    用正常 eval recorder 重新抓三任务关键帧,写 evidence manifest,并要求三张报告图独立视觉 QA 全部 PASS

    +

    用正常 evaluator/readback 路径重新抓关键帧,写 evidence manifest,并要求三任务独立视觉 QA 全部 PASS 后再签收。

    P3 @@ -826,7 +951,7 @@

    相关文档

    Render visual investigation - 6/23 多角度复核:三张旧图未通过视觉验收;后续 P0a/P0b 让 pick/place readback 非黑,但仍低纹理,open_door 仍待闭环。 + 6/23 多角度复核:旧图问题、P0 黑屏修复、P1 静态资产归一、任务级隐藏后的新 eval readback 图和 open_door 未验收边界。 打开 @@ -839,6 +964,11 @@

    相关文档

    camera axes/pose、deterministic light 和 pick/place 非黑 readback 证据。 打开 + + P1 asset/layout manifest + 三任务 eval readback 图、静态 USD 坐标、任务级隐藏、open_door 关闭位、单把手正面诊断和 render_validation_not_passed 边界。 + 打开 + Render/layout closure plan P0-P2 修复计划:camera 诊断、reset 布局、eval-path 重拍和视觉 QA。 diff --git a/docs/superpowers/plans/2026-06-22-ebench-labutopia-poc.md b/docs/superpowers/plans/2026-06-22-ebench-labutopia-poc.md index c9ccc89..dbe7635 100644 --- a/docs/superpowers/plans/2026-06-22-ebench-labutopia-poc.md +++ b/docs/superpowers/plans/2026-06-22-ebench-labutopia-poc.md @@ -1,6 +1,6 @@ # EBench LabUtopia POC Status -Updated: 2026-06-22 +Updated: 2026-06-23 ## PM Summary @@ -17,8 +17,8 @@ This is a wiring and platform-readiness milestone, not a task-solving milestone. | Scene metadata | Ready for smoke | Missing collected-package `meta_info.pkl` is synthesized from the live LabUtopia scene for POC tasks. | | Camera cleanup | Ready for smoke | POC camera configs now include cleanup flags required by GenManip camera reset. | | Result lifecycle | Ready for smoke | Episodes that terminate before recorder finalize persist minimal `result_info.json`; real post-processing exceptions now fail fast instead of being hidden as completed results. | -| Render/readback evidence | Blocked | 2026-06-23 pre-fix diagnostics showed all three tasks as `readback_black_before_recorder`; P0a/P0b follow-up moved pick/place to `readback_visible`, but the frames are still low-texture and open_door is not revalidated. | -| Asset/layout acceptance | Blocked | Selected objects remain in source LabUtopia coordinates and the open-door handle transform is invalid after direct child payload import. | +| Render/readback evidence | Partially readable | 2026-06-23 diagnostics moved all three tasks to `readback_visible`; task-level hiding makes `level1_pick` readable and `level1_place` basically readable for PM diagnosis. Latest `level1_open_door` is now diagnosable after the earlier joint explosion: the closed-start joint target matches `[0.0]` and a high-contrast handle marker is visible, but door/handle visual QA is still WARN/not accepted. | +| Asset/layout acceptance | Partially resolved | Static selected objects now sit in the Franka workspace and the DryingBox handle is nested again. DryingBox runtime USD/PhysX topology is stabilized for the POC through a sanitized surrogate, but visual acceptance is still open. | | Lift2 official baseline | Not yet proven | Architecture is prepared to add/evaluate the lift2 candidate profile, but the official baseline smoke still needs to be run. | ## Evidence @@ -30,7 +30,7 @@ Latest isolated smoke: - Existing EOS/other-engineer port: `8087`, left untouched and still online after the run - Final status: `complete`, `3/3` episodes completed - Final result: all three tasks recorded `score=0.0`, `sr=0.0` -- Regression tests: `python -m pytest tests/labutopia_poc -q` -> `34 passed, 1 skipped` +- Regression tests: `python -m pytest tests/labutopia_poc -q` -> `56 passed, 1 skipped` - Diagnostics contract: `python -m pytest tests/labutopia_poc/test_render_diagnostics_contract.py -q` -> `2 passed` - Package validator: `python standalone_tools/labutopia_poc/validate_task_package.py` -> `LabUtopia task package validation OK` @@ -49,11 +49,12 @@ Reason: it is already compatible with Isaac Sim 4.1, Ray, GenManip client, cuRob The next lane is planned in `docs/superpowers/plans/2026-06-22-ebench-labutopia-lift2-baseline-lane.md`. -1. P0 render source fix: camera axes/pose handling and deterministic lighting now make pick/place `camera2` readback non-black; keep open_door and visual acceptance scoped to P1/P2. -2. P1 asset/layout fix: rebuild or supplement the LabUtopia overlay so required objects and nested parts preserve valid transforms and sit in the robot workspace. -3. P2 evidence regeneration: rerun the three Franka tasks through eval-path capture, require visual QA PASS, and write a manifest before replacing report images. -4. P3 runtime asset preflight: build/verify a LabUtopia composite asset root before running lift2. Current observation: the scene overlay exists, but lift2 robot and cuRobo assets are not present under the scene-only overlay root. -5. P4 lift2 dry smoke: run `ebench/labutopia_lab_poc/lift2_candidate` on an isolated port, require `complete` and `3/3` result files, keep `official_baseline_execution=false`. -6. P5 official baseline discovery: locate official EBench lift2/OpenPI runner files, retain paths and hashes, do not execute policy yet. -7. P6 official baseline local contrast: run one official-style online loop only after P5 passes, retain source terminal evidence, keep `standard_model_score=null` until a separate score-release gate. -8. Closure: write EOS-style dated records with artifact linkage, forbidden-claims, and path-leakage checks before changing PM-facing claims. +1. P0 render source fix: camera axes/pose handling and deterministic lighting now make all three tasks `camera2` readback non-black. +2. P1 asset/layout fix: static objects and nested handle are normalized into the robot workspace; task-level hiding now makes pick/place PM-readable diagnostics. +3. P1b open_door physics fix: stronger DryingBox USD/PhysX validation and sanitized articulation topology are in place for the POC; keep the runtime sanity gate active. +4. P2 evidence regeneration: retake open_door with a closer front-oblique camera, rerun the three Franka tasks through eval-path capture, require visual QA PASS, and write a manifest before any accepted-render claim. +5. P3 runtime asset preflight: build/verify a LabUtopia composite asset root before running lift2. Current observation: the scene overlay exists, but lift2 robot and cuRobo assets are not present under the scene-only overlay root. +6. P4 lift2 dry smoke: run `ebench/labutopia_lab_poc/lift2_candidate` on an isolated port, require `complete` and `3/3` result files, keep `official_baseline_execution=false`. +7. P5 official baseline discovery: locate official EBench lift2/OpenPI runner files, retain paths and hashes, do not execute policy yet. +8. P6 official baseline local contrast: run one official-style online loop only after P5 passes, retain source terminal evidence, keep `standard_model_score=null` until a separate score-release gate. +9. Closure: write EOS-style dated records with artifact linkage, forbidden-claims, and path-leakage checks before changing PM-facing claims. diff --git a/docs/superpowers/plans/2026-06-23-labutopia-ebench-render-layout-closure.md b/docs/superpowers/plans/2026-06-23-labutopia-ebench-render-layout-closure.md index 30546d3..69e55c0 100644 --- a/docs/superpowers/plans/2026-06-23-labutopia-ebench-render-layout-closure.md +++ b/docs/superpowers/plans/2026-06-23-labutopia-ebench-render-layout-closure.md @@ -8,6 +8,8 @@ **Tech Stack:** Python 3.10, Isaac Sim 4.1 conda env, GenManip evaluator, EBench server/client, Pillow-based image stats, pytest, static GitHub Pages docs. +**Current status update, 2026-06-24 00:30 UTC:** P0 black-frame readback is resolved for all three Franka POC tasks. P1 static asset/layout normalization is partially resolved: required objects are in the Franka workspace and the DryingBox handle is a nested part again. Task-level visibility isolation now makes `level1_pick` readable and `level1_place` basically readable for PM diagnosis. The `open_door` runtime articulation has been stabilized with a sanitized DryingBox surrogate, aligned hinge, target replay, handle-side correction, duplicate marker removal, and a formal front-camera config; the latest formal retake starts at the expected closed joint target and shows the DryingBox frame, door panel, and one orange handle/action point. Independent image-only QA rates the old-vs-current PM comparison PASS, with open_door PASS/WARN as diagnostic evidence. Formal task render acceptance remains false because diagnostics still report `render_validation_not_passed`. + --- ## Claim Boundary @@ -66,9 +68,13 @@ after P0a/P0b: level1_pick: readback_visible, low-texture frame, not task accepted level1_place: readback_visible, low-texture frame, not task accepted level1_open_door: not revalidated; remains blocked by asset/layout work +after P1 asset/layout normalization: +level1_pick: readback_visible, target visible after task-level hiding, pending formal QA +level1_place: readback_visible, beaker/target relation basically readable after task-level hiding, pending formal QA +level1_open_door: readback_visible, runtime stable after sanitized surrogate and target replay, visual QA WARN/not accepted ``` -Recorder writing is now ruled out as the primary black-frame source. P0a/P0b source fixes remove the pure-black readback failure for controlled pick/place diagnostics, but task render acceptance remains blocked by asset/layout defects. +Recorder writing is now ruled out as the primary black-frame source. P0a/P0b source fixes remove the pure-black readback failure. Task-level visibility isolation improves pick/place screenshots. The open-door physics blocker has been reduced to a visual framing blocker; task render acceptance remains blocked until formal visual QA passes. **Files:** - Create: `standalone_tools/labutopia_poc/capture_eval_render_diagnostics.py` @@ -333,20 +339,54 @@ python standalone_tools/labutopia_poc/validate_task_package.py -> OK ``` 3. Asset/layout normalization: - - Rebuild the overlay or add explicit local task assets so required objects are in the robot workspace and nested parts preserve composed transforms. - - Acceptance: object centers/scales/bounds pass static validation before Isaac runtime capture. + - Static layer is partially done: required objects are in the robot workspace and the nested handle preserves the DryingBox parent transform. + - Task-level visibility isolation is done for POC diagnosis: pick hides beaker/target/DryingBox, place hides bottle/DryingBox, open_door hides bottle/beaker/target. + - Runtime physics is now diagnosable for the POC `open_door` diagnostic after the sanitized DryingBox surrogate, aligned hinge, and target replay; the closed-start joint now matches the expected `0.0`. + - Remaining acceptance gap: `open_door` now shows the DryingBox frame, door panel, and one orange handle/action point, but must not be upgraded beyond diagnostic evidence until `render_validation` and formal visual QA pass. 4. Eval-path regeneration: - - Rerun the three diagnostics and independent visual QA. - - Acceptance: all three task frames are non-black and show task-relevant objects. + - Three P1 diagnostics now produce non-black evaluator readback frames. + - Current P1 visibility images make pick readable and place basically readable, while the latest open_door single-handle front-camera image is PM-usable diagnostic evidence but still not accepted for baseline claims. ## P1: Reset-Time Task Layout Closure +Current P1 status: + +```text +static_usd_ok: true +camera_readback_visible: true for level1_pick/place/open_door +task_visibility_isolated: true for level1_pick/place/open_door +pick_place_pm_readable: true, pending formal visual QA +runtime_physics_stable: true for latest open_door diagnostic, joint_positions = [0.0], expected_joint_positions = [0.0] +open_door_visual_qa: PASS/WARN diagnostic/not accepted, DryingBox frame + door panel + single handle visible, render_validation_not_passed +task_render_accepted: false +official_baseline_evaluable: false +``` + +Evidence: + +```text +saved/diagnostics/labutopia_p1_visibility_pick_20260623_175050/diagnostics.json +saved/diagnostics/labutopia_p1_visibility_place_20260623_175232/diagnostics.json +saved/diagnostics/labutopia_p1_open_door_single_handle_formal_20260624_0001/diagnostics.json +docs/labutopia_lab_poc/evidence_manifests/render_p1_asset_layout_20260623.json +``` + +Immediate next order: + +1. Keep static validation so malformed DryingBox USD/PhysX topology fails before runtime. +2. Keep the sanitized DryingBox asset and runtime sanity gate in place; do not regress to source DryingBox physics. +3. Keep the runtime sanity gate for finite articulation joint positions and finite object transforms. +4. Run formal visual QA for pick/place; keep `task_render_accepted=false` until signed off. +5. Keep the formal single-handle front-camera open_door image as PM-facing diagnostic evidence only; rerun browser display QA and formal task visual QA, and keep `task_render_accepted=false` until `render_validation` passes. + **Files:** - Modify: `configs/tasks/ebench/labutopia_lab_poc/franka_poc/level1_pick.yml` - Modify: `configs/tasks/ebench/labutopia_lab_poc/franka_poc/level1_place.yml` - Modify: `configs/tasks/ebench/labutopia_lab_poc/franka_poc/level1_open_door.yml` - Modify: `configs/cameras/labutopia_franka_poc.yml` or create task-specific camera configs +- Modify: `genmanip/utils/loader/preprocess_rules.py` - Test: `tests/labutopia_poc/test_validate_task_package.py` +- Test: `tests/labutopia_poc/test_scene_preprocess_rules.py` - [ ] **Step 1: Add static validation for render-layout readiness** @@ -370,6 +410,14 @@ level1_place: obj_beaker2, obj_target_plat level1_open_door: obj_DryingBox_01, obj_DryingBox_01_handle ``` +Also require each task to declare the non-task objects hidden for diagnostic readability: + +```text +level1_pick hidden: obj_beaker2, obj_target_plat, obj_DryingBox_01 +level1_place hidden: obj_conical_bottle02, obj_DryingBox_01 +level1_open_door hidden: obj_conical_bottle02, obj_beaker2, obj_target_plat +``` + - [ ] **Step 2: Add failing tests for missing render-validation block** Update `tests/labutopia_poc/test_validate_task_package.py` with assertions that every Franka POC YAML includes the required block and object list. @@ -439,6 +487,42 @@ Outcome D: task-specific view needed -> create task-specific camera config for o Do not change all variables at once. +- [x] **Step 4a: Add task-level visibility isolation** + +Implemented `set_object_active` preprocessing so the runtime can hide non-task objects before eval readback. Targeted tests: + +```text +python -m pytest tests/labutopia_poc/test_scene_preprocess_rules.py -q +2 passed +python -m pytest tests/labutopia_poc/test_validate_task_package.py::test_franka_tasks_hide_non_task_objects_for_evidence_readability -q +1 passed +python standalone_tools/labutopia_poc/validate_task_package.py +LabUtopia task package validation OK +``` + +Latest diagnostic outcome: + +```text +level1_pick: readback_visible, PM-readable target bottle +level1_place: readback_visible, beaker and target platform visible together +level1_open_door: readback_visible, runtime_stable, visual_QA_WARN_NOT_ACCEPTED +``` + +- [ ] **Step 4b: Add DryingBox articulation topology validation** + +Extend static validation to fail on: + +```text +non-identity articulation root scale +duplicate rigid-link basenames such as mesh +non-finite physics:centerOfMass +zero or invalid physics:principalAxes +joint body targets that are not PhysicsRigidBodyAPI prims +unexpected extra DOFs if open-door should only expose the door revolute joint +``` + +This step must go red on the current overlay before building a sanitized DryingBox runtime asset. + - [ ] **Step 5: Run package validation** Run: @@ -531,19 +615,25 @@ Reject the manifest if any image has `direct_render=true`, missing `source_frame - [ ] **Step 4: Replace report images only after visual QA passes** -Replace: +Keep old JPGs in the report as historical failed samples. Replace or add current diagnostic PNGs only with clear labels: ```text -docs/records/evidence/2026-06-22-labutopia-ebench-weekly-report/assets/labutopia-franka-level1-pick.jpg -docs/records/evidence/2026-06-22-labutopia-ebench-weekly-report/assets/labutopia-franka-level1-place.jpg -docs/records/evidence/2026-06-22-labutopia-ebench-weekly-report/assets/labutopia-franka-level1-open-door.jpg +docs/records/evidence/2026-06-22-labutopia-ebench-weekly-report/assets/labutopia-franka-level1-pick-eval-readback-p1.png +docs/records/evidence/2026-06-22-labutopia-ebench-weekly-report/assets/labutopia-franka-level1-place-eval-readback-p1.png +docs/records/evidence/2026-06-22-labutopia-ebench-weekly-report/assets/labutopia-franka-level1-open-door-eval-readback-p1.png ``` Do not replace them with direct-render images unless the report clearly labels them as non-eval-path diagnostic images. - [ ] **Step 5: Update PM report wording** -Allowed wording after the gate passes: +Allowed wording after the partial P1 visibility update: + +```text +旧 JPG 是历史失败样例;新 PNG 来自 evaluator camera readback。当前 pick 已清楚、place 基本可读,open_door 已从物理爆值和黑箱角推进到关闭位正确、门板/框架/单个橙色把手可识别。该图可用于 PM 诊断汇报,但 diagnostics 仍是 render_validation_not_passed,不能作为 baseline 可评证据。 +``` + +Allowed wording after the full gate passes: ```text 三任务已有可复现的 eval-path reset 渲染证据,能看到各自任务关键对象;这证明渲染/布局闭环,不代表策略求解成功,也不代表官方 Lift2 baseline 成绩。 @@ -561,7 +651,8 @@ python - <<'PY' from pathlib import Path html = Path('docs/records/evidence/2026-06-22-labutopia-ebench-weekly-report/index.html').read_text(encoding='utf-8') for text in [ - '当前三张渲染图未通过视觉验收', + '旧图:历史失败样例', + 'level1_open_door · 可诊断 / 未验收', 'render_visual_investigation_20260623.md', '2026-06-23-labutopia-ebench-render-layout-closure.md', ]: diff --git a/genmanip/core/evaluator/env.py b/genmanip/core/evaluator/env.py index a7e7cc5..dd08dd0 100644 --- a/genmanip/core/evaluator/env.py +++ b/genmanip/core/evaluator/env.py @@ -24,6 +24,7 @@ clear_scene, recovery_scene, ) +from genmanip.utils.loader.preprocess_rules import apply_articulation_initial_targets from genmanip.utils.standalone.file_utils import ( make_dir, ) @@ -686,6 +687,7 @@ def _setup_episode_layout(self): # Warmup for _ in range(50): scene.world.step() + apply_articulation_initial_targets(scene) base_traj_dir = os.path.join( self.default_config["EVAL_RESULT_DIR"], diff --git a/genmanip/core/evaluator/labutopia_layout.py b/genmanip/core/evaluator/labutopia_layout.py index a9ebf3e..612f186 100644 --- a/genmanip/core/evaluator/labutopia_layout.py +++ b/genmanip/core/evaluator/labutopia_layout.py @@ -5,6 +5,8 @@ from pathlib import Path from typing import Any +from genmanip.utils.loader.preprocess_rules import articulation_target_for_uid + LABUTOPIA_POC_TASK_PREFIX = "ebench/labutopia_lab_poc/" @@ -18,7 +20,7 @@ def _object_world_pose(obj: Any) -> tuple[Any, Any]: return position, orientation -def _build_initial_layout(scene: Any) -> dict[str, dict[str, Any]]: +def _build_initial_layout(scene: Any, scene_config: Any) -> dict[str, dict[str, Any]]: layout: dict[str, dict[str, Any]] = {} cache_library = getattr(scene, "cache_library", None) usd_path_list = getattr(cache_library, "preloaded_object_path_list", {}) or {} @@ -46,12 +48,17 @@ def _build_initial_layout(scene: Any) -> dict[str, dict[str, Any]]: for key, articulation in scene.articulation_list.items(): position, orientation = articulation.get_world_pose() + target_positions = articulation_target_for_uid(scene_config, key) layout[key] = { "type": "articulation", "position": position, "orientation": orientation, "scale": articulation.get_local_scale(), - "joint_positions": articulation.get_joint_positions(), + "joint_positions": ( + target_positions + if target_positions is not None + else articulation.get_joint_positions() + ), "prim_path": articulation.prim_path, } @@ -74,7 +81,7 @@ def build_labutopia_poc_meta_info( """Build minimal eval metadata for LabUtopia POC tasks from the live scene.""" task_data = { "initial_scene_graph": None, - "initial_layout": _build_initial_layout(scene), + "initial_layout": _build_initial_layout(scene, scene_config), "goal": copy.deepcopy(scene_config.generation_config.goal), "instruction": scene_config.instruction, "frame_status": {}, diff --git a/genmanip/core/scene/scene.py b/genmanip/core/scene/scene.py index 5497c46..aabb6f0 100644 --- a/genmanip/core/scene/scene.py +++ b/genmanip/core/scene/scene.py @@ -38,6 +38,10 @@ from genmanip.utils.usd_utils import get_src, setup_physics_scene from genmanip.utils.pointcloud.utils import MeshInfo, PointCloudInfo from genmanip.utils.loader.scene import relate_object_from_data +from genmanip.utils.loader.preprocess_rules import ( + apply_articulation_initial_targets, + resettable_scene_object_uids, +) class AssetsLibrary: @@ -278,6 +282,7 @@ def _initialize_after_reset( for key, articulation in self.articulation_list.items(): if self.articulation_data[key]["is_articulated"]: articulation._articulation_view.initialize() + apply_articulation_initial_targets(self) if not is_render or save_pointcloud: self.cache_library.mesh_dict = objectList2meshList( self.object_list, @@ -312,8 +317,12 @@ def _initialize_after_reset( "configs/robots/tcp/franka_tcp.yaml", ) ) - reset_object_xyz(self.object_list, self.meta_infos["world_pose_list"]) - for key in self.object_list: + resettable_uids = resettable_scene_object_uids(self) + reset_object_xyz( + {key: self.object_list[key] for key in resettable_uids}, + self.meta_infos["world_pose_list"], + ) + for key in resettable_uids: clean_prim_velocity(self.object_list[key].prim_path) def _preprocess_scene(self) -> None: @@ -377,6 +386,7 @@ def initialize( def post_initialize(self, skip_warmup: bool = False) -> None: if not skip_warmup: self._warmup_world() + apply_articulation_initial_targets(self) self.collect_meta_infos() def build_metrics_manager( diff --git a/genmanip/utils/loader/preprocess_rules.py b/genmanip/utils/loader/preprocess_rules.py new file mode 100644 index 0000000..0549b5c --- /dev/null +++ b/genmanip/utils/loader/preprocess_rules.py @@ -0,0 +1,124 @@ +"""Small preprocessing rules that do not require importing Isaac modules.""" + +from __future__ import annotations + +from collections.abc import Iterable +from typing import Any + + +def _as_float_list(values: Iterable[Any]) -> list[float]: + return [float(value) for value in values] + + +def _find_scene_entry(scene: Any, uid: str) -> Any: + for collection_name in ( + "object_list", + "articulation_list", + "articulation_part_list", + ): + collection = getattr(scene, collection_name, {}) or {} + if uid in collection: + return collection[uid] + raise KeyError(uid) + + +def _entry_prim(entry: Any) -> Any: + prim = getattr(entry, "prim", None) + if prim is not None: + return prim + get_prim = getattr(entry, "GetPrim", None) + if callable(get_prim): + return get_prim() + raise AttributeError(f"{entry!r} does not expose a USD prim") + + +def set_scene_object_active(scene: Any, uids: Iterable[str], active: bool) -> None: + """Toggle runtime scene object prims by LabUtopia/GenManip uid.""" + + for uid in uids: + entry = _find_scene_entry(scene, uid) + prim = _entry_prim(entry) + if prim.IsActive() != active: + prim.SetActive(active) + if not active: + cache_library = getattr(scene, "cache_library", None) + mesh_dict = getattr(cache_library, "mesh_dict", None) + if isinstance(mesh_dict, dict): + mesh_dict.pop(uid, None) + + +def resettable_scene_object_uids(scene: Any) -> list[str]: + """Return object_list uids safe for direct world-pose reset and velocity cleanup.""" + + object_list = getattr(scene, "object_list", {}) or {} + articulation_uids = set(getattr(scene, "articulation_list", {}) or {}) + articulation_part_uids = set(getattr(scene, "articulation_part_list", {}) or {}) + skipped = articulation_uids | articulation_part_uids + return sorted(uid for uid in object_list if uid not in skipped) + + +def configured_articulation_targets(scene_config: Any) -> dict[str, list[float]]: + """Return articulation target positions from a SceneConfig-like object.""" + + generation_config = getattr(scene_config, "generation_config", None) + articulation_config = getattr(generation_config, "articulation", {}) or {} + targets: dict[str, list[float]] = {} + for uid, config in articulation_config.items(): + target_positions = None + if isinstance(config, dict): + target_positions = config.get("target_positions") + else: + target_positions = getattr(config, "target_positions", None) + if target_positions is None: + continue + targets[uid] = _as_float_list(target_positions) + return targets + + +def articulation_target_for_uid(scene_config: Any, uid: str) -> list[float] | None: + return configured_articulation_targets(scene_config).get(uid) + + +def _call_optional_setter(handle: Any, setter_name: str, values: list[float]) -> bool: + setter = getattr(handle, setter_name, None) + if not callable(setter): + return False + setter(values) + return True + + +def apply_articulation_initial_targets(scene: Any) -> dict[str, list[float]]: + """Replay configured articulation starts after world resets or warmup steps.""" + + targets = configured_articulation_targets(getattr(scene, "scene_config", None)) + articulation_list = getattr(scene, "articulation_list", {}) or {} + articulation_data = getattr(scene, "articulation_data", {}) or {} + applied: dict[str, list[float]] = {} + for uid, target_positions in targets.items(): + articulation = articulation_list.get(uid) + if articulation is None: + continue + data = articulation_data.get(uid, {}) + if data and not data.get("is_articulated", True): + continue + view = getattr(articulation, "_articulation_view", None) + position_written = _call_optional_setter( + articulation, "set_joint_positions", target_positions + ) + if not position_written and view is not None: + position_written = _call_optional_setter( + view, "set_joint_positions", target_positions + ) + if not position_written: + continue + zero_velocities = [0.0 for _ in target_positions] + if not _call_optional_setter(articulation, "set_joint_velocities", zero_velocities): + if view is not None: + _call_optional_setter(view, "set_joint_velocities", zero_velocities) + if not _call_optional_setter( + articulation, "set_joint_position_targets", target_positions + ): + if view is not None: + _call_optional_setter(view, "set_joint_position_targets", target_positions) + applied[uid] = target_positions + return applied diff --git a/genmanip/utils/loader/scene.py b/genmanip/utils/loader/scene.py index a078193..fccb6e2 100644 --- a/genmanip/utils/loader/scene.py +++ b/genmanip/utils/loader/scene.py @@ -42,6 +42,7 @@ generate_long_horizon_by_materials, generate_long_horizon_by_shape, ) +from genmanip.utils.loader.preprocess_rules import set_scene_object_active from genmanip.utils.usd_utils import set_mdl, create_dome_light, set_texture from genmanip.utils.pointcloud.pointcloud import ( objectList2meshList, @@ -905,6 +906,9 @@ def collect_assets(assets_dir: str) -> dict: def preprocess_scene(scene: "Scene", demogen_config: SceneConfig) -> None: preprocess_config = demogen_config.preprocess_config for preprocess_info in preprocess_config: + if preprocess_info["type"] == "set_object_active": + config = preprocess_info["config"] + set_scene_object_active(scene, config["uids"], bool(config["active"])) if preprocess_info["type"] == "disable_contact_offset": for object in scene.object_list.values(): remove_contact_offset(object.prim_path) diff --git a/standalone_tools/labutopia_poc/build_asset_overlay.py b/standalone_tools/labutopia_poc/build_asset_overlay.py index c0885e9..7ed2c93 100644 --- a/standalone_tools/labutopia_poc/build_asset_overlay.py +++ b/standalone_tools/labutopia_poc/build_asset_overlay.py @@ -29,6 +29,11 @@ "/World/DryingBox_01/handle": "obj_DryingBox_01_handle", "/World/table": "table", } +TOP_LEVEL_SOURCE_TO_RUNTIME_OBJECT_KEY = { + source_path: runtime_key + for source_path, runtime_key in SOURCE_TO_RUNTIME_OBJECT_KEY.items() + if source_path != "/World/DryingBox_01/handle" +} SOURCE_TASK_PRIMS = { "level1_pick": ["/World/conical_bottle02"], "level1_place": ["/World/beaker2", "/World/target_plat"], @@ -47,6 +52,106 @@ "obj_table", ] TABLE_UID = "table" +ARTICULATION_PART_PATHS = { + "obj_DryingBox_01_handle": f"/World/{SCENE_UID}/obj_obj_DryingBox_01/handle", +} +RUNTIME_TRANSLATION_OVERRIDES = { + "obj_conical_bottle02": [0.28, 0.0, 0.8], + "obj_beaker2": [0.27, 0.18, 0.84], + "obj_target_plat": [0.26, -0.24, 0.776], + "obj_DryingBox_01": [0.75, 0.1, 0.78], +} +RENDER_OBJECT_CONTRACTS = { + "obj_conical_bottle02": { + "source_prim_path": "/World/conical_bottle02", + "role": "pick_target", + "desired_runtime_translation": RUNTIME_TRANSLATION_OVERRIDES[ + "obj_conical_bottle02" + ], + "expected_world_bbox_lwh_m": { + "min": [0.05, 0.05, 0.10], + "max": [0.14, 0.14, 0.22], + }, + "display_color": [0.10, 0.48, 0.95], + "display_override_paths": ["mesh"], + }, + "obj_beaker2": { + "source_prim_path": "/World/beaker2", + "role": "place_object", + "desired_runtime_translation": RUNTIME_TRANSLATION_OVERRIDES["obj_beaker2"], + "expected_world_bbox_lwh_m": { + "min": [0.07, 0.07, 0.05], + "max": [0.16, 0.16, 0.14], + }, + "display_color": [0.10, 0.72, 0.54], + "display_override_paths": ["mesh"], + }, + "obj_target_plat": { + "source_prim_path": "/World/target_plat", + "role": "place_target", + "desired_runtime_translation": RUNTIME_TRANSLATION_OVERRIDES[ + "obj_target_plat" + ], + "expected_world_bbox_lwh_m": { + "min": [0.08, 0.08, 0.00005], + "max": [0.14, 0.14, 0.02], + }, + "display_color": [0.95, 0.78, 0.12], + "display_override_paths": ["mesh"], + }, + "obj_DryingBox_01": { + "source_prim_path": "/World/DryingBox_01", + "role": "articulated_drying_box", + "desired_runtime_translation": RUNTIME_TRANSLATION_OVERRIDES[ + "obj_DryingBox_01" + ], + "expected_world_bbox_lwh_m": { + "min": [0.45, 0.50, 0.45], + "max": [0.75, 0.90, 0.80], + }, + "display_color": [0.82, 0.84, 0.88], + "display_override_paths": [ + "body/body/mesh", + "panel", + "button", + ], + }, + "obj_DryingBox_01_handle": { + "source_prim_path": "/World/DryingBox_01/handle", + "role": "door_handle", + "wrapper_prim_path": ARTICULATION_PART_PATHS["obj_DryingBox_01_handle"], + "compose_nested_transform_with_parent": "obj_DryingBox_01", + "expected_world_bbox_lwh_m": { + "min": [0.03, 0.03, 0.14], + "max": [0.08, 0.08, 0.26], + }, + "display_color": [1.0, 0.18, 0.04], + "display_override_paths": ["handle/mesh"], + }, +} +DRYING_BOX_DOOR_PANEL_COLOR = [0.28, 0.34, 0.42] +DRYING_BOX_DOOR_SEAM_COLOR = [0.04, 0.05, 0.06] +DRYING_BOX_HANDLE_MOUNT_COLOR = [0.05, 0.07, 0.09] +DRYING_BOX_VISUAL_AFFORDANCES = [ + { + "name": "high_contrast_door_panel", + "display_color": DRYING_BOX_DOOR_PANEL_COLOR, + }, + { + "name": "door_outline_seams", + "display_color": DRYING_BOX_DOOR_SEAM_COLOR, + }, + { + "name": "handle_mount_backplate", + "display_color": DRYING_BOX_HANDLE_MOUNT_COLOR, + }, + { + "name": "high_contrast_handle", + "display_color": RENDER_OBJECT_CONTRACTS["obj_DryingBox_01_handle"][ + "display_color" + ], + }, +] DETERMINISTIC_LIGHTS = [ { "prim_path": f"/World/{SCENE_UID}/DeterministicDomeLight", @@ -54,6 +159,29 @@ "intensity": 1000, } ] +DRYING_BOX_PHYSICS_OVERRIDES = { + "body/body/mesh": { + "mass": 2.0, + "diagonal_inertia": [0.05, 0.05, 0.05], + }, + "body/Group/door/mesh": { + "mass": 0.5, + "diagonal_inertia": [0.01, 0.01, 0.01], + }, + "handle/mesh": { + "mass": 0.1, + "diagonal_inertia": [0.002, 0.002, 0.002], + }, +} +DRYING_BOX_RUNTIME_ASSET = { + "strategy": "sanitized_surrogate", + "wrapper_prim_path": f"/World/{SCENE_UID}/obj_obj_DryingBox_01", + "base_joint_name": "BaseFixedJoint", + "joint_name": "RevoluteJoint", + "removed_source_joint_types": ["PhysicsPrismaticJoint"], + "source_payload_used": False, + "visual_affordances": DRYING_BOX_VISUAL_AFFORDANCES, +} def _wrapper_name(runtime_object_key: str) -> str: @@ -63,12 +191,421 @@ def _wrapper_name(runtime_object_key: str) -> str: def _wrapper_prim_paths() -> dict[str, str]: - return { + paths = { runtime_key: f"/World/{SCENE_UID}/{_wrapper_name(runtime_key)}" - for runtime_key in SOURCE_TO_RUNTIME_OBJECT_KEY.values() + for runtime_key in TOP_LEVEL_SOURCE_TO_RUNTIME_OBJECT_KEY.values() + } + paths.update(ARTICULATION_PART_PATHS) + return paths + + +def _render_object_contracts() -> dict[str, dict[str, object]]: + wrapper_paths = _wrapper_prim_paths() + contracts = {} + for runtime_key, contract in RENDER_OBJECT_CONTRACTS.items(): + item = { + key: value + for key, value in contract.items() + if key != "display_override_paths" + } + item["wrapper_prim_path"] = wrapper_paths[runtime_key] + contracts[runtime_key] = item + return contracts + + +def _usd_float(value: float) -> str: + number = float(value) + if number.is_integer(): + return str(int(number)) + return f"{number:.6g}" + + +def _usd_vec3(values: list[float]) -> str: + return "(" + ", ".join(_usd_float(value) for value in values) + ")" + + +def _usd_quat(values: list[float]) -> str: + return "(" + ", ".join(_usd_float(value) for value in values) + ")" + + +def _preview_surface_material_def( + name: str, + *, + color: list[float], + indent_level: int = 12, +) -> str: + indent = " " * indent_level + attr_indent = " " * (indent_level + 4) + shader_indent = " " * (indent_level + 8) + return ( + f'{indent}def Material "{name}"\n' + f"{indent}{{\n" + f"{attr_indent}token outputs:surface.connect = " + f"\n" + f"{attr_indent}token outputs:mdl:surface.connect = " + f"\n" + f"{attr_indent}token outputs:mdl:displacement.connect = " + f"\n" + f"{attr_indent}token outputs:mdl:volume.connect = " + f"\n" + f'{attr_indent}def Shader "PreviewSurface"\n' + f"{attr_indent}{{\n" + f'{shader_indent}uniform token info:id = "UsdPreviewSurface"\n' + f"{shader_indent}color3f inputs:diffuseColor = {_usd_vec3(color)}\n" + f"{shader_indent}float inputs:metallic = 0\n" + f"{shader_indent}float inputs:roughness = 0.85\n" + f"{shader_indent}token outputs:surface\n" + f"{attr_indent}}}\n" + f'{attr_indent}def Shader "OmniPBR"\n' + f"{attr_indent}{{\n" + f'{shader_indent}uniform token info:implementationSource = "sourceAsset"\n' + f"{shader_indent}asset info:mdl:sourceAsset = @OmniPBR.mdl@\n" + f'{shader_indent}token info:mdl:sourceAsset:subIdentifier = "OmniPBR"\n' + f"{shader_indent}color3f inputs:diffuse_color_constant = {_usd_vec3(color)}\n" + f"{shader_indent}float inputs:reflection_roughness_constant = 0.45\n" + f"{shader_indent}token outputs:out\n" + f"{attr_indent}}}\n" + f"{indent}}}" + ) + + +def _drying_box_material_scope_def() -> str: + body_contract = RENDER_OBJECT_CONTRACTS["obj_DryingBox_01"] + handle_contract = RENDER_OBJECT_CONTRACTS["obj_DryingBox_01_handle"] + indent = " " * 12 + return ( + f'{indent}def Scope "Looks"\n' + f"{indent}{{\n" + f"{_preview_surface_material_def('body_mat', color=body_contract['display_color'], indent_level=16)}\n" + f"{_preview_surface_material_def('door_panel_mat', color=DRYING_BOX_DOOR_PANEL_COLOR, indent_level=16)}\n" + f"{_preview_surface_material_def('door_seam_mat', color=DRYING_BOX_DOOR_SEAM_COLOR, indent_level=16)}\n" + f"{_preview_surface_material_def('handle_mount_mat', color=DRYING_BOX_HANDLE_MOUNT_COLOR, indent_level=16)}\n" + f"{_preview_surface_material_def('handle_mat', color=handle_contract['display_color'], indent_level=16)}\n" + f"{indent}}}" + ) + + +def _display_color_attr(color: list[float], indent: str) -> str: + return ( + f"{indent}color3f[] primvars:displayColor = [{_usd_vec3(color)}]\n" + f'{indent}uniform token primvars:displayColor:interpolation = "constant"' + ) + + +def _nested_display_override(path: str, color: list[float], indent_level: int = 4) -> str: + parts = path.split("/") + indent = " " * indent_level + if len(parts) == 1: + attr_indent = " " * (indent_level + 4) + return ( + f'{indent}over "{parts[0]}"\n' + f"{indent}{{\n" + f"{_display_color_attr(color, attr_indent)}\n" + f"{indent}}}" + ) + inner = _nested_display_override( + "/".join(parts[1:]), color, indent_level + 4 + ) + return f'{indent}over "{parts[0]}"\n{indent}{{\n{inner}\n{indent}}}' + + +def _mass_api_attr_block(mass: float, diagonal_inertia: list[float], indent: str) -> str: + return ( + f"{indent}float physics:mass = {_usd_float(mass)}\n" + f"{indent}point3f physics:diagonalInertia = {_usd_vec3(diagonal_inertia)}\n" + f"{indent}point3f physics:centerOfMass = (0, 0, 0)\n" + f"{indent}quatf physics:principalAxes = (1, 0, 0, 0)" + ) + + +def _nested_mass_override( + path: str, + *, + mass: float, + diagonal_inertia: list[float], + indent_level: int = 4, +) -> str: + parts = path.split("/") + indent = " " * indent_level + if len(parts) == 1: + attr_indent = " " * (indent_level + 4) + return ( + f'{indent}over "{parts[0]}" (\n' + f'{indent} prepend apiSchemas = ["PhysicsMassAPI"]\n' + f"{indent})\n" + f"{indent}{{\n" + f"{_mass_api_attr_block(mass, diagonal_inertia, attr_indent)}\n" + f"{indent}}}" + ) + inner = _nested_mass_override( + "/".join(parts[1:]), + mass=mass, + diagonal_inertia=diagonal_inertia, + indent_level=indent_level + 4, + ) + return f'{indent}over "{parts[0]}"\n{indent}{{\n{inner}\n{indent}}}' + + +def _new_override_node() -> dict[str, object]: + return { + "children": {}, + "display_color": None, + "mass": None, + "diagonal_inertia": None, } +def _override_node_for_path( + tree: dict[str, dict[str, object]], + path: str, +) -> dict[str, object]: + current = tree + node: dict[str, object] | None = None + for part in path.split("/"): + node = current.setdefault(part, _new_override_node()) + current = node["children"] # type: ignore[assignment] + assert node is not None + return node + + +def _add_display_override( + tree: dict[str, dict[str, object]], + path: str, + color: list[float], +) -> None: + _override_node_for_path(tree, path)["display_color"] = color + + +def _add_mass_override( + tree: dict[str, dict[str, object]], + path: str, + *, + mass: float, + diagonal_inertia: list[float], +) -> None: + node = _override_node_for_path(tree, path) + node["mass"] = mass + node["diagonal_inertia"] = diagonal_inertia + + +def _render_override_tree( + name: str, + node: dict[str, object], + *, + indent_level: int, +) -> str: + indent = " " * indent_level + attr_indent = " " * (indent_level + 4) + mass = node.get("mass") + if mass is None: + header = f'{indent}over "{name}"' + else: + header = ( + f'{indent}over "{name}" (\n' + f'{indent} prepend apiSchemas = ["PhysicsMassAPI"]\n' + f"{indent})" + ) + body_lines: list[str] = [] + display_color = node.get("display_color") + if display_color is not None: + body_lines.append(_display_color_attr(display_color, attr_indent)) # type: ignore[arg-type] + if mass is not None: + body_lines.append( + _mass_api_attr_block( + float(mass), + node["diagonal_inertia"], # type: ignore[arg-type] + attr_indent, + ) + ) + children = node["children"] # type: ignore[assignment] + for child_name, child_node in children.items(): # type: ignore[union-attr] + body_lines.append( + _render_override_tree( + child_name, + child_node, + indent_level=indent_level + 4, + ) + ) + return f"{header}\n{indent}{{\n" + "\n".join(body_lines) + f"\n{indent}}}" + + +def _wrapper_body(source_path: str, runtime_key: str) -> str: + body_lines = [] + translation = RUNTIME_TRANSLATION_OVERRIDES.get(runtime_key) + if translation is not None: + body_lines.append( + f" double3 xformOp:translate = {_usd_vec3(translation)}" + ) + override_tree: dict[str, dict[str, object]] = {} + contract = RENDER_OBJECT_CONTRACTS.get(runtime_key) + if contract is not None: + display_color = contract["display_color"] + for override_path in contract["display_override_paths"]: + _add_display_override(override_tree, override_path, display_color) + if runtime_key == "obj_DryingBox_01": + handle_contract = RENDER_OBJECT_CONTRACTS["obj_DryingBox_01_handle"] + for override_path in handle_contract["display_override_paths"]: + _add_display_override( + override_tree, + override_path, + handle_contract["display_color"], + ) + for override_path, physics in DRYING_BOX_PHYSICS_OVERRIDES.items(): + _add_mass_override( + override_tree, + override_path, + mass=physics["mass"], + diagonal_inertia=physics["diagonal_inertia"], + ) + for child_name, child_node in override_tree.items(): + body_lines.append( + _render_override_tree(child_name, child_node, indent_level=12) + ) + return "\n".join(body_lines) + + +def _rigid_cube_def( + name: str, + *, + translate: list[float], + scale: list[float], + color: list[float], + mass: float, + diagonal_inertia: list[float], + material_path: str | None = None, + indent_level: int = 12, +) -> str: + indent = " " * indent_level + attr_indent = " " * (indent_level + 4) + api_schemas = [ + "PhysicsRigidBodyAPI", + "PhysicsCollisionAPI", + "PhysicsMassAPI", + ] + if material_path is not None: + api_schemas.append("MaterialBindingAPI") + api_schema_text = ", ".join(f'"{schema}"' for schema in api_schemas) + material_binding = ( + f"{attr_indent}rel material:binding = <{material_path}>\n" + if material_path is not None + else "" + ) + return ( + f'{indent}def Cube "{name}" (\n' + f"{indent} prepend apiSchemas = [{api_schema_text}]\n" + f"{indent})\n" + f"{indent}{{\n" + f"{attr_indent}double size = 1\n" + f"{attr_indent}double3 xformOp:translate = {_usd_vec3(translate)}\n" + f"{attr_indent}double3 xformOp:scale = {_usd_vec3(scale)}\n" + f'{attr_indent}uniform token[] xformOpOrder = ["xformOp:translate", ' + f'"xformOp:scale"]\n' + f"{_display_color_attr(color, attr_indent)}\n" + f"{material_binding}" + f"{_mass_api_attr_block(mass, diagonal_inertia, attr_indent)}\n" + f"{indent}}}" + ) + + +def _visual_cube_def( + name: str, + *, + translate: list[float], + scale: list[float], + color: list[float], + material_path: str, + indent_level: int = 12, +) -> str: + indent = " " * indent_level + attr_indent = " " * (indent_level + 4) + return ( + f'{indent}def Cube "{name}" (\n' + f'{indent} prepend apiSchemas = ["MaterialBindingAPI"]\n' + f"{indent})\n" + f"{indent}{{\n" + f"{attr_indent}double size = 1\n" + f"{attr_indent}double3 xformOp:translate = {_usd_vec3(translate)}\n" + f"{attr_indent}double3 xformOp:scale = {_usd_vec3(scale)}\n" + f'{attr_indent}uniform token[] xformOpOrder = ["xformOp:translate", ' + f'"xformOp:scale"]\n' + f"{_display_color_attr(color, attr_indent)}\n" + f"{attr_indent}rel material:binding = <{material_path}>\n" + f"{indent}}}" + ) + + +def _joint_relationships( + root_path: str, + *, + body0: str, + body1: str, + indent: str, +) -> str: + return ( + f"{indent}rel physics:body0 = <{root_path}/{body0}>\n" + f"{indent}rel physics:body1 = <{root_path}/{body1}>" + ) + + +def _drying_box_surrogate_def(runtime_key: str) -> str: + wrapper_name = _wrapper_name(runtime_key) + root_path = f"/World/{SCENE_UID}/{wrapper_name}" + translation = RUNTIME_TRANSLATION_OVERRIDES[runtime_key] + body_contract = RENDER_OBJECT_CONTRACTS["obj_DryingBox_01"] + handle_contract = RENDER_OBJECT_CONTRACTS["obj_DryingBox_01_handle"] + body_color = body_contract["display_color"] + handle_color = handle_contract["display_color"] + door_color = DRYING_BOX_DOOR_PANEL_COLOR + seam_color = DRYING_BOX_DOOR_SEAM_COLOR + handle_mount_color = DRYING_BOX_HANDLE_MOUNT_COLOR + joint_indent = " " * 16 + return ( + f' def Xform "{wrapper_name}" (\n' + f' prepend apiSchemas = ["PhysicsArticulationRootAPI"]\n' + f" )\n" + f" {{\n" + f" double3 xformOp:translate = {_usd_vec3(translation)}\n" + f' uniform token[] xformOpOrder = ["xformOp:translate"]\n' + f"{_drying_box_material_scope_def()}\n" + f"{_rigid_cube_def('body_link', translate=[0, 0.08, 0], scale=[0.58, 0.30, 0.52], color=body_color, mass=2.0, diagonal_inertia=[0.05, 0.05, 0.05], material_path=f'{root_path}/Looks/body_mat')}\n" + f"{_rigid_cube_def('door_link', translate=[0, -0.12, 0.01], scale=[0.5, 0.04, 0.42], color=door_color, mass=0.5, diagonal_inertia=[0.01, 0.01, 0.01], material_path=f'{root_path}/Looks/door_panel_mat')}\n" + f"{_visual_cube_def('door_left_seam', translate=[-0.255, -0.168, 0.01], scale=[0.012, 0.012, 0.43], color=seam_color, material_path=f'{root_path}/Looks/door_seam_mat')}\n" + f"{_visual_cube_def('door_right_seam', translate=[0.255, -0.168, 0.01], scale=[0.012, 0.012, 0.43], color=seam_color, material_path=f'{root_path}/Looks/door_seam_mat')}\n" + f"{_visual_cube_def('door_top_seam', translate=[0, -0.168, 0.225], scale=[0.52, 0.012, 0.012], color=seam_color, material_path=f'{root_path}/Looks/door_seam_mat')}\n" + f"{_visual_cube_def('door_bottom_seam', translate=[0, -0.168, -0.205], scale=[0.52, 0.012, 0.012], color=seam_color, material_path=f'{root_path}/Looks/door_seam_mat')}\n" + f"{_visual_cube_def('handle_mount_backplate', translate=[0.18, -0.174, 0.05], scale=[0.075, 0.014, 0.28], color=handle_mount_color, material_path=f'{root_path}/Looks/handle_mount_mat')}\n" + f"{_rigid_cube_def('handle', translate=[0.18, -0.22, 0.05], scale=[0.08, 0.06, 0.24], color=handle_color, mass=0.1, diagonal_inertia=[0.002, 0.002, 0.002], material_path=f'{root_path}/Looks/handle_mat')}\n" + f' def PhysicsFixedJoint "BaseFixedJoint"\n' + f" {{\n" + f" point3f physics:localPos0 = (0, 0, 0)\n" + f" point3f physics:localPos1 = (0, 0, 0)\n" + f" quatf physics:localRot0 = (1, 0, 0, 0)\n" + f" quatf physics:localRot1 = (1, 0, 0, 0)\n" + f" rel physics:body1 = <{root_path}/body_link>\n" + f" }}\n" + f' def PhysicsRevoluteJoint "RevoluteJoint"\n' + f" {{\n" + f' token physics:axis = "Z"\n' + f" float physics:lowerLimit = 0\n" + f" float physics:upperLimit = 120\n" + f" point3f physics:localPos0 = (-0.25, -0.2, 0.01)\n" + f" point3f physics:localPos1 = (-0.25, 0, 0)\n" + f" quatf physics:localRot0 = (1, 0, 0, 0)\n" + f" quatf physics:localRot1 = (1, 0, 0, 0)\n" + f"{_joint_relationships(root_path, body0='body_link', body1='door_link', indent=joint_indent)}\n" + f" }}\n" + f' def PhysicsFixedJoint "HandleFixedJoint"\n' + f" {{\n" + f" point3f physics:localPos0 = (0.18, -0.10, 0.04)\n" + f" point3f physics:localPos1 = (0, 0, 0)\n" + f" quatf physics:localRot0 = (1, 0, 0, 0)\n" + f" quatf physics:localRot1 = (1, 0, 0, 0)\n" + f"{_joint_relationships(root_path, body0='door_link', body1='handle', indent=joint_indent)}\n" + f" }}\n" + f" }}" + ) + + def _sha256(path: Path) -> str: digest = hashlib.sha256() with path.open("rb") as file: @@ -79,14 +616,19 @@ def _sha256(path: Path) -> str: def _write_scene_wrapper(path: Path) -> None: wrapper_defs = [] - for source_path, runtime_key in SOURCE_TO_RUNTIME_OBJECT_KEY.items(): - wrapper_defs.append( - f""" def Xform "{_wrapper_name(runtime_key)}" ( + for source_path, runtime_key in TOP_LEVEL_SOURCE_TO_RUNTIME_OBJECT_KEY.items(): + if runtime_key == "obj_DryingBox_01": + wrapper_defs.append(_drying_box_surrogate_def(runtime_key)) + else: + wrapper_body = _wrapper_body(source_path, runtime_key) + wrapper_defs.append( + f""" def Xform "{_wrapper_name(runtime_key)}" ( prepend payload = @scene.usd@<{source_path}> ) {{ +{wrapper_body} }}""" - ) + ) deterministic_light_defs = """ def DomeLight "DeterministicDomeLight" @@ -183,13 +725,19 @@ def build_asset_overlay( "source_to_runtime_object_key": SOURCE_TO_RUNTIME_OBJECT_KEY, "runtime_object_keys": list(SOURCE_TO_RUNTIME_OBJECT_KEY.values()), "wrapper_prim_paths": _wrapper_prim_paths(), + "articulation_part_paths": ARTICULATION_PART_PATHS, + "render_object_contracts": _render_object_contracts(), + "drying_box_runtime_asset": DRYING_BOX_RUNTIME_ASSET, "table_uid": TABLE_UID, "required_genmanip_object_uids": REQUIRED_GENMANIP_OBJECT_UIDS, "deterministic_lights": DETERMINISTIC_LIGHTS, "copied_files": _copied_files(overlay_root, copied_paths), "notes": [ "scene.usda exposes a single scene uid under /World for GenManip discovery.", - "Immediate obj_* wrapper prims payload the selected LabUtopia source prims.", + "Immediate obj_* wrapper prims payload top-level LabUtopia source prims except DryingBox_01.", + "DryingBox_01 uses a sanitized runtime surrogate with identity root scale and finite inertial attributes.", + "The drying-box handle is exposed as an articulation part, not an independent payload.", + "Task object wrapper translations normalize LabUtopia source coordinates into the robot/table workspace.", "A deterministic dome light is authored in the runtime wrapper scene.", "Runtime object keys strip one leading obj_ from wrapper prim names.", ], diff --git a/standalone_tools/labutopia_poc/capture_eval_render_diagnostics.py b/standalone_tools/labutopia_poc/capture_eval_render_diagnostics.py index 97fe249..d564dd3 100644 --- a/standalone_tools/labutopia_poc/capture_eval_render_diagnostics.py +++ b/standalone_tools/labutopia_poc/capture_eval_render_diagnostics.py @@ -6,8 +6,10 @@ import argparse import copy import json +import math import os import sys +import traceback from dataclasses import asdict, dataclass from pathlib import Path from types import SimpleNamespace @@ -90,6 +92,118 @@ def classify_boundary( return "readback_visible" +def classify_articulation_runtime_state( + articulation_state: dict[str, dict[str, Any]], + *, + required_articulations: list[str] | None = None, + max_abs_joint_position_rad: float = math.tau, + expected_joint_positions: dict[str, list[float]] | None = None, + joint_position_tolerance_rad: float = 1e-3, +) -> dict[str, Any]: + required = sorted(set(required_articulations or [])) + expected = expected_joint_positions or {} + missing = [name for name in required if name not in articulation_state] + report: dict[str, Any] = { + "runtime_physics_stable": not missing, + "required_articulations": required, + "missing_articulations": missing, + "expected_joint_positions": expected, + "joint_position_tolerance_rad": joint_position_tolerance_rad, + "articulations": {}, + } + for name in missing: + report["articulations"][name] = { + "status": "missing_articulation", + "joint_positions": None, + "invalid_joint_positions": [], + } + for name, state in sorted(articulation_state.items()): + item: dict[str, Any] = { + "status": "stable", + "joint_positions": state.get("joint_positions"), + } + if "dof_names" in state: + item["dof_names"] = state["dof_names"] + if "joint_positions_error" in state: + item["status"] = "joint_positions_error" + item["joint_positions_error"] = state["joint_positions_error"] + item["invalid_joint_positions"] = [] + report["runtime_physics_stable"] = False + report["articulations"][name] = item + continue + joint_positions = state.get("joint_positions") + if not isinstance(joint_positions, list): + item["status"] = "missing_joint_positions" + item["invalid_joint_positions"] = [] + report["runtime_physics_stable"] = False + report["articulations"][name] = item + continue + invalid_positions: list[float] = [] + for position in joint_positions: + try: + value = float(position) + except (TypeError, ValueError): + invalid_positions.append(position) # type: ignore[arg-type] + continue + if not math.isfinite(value) or abs(value) > max_abs_joint_position_rad: + invalid_positions.append(value) + if invalid_positions: + item["status"] = "unstable_joint_positions" + item["invalid_joint_positions"] = invalid_positions + item["max_abs_joint_position_rad"] = max_abs_joint_position_rad + report["runtime_physics_stable"] = False + else: + item["invalid_joint_positions"] = [] + expected_positions = expected.get(name) + if expected_positions is not None and not invalid_positions: + observed = [float(position) for position in joint_positions] + expected_values = [float(position) for position in expected_positions] + errors = [ + abs(obs - exp) + for obs, exp in zip(observed, expected_values) + ] + length_mismatch = len(observed) != len(expected_values) + if length_mismatch or any( + error > joint_position_tolerance_rad for error in errors + ): + item["status"] = "target_position_mismatch" + item["expected_joint_positions"] = expected_values + item["joint_position_errors"] = errors + item["joint_position_length_mismatch"] = length_mismatch + report["runtime_physics_stable"] = False + report["articulations"][name] = item + return report + + +def build_claim_boundary( + *, + boundary_classification: str | None, + render_validation_passed: bool, + runtime_physics_stable: bool, + diagnostic_completed: bool = True, + diagnostic_error: dict[str, Any] | None = None, +) -> dict[str, Any]: + blockers: list[str] = [] + if not diagnostic_completed: + blockers.append("runtime_diagnostic_not_completed") + if diagnostic_error is not None: + blockers.append("runtime_diagnostic_exception") + readback_visible = boundary_classification == "readback_visible" + if not readback_visible: + blockers.append("eval_camera_readback_not_visible") + if not render_validation_passed: + blockers.append("render_validation_not_passed") + if not runtime_physics_stable: + blockers.append("runtime_physics_unstable") + task_render_accepted = readback_visible and render_validation_passed + official_baseline_evaluable = task_render_accepted and runtime_physics_stable + return { + "task_render_accepted": task_render_accepted, + "official_baseline_evaluable": official_baseline_evaluable, + "blockers": blockers, + } + + def _jsonable(value: Any) -> Any: if hasattr(value, "tolist"): return value.tolist() @@ -220,6 +334,26 @@ def _apply_env_vars(eval_config: dict[str, Any], default_config: dict[str, Any]) return applied +def apply_camera_config_override( + eval_config: dict[str, Any], + override_config_path: str | None, +) -> dict[str, str] | None: + if not override_config_path: + return None + domain_randomization = eval_config.setdefault("domain_randomization", {}) + if not isinstance(domain_randomization, dict): + raise TypeError("domain_randomization must be a mapping") + cameras = domain_randomization.setdefault("cameras", {}) + if not isinstance(cameras, dict): + raise TypeError("domain_randomization.cameras must be a mapping") + previous = cameras.get("config_path") + cameras["config_path"] = override_config_path + return { + "previous_config_path": str(previous) if previous is not None else "", + "override_config_path": override_config_path, + } + + def _camera_render_product_path(camera: Any) -> str | None: for attr_name in ("render_product_path", "_render_product_path"): value = getattr(camera, attr_name, None) @@ -292,6 +426,16 @@ def _named_prim_metadata(named_prims: dict[str, Any]) -> dict[str, dict[str, Any item["joint_positions"] = _jsonable(prim.get_joint_positions()) except Exception as exc: # pragma: no cover - Isaac-only diagnostics. item["joint_positions_error"] = repr(exc) + dof_names = getattr(prim, "dof_names", None) + if dof_names is None: + articulation_view = getattr(prim, "_articulation_view", None) + dof_names = getattr(articulation_view, "dof_names", None) + if dof_names is not None: + item["dof_names"] = _jsonable(dof_names) + try: + item["dof_count"] = len(dof_names) + except TypeError: + pass metadata[str(name)] = item return metadata @@ -327,6 +471,47 @@ def _write_json(path: Path, payload: dict[str, Any]) -> None: ) +def _required_articulation_uids(eval_config: dict[str, Any]) -> list[str]: + required: list[str] = [] + for uid, cfg in (eval_config.get("object_config") or {}).items(): + if not isinstance(cfg, dict): + continue + articulation_info = cfg.get("articulation_info") or {} + if cfg.get("is_articulated") is True or articulation_info.get("is_articulated") is True: + required.append(str(uid)) + return sorted(set(required)) + + +def _expected_articulation_joint_positions( + eval_config: dict[str, Any], +) -> dict[str, list[float]]: + expected: dict[str, list[float]] = {} + for uid, cfg in (eval_config.get("object_config") or {}).items(): + if not isinstance(cfg, dict): + continue + target_positions = cfg.get("target_positions") + if target_positions is None: + continue + expected[str(uid)] = [float(position) for position in target_positions] + return expected + + +def _scene_collection_keys(scene: Any | None) -> dict[str, Any]: + if scene is None: + return { + "camera_names": [], + "object_uids": [], + "articulation_uids": [], + "robot_count": 0, + } + return { + "camera_names": sorted(str(key) for key in (getattr(scene, "camera_list", {}) or {}).keys()), + "object_uids": sorted(str(key) for key in (getattr(scene, "object_list", {}) or {}).keys()), + "articulation_uids": sorted(str(key) for key in (getattr(scene, "articulation_list", {}) or {}).keys()), + "robot_count": len(getattr(scene, "robot_list", []) or []), + } + + def run_runtime_diagnostics(args: argparse.Namespace) -> dict[str, Any]: current_dir = REPO_ROOT output_dir = Path(args.output_dir) @@ -351,7 +536,13 @@ def run_runtime_diagnostics(args: argparse.Namespace) -> dict[str, Any]: task_name=args.task, current_dir=current_dir, ) + camera_config_override = apply_camera_config_override( + eval_config, + args.camera_config_override, + ) applied_env_vars = _apply_env_vars(eval_config, default_config) + required_articulations = _required_articulation_uids(eval_config) + expected_joint_positions = _expected_articulation_joint_positions(eval_config) from isaacsim import SimulationApp # type: ignore @@ -369,19 +560,32 @@ def run_runtime_diagnostics(args: argparse.Namespace) -> dict[str, Any]: "port": args.port, "reset_frame_capture": True, "assets_override": asdict(assets_override) if assets_override is not None else None, + "camera_config_override": camera_config_override, "applied_env_vars": applied_env_vars, "camera_frames": [], "camera_poses": {}, "render_products": {}, "render_product_binding": {}, + "scene_collections": _scene_collection_keys(None), "object_world_poses": {}, "object_extents": {}, "projected_object_centers": {}, "articulation_state": {}, - "claim_boundary": { - "task_render_accepted": False, - "official_baseline_evaluable": False, - }, + "required_articulations": required_articulations, + "expected_articulation_joint_positions": expected_joint_positions, + "runtime_sanity": classify_articulation_runtime_state( + {}, + required_articulations=required_articulations, + expected_joint_positions=expected_joint_positions, + ), + "boundary_classification": None, + "diagnostic_error": None, + "claim_boundary": build_claim_boundary( + boundary_classification=None, + render_validation_passed=False, + runtime_physics_stable=False, + diagnostic_completed=False, + ), } try: @@ -437,6 +641,7 @@ def capture_get_eval_camera_data(camera_list: dict[str, Any]) -> dict[str, Any]: ) env.reset(args.seed, eval_config, default_config) scene = env.scene + diagnostics["scene_collections"] = _scene_collection_keys(scene) if scene is not None: diagnostics["object_world_poses"] = _named_prim_metadata( getattr(scene, "object_list", {}) or {} @@ -456,7 +661,41 @@ def capture_get_eval_camera_data(camera_list: dict[str, Any]) -> dict[str, Any]: readback_stats, recorder_stats, ) + diagnostics["runtime_sanity"] = classify_articulation_runtime_state( + diagnostics["articulation_state"], + required_articulations=required_articulations, + expected_joint_positions=expected_joint_positions, + ) + diagnostics["claim_boundary"] = build_claim_boundary( + boundary_classification=diagnostics["boundary_classification"], + render_validation_passed=False, + runtime_physics_stable=bool( + diagnostics["runtime_sanity"]["runtime_physics_stable"] + ), + diagnostic_completed=True, + ) diagnostics["recorder_traj_log_dir"] = env.traj_log_dir if env is not None else None + except Exception as exc: + diagnostics["diagnostic_error"] = { + "type": type(exc).__name__, + "repr": repr(exc), + "traceback": traceback.format_exc(), + } + diagnostics["scene_collections"] = _scene_collection_keys( + env.scene if env is not None else None + ) + diagnostics["runtime_sanity"] = classify_articulation_runtime_state( + diagnostics["articulation_state"], + required_articulations=required_articulations, + expected_joint_positions=expected_joint_positions, + ) + diagnostics["claim_boundary"] = build_claim_boundary( + boundary_classification=diagnostics["boundary_classification"], + render_validation_passed=False, + runtime_physics_stable=False, + diagnostic_completed=False, + diagnostic_error=diagnostics["diagnostic_error"], + ) finally: _write_json(output_dir / "diagnostics.json", diagnostics) if env is not None: @@ -506,6 +745,11 @@ def parse_args(argv: list[str] | None = None) -> argparse.Namespace: help="Compatibility flag. Reset-frame capture is always enabled and writes 00000.png.", ) parser.add_argument("--local", action="store_true", help="Run Isaac with GUI.") + parser.add_argument( + "--camera-config-override", + default=None, + help="Diagnostics-only camera config path to apply to the selected eval config.", + ) return parser.parse_args(argv) diff --git a/standalone_tools/labutopia_poc/validate_task_package.py b/standalone_tools/labutopia_poc/validate_task_package.py index d951302..b172791 100644 --- a/standalone_tools/labutopia_poc/validate_task_package.py +++ b/standalone_tools/labutopia_poc/validate_task_package.py @@ -7,7 +7,9 @@ import copy import io import json +import math import sys +from collections import Counter from pathlib import Path from typing import Any @@ -38,9 +40,89 @@ "obj_beaker2": "/World/labutopia_level1_poc/obj_obj_beaker2", "obj_target_plat": "/World/labutopia_level1_poc/obj_obj_target_plat", "obj_DryingBox_01": "/World/labutopia_level1_poc/obj_obj_DryingBox_01", - "obj_DryingBox_01_handle": "/World/labutopia_level1_poc/obj_obj_DryingBox_01_handle", + "obj_DryingBox_01_handle": "/World/labutopia_level1_poc/obj_obj_DryingBox_01/handle", "table": "/World/labutopia_level1_poc/obj_table", } +EXPECTED_ARTICULATION_PART_PATHS = { + "obj_DryingBox_01_handle": "/World/labutopia_level1_poc/obj_obj_DryingBox_01/handle", +} +EXPECTED_RENDER_VISIBLE_OBJECTS = { + "level1_pick": ["obj_conical_bottle02"], + "level1_place": ["obj_beaker2", "obj_target_plat"], + "level1_open_door": ["obj_DryingBox_01", "obj_DryingBox_01_handle"], +} +EXPECTED_HIDDEN_NON_TASK_OBJECTS = { + "level1_pick": ["obj_beaker2", "obj_target_plat", "obj_DryingBox_01"], + "level1_place": ["obj_conical_bottle02", "obj_DryingBox_01"], + "level1_open_door": [ + "obj_conical_bottle02", + "obj_beaker2", + "obj_target_plat", + ], +} +EXPECTED_FRANKA_TASK_CAMERA_CONFIGS = { + "level1_pick": "configs/cameras/labutopia_franka_poc_pick.yml", + "level1_place": "configs/cameras/labutopia_franka_poc_place.yml", + "level1_open_door": "configs/cameras/labutopia_franka_poc_open_door.yml", +} +EXPECTED_FRANKA_TASK_CAMERA2_CONTRACTS = { + "level1_pick": { + "position": [0.28, -0.55, 1.2], + "orientation": [0.87184, 0.4898, 0.0, 0.0], + "resolution": [512, 512], + }, + "level1_place": { + "position": [0.26, -0.7, 1.32], + "orientation": [0.87184, 0.4898, 0.0, 0.0], + "resolution": [512, 512], + }, + "level1_open_door": { + "position": [0.75, -0.56, 1.02], + "orientation": [0.87184, 0.4898, 0.0, 0.0], + "resolution": [512, 512], + "focal_length": 5.0, + "horizontal_aperture": 10.0, + }, +} +EXPECTED_RENDER_PIXEL_THRESHOLDS = { + "level1_pick": { + "obj_conical_bottle02": { + "min_width_px": 36, + "min_height_px": 48, + "min_area_fraction": 0.01, + }, + }, + "level1_place": { + "obj_beaker2": { + "min_width_px": 34, + "min_height_px": 34, + "min_area_fraction": 0.008, + }, + "obj_target_plat": { + "min_width_px": 42, + "min_height_px": 24, + "min_area_fraction": 0.006, + }, + }, + "level1_open_door": { + "obj_DryingBox_01": { + "min_width_px": 160, + "min_height_px": 150, + "min_area_fraction": 0.12, + }, + "obj_DryingBox_01_handle": { + "min_width_px": 18, + "min_height_px": 64, + "min_area_fraction": 0.004, + }, + }, +} +EXPECTED_RENDER_REJECTIONS = { + "black_frame", + "low_texture", + "required_object_missing", + "severe_clipping", +} EXPECTED_DETERMINISTIC_LIGHTS = [ { "prim_path": "/World/labutopia_level1_poc/DeterministicDomeLight", @@ -48,11 +130,51 @@ "intensity": 1000, } ] +EXPECTED_DRYING_BOX_RUNTIME_ASSET = { + "strategy": "sanitized_surrogate", + "wrapper_prim_path": "/World/labutopia_level1_poc/obj_obj_DryingBox_01", + "base_joint_name": "BaseFixedJoint", + "joint_name": "RevoluteJoint", + "removed_source_joint_types": ["PhysicsPrismaticJoint"], + "source_payload_used": False, + "visual_affordances": [ + { + "name": "high_contrast_door_panel", + "display_color": [0.28, 0.34, 0.42], + }, + { + "name": "door_outline_seams", + "display_color": [0.04, 0.05, 0.06], + }, + { + "name": "handle_mount_backplate", + "display_color": [0.05, 0.07, 0.09], + }, + { + "name": "high_contrast_handle", + "display_color": [1.0, 0.18, 0.04], + }, + ], +} +EXPECTED_DRYING_BOX_VISUAL_AFFORDANCE_TOKENS = [ + 'def Material "door_seam_mat"', + 'def Material "handle_mount_mat"', + 'def Cube "door_left_seam"', + 'def Cube "door_right_seam"', + 'def Cube "door_top_seam"', + 'def Cube "door_bottom_seam"', + 'def Cube "handle_mount_backplate"', + "double3 xformOp:translate = (0.18, -0.174, 0.05)", + "double3 xformOp:translate = (0.18, -0.22, 0.05)", + "double3 xformOp:scale = (0.075, 0.014, 0.28)", + "point3f physics:localPos0 = (0.18, -0.10, 0.04)", +] EXPECTED_FRANKA_CAMERA_AXES = { "camera1": "usd", "camera2": "usd", } -EXPECTED_FRANKA_CAMERA2_POSITION = [9.6, 0.0, 2.5] +EXPECTED_FRANKA_CAMERA2_POSITION = [0.45, -1.1, 1.55] +EXPECTED_FRANKA_CAMERA2_ORIENTATION = [0.87184, 0.4898, 0.0, 0.0] PROFILE_EXPECTATIONS = { "franka_poc": { "robot_type": "manip/franka/panda_hand", @@ -92,6 +214,13 @@ "skip_steps", "succ_cnts", }, + "manip/default/check_joint_angle": { + "articulation_obj_uid", + "joint_name", + "angle_deg_range", + "skip_steps", + "succ_cnts", + }, } @@ -195,10 +324,58 @@ def _validate_assets_manifest() -> None: manifest.get("wrapper_prim_paths") == EXPECTED_WRAPPER_PRIM_PATHS, f"{path}: wrapper_prim_paths must preserve GenManip key stripping", ) + _assert( + manifest.get("articulation_part_paths") == EXPECTED_ARTICULATION_PART_PATHS, + f"{path}: articulation_part_paths must expose the nested drying-box handle", + ) + contracts = manifest.get("render_object_contracts") + _assert( + isinstance(contracts, dict), + f"{path}: render_object_contracts must be a mapping", + ) + required_render_uids = { + uid + for required in EXPECTED_RENDER_VISIBLE_OBJECTS.values() + for uid in required + } + for uid in sorted(required_render_uids): + contract = contracts.get(uid) + _assert(isinstance(contract, dict), f"{path}: missing render contract {uid}") + _assert( + contract.get("wrapper_prim_path") == EXPECTED_WRAPPER_PRIM_PATHS[uid], + f"{path}: render contract {uid} wrapper_prim_path mismatch", + ) + color = contract.get("display_color") + _assert( + isinstance(color, list) + and len(color) == 3 + and all(isinstance(value, (int, float)) for value in color) + and all(0.0 <= float(value) <= 1.0 for value in color) + and color != [0.5, 0.5, 0.5], + f"{path}: render contract {uid} must declare visible display_color", + ) + bbox = contract.get("expected_world_bbox_lwh_m") + _assert( + isinstance(bbox, dict) + and isinstance(bbox.get("min"), list) + and isinstance(bbox.get("max"), list) + and len(bbox["min"]) == len(bbox["max"]) == 3, + f"{path}: render contract {uid} must declare bbox min/max", + ) + handle_contract = contracts.get("obj_DryingBox_01_handle", {}) + _assert( + handle_contract.get("compose_nested_transform_with_parent") + == "obj_DryingBox_01", + f"{path}: handle contract must compose through obj_DryingBox_01", + ) _assert( manifest.get("deterministic_lights") == EXPECTED_DETERMINISTIC_LIGHTS, f"{path}: deterministic_lights must declare the runtime wrapper light", ) + _assert( + manifest.get("drying_box_runtime_asset") == EXPECTED_DRYING_BOX_RUNTIME_ASSET, + f"{path}: drying_box_runtime_asset must declare the sanitized surrogate", + ) runtime_scene_text = runtime_scene.read_text(encoding="utf-8") _assert( 'def DomeLight "DeterministicDomeLight"' in runtime_scene_text, @@ -212,6 +389,23 @@ def _validate_assets_manifest() -> None: "inputs:texture:file" not in runtime_scene_text, f"{runtime_scene}: DeterministicDomeLight must not depend on HDR texture", ) + _assert( + 'def Xform "obj_obj_DryingBox_01_handle" (' not in runtime_scene_text, + f"{runtime_scene}: drying-box handle must not be an independent payload", + ) + _assert( + "primvars:displayColor" in runtime_scene_text, + f"{runtime_scene}: missing task object displayColor overrides", + ) + _assert( + '"MaterialBindingAPI"' in runtime_scene_text, + f"{runtime_scene}: material-bound task objects must apply MaterialBindingAPI", + ) + for token in EXPECTED_DRYING_BOX_VISUAL_AFFORDANCE_TOKENS: + _assert( + token in runtime_scene_text, + f"{runtime_scene}: drying-box door visual affordance missing {token}", + ) generated_manifest = manifest.get("generated_manifest") _assert( @@ -231,6 +425,9 @@ def _validate_assets_manifest() -> None: "wrapper_prim_paths": "wrapper_prim_paths", "source_to_runtime_object_key": "source_to_runtime_object_key", "deterministic_lights": "deterministic_lights", + "articulation_part_paths": "articulation_part_paths", + "render_object_contracts": "render_object_contracts", + "drying_box_runtime_asset": "drying_box_runtime_asset", }.items(): _assert( manifest.get(common_key) == generated.get(generated_key), @@ -261,35 +458,199 @@ def _validate_task_semantics() -> None: _assert(key in settings, f"{path}: open_door preferred metric missing {key}") +def _inspect_drying_box_articulation_physics(runtime_scene: Path) -> dict[str, Any]: + from pxr import Usd, UsdPhysics + + stage = Usd.Stage.Open(str(runtime_scene)) + _assert(stage is not None, f"{runtime_scene}: failed to open USD stage") + root_path = EXPECTED_WRAPPER_PRIM_PATHS["obj_DryingBox_01"] + root = stage.GetPrimAtPath(root_path) + _assert(root and root.IsValid(), f"{runtime_scene}: missing {root_path}") + + def _attr_value(prim: Any, attr_name: str) -> Any: + attr = prim.GetAttribute(attr_name) + if not attr or not attr.IsValid(): + return None + return attr.Get() + + def _float_list(value: Any) -> list[float]: + if value is None: + return [] + if hasattr(value, "GetReal") and hasattr(value, "GetImaginary"): + imaginary = value.GetImaginary() + return [float(value.GetReal())] + [float(item) for item in imaginary] + try: + return [float(item) for item in value] + except TypeError: + return [float(value)] + + def _rounded(values: list[float]) -> list[float]: + return [round(value, 6) for value in values] + + def _all_finite(values: list[float]) -> bool: + return bool(values) and all(math.isfinite(value) for value in values) + + def _is_zero_principal_axes(values: list[float]) -> bool: + return bool(values) and all(abs(value) <= 1e-9 for value in values) + + root_scale = _float_list(_attr_value(root, "xformOp:scale")) + rounded_root_scale = _rounded(root_scale) + non_identity_root_scale = bool(root_scale) and any( + abs(value - 1.0) > 1e-6 for value in root_scale + ) + + rigid_link_paths: list[str] = [] + zero_mass_links: list[str] = [] + zero_inertia_links: list[str] = [] + invalid_center_of_mass_links: list[str] = [] + invalid_principal_axes_links: list[str] = [] + for prim in Usd.PrimRange(root): + if not prim.HasAPI(UsdPhysics.RigidBodyAPI): + continue + path = str(prim.GetPath()) + rigid_link_paths.append(path) + mass_api = UsdPhysics.MassAPI(prim) + mass_attr = mass_api.GetMassAttr() + mass = mass_attr.Get() if mass_attr and mass_attr.IsValid() else None + if mass is not None and float(mass) <= 0.0: + zero_mass_links.append(path) + inertia_attr = mass_api.GetDiagonalInertiaAttr() + inertia = inertia_attr.Get() if inertia_attr and inertia_attr.IsValid() else None + if inertia is not None and all(float(value) <= 0.0 for value in inertia): + zero_inertia_links.append(path) + center_of_mass = _float_list(_attr_value(prim, "physics:centerOfMass")) + if center_of_mass and not _all_finite(center_of_mass): + invalid_center_of_mass_links.append(path) + principal_axes = _float_list(_attr_value(prim, "physics:principalAxes")) + if principal_axes and ( + not _all_finite(principal_axes) + or _is_zero_principal_axes(principal_axes) + ): + invalid_principal_axes_links.append(path) + + duplicate_rigid_link_names = { + name: count + for name, count in Counter(Path(path).name for path in rigid_link_paths).items() + if count > 1 + } + + expected_joint_types = {"PhysicsFixedJoint", "PhysicsRevoluteJoint"} + unexpected_joint_types: list[str] = [] + invalid_joint_body_targets: list[dict[str, str]] = [] + world_fixed_base_joint_paths: list[str] = [] + joint_paths: list[str] = [] + body_link_path = f"{root_path}/body_link" + for prim in Usd.PrimRange(root): + type_name = prim.GetTypeName() + if "Joint" not in type_name: + continue + path = str(prim.GetPath()) + joint_paths.append(path) + if type_name not in expected_joint_types and type_name not in unexpected_joint_types: + unexpected_joint_types.append(type_name) + for rel_name in ("physics:body0", "physics:body1"): + relationship = prim.GetRelationship(rel_name) + if not relationship: + continue + for target in relationship.GetTargets(): + target_prim = stage.GetPrimAtPath(target) + if not target_prim or not target_prim.HasAPI(UsdPhysics.RigidBodyAPI): + invalid_joint_body_targets.append( + { + "joint_path": path, + "relationship": rel_name, + "target": str(target), + } + ) + if type_name == "PhysicsFixedJoint": + body0_rel = prim.GetRelationship("physics:body0") + body1_rel = prim.GetRelationship("physics:body1") + body0_targets = body0_rel.GetTargets() if body0_rel else [] + body1_targets = body1_rel.GetTargets() if body1_rel else [] + if not body0_targets and [str(target) for target in body1_targets] == [ + body_link_path + ]: + world_fixed_base_joint_paths.append(path) + + runtime_topology_ready = not any( + [ + non_identity_root_scale, + duplicate_rigid_link_names, + invalid_center_of_mass_links, + invalid_principal_axes_links, + invalid_joint_body_targets, + unexpected_joint_types, + not world_fixed_base_joint_paths, + ] + ) + + return { + "root_path": root_path, + "root_scale": rounded_root_scale, + "non_identity_root_scale": non_identity_root_scale, + "rigid_link_paths": rigid_link_paths, + "duplicate_rigid_link_names": duplicate_rigid_link_names, + "zero_mass_links": zero_mass_links, + "zero_inertia_links": zero_inertia_links, + "invalid_center_of_mass_links": invalid_center_of_mass_links, + "invalid_principal_axes_links": invalid_principal_axes_links, + "joint_paths": joint_paths, + "world_fixed_base_joint_paths": world_fixed_base_joint_paths, + "invalid_joint_body_targets": invalid_joint_body_targets, + "unexpected_joint_types": sorted(unexpected_joint_types), + "runtime_topology_ready": runtime_topology_ready, + "sanitized_for_physx": not zero_mass_links and not zero_inertia_links, + } + + def _validate_camera_configs() -> None: - for profile, expected in PROFILE_EXPECTATIONS.items(): - path = ROOT / expected["camera_config"] - data = _load_yaml(path) - _assert(isinstance(data, dict), f"{path}: expected camera mapping") - if profile == "franka_poc": - _assert( - set(EXPECTED_FRANKA_CAMERA_AXES).issubset(data), - f"{path}: franka_poc cameras must include {sorted(EXPECTED_FRANKA_CAMERA_AXES)}", - ) - for camera_name, camera in data.items(): - _assert( - isinstance(camera, dict), - f"{path}:{camera_name}: expected camera settings mapping", - ) - missing = CAMERA_CLEANUP_FLAGS - set(camera) - _assert( - not missing, - f"{path}:{camera_name}: {profile} camera missing cleanup flags {missing}", + camera_config_paths: dict[str, list[tuple[str, str | None]]] = { + "franka_poc": [("base", None)] + + [ + (config_path, task_name) + for task_name, config_path in EXPECTED_FRANKA_TASK_CAMERA_CONFIGS.items() + ], + "lift2_candidate": [ + (PROFILE_EXPECTATIONS["lift2_candidate"]["camera_config"], None) + ], + } + for profile, path_items in camera_config_paths.items(): + expected = PROFILE_EXPECTATIONS[profile] + for config_path, task_name in path_items: + path = ROOT / ( + expected["camera_config"] if config_path == "base" else config_path ) - if profile == "franka_poc": - expected_axes = EXPECTED_FRANKA_CAMERA_AXES.get(camera_name) - if expected_axes is not None: - _assert( - camera.get("camera_axes") == expected_axes, - f"{path}:{camera_name}: camera_axes must remain {expected_axes!r}", - ) + _validate_camera_config_file(path, profile, task_name) + + +def _validate_camera_config_file(path: Path, profile: str, task_name: str | None) -> None: + data = _load_yaml(path) + _assert(isinstance(data, dict), f"{path}: expected camera mapping") + if profile == "franka_poc": + _assert( + set(EXPECTED_FRANKA_CAMERA_AXES).issubset(data), + f"{path}: franka_poc cameras must include {sorted(EXPECTED_FRANKA_CAMERA_AXES)}", + ) + for camera_name, camera in data.items(): + _assert( + isinstance(camera, dict), + f"{path}:{camera_name}: expected camera settings mapping", + ) + missing = CAMERA_CLEANUP_FLAGS - set(camera) + _assert( + not missing, + f"{path}:{camera_name}: {profile} camera missing cleanup flags {missing}", + ) if profile == "franka_poc": - camera2 = data.get("camera2", {}) + expected_axes = EXPECTED_FRANKA_CAMERA_AXES.get(camera_name) + if expected_axes is not None: + _assert( + camera.get("camera_axes") == expected_axes, + f"{path}:{camera_name}: camera_axes must remain {expected_axes!r}", + ) + if profile == "franka_poc": + camera2 = data.get("camera2", {}) + if task_name is None: position = camera2.get("position") _assert( isinstance(position, list) and len(position) == 3, @@ -302,6 +663,32 @@ def _validate_camera_configs() -> None: ), f"{path}:camera2 position must remain {EXPECTED_FRANKA_CAMERA2_POSITION!r}", ) + orientation = camera2.get("orientation") + _assert( + isinstance(orientation, list) and len(orientation) == 4, + f"{path}:camera2 orientation must be a quaternion", + ) + _assert( + all( + abs(float(actual) - expected) < 1e-6 + for actual, expected in zip( + orientation, + EXPECTED_FRANKA_CAMERA2_ORIENTATION, + ) + ), + f"{path}:camera2 orientation must remain {EXPECTED_FRANKA_CAMERA2_ORIENTATION!r}", + ) + else: + contract = EXPECTED_FRANKA_TASK_CAMERA2_CONTRACTS[task_name] + for key, expected_value in contract.items(): + _assert( + camera2.get(key) == expected_value, + f"{path}:camera2 {key} must be {expected_value!r}", + ) + _assert( + camera2.get("task_view") == task_name, + f"{path}:camera2 task_view must be {task_name!r}", + ) def _walk_goal_dicts(value: Any, path: Path) -> list[dict[str, Any]]: @@ -326,6 +713,148 @@ def _validate_metric(metric: dict[str, Any], path: Path) -> None: _assert(not missing, f"{path}: metric {metric_type} missing top-level params {missing}") +def _task_leaf_name(task_name: str) -> str: + return task_name.rsplit("/", 1)[-1] + + +def _validate_render_validation( + cfg: dict[str, Any], path: Path, camera_names: set[str] +) -> None: + task_name = str(cfg.get("task_name")) + leaf_name = _task_leaf_name(task_name) + if path.parent.name != "franka_poc": + return + expected_objects = EXPECTED_RENDER_VISIBLE_OBJECTS.get(leaf_name) + _assert( + expected_objects is not None, + f"{path}: unexpected Franka task leaf {leaf_name!r}", + ) + validation = cfg.get("labutopia_render_validation") + _assert( + isinstance(validation, dict), + f"{path}: missing labutopia_render_validation", + ) + _assert( + validation.get("schema_version") == 1, + f"{path}: labutopia_render_validation.schema_version must be 1", + ) + _assert( + validation.get("primary_camera") == "camera2", + f"{path}: primary_camera must be camera2", + ) + expected_camera_config = EXPECTED_FRANKA_TASK_CAMERA_CONFIGS[leaf_name] + _assert( + cfg.get("domain_randomization", {}) + .get("cameras", {}) + .get("config_path") + == expected_camera_config, + f"{path}: franka_poc camera config must be {expected_camera_config!r}", + ) + _assert( + validation.get("evidence_camera_config") == expected_camera_config, + f"{path}: evidence_camera_config must be {expected_camera_config!r}", + ) + required_cameras = validation.get("required_camera_names") + _assert( + isinstance(required_cameras, list) + and set(required_cameras).issubset(camera_names), + f"{path}: required_camera_names must exist in camera config", + ) + _assert( + validation.get("required_visible_objects") == expected_objects, + f"{path}: required_visible_objects must be {expected_objects!r}", + ) + expected_hidden_objects = EXPECTED_HIDDEN_NON_TASK_OBJECTS[leaf_name] + _assert( + validation.get("hidden_non_task_objects") == expected_hidden_objects, + f"{path}: hidden_non_task_objects must be {expected_hidden_objects!r}", + ) + active_rules = [ + item + for item in cfg.get("preprocess_config", []) + if isinstance(item, dict) and item.get("type") == "set_object_active" + ] + _assert( + active_rules + == [ + { + "type": "set_object_active", + "config": {"active": False, "uids": expected_hidden_objects}, + } + ], + f"{path}: preprocess_config must hide exactly {expected_hidden_objects!r}", + ) + thresholds = validation.get("object_pixel_thresholds") + expected_thresholds = EXPECTED_RENDER_PIXEL_THRESHOLDS[leaf_name] + _assert( + isinstance(thresholds, dict), + f"{path}: missing object_pixel_thresholds", + ) + _assert( + set(thresholds) == set(expected_thresholds), + f"{path}: object_pixel_thresholds must cover {sorted(expected_thresholds)}", + ) + for uid, expected in expected_thresholds.items(): + actual = thresholds.get(uid) + _assert(isinstance(actual, dict), f"{path}: {uid} threshold must be a mapping") + for key, value in expected.items(): + _assert( + actual.get(key) == value, + f"{path}: {uid}.{key} must be {value!r}", + ) + _assert( + validation.get("evidence_policy") == {"direct_render": False}, + f"{path}: evidence_policy must forbid direct render evidence", + ) + rejection_rules = validation.get("reject_frame_if") + _assert( + isinstance(rejection_rules, list) + and EXPECTED_RENDER_REJECTIONS.issubset(set(rejection_rules)), + f"{path}: reject_frame_if missing required rules", + ) + + +def _validate_open_door_articulation_contract(cfg: dict[str, Any], path: Path) -> None: + if _task_leaf_name(str(cfg.get("task_name"))) != "level1_open_door": + return + object_config = cfg.get("object_config") + _assert(isinstance(object_config, dict), f"{path}: object_config must be a mapping") + drying_box = object_config.get("obj_DryingBox_01") + _assert( + isinstance(drying_box, dict), + f"{path}: open_door must configure obj_DryingBox_01 articulation", + ) + _assert( + drying_box.get("type") == "existed_object", + f"{path}: obj_DryingBox_01 must be an existed_object", + ) + _assert( + drying_box.get("uid_list") == ["obj_DryingBox_01"], + f"{path}: obj_DryingBox_01 uid_list mismatch", + ) + _assert( + drying_box.get("is_articulated") is True, + f"{path}: obj_DryingBox_01 must be articulated", + ) + _assert( + drying_box.get("target_positions") == [0.0], + f"{path}: obj_DryingBox_01 must start closed with target_positions [0.0]", + ) + articulation_info = drying_box.get("articulation_info") + _assert( + isinstance(articulation_info, dict), + f"{path}: obj_DryingBox_01 missing articulation_info", + ) + _assert( + articulation_info.get("is_articulated") is True, + f"{path}: articulation_info.is_articulated must be true", + ) + _assert( + articulation_info.get("part", {}).get("handle") == "/handle", + f"{path}: articulation_info.part.handle must point to /handle", + ) + + def _validate_runtime_task(path: Path) -> None: sys.path.insert(0, str(ROOT)) from genmanip.core.scene.scene_config import SceneConfig @@ -358,10 +887,19 @@ def _validate_runtime_task(path: Path) -> None: f"{path}: {profile} robot type must be {expected['robot_type']!r}", ) camera = cfg.get("domain_randomization", {}).get("cameras", {}) + expected_camera_config = ( + EXPECTED_FRANKA_TASK_CAMERA_CONFIGS[_task_leaf_name(str(task_name))] + if profile == "franka_poc" + else expected["camera_config"] + ) _assert( - camera.get("config_path") == expected["camera_config"], - f"{path}: {profile} camera config must be {expected['camera_config']!r}", + camera.get("config_path") == expected_camera_config, + f"{path}: {profile} camera config must be {expected_camera_config!r}", ) + camera_path = ROOT / expected_camera_config + camera_names = set(_load_yaml(camera_path)) + _validate_render_validation(cfg, path, camera_names) + _validate_open_door_articulation_contract(cfg, path) generation_config = cfg.get("generation_config") _assert( @@ -425,6 +963,19 @@ def _validate_metrics_manager_lazy_registration() -> None: def validate_task_package() -> None: _validate_assets_manifest() + manifest = _load_json(PACKAGE_ROOT / "common/assets_manifest.json") + runtime_scene = ( + Path(manifest["overlay_root"]) / f"{manifest['runtime_usd_name']}.usda" + ) + physics_report = _inspect_drying_box_articulation_physics(runtime_scene) + _assert( + physics_report["sanitized_for_physx"], + f"{runtime_scene}: DryingBox articulation physics is not sanitized: {physics_report}", + ) + _assert( + physics_report["runtime_topology_ready"], + f"{runtime_scene}: DryingBox articulation topology is not runtime-ready: {physics_report}", + ) _validate_task_semantics() _validate_camera_configs() for path in _indexed_task_yaml_paths(): diff --git a/tests/labutopia_poc/test_build_asset_overlay.py b/tests/labutopia_poc/test_build_asset_overlay.py index 71869b5..9df422d 100644 --- a/tests/labutopia_poc/test_build_asset_overlay.py +++ b/tests/labutopia_poc/test_build_asset_overlay.py @@ -36,10 +36,70 @@ def test_build_asset_overlay_writes_scene_wrapper_manifest_and_cleans_reruns( 'def Xform "obj_obj_conical_bottle02" (\n' ' prepend payload = @scene.usd@' ) in scene_usda + assert 'prepend payload = @scene.usd@' not in scene_usda + assert 'def Xform "obj_obj_DryingBox_01" (' in scene_usda + assert 'prepend apiSchemas = ["PhysicsArticulationRootAPI"]' in scene_usda + assert 'def Cube "body_link"' in scene_usda + assert 'def Cube "door_link"' in scene_usda + assert 'def Cube "handle"' in scene_usda + assert 'def Scope "Looks"' in scene_usda + assert 'def Material "door_panel_mat"' in scene_usda + assert 'def Material "door_seam_mat"' in scene_usda + assert 'def Material "handle_mount_mat"' in scene_usda + assert 'def Material "handle_mat"' in scene_usda + assert "outputs:mdl:surface.connect" in scene_usda + assert "OmniPBR.mdl" in scene_usda + assert "inputs:diffuse_color_constant = (1, 0.18, 0.04)" in scene_usda + assert "color3f inputs:diffuseColor = (0.28, 0.34, 0.42)" in scene_usda + assert "color3f inputs:diffuseColor = (1, 0.18, 0.04)" in scene_usda + assert '"MaterialBindingAPI"' in scene_usda assert ( - 'def Xform "obj_obj_DryingBox_01_handle" (\n' - ' prepend payload = @scene.usd@' + 'prepend apiSchemas = ["PhysicsRigidBodyAPI", "PhysicsCollisionAPI", ' + '"PhysicsMassAPI", "MaterialBindingAPI"]' ) in scene_usda + assert ( + "rel material:binding = " + "" + ) in scene_usda + assert ( + "rel material:binding = " + "" + ) in scene_usda + assert 'def Cube "door_left_seam"' in scene_usda + assert 'def Cube "door_right_seam"' in scene_usda + assert 'def Cube "door_top_seam"' in scene_usda + assert 'def Cube "door_bottom_seam"' in scene_usda + assert "double3 xformOp:translate = (-0.255, -0.168, 0.01)" in scene_usda + assert "double3 xformOp:scale = (0.012, 0.012, 0.43)" in scene_usda + assert "color3f inputs:diffuseColor = (0.04, 0.05, 0.06)" in scene_usda + assert 'def Cube "handle_mount_backplate"' in scene_usda + assert "double3 xformOp:translate = (0.18, -0.174, 0.05)" in scene_usda + assert "double3 xformOp:scale = (0.075, 0.014, 0.28)" in scene_usda + assert "double3 xformOp:translate = (0.18, -0.22, 0.05)" in scene_usda + assert 'def Cube "handle_visual_marker"' not in scene_usda + assert "double3 xformOp:translate = (0.18, -0.165, 0.05)" not in scene_usda + assert "double3 xformOp:scale = (0.035, 0.06, 0.18)" not in scene_usda + assert 'def PhysicsFixedJoint "BaseFixedJoint"' in scene_usda + assert ( + "rel physics:body1 = " + "" + ) in scene_usda + assert 'def PhysicsRevoluteJoint "RevoluteJoint"' in scene_usda + assert "point3f physics:localPos0 = (-0.25, -0.2, 0.01)" in scene_usda + assert "point3f physics:localPos1 = (-0.25, 0, 0)" in scene_usda + assert "point3f physics:localPos0 = (0.18, -0.10, 0.04)" in scene_usda + assert "double3 xformOp:translate = (-0.18, -0.22, 0.05)" not in scene_usda + assert "double3 xformOp:translate = (-0.18, -0.165, 0.05)" not in scene_usda + assert 'PhysicsPrismaticJoint' not in scene_usda + assert 'def Xform "obj_obj_DryingBox_01_handle" (' not in scene_usda + assert "primvars:displayColor" in scene_usda + assert '"PhysicsMassAPI"' in scene_usda + assert "float physics:mass = 0.5" in scene_usda + assert "point3f physics:diagonalInertia = (0.01, 0.01, 0.01)" in scene_usda + assert "point3f physics:centerOfMass = (0, 0, 0)" in scene_usda + assert "quatf physics:principalAxes = (1, 0, 0, 0)" in scene_usda + assert "double3 xformOp:translate = (0.28, 0, 0.8)" in scene_usda + assert "double3 xformOp:translate = (0.75, 0.1, 0.78)" in scene_usda assert ( 'def Xform "obj_table" (\n' ' prepend payload = @scene.usd@' @@ -85,9 +145,38 @@ def test_build_asset_overlay_writes_scene_wrapper_manifest_and_cleans_reruns( "obj_beaker2": "/World/labutopia_level1_poc/obj_obj_beaker2", "obj_target_plat": "/World/labutopia_level1_poc/obj_obj_target_plat", "obj_DryingBox_01": "/World/labutopia_level1_poc/obj_obj_DryingBox_01", - "obj_DryingBox_01_handle": "/World/labutopia_level1_poc/obj_obj_DryingBox_01_handle", + "obj_DryingBox_01_handle": "/World/labutopia_level1_poc/obj_obj_DryingBox_01/handle", "table": "/World/labutopia_level1_poc/obj_table", } + assert manifest["articulation_part_paths"] == { + "obj_DryingBox_01_handle": "/World/labutopia_level1_poc/obj_obj_DryingBox_01/handle" + } + contracts = manifest["render_object_contracts"] + assert contracts["obj_conical_bottle02"]["desired_runtime_translation"] == [ + 0.28, + 0.0, + 0.8, + ] + assert contracts["obj_DryingBox_01"]["desired_runtime_translation"] == [ + 0.75, + 0.1, + 0.78, + ] + assert contracts["obj_DryingBox_01_handle"][ + "compose_nested_transform_with_parent" + ] == "obj_DryingBox_01" + for uid in ( + "obj_conical_bottle02", + "obj_beaker2", + "obj_target_plat", + "obj_DryingBox_01", + "obj_DryingBox_01_handle", + ): + color = contracts[uid]["display_color"] + assert len(color) == 3 + assert all(0.0 <= channel <= 1.0 for channel in color) + assert contracts[uid]["expected_world_bbox_lwh_m"]["min"] + assert contracts[uid]["expected_world_bbox_lwh_m"]["max"] assert manifest["required_genmanip_object_uids"] == [ "obj_conical_bottle02", "obj_beaker2", @@ -104,9 +193,38 @@ def test_build_asset_overlay_writes_scene_wrapper_manifest_and_cleans_reruns( "intensity": 1000, } ] + assert manifest["drying_box_runtime_asset"] == { + "strategy": "sanitized_surrogate", + "wrapper_prim_path": "/World/labutopia_level1_poc/obj_obj_DryingBox_01", + "base_joint_name": "BaseFixedJoint", + "joint_name": "RevoluteJoint", + "removed_source_joint_types": ["PhysicsPrismaticJoint"], + "source_payload_used": False, + "visual_affordances": [ + { + "name": "high_contrast_door_panel", + "display_color": [0.28, 0.34, 0.42], + }, + { + "name": "door_outline_seams", + "display_color": [0.04, 0.05, 0.06], + }, + { + "name": "handle_mount_backplate", + "display_color": [0.05, 0.07, 0.09], + }, + { + "name": "high_contrast_handle", + "display_color": [1.0, 0.18, 0.04], + }, + ], + } assert manifest["notes"] == [ "scene.usda exposes a single scene uid under /World for GenManip discovery.", - "Immediate obj_* wrapper prims payload the selected LabUtopia source prims.", + "Immediate obj_* wrapper prims payload top-level LabUtopia source prims except DryingBox_01.", + "DryingBox_01 uses a sanitized runtime surrogate with identity root scale and finite inertial attributes.", + "The drying-box handle is exposed as an articulation part, not an independent payload.", + "Task object wrapper translations normalize LabUtopia source coordinates into the robot/table workspace.", "A deterministic dome light is authored in the runtime wrapper scene.", "Runtime object keys strip one leading obj_ from wrapper prim names.", ] diff --git a/tests/labutopia_poc/test_labutopia_layout_fallback.py b/tests/labutopia_poc/test_labutopia_layout_fallback.py index a4409bc..e567d99 100644 --- a/tests/labutopia_poc/test_labutopia_layout_fallback.py +++ b/tests/labutopia_poc/test_labutopia_layout_fallback.py @@ -47,6 +47,7 @@ def __init__(self): class _GenerationConfig: goal = [[{"type": "manip/labutopia/object_height_delta", "obj_uid": "obj"}]] + articulation = {} class _SceneConfig: @@ -84,6 +85,34 @@ class _Scene: cache_library = _CacheLibrary() +class _Articulation: + prim_path = "/World/labutopia_level1_poc/obj_obj_DryingBox_01" + + def get_world_pose(self): + return [0.75, 0.18, 0.78], [1.0, 0.0, 0.0, 0.0] + + def get_local_scale(self): + return [1.0, 1.0, 1.0] + + def get_joint_positions(self): + return [0.7112835] + + +class _OpenDoorGenerationConfig: + goal = [] + articulation = {"obj_DryingBox_01": {"target_positions": [0.0]}} + + +class _OpenDoorSceneConfig: + task_name = "ebench/labutopia_lab_poc/franka_poc/level1_open_door" + instruction = "Open the drying box door." + generation_config = _OpenDoorGenerationConfig() + + +class _OpenDoorScene(_Scene): + articulation_list = {"obj_DryingBox_01": _Articulation()} + + def test_build_labutopia_poc_meta_info_from_current_scene(): meta_info = build_labutopia_poc_meta_info(_Scene(), _SceneConfig(), "000") @@ -108,3 +137,13 @@ def test_load_or_build_rejects_missing_non_labutopia_meta_info(tmp_path): _Scene(), _SceneConfig(), ) + + +def test_build_labutopia_poc_meta_info_uses_configured_articulation_target(): + meta_info = build_labutopia_poc_meta_info( + _OpenDoorScene(), _OpenDoorSceneConfig(), "000" + ) + + articulation_layout = meta_info["task_data"]["initial_layout"]["obj_DryingBox_01"] + + assert articulation_layout["joint_positions"] == [0.0] diff --git a/tests/labutopia_poc/test_render_diagnostics_contract.py b/tests/labutopia_poc/test_render_diagnostics_contract.py index aeb27e9..19eb36d 100644 --- a/tests/labutopia_poc/test_render_diagnostics_contract.py +++ b/tests/labutopia_poc/test_render_diagnostics_contract.py @@ -1,5 +1,8 @@ from standalone_tools.labutopia_poc.capture_eval_render_diagnostics import ( + apply_camera_config_override, build_camera_frame_stats, + build_claim_boundary, + classify_articulation_runtime_state, classify_frame_stats, ) @@ -32,3 +35,104 @@ def test_classify_visible_frame_as_pass(): ) assert classify_frame_stats(stats) == "visible_frame" + + +def test_classify_huge_articulation_joint_position_as_unstable(): + report = classify_articulation_runtime_state( + { + "obj_DryingBox_01": { + "joint_positions": [3.888585221393613e16, 0.0], + "dof_names": ["RevoluteJoint", "FixedJoint"], + } + } + ) + + assert report["runtime_physics_stable"] is False + assert report["articulations"]["obj_DryingBox_01"]["status"] == ( + "unstable_joint_positions" + ) + assert report["articulations"]["obj_DryingBox_01"]["invalid_joint_positions"] == [ + 3.888585221393613e16 + ] + + +def test_required_articulation_missing_is_unstable(): + report = classify_articulation_runtime_state( + {}, + required_articulations=["obj_DryingBox_01"], + ) + + assert report["runtime_physics_stable"] is False + assert report["missing_articulations"] == ["obj_DryingBox_01"] + assert report["articulations"]["obj_DryingBox_01"]["status"] == ( + "missing_articulation" + ) + + +def test_configured_articulation_target_mismatch_is_unstable(): + report = classify_articulation_runtime_state( + { + "obj_DryingBox_01": { + "joint_positions": [0.7112835049629211], + "dof_names": ["RevoluteJoint"], + } + }, + expected_joint_positions={"obj_DryingBox_01": [0.0]}, + joint_position_tolerance_rad=1e-3, + ) + + item = report["articulations"]["obj_DryingBox_01"] + assert report["runtime_physics_stable"] is False + assert item["status"] == "target_position_mismatch" + assert item["expected_joint_positions"] == [0.0] + assert item["joint_position_errors"][0] > 0.7 + + +def test_claim_boundary_requires_runtime_physics_for_baseline(): + claim_boundary = build_claim_boundary( + boundary_classification="readback_visible", + render_validation_passed=True, + runtime_physics_stable=False, + ) + + assert claim_boundary["task_render_accepted"] is True + assert claim_boundary["official_baseline_evaluable"] is False + assert "runtime_physics_unstable" in claim_boundary["blockers"] + + +def test_claim_boundary_marks_incomplete_diagnostic_not_evaluable(): + claim_boundary = build_claim_boundary( + boundary_classification=None, + render_validation_passed=False, + runtime_physics_stable=True, + diagnostic_completed=False, + ) + + assert claim_boundary["task_render_accepted"] is False + assert claim_boundary["official_baseline_evaluable"] is False + assert "runtime_diagnostic_not_completed" in claim_boundary["blockers"] + + +def test_camera_config_override_is_scoped_to_diagnostic_eval_config(): + eval_config = { + "domain_randomization": { + "cameras": { + "type": "fixed", + "config_path": "configs/cameras/original.yml", + } + } + } + + applied = apply_camera_config_override( + eval_config, + "configs/cameras/open_door_trial.yml", + ) + + assert applied == { + "previous_config_path": "configs/cameras/original.yml", + "override_config_path": "configs/cameras/open_door_trial.yml", + } + assert ( + eval_config["domain_randomization"]["cameras"]["config_path"] + == "configs/cameras/open_door_trial.yml" + ) diff --git a/tests/labutopia_poc/test_scene_preprocess_rules.py b/tests/labutopia_poc/test_scene_preprocess_rules.py new file mode 100644 index 0000000..1240c40 --- /dev/null +++ b/tests/labutopia_poc/test_scene_preprocess_rules.py @@ -0,0 +1,121 @@ +from types import SimpleNamespace + +import pytest + + +class _FakePrim: + def __init__(self, active=True): + self._active = active + self.set_calls = [] + + def IsActive(self): + return self._active + + def SetActive(self, active): + self._active = active + self.set_calls.append(active) + + +def _fake_scene(): + bottle = SimpleNamespace(prim=_FakePrim(True), prim_path="/World/bottle") + box = SimpleNamespace(prim=_FakePrim(True), prim_path="/World/box") + handle = SimpleNamespace(prim=_FakePrim(True), prim_path="/World/box/handle") + return SimpleNamespace( + object_list={"obj_conical_bottle02": bottle}, + articulation_list={"obj_DryingBox_01": box}, + articulation_part_list={"obj_DryingBox_01_handle": handle}, + cache_library=SimpleNamespace( + mesh_dict={ + "obj_conical_bottle02": object(), + "obj_DryingBox_01": object(), + "obj_DryingBox_01_handle": object(), + } + ), + ) + + +def test_set_scene_object_active_can_hide_objects_articulations_and_parts(): + from genmanip.utils.loader.preprocess_rules import set_scene_object_active + + scene = _fake_scene() + + set_scene_object_active( + scene, + ["obj_conical_bottle02", "obj_DryingBox_01", "obj_DryingBox_01_handle"], + active=False, + ) + + assert scene.object_list["obj_conical_bottle02"].prim.IsActive() is False + assert scene.articulation_list["obj_DryingBox_01"].prim.IsActive() is False + assert scene.articulation_part_list["obj_DryingBox_01_handle"].prim.IsActive() is False + assert scene.cache_library.mesh_dict == {} + + +def test_set_scene_object_active_fails_fast_for_unknown_uids(): + from genmanip.utils.loader.preprocess_rules import set_scene_object_active + + with pytest.raises(KeyError, match="missing_uid"): + set_scene_object_active(_fake_scene(), ["missing_uid"], active=False) + + +def test_resettable_scene_object_uids_skip_articulations_and_parts(): + from genmanip.utils.loader.preprocess_rules import resettable_scene_object_uids + + scene = _fake_scene() + scene.object_list["obj_beaker2"] = SimpleNamespace( + prim=_FakePrim(True), prim_path="/World/beaker" + ) + scene.object_list["obj_DryingBox_01_handle"] = scene.articulation_part_list[ + "obj_DryingBox_01_handle" + ] + scene.object_list["obj_DryingBox_01"] = scene.articulation_list["obj_DryingBox_01"] + + assert resettable_scene_object_uids(scene) == [ + "obj_beaker2", + "obj_conical_bottle02", + ] + + +class _FakeArticulationView: + def __init__(self): + self.positions = None + self.velocities = None + self.position_targets = None + + def set_joint_positions(self, positions): + self.positions = list(positions) + + def set_joint_velocities(self, velocities): + self.velocities = list(velocities) + + def set_joint_position_targets(self, positions): + self.position_targets = list(positions) + + +def test_apply_articulation_initial_targets_replays_post_reset_target(): + from genmanip.utils.loader.preprocess_rules import apply_articulation_initial_targets + + articulation_view = _FakeArticulationView() + scene = SimpleNamespace( + articulation_list={ + "obj_DryingBox_01": SimpleNamespace( + _articulation_view=articulation_view, + ), + }, + articulation_data={"obj_DryingBox_01": {"is_articulated": True}}, + scene_config=SimpleNamespace( + generation_config=SimpleNamespace( + articulation={ + "obj_DryingBox_01": {"target_positions": [0.0]}, + "obj_ignored": {"target_positions": [0.5]}, + }, + ), + ), + ) + + applied = apply_articulation_initial_targets(scene) + + assert applied == {"obj_DryingBox_01": [0.0]} + assert articulation_view.positions == [0.0] + assert articulation_view.velocities == [0.0] + assert articulation_view.position_targets == [0.0] diff --git a/tests/labutopia_poc/test_validate_task_package.py b/tests/labutopia_poc/test_validate_task_package.py index 273660b..8e0770e 100644 --- a/tests/labutopia_poc/test_validate_task_package.py +++ b/tests/labutopia_poc/test_validate_task_package.py @@ -1,4 +1,6 @@ +import copy import json +import math import subprocess import sys import types @@ -14,6 +16,16 @@ "ebench/labutopia_lab_poc/lift2_candidate/lift2_candidate.json", ] EXPECTED_TASKS = ["level1_pick", "level1_place", "level1_open_door"] +EXPECTED_RENDER_OBJECTS = { + "level1_pick": ["obj_conical_bottle02"], + "level1_place": ["obj_beaker2", "obj_target_plat"], + "level1_open_door": ["obj_DryingBox_01", "obj_DryingBox_01_handle"], +} +EXPECTED_TASK_CAMERA_CONFIGS = { + "level1_pick": "configs/cameras/labutopia_franka_poc_pick.yml", + "level1_place": "configs/cameras/labutopia_franka_poc_place.yml", + "level1_open_door": "configs/cameras/labutopia_franka_poc_open_door.yml", +} CAMERA_CLEANUP_FLAGS = { "with_bbox2d", "with_bbox3d", @@ -29,8 +41,8 @@ **{flag: False for flag in CAMERA_CLEANUP_FLAGS}, }, "camera2": { - "position": [9.6, 0.0, 2.5], - "orientation": [0.70711, 0.0, 0.0, -0.70711], + "position": [0.45, -1.1, 1.55], + "orientation": [0.87184, 0.4898, 0.0, 0.0], "camera_axes": "usd", **{flag: False for flag in CAMERA_CLEANUP_FLAGS}, }, @@ -81,6 +93,19 @@ def _write_camera_config_fixture(tmp_root, franka_cameras): tmp_root / "configs/cameras/fixed_camera_lift2_simbox.yml", BASE_LIFT2_CAMERAS, ) + for ( + task_name, + config_path, + ) in validate_task_package.EXPECTED_FRANKA_TASK_CAMERA_CONFIGS.items(): + task_cameras = { + camera_name: dict(camera) + for camera_name, camera in BASE_FRANKA_CAMERAS.items() + } + task_cameras["camera2"].update( + validate_task_package.EXPECTED_FRANKA_TASK_CAMERA2_CONTRACTS[task_name] + ) + task_cameras["camera2"]["task_view"] = task_name + _write_yaml(tmp_root / config_path, task_cameras) def test_indexed_task_yaml_paths_rejects_duplicate_profile_entries(tmp_path, monkeypatch): @@ -165,6 +190,68 @@ def test_assets_manifest_rejects_missing_overlay_runtime_scene(tmp_path, monkeyp validate_task_package._validate_assets_manifest() +def test_assets_manifest_rejects_missing_drying_box_visual_affordance_geometry( + tmp_path, + monkeypatch, +): + package_root = tmp_path / "tasks/ebench/labutopia_lab_poc" + common_root = package_root / "common" + common_root.mkdir(parents=True) + overlay_root = tmp_path / "overlay/assets" + runtime_scene = overlay_root / f"{validate_task_package.RUNTIME_USD_NAME}.usda" + runtime_scene.parent.mkdir(parents=True) + runtime_scene.write_text( + """ +#usda 1.0 +def Xform "World" +{ + def Xform "labutopia_level1_poc" + { + def Cube "handle_visual_marker" ( + prepend apiSchemas = ["MaterialBindingAPI"] + ) + { + color3f[] primvars:displayColor = [(1, 0.18, 0.04)] + } + def DomeLight "DeterministicDomeLight" + { + float inputs:intensity = 1000 + } + } +} +""", + encoding="utf-8", + ) + real_manifest = validate_task_package._load_json( + validate_task_package.PACKAGE_ROOT / "common/assets_manifest.json" + ) + manifest = copy.deepcopy(real_manifest) + manifest["overlay_root"] = str(overlay_root) + manifest["generated_manifest"] = str(tmp_path / "generated_manifest.json") + generated = { + "usd_name": manifest["runtime_usd_name"], + "scene_uid": manifest["scene_uid"], + "runtime_object_keys": manifest["runtime_object_keys"], + "wrapper_prim_paths": manifest["wrapper_prim_paths"], + "source_to_runtime_object_key": manifest["source_to_runtime_object_key"], + "deterministic_lights": manifest["deterministic_lights"], + "articulation_part_paths": manifest["articulation_part_paths"], + "render_object_contracts": manifest["render_object_contracts"], + "drying_box_runtime_asset": manifest["drying_box_runtime_asset"], + } + _write_json(validate_task_package.Path(manifest["generated_manifest"]), generated) + _write_json(common_root / "assets_manifest.json", manifest) + monkeypatch.setattr(validate_task_package, "PACKAGE_ROOT", package_root) + monkeypatch.setattr( + validate_task_package, + "EXPECTED_DRYING_BOX_RUNTIME_ASSET", + manifest["drying_box_runtime_asset"], + ) + + with pytest.raises(AssertionError, match="drying-box door visual affordance"): + validate_task_package._validate_assets_manifest() + + def test_labutopia_tasks_define_runtime_articulation_contract(): for path in validate_task_package._indexed_task_yaml_paths(): data = validate_task_package._load_yaml(path) @@ -173,6 +260,289 @@ def test_labutopia_tasks_define_runtime_articulation_contract(): assert "articulation" in cfg["generation_config"], str(path) +def test_labutopia_assets_manifest_declares_p1_render_object_contracts(): + manifest_path = ( + validate_task_package.PACKAGE_ROOT / "common/assets_manifest.json" + ) + manifest = validate_task_package._load_json(manifest_path) + + contracts = manifest["render_object_contracts"] + for uid in { + uid + for required in EXPECTED_RENDER_OBJECTS.values() + for uid in required + }: + contract = contracts[uid] + assert contract["wrapper_prim_path"] == manifest["wrapper_prim_paths"][uid] + assert contract["display_color"] != [0.5, 0.5, 0.5] + assert contract["expected_world_bbox_lwh_m"]["min"] + assert contract["expected_world_bbox_lwh_m"]["max"] + + assert manifest["wrapper_prim_paths"]["obj_DryingBox_01_handle"] == ( + "/World/labutopia_level1_poc/obj_obj_DryingBox_01/handle" + ) + assert manifest["articulation_part_paths"] == { + "obj_DryingBox_01_handle": "/World/labutopia_level1_poc/obj_obj_DryingBox_01/handle" + } + assert manifest["drying_box_runtime_asset"] == { + "strategy": "sanitized_surrogate", + "wrapper_prim_path": "/World/labutopia_level1_poc/obj_obj_DryingBox_01", + "base_joint_name": "BaseFixedJoint", + "joint_name": "RevoluteJoint", + "removed_source_joint_types": ["PhysicsPrismaticJoint"], + "source_payload_used": False, + "visual_affordances": [ + { + "name": "high_contrast_door_panel", + "display_color": [0.28, 0.34, 0.42], + }, + { + "name": "door_outline_seams", + "display_color": [0.04, 0.05, 0.06], + }, + { + "name": "handle_mount_backplate", + "display_color": [0.05, 0.07, 0.09], + }, + { + "name": "high_contrast_handle", + "display_color": [1.0, 0.18, 0.04], + }, + ], + } + + runtime_scene = ( + validate_task_package.Path(manifest["overlay_root"]) + / f"{manifest['runtime_usd_name']}.usda" + ) + scene_text = runtime_scene.read_text(encoding="utf-8") + assert 'def Xform "obj_obj_DryingBox_01_handle" (' not in scene_text + assert "double3 xformOp:translate = (0.18, -0.22, 0.05)" in scene_text + assert 'def Cube "handle_visual_marker"' not in scene_text + assert "double3 xformOp:translate = (0.18, -0.165, 0.05)" not in scene_text + assert "double3 xformOp:translate = (-0.18, -0.22, 0.05)" not in scene_text + assert "double3 xformOp:translate = (-0.18, -0.165, 0.05)" not in scene_text + + +def test_franka_tasks_define_render_validation_contract(): + for task_name, required_objects in EXPECTED_RENDER_OBJECTS.items(): + path = ( + validate_task_package.PACKAGE_ROOT + / "franka_poc" + / f"{task_name}.yml" + ) + cfg = validate_task_package._load_yaml(path)["evaluation_configs"][0] + validation = cfg["labutopia_render_validation"] + camera_config_path = cfg["domain_randomization"]["cameras"]["config_path"] + camera_path = validate_task_package.ROOT / camera_config_path + cameras = validate_task_package._load_yaml(camera_path) + + assert validation["schema_version"] == 1 + assert validation["primary_camera"] == "camera2" + assert validation["required_visible_objects"] == required_objects + assert validation["evidence_policy"] == {"direct_render": False} + assert set(validation["required_camera_names"]).issubset(cameras) + assert { + "black_frame", + "low_texture", + "required_object_missing", + "severe_clipping", + }.issubset(set(validation["reject_frame_if"])) + + +def test_franka_tasks_use_task_specific_evidence_camera_configs(): + seen_configs = set() + for task_name, expected_config in EXPECTED_TASK_CAMERA_CONFIGS.items(): + path = ( + validate_task_package.PACKAGE_ROOT + / "franka_poc" + / f"{task_name}.yml" + ) + cfg = validate_task_package._load_yaml(path)["evaluation_configs"][0] + camera_config_path = cfg["domain_randomization"]["cameras"]["config_path"] + validation = cfg["labutopia_render_validation"] + + assert camera_config_path == expected_config + seen_configs.add(camera_config_path) + assert validation["primary_camera"] == "camera2" + assert validation["evidence_camera_config"] == expected_config + + cameras = validate_task_package._load_yaml( + validate_task_package.ROOT / camera_config_path + ) + assert cameras["camera2"]["camera_axes"] == "usd" + assert cameras["camera2"]["resolution"] == [512, 512] + assert cameras["camera2"]["task_view"] == task_name + + assert len(seen_configs) == len(EXPECTED_TASK_CAMERA_CONFIGS) + + +def test_open_door_evidence_camera_is_close_to_handle_for_visual_qa(): + camera_path = ( + validate_task_package.ROOT + / "configs/cameras/labutopia_franka_poc_open_door.yml" + ) + cameras = validate_task_package._load_yaml(camera_path) + camera2 = cameras["camera2"] + + position = camera2["position"] + handle_anchor = [0.77, -0.02, 0.80] + + distance_to_handle = math.dist(position, handle_anchor) + + assert 0.54 <= distance_to_handle <= 0.64 + assert 0.72 <= position[0] <= 0.78 + assert -0.60 <= position[1] <= -0.52 + assert 0.99 <= position[2] <= 1.05 + assert 4.5 <= camera2["focal_length"] <= 5.5 + assert 9.0 <= camera2["horizontal_aperture"] <= 11.0 + + +def test_franka_render_validation_declares_object_pixel_readability_thresholds(): + expected_minimums = { + "level1_pick": { + "obj_conical_bottle02": {"min_width_px": 36, "min_height_px": 48}, + }, + "level1_place": { + "obj_beaker2": {"min_width_px": 34, "min_height_px": 34}, + "obj_target_plat": {"min_width_px": 42, "min_height_px": 24}, + }, + "level1_open_door": { + "obj_DryingBox_01": {"min_width_px": 160, "min_height_px": 150}, + "obj_DryingBox_01_handle": { + "min_width_px": 18, + "min_height_px": 64, + }, + }, + } + + for task_name, object_thresholds in expected_minimums.items(): + path = ( + validate_task_package.PACKAGE_ROOT + / "franka_poc" + / f"{task_name}.yml" + ) + cfg = validate_task_package._load_yaml(path)["evaluation_configs"][0] + thresholds = cfg["labutopia_render_validation"]["object_pixel_thresholds"] + + for uid, minimums in object_thresholds.items(): + actual = thresholds[uid] + assert actual["min_width_px"] >= minimums["min_width_px"] + assert actual["min_height_px"] >= minimums["min_height_px"] + assert actual["min_area_fraction"] > 0.0 + + +def test_franka_tasks_hide_non_task_objects_for_evidence_readability(): + expected_hidden = { + "level1_pick": [ + "obj_beaker2", + "obj_target_plat", + "obj_DryingBox_01", + ], + "level1_place": ["obj_conical_bottle02", "obj_DryingBox_01"], + "level1_open_door": [ + "obj_conical_bottle02", + "obj_beaker2", + "obj_target_plat", + ], + } + + for task_name, hidden_uids in expected_hidden.items(): + path = ( + validate_task_package.PACKAGE_ROOT + / "franka_poc" + / f"{task_name}.yml" + ) + cfg = validate_task_package._load_yaml(path)["evaluation_configs"][0] + active_rules = [ + item + for item in cfg["preprocess_config"] + if item.get("type") == "set_object_active" + ] + + assert active_rules == [ + {"type": "set_object_active", "config": {"active": False, "uids": hidden_uids}} + ] + assert cfg["labutopia_render_validation"]["hidden_non_task_objects"] == hidden_uids + + +def test_open_door_uses_nested_handle_articulation_part_contract(): + path = ( + validate_task_package.PACKAGE_ROOT + / "franka_poc" + / "level1_open_door.yml" + ) + cfg = validate_task_package._load_yaml(path)["evaluation_configs"][0] + + drying_box = cfg["object_config"]["obj_DryingBox_01"] + assert drying_box["type"] == "existed_object" + assert drying_box["uid_list"] == ["obj_DryingBox_01"] + assert drying_box["is_articulated"] is True + assert drying_box["articulation_info"]["is_articulated"] is True + assert drying_box["articulation_info"]["part"]["handle"] == "/handle" + + metric = cfg["generation_config"]["goal"][0][0][0] + assert metric["type"] == "manip/default/check_joint_angle" + assert metric["articulation_obj_uid"] == "obj_DryingBox_01" + assert metric["joint_name"] == "RevoluteJoint" + + +def test_open_door_initializes_drying_box_closed_for_eval_start(): + for profile in ("franka_poc", "lift2_candidate"): + path = validate_task_package.PACKAGE_ROOT / profile / "level1_open_door.yml" + cfg = validate_task_package._load_yaml(path)["evaluation_configs"][0] + drying_box = cfg["object_config"]["obj_DryingBox_01"] + + assert drying_box["target_positions"] == [0.0], str(path) + + +def test_drying_box_articulation_physics_is_sanitized_for_runtime(): + manifest_path = ( + validate_task_package.PACKAGE_ROOT / "common/assets_manifest.json" + ) + manifest = validate_task_package._load_json(manifest_path) + runtime_scene = ( + validate_task_package.Path(manifest["overlay_root"]) + / f"{manifest['runtime_usd_name']}.usda" + ) + + report = validate_task_package._inspect_drying_box_articulation_physics( + runtime_scene + ) + + assert report["root_path"] == manifest["wrapper_prim_paths"]["obj_DryingBox_01"] + assert report["zero_mass_links"] == [] + assert report["zero_inertia_links"] == [] + assert report["sanitized_for_physx"] is True + + +def test_drying_box_articulation_topology_is_ready_for_runtime(): + manifest_path = ( + validate_task_package.PACKAGE_ROOT / "common/assets_manifest.json" + ) + manifest = validate_task_package._load_json(manifest_path) + runtime_scene = ( + validate_task_package.Path(manifest["overlay_root"]) + / f"{manifest['runtime_usd_name']}.usda" + ) + + report = validate_task_package._inspect_drying_box_articulation_physics( + runtime_scene + ) + + assert report["root_scale"] in ([], [1.0, 1.0, 1.0]) + assert report["non_identity_root_scale"] is False + assert report["duplicate_rigid_link_names"] == {} + assert report["invalid_center_of_mass_links"] == [] + assert report["invalid_principal_axes_links"] == [] + assert report["invalid_joint_body_targets"] == [] + assert report["world_fixed_base_joint_paths"] == [ + "/World/labutopia_level1_poc/obj_obj_DryingBox_01/BaseFixedJoint" + ] + assert report["unexpected_joint_types"] == [] + assert report["runtime_topology_ready"] is True + + def test_labutopia_camera_configs_define_cleanup_flags(): for expectation in validate_task_package.PROFILE_EXPECTATIONS.values(): camera_path = validate_task_package.ROOT / expectation["camera_config"] @@ -195,6 +565,22 @@ def test_labutopia_franka_camera_config_declares_axes_and_task_view(): cameras["camera2"]["position"] == validate_task_package.EXPECTED_FRANKA_CAMERA2_POSITION ) + assert ( + cameras["camera2"]["orientation"] + == validate_task_package.EXPECTED_FRANKA_CAMERA2_ORIENTATION + ) + + +def test_labutopia_franka_primary_camera_is_over_runtime_workspace(): + camera_path = validate_task_package.ROOT / "configs/cameras/labutopia_franka_poc.yml" + cameras = validate_task_package._load_yaml(camera_path) + + x, y, z = cameras["camera2"]["position"] + assert 0.0 <= x <= 1.2 + assert -1.3 <= y <= 0.8 + assert 1.2 <= z <= 2.0 + assert cameras["camera2"]["position"] != [9.6, 0.0, 2.5] + assert cameras["camera2"]["orientation"] == [0.87184, 0.4898, 0.0, 0.0] def test_validate_camera_configs_rejects_franka_camera_axes_regression( @@ -227,6 +613,43 @@ def test_validate_camera_configs_rejects_franka_camera2_position_regression( validate_task_package._validate_camera_configs() +def test_validate_camera_configs_rejects_franka_camera2_orientation_regression( + tmp_path, monkeypatch +): + franka_cameras = { + camera_name: dict(camera) + for camera_name, camera in BASE_FRANKA_CAMERAS.items() + } + franka_cameras["camera2"]["orientation"] = [0.70711, 0.0, 0.0, -0.70711] + _write_camera_config_fixture(tmp_path, franka_cameras) + monkeypatch.setattr(validate_task_package, "ROOT", tmp_path) + + with pytest.raises(AssertionError, match="camera2 orientation must remain"): + validate_task_package._validate_camera_configs() + + +def test_validate_camera_configs_rejects_open_door_lens_regression( + tmp_path, monkeypatch +): + franka_cameras = { + camera_name: dict(camera) + for camera_name, camera in BASE_FRANKA_CAMERAS.items() + } + _write_camera_config_fixture(tmp_path, franka_cameras) + open_door_camera_path = ( + tmp_path / "configs/cameras/labutopia_franka_poc_open_door.yml" + ) + open_door_cameras = yaml.safe_load( + open_door_camera_path.read_text(encoding="utf-8") + ) + del open_door_cameras["camera2"]["focal_length"] + _write_yaml(open_door_camera_path, open_door_cameras) + monkeypatch.setattr(validate_task_package, "ROOT", tmp_path) + + with pytest.raises(AssertionError, match="camera2 focal_length must be 5.0"): + validate_task_package._validate_camera_configs() + + def test_assets_manifest_declares_deterministic_runtime_light(): manifest_path = ( validate_task_package.PACKAGE_ROOT / "common/assets_manifest.json" From 3157b0be9f7360b93fdec7779c0a557318f6ef6e Mon Sep 17 00:00:00 2001 From: Codex Date: Wed, 24 Jun 2026 01:58:09 +0800 Subject: [PATCH 18/30] feat: close LabUtopia P1 render gate --- .../labutopia_franka_poc_open_door.yml | 2 +- configs/cameras/labutopia_franka_poc_pick.yml | 2 + .../cameras/labutopia_franka_poc_place.yml | 2 + .../franka_poc/level1_open_door.yml | 4 +- .../franka_poc/level1_pick.yml | 2 +- .../franka_poc/level1_place.yml | 4 +- .../render_p1_asset_layout_20260623.json | 251 +++++++++------- .../render_visual_investigation_20260623.md | 93 ++++-- ...26-06-22-labutopia-ebench-weekly-report.md | 44 +-- ...anka-level1-open-door-eval-readback-p1.png | Bin 98720 -> 97117 bytes ...ia-franka-level1-pick-eval-readback-p1.png | Bin 60884 -> 67000 bytes ...a-franka-level1-place-eval-readback-p1.png | Bin 62724 -> 69475 bytes .../index.html | 82 +++--- ...-labutopia-ebench-render-layout-closure.md | 56 ++-- .../labutopia_poc/build_asset_overlay.py | 2 +- .../capture_eval_render_diagnostics.py | 275 +++++++++++++++++- .../labutopia_poc/validate_task_package.py | 17 +- .../labutopia_poc/test_build_asset_overlay.py | 2 + .../test_render_diagnostics_contract.py | 154 ++++++++++ .../test_validate_task_package.py | 4 +- 20 files changed, 754 insertions(+), 242 deletions(-) diff --git a/configs/cameras/labutopia_franka_poc_open_door.yml b/configs/cameras/labutopia_franka_poc_open_door.yml index 057752f..c4c945a 100644 --- a/configs/cameras/labutopia_franka_poc_open_door.yml +++ b/configs/cameras/labutopia_franka_poc_open_door.yml @@ -20,7 +20,7 @@ camera2: orientation: [0.87184, 0.4898, 0.0, 0.0] camera_axes: usd resolution: [512, 512] - focal_length: 5.0 + focal_length: 5.4 horizontal_aperture: 10.0 image_type: rgb task_view: level1_open_door diff --git a/configs/cameras/labutopia_franka_poc_pick.yml b/configs/cameras/labutopia_franka_poc_pick.yml index 67ab832..539e840 100644 --- a/configs/cameras/labutopia_franka_poc_pick.yml +++ b/configs/cameras/labutopia_franka_poc_pick.yml @@ -20,6 +20,8 @@ camera2: orientation: [0.87184, 0.4898, 0.0, 0.0] camera_axes: usd resolution: [512, 512] + focal_length: 5.6 + horizontal_aperture: 10.0 image_type: rgb task_view: level1_pick with_distance: false diff --git a/configs/cameras/labutopia_franka_poc_place.yml b/configs/cameras/labutopia_franka_poc_place.yml index e4cfcf7..3e0c135 100644 --- a/configs/cameras/labutopia_franka_poc_place.yml +++ b/configs/cameras/labutopia_franka_poc_place.yml @@ -20,6 +20,8 @@ camera2: orientation: [0.87184, 0.4898, 0.0, 0.0] camera_axes: usd resolution: [512, 512] + focal_length: 10.0 + horizontal_aperture: 10.0 image_type: rgb task_view: level1_place with_distance: false diff --git a/configs/tasks/ebench/labutopia_lab_poc/franka_poc/level1_open_door.yml b/configs/tasks/ebench/labutopia_lab_poc/franka_poc/level1_open_door.yml index 25b82af..7300bd6 100644 --- a/configs/tasks/ebench/labutopia_lab_poc/franka_poc/level1_open_door.yml +++ b/configs/tasks/ebench/labutopia_lab_poc/franka_poc/level1_open_door.yml @@ -78,11 +78,11 @@ evaluation_configs: obj_DryingBox_01: min_width_px: 160 min_height_px: 150 - min_area_fraction: 0.12 + min_bbox_area_fraction: 0.12 obj_DryingBox_01_handle: min_width_px: 18 min_height_px: 64 - min_area_fraction: 0.004 + min_bbox_area_fraction: 0.004 task_visual_goal: door_and_handle_visible reject_frame_if: - black_frame diff --git a/configs/tasks/ebench/labutopia_lab_poc/franka_poc/level1_pick.yml b/configs/tasks/ebench/labutopia_lab_poc/franka_poc/level1_pick.yml index 38f8b92..b0d29dc 100644 --- a/configs/tasks/ebench/labutopia_lab_poc/franka_poc/level1_pick.yml +++ b/configs/tasks/ebench/labutopia_lab_poc/franka_poc/level1_pick.yml @@ -67,7 +67,7 @@ evaluation_configs: obj_conical_bottle02: min_width_px: 36 min_height_px: 48 - min_area_fraction: 0.01 + min_bbox_area_fraction: 0.01 task_visual_goal: pick_target_visible reject_frame_if: - black_frame diff --git a/configs/tasks/ebench/labutopia_lab_poc/franka_poc/level1_place.yml b/configs/tasks/ebench/labutopia_lab_poc/franka_poc/level1_place.yml index 7479a59..3e9e14f 100644 --- a/configs/tasks/ebench/labutopia_lab_poc/franka_poc/level1_place.yml +++ b/configs/tasks/ebench/labutopia_lab_poc/franka_poc/level1_place.yml @@ -67,11 +67,11 @@ evaluation_configs: obj_beaker2: min_width_px: 34 min_height_px: 34 - min_area_fraction: 0.008 + min_bbox_area_fraction: 0.008 obj_target_plat: min_width_px: 42 min_height_px: 24 - min_area_fraction: 0.006 + min_bbox_area_fraction: 0.006 task_visual_goal: beaker_and_target_visible reject_frame_if: - black_frame diff --git a/docs/labutopia_lab_poc/evidence_manifests/render_p1_asset_layout_20260623.json b/docs/labutopia_lab_poc/evidence_manifests/render_p1_asset_layout_20260623.json index ae30767..a6b1c29 100644 --- a/docs/labutopia_lab_poc/evidence_manifests/render_p1_asset_layout_20260623.json +++ b/docs/labutopia_lab_poc/evidence_manifests/render_p1_asset_layout_20260623.json @@ -1,12 +1,12 @@ { - "date": "2026-06-23", + "date": "2026-06-24", "base_commit": "76e1da24e73f314808cadfe7e6ef36755a7a181f", "working_tree_changes_required": true, "direct_render": false, "official_baseline_execution": false, - "task_render_accepted": false, + "task_render_accepted": true, "official_baseline_evaluable": false, - "summary": "P1 moved the LabUtopia POC from black or cluttered frames to three evaluator readback-visible frames. Static USD placement and nested DryingBox handle discovery are sane. Task-level hiding makes pick readable and place basically readable for PM diagnosis. open_door runtime physics now starts at the expected closed joint target, and the latest formal single-handle front-camera retake makes the DryingBox frame, door panel, and one orange handle/action point visible. Independent image-only QA rates the old-vs-current PM comparison PASS, with open_door PASS/WARN as diagnostic evidence only. render_validation_not_passed remains the release blocker, so task_render_accepted and official_baseline_evaluable stay false.", + "summary": "P1 moved the LabUtopia POC from black or cluttered frames to three evaluator readback-visible task frames. Static USD placement, nested DryingBox handle discovery, task-level visibility isolation, and task-specific cameras are now sufficient for the three Franka POC task render gates. The latest formal diagnostics for pick, place, and open_door all report render_validation.passed=true and task_render_accepted=true. The official Lift2 baseline remains not evaluable because Lift2 composite assets and the official runner have not been validated.", "environment": { "conda_env": "/cpfs/shared/simulation/zhuzihou/dev/conda-managed/envs/embodied-eval-os-sim-isaacsim41-genmanip-py310", "config": "ebench/labutopia_lab_poc/franka_poc", @@ -18,33 +18,33 @@ "level1_open_door": "configs/cameras/labutopia_franka_poc_open_door.yml" }, "runtime_isolation": { - "ports_used": [18102, 18103, 18104, 18131, 18132, 18133, 18134, 18135], + "ports_used_for_final_formal_runs": [18091, 18092, 18093], "eos_port_8087_touched": false } }, - "static_usd_readback": { - "scene_opens": true, - "obj_conical_bottle02": { - "world_translate": [0.28, 0.0, 0.8], - "bbox_size": [0.092574, 0.092573, 0.164524] - }, - "obj_beaker2": { - "world_translate": [0.27, 0.18, 0.84], - "bbox_size": [0.110653, 0.110653, 0.0904] - }, - "obj_target_plat": { - "world_translate": [0.26, -0.24, 0.776], - "bbox_size": [0.1, 0.1, 0.0001] + "historical_failed_images": { + "purpose": "Kept in the weekly HTML report as before/after evidence for PM readers.", + "level1_pick": { + "path": "docs/records/evidence/2026-06-22-labutopia-ebench-weekly-report/assets/labutopia-franka-level1-pick.jpg", + "problem": "Target bottle was not identifiable, so the image could not explain the pick task." }, - "obj_DryingBox_01": { - "world_translate": [0.75, 0.1, 0.78], - "bbox_size": [0.57642, 0.740892, 0.63] + "level1_place": { + "path": "docs/records/evidence/2026-06-22-labutopia-ebench-weekly-report/assets/labutopia-franka-level1-place.jpg", + "problem": "The beaker and target platform relation was missing, so the image could not explain the place task." }, - "obj_DryingBox_01_handle": { + "level1_open_door": { + "path": "docs/records/evidence/2026-06-22-labutopia-ebench-weekly-report/assets/labutopia-franka-level1-open-door.jpg", + "problem": "The frame only showed a dark box corner; door, hinge, handle, and action point were not clear." + } + }, + "static_asset_layout": { + "scene_opens": true, + "objects_normalized_to_franka_workspace": true, + "drying_box_handle": { "wrapper_prim_path": "/World/labutopia_level1_poc/obj_obj_DryingBox_01/handle", "top_level_duplicate_removed": true, - "world_translate": [0.455606, 0.248763, 1.108592], - "bbox_size": [0.048367, 0.047912, 0.201624] + "runtime_handle_scale": [0.045, 0.075, 0.25], + "resolution": "The old oversized orange-panel handle was replaced with a slimmer high-contrast handle that remains a nested DryingBox part." } }, "task_level_visibility_isolation": { @@ -63,135 +63,170 @@ }, "diagnostics": { "level1_pick": { - "run_id": "labutopia_p1_visibility_pick_20260623_175050", - "diagnostics_json": "saved/diagnostics/labutopia_p1_visibility_pick_20260623_175050/diagnostics.json", + "run_id": "labutopia_p1_gate_pick_formal_20260624_0001", + "diagnostics_json": "saved/diagnostics/labutopia_p1_gate_pick_formal_20260624_0001/diagnostics.json", "boundary_classification": "readback_visible", - "readback_frame": "saved/diagnostics/labutopia_p1_visibility_pick_20260623_175050/readback_after_get_eval_camera_data/camera2/00000.png", + "readback_frame": "saved/diagnostics/labutopia_p1_gate_pick_formal_20260624_0001/readback_after_get_eval_camera_data/camera2/00000.png", "report_image": "docs/records/evidence/2026-06-22-labutopia-ebench-weekly-report/assets/labutopia-franka-level1-pick-eval-readback-p1.png", - "sha256": "78e193beb7bc469c5f8dd40eecb9e532c6cfe3b836b1db4a12c3275253e7755d", - "channel_min": [65.0, 145.0, 146.0], - "channel_max": [228.0, 228.0, 228.0], - "channel_mean": [179.716, 180.23, 180.45], + "sha256": "3bc822dec8204b1aa278b844e101076dede956b50fd1019679b3770dada49c81", + "channel_min": [61.0, 144.0, 147.0], + "channel_max": [228.0, 228.0, 229.0], + "channel_mean": [176.342, 177.134, 177.471], "nonzero_pixels": 262144, - "unique_rgb_colors": 1075, - "runtime_physics_stable": true, - "visual_review": "READABLE_PENDING_FORMAL_QA", - "visual_reason": "Task-level hiding leaves the target bottle clearly visible on the table. This is PM-readable diagnostic evidence, but formal visual/pixel QA has not yet been signed off." + "unique_rgb_colors": 1210, + "render_validation": { + "passed": true, + "required_objects": { + "obj_conical_bottle02": { + "bbox": [236, 222, 275, 299], + "bbox_area_fraction": 0.01190185546875, + "width_px": 40, + "height_px": 78, + "passed": true + } + } + }, + "claim_boundary": { + "task_render_accepted": true, + "official_baseline_evaluable": false, + "baseline_blockers": ["official_baseline_not_validated"] + }, + "visual_review": "PASS", + "visual_reason": "Task-level hiding leaves the target bottle clearly visible on the table." }, "level1_place": { - "run_id": "labutopia_p1_visibility_place_20260623_175232", - "diagnostics_json": "saved/diagnostics/labutopia_p1_visibility_place_20260623_175232/diagnostics.json", + "run_id": "labutopia_p1_gate_place_formal_20260624_0001", + "diagnostics_json": "saved/diagnostics/labutopia_p1_gate_place_formal_20260624_0001/diagnostics.json", "boundary_classification": "readback_visible", - "readback_frame": "saved/diagnostics/labutopia_p1_visibility_place_20260623_175232/readback_after_get_eval_camera_data/camera2/00000.png", + "readback_frame": "saved/diagnostics/labutopia_p1_gate_place_formal_20260624_0001/readback_after_get_eval_camera_data/camera2/00000.png", "report_image": "docs/records/evidence/2026-06-22-labutopia-ebench-weekly-report/assets/labutopia-franka-level1-place-eval-readback-p1.png", - "sha256": "4a5603401f35f18c0039f31ebab2efba6cb895ff606842a709febe2a2402ecac", - "channel_min": [62.0, 145.0, 116.0], + "sha256": "8e80231ab01ff998ec516e74a7187eca6ac028a8a3e887016d8b1d31ef1aa419", + "channel_min": [57.0, 143.0, 116.0], "channel_max": [228.0, 228.0, 228.0], - "channel_mean": [182.224, 182.371, 181.986], + "channel_mean": [169.452, 170.169, 168.271], "nonzero_pixels": 262144, - "unique_rgb_colors": 891, - "runtime_physics_stable": true, - "visual_review": "READABLE_PENDING_FORMAL_QA", - "visual_reason": "The beaker and yellow target platform are visible in the same evaluator frame. The relation is understandable, though a closer camera would improve final PM polish." + "unique_rgb_colors": 1334, + "render_validation": { + "passed": true, + "required_objects": { + "obj_beaker2": { + "bbox": [251, 208, 289, 263], + "bbox_area_fraction": 0.008331298828125, + "width_px": 39, + "height_px": 56, + "passed": true + }, + "obj_target_plat": { + "bbox": [215, 398, 296, 459], + "bbox_area_fraction": 0.0193939208984375, + "width_px": 82, + "height_px": 62, + "passed": true + } + } + }, + "claim_boundary": { + "task_render_accepted": true, + "official_baseline_evaluable": false, + "baseline_blockers": ["official_baseline_not_validated"] + }, + "visual_review": "PASS", + "visual_reason": "The beaker and yellow target platform are visible in the same evaluator frame." }, "level1_open_door": { - "run_id": "labutopia_p1_open_door_single_handle_formal_20260624_0001", - "diagnostics_json": "saved/diagnostics/labutopia_p1_open_door_single_handle_formal_20260624_0001/diagnostics.json", + "run_id": "labutopia_p1_gate_open_door_formal_20260624_0002", + "diagnostics_json": "saved/diagnostics/labutopia_p1_gate_open_door_formal_20260624_0002/diagnostics.json", "boundary_classification": "readback_visible", - "readback_frame": "saved/diagnostics/labutopia_p1_open_door_single_handle_formal_20260624_0001/readback_after_get_eval_camera_data/camera2/00000.png", + "readback_frame": "saved/diagnostics/labutopia_p1_gate_open_door_formal_20260624_0002/readback_after_get_eval_camera_data/camera2/00000.png", "report_image": "docs/records/evidence/2026-06-22-labutopia-ebench-weekly-report/assets/labutopia-franka-level1-open-door-eval-readback-p1.png", - "sha256": "f9b6c1ee41ac0a51a2377e4eed372b50a51ac4c793e5c0a52f7ef4f27a9e3e86", - "channel_min": [27.0, 29.0, 31.0], + "sha256": "da670293ef61e0136b3522a07c9f2421a0ec73bca79ac0304eb1adf818644502", + "channel_min": [27.0, 28.0, 30.0], "channel_max": [228.0, 228.0, 228.0], - "channel_mean": [165.751, 165.583, 166.099], + "channel_mean": [162.518, 162.711, 163.53], "nonzero_pixels": 262144, - "unique_rgb_colors": 3860, - "pixel_stats": { - "orange_red_pixels": 2195, - "orange_red_bbox_xyxy": [241, 157, 276, 233], - "non_floor_dark_mid_pixels": 7607, - "non_floor_dark_mid_bbox_xyxy": [35, 107, 413, 245], - "bluegrey_door_panel_pixels": 163648, - "bluegrey_door_panel_bbox_xyxy": [0, 106, 511, 511] + "unique_rgb_colors": 4088, + "render_validation": { + "passed": true, + "required_objects": { + "obj_DryingBox_01": { + "bbox": [85, 94, 426, 246], + "bbox_area_fraction": 0.19960784912109375, + "width_px": 342, + "height_px": 153, + "passed": true + }, + "obj_DryingBox_01_handle": { + "bbox": [246, 146, 273, 230], + "bbox_area_fraction": 0.0090789794921875, + "width_px": 28, + "height_px": 85, + "passed": true + } + } + }, + "claim_boundary": { + "task_render_accepted": true, + "official_baseline_evaluable": false, + "baseline_blockers": ["official_baseline_not_validated"] }, - "visual_review": "WARN_DIAGNOSABLE_BUT_NOT_ACCEPTED", - "visual_reason": "The formal single-handle front-camera retake shows the DryingBox/cabinet frame, door panel, and one orange handle/action point in one evaluator readback frame. Independent image-only QA rates it PASS/WARN as PM-facing diagnostic evidence. It is not official task acceptance because diagnostics still report render_validation_not_passed.", "runtime_physics": { "articulation_joint_positions": [0.0], "expected_articulation_joint_positions": [0.0], "dof_names": ["RevoluteJoint"], - "world_position": [0.75, 0.18000000715255737, 0.7799999713897705], - "runtime_physics_stable": true, - "status": "CLOSED_START_FIXED_SINGLE_HANDLE_FRONT_CAMERA_DIAGNOSTIC_ONLY" - } + "runtime_physics_stable": true + }, + "visual_review": "PASS", + "visual_reason": "The final thin-handle retake shows the DryingBox frame, door panel, and orange handle/action point clearly enough for the task render gate." } }, "open_door_root_cause_review": { - "summary": "GenManip task UID/articulation discovery is working; the instability is downstream of malformed DryingBox USD/PhysX topology.", - "findings": [ - "The composed runtime prim still has root scale 0.001 instead of a baked identity-scale articulation.", - "Multiple rigid links use the basename mesh, matching duplicate link-name warnings.", - "Some rigid links retain non-finite centerOfMass or invalid principalAxes.", - "At least one fixed-joint body target resolves to a collision prim without PhysicsRigidBodyAPI.", - "The button prismatic joint adds an extra DOF, so runtime reports RevoluteJoint and PrismaticJoint." - ], - "recommended_fix_sequence": [ - "Extend static validation to fail on non-identity articulation root scale, duplicate rigid-link basenames, non-finite COM, invalid principal axes, joint targets without rigid bodies, and unexpected extra DOFs.", - "Replace incremental over fixes with a sanitized DryingBox runtime asset: baked scale, identity root scale, unique rigid link names, required links/joints only, and stable handle placement.", - "Regenerate the LabUtopia overlay scene.usda and rerun open_door diagnostics.", - "Accept open_door only after finite joint positions, no duplicate-link warnings, stable runtime_sanity, and a visually plausible door/handle frame." + "problem": "The original DryingBox runtime asset could expose unstable articulation state and the early PM image did not show a useful door/handle target. An intermediate retake made the handle look like a broad orange panel.", + "fix_sequence": [ + "Use a sanitized DryingBox surrogate with fixed base and one aligned revolute door joint.", + "Replay the expected closed joint target so reset starts at [0.0].", + "Keep the handle as a nested DryingBox child, move it to the non-hinge side, delete duplicate orange marker blocks, and reduce the handle scale to [0.045, 0.075, 0.25].", + "Use the formal front camera for the final task render gate." ], "latest_resolution": { - "strategy": "sanitized DryingBox surrogate with fixed base and one aligned revolute door joint", - "static_topology_ready": true, "runtime_joint_stable": true, "runtime_joint_target_matches": true, - "visible_high_contrast_marker": false, "single_physical_handle_visible": true, "duplicate_marker_removed": true, - "remaining_blocker": "diagnostics still report render_validation_not_passed, so official task render acceptance and baseline evaluability remain blocked" - } - }, - "review": { - "visual_reviewer": { - "level1_pick": "READABLE_PENDING_FORMAL_QA", - "level1_place": "READABLE_PENDING_FORMAL_QA", - "level1_open_door": "PASS_WARN_DIAGNOSTIC_NOT_ACCEPTED" - }, - "baseline_evaluability_reviewer": { - "official_baseline_evaluable": false, - "primary_blocker": "formal task render acceptance is not signed off", - "secondary_blocker": "Lift2 composite assets and official runner execution are not closed" + "task_render_accepted": true, + "remaining_baseline_blocker": "official_baseline_not_validated" } }, "browser_visual_review": { - "tool": "Chromium headless CLI from ~/.cache/ms-playwright/chromium-1228", - "desktop_screenshot": "/tmp/labutopia_weekly_old_new_review_20260624/desktop_full_report.png", - "mobile_screenshot": "/tmp/labutopia_weekly_old_new_review_20260624/mobile_full_report_long.png", - "audit_json": "/tmp/labutopia_weekly_old_new_review_20260624/audit.json", - "image_requests": "static HTML parse found six report images and all six files exist; DOM dump contains required old/new/open_door/render_validation text", + "tool": "Playwright 1.61 via npx with cached Chromium", + "preview_url": "http://127.0.0.1:18080/records/evidence/2026-06-22-labutopia-ebench-weekly-report/index.html", + "desktop_screenshot": "/tmp/labutopia_weekly_p1_gate_review_20260624/desktop_full_report.png", + "tablet_screenshot": "/tmp/labutopia_weekly_p1_gate_review_20260624/tablet_full_report.png", + "mobile_screenshot": "/tmp/labutopia_weekly_p1_gate_review_20260624/mobile_full_report.png", + "desktop_audit_json": "/tmp/labutopia_weekly_p1_gate_review_20260624/desktop_audit.json", + "tablet_audit_json": "/tmp/labutopia_weekly_p1_gate_review_20260624/tablet_audit.json", + "mobile_audit_json": "/tmp/labutopia_weekly_p1_gate_review_20260624/mobile_audit.json", "result": "PASS_FOR_REPORT_DISPLAY", - "notes": "Desktop and mobile long screenshots show old JPGs and new square evaluator PNGs, including the latest single-handle open_door eval PNG. Chromium emitted expected headless DBus warnings only. This display audit does not mean task visual acceptance." + "notes": "Desktop, tablet, and mobile full-page audits passed: six report images loaded, required old/new/render-gate/baseline-boundary text was present, no failed requests or horizontal page overflow were detected, and stale render_validation_not_passed wording was absent from the HTML report." }, "claim_boundary": { "allowed": [ "All three Franka POC tasks now produce non-black evaluator camera2 readback frames.", "Static USD readback shows task objects and the nested DryingBox handle in plausible workspace coordinates.", "The old top-level duplicated handle payload is removed; the handle is represented as part of the DryingBox assembly.", - "Task-level hiding makes pick and place readable as PM-facing diagnostic frames.", - "The latest open_door runtime diagnostic has stable DryingBox joint positions, matches the expected closed target [0.0], only exposes RevoluteJoint, and shows the DryingBox frame, door panel, and one orange handle/action point in the same eval readback frame." + "Task-level hiding makes pick and place readable as PM-facing task frames.", + "The latest open_door runtime diagnostic has stable DryingBox joint positions, matches the expected closed target [0.0], only exposes RevoluteJoint, and shows the DryingBox frame, door panel, and thin orange handle in the same eval readback frame.", + "The three Franka POC task render gates now pass with task_render_accepted=true." ], "blocked": [ - "task_render_accepted=true.", "Official Lift2 baseline evaluability.", - "Any claim that open_door visual evidence is reliable for baseline evaluation.", - "Any claim that the current report display QA equals task visual acceptance." + "Any claim that task render acceptance equals policy success or score improvement.", + "Any claim that the official Lift2 runner has been executed.", + "Any claim that the current report display QA equals official baseline validation." ] }, "next_steps": [ - "Run formal visual QA for pick/place and keep task_render_accepted=false until signed off.", - "Keep diagnostics claim_boundary as the release gate; do not upgrade claims from readback_visible alone.", - "Use the latest single-handle front-camera open_door frame as PM-facing diagnostic evidence only.", - "Refresh browser display QA and formal task visual QA; keep task_render_accepted=false until render_validation passes." + "Build and validate the Lift2 composite asset root: LabUtopia scene overlay plus default robot_usds/lift2 plus default miscs/curobo.", + "Locate and hash the official EBench/OpenPI/Lift2 runner entrypoint before executing any official-style loop.", + "Run a Lift2 dry smoke in an isolated port and keep official_baseline_execution=false until the runner and assets are proven." ] } diff --git a/docs/labutopia_lab_poc/render_visual_investigation_20260623.md b/docs/labutopia_lab_poc/render_visual_investigation_20260623.md index f9828e1..b672d28 100644 --- a/docs/labutopia_lab_poc/render_visual_investigation_20260623.md +++ b/docs/labutopia_lab_poc/render_visual_investigation_20260623.md @@ -6,7 +6,7 @@ Date: 2026-06-23 This record summarizes the visual and technical investigation for the LabUtopia Franka POC render images that were added to the 2026-06-22 weekly report. -The original JPG images are historical failed evidence. The latest evaluator readback PNGs are now non-black and task-level visibility isolation makes `level1_pick` readable and `level1_place` basically readable for PM diagnosis. `level1_open_door` runtime physics has been stabilized with a sanitized DryingBox surrogate and now starts at the expected closed joint target. The 2026-06-24 formal single-handle front-camera retake makes the DryingBox frame, door panel, and one orange handle/action point visible. Independent image-only QA rates the old-vs-current comparison PASS for PM diagnostic reporting, with `open_door` PASS/WARN as diagnostic evidence only. Therefore `task_render_accepted=false` and `official_baseline_evaluable=false` remain the correct claim boundary until `render_validation` and formal task visual QA pass. +The original JPG images are historical failed evidence. The latest evaluator readback PNGs are now non-black and task-level visibility isolation makes `level1_pick` readable, `level1_place` relation-readable, and `level1_open_door` visually interpretable after the thin-handle retake. `level1_open_door` runtime physics has been stabilized with a sanitized DryingBox surrogate and now starts at the expected closed joint target. The final 2026-06-24 formal retake makes the DryingBox frame, door panel, and thin orange handle/action point visible. Independent image-only QA rates the old-vs-current comparison PASS for PM evidence, and the latest formal diagnostics for all three tasks report `render_validation.passed=true`. Therefore `task_render_accepted=true` and `official_baseline_evaluable=false` are the current claim boundary: task render is accepted, official Lift2 baseline is not yet validated. ## Reviewed Images @@ -351,13 +351,13 @@ Intermediate camera retake that was rejected: | --- | --- | --- | --- | --- | | `level1_open_door` | `labutopia_p1_open_door_close_camera_20260623_123833` | `readback_visible` | FAIL | The orange handle/interaction point was clear, but the camera was too close: the drying-box body became an unidentifiable white/gray fragment, so a reviewer could not understand the open-door task context. | -Current runtime diagnostic after target replay, duplicate marker removal, handle-side correction, and formal front-camera retake: +Superseded runtime diagnostic after target replay, duplicate marker removal, handle-side correction, and formal front-camera retake: | Task | Run ID | Boundary | Runtime status | Visual QA | | --- | --- | --- | --- | --- | -| `level1_open_door` | `labutopia_p1_open_door_single_handle_formal_20260624_0001` | `readback_visible` | closed start fixed, door/frame/single handle visible | PASS/WARN diagnostic / not accepted | +| `level1_open_door` | `labutopia_p1_open_door_single_handle_formal_20260624_0001` | `readback_visible` | closed start fixed, door/frame/single handle visible | superseded by final thin-handle retake | -Runtime evidence: +Superseded runtime evidence: ```text diagnostic_error = None @@ -374,15 +374,30 @@ official_baseline_evaluable = false task_render_accepted = false ``` -Report asset now copied from: +This run is retained as history because independent visual QA warned that the orange handle still read like an oversized panel/action block. It has been superseded by: ```text -saved/diagnostics/labutopia_p1_open_door_single_handle_formal_20260624_0001/readback_after_get_eval_camera_data/camera2/00000.png +saved/diagnostics/labutopia_p1_gate_open_door_formal_20260624_0002/readback_after_get_eval_camera_data/camera2/00000.png docs/records/evidence/2026-06-22-labutopia-ebench-weekly-report/assets/labutopia-franka-level1-open-door-eval-readback-p1.png -sha256 = f9b6c1ee41ac0a51a2377e4eed372b50a51ac4c793e5c0a52f7ef4f27a9e3e86 +sha256 = da670293ef61e0136b3522a07c9f2421a0ec73bca79ac0304eb1adf818644502 +``` + +Final formal open_door evidence: + +```text +diagnostic_error = None +runtime_physics_stable = true +dof_names = [RevoluteJoint] +joint_positions = [0.0] +expected_closed_start = [0.0] +DryingBox bbox = [85, 94, 426, 246] +handle bbox = [246, 146, 273, 230] +render_validation.passed = true +official_baseline_evaluable = false +task_render_accepted = true ``` -Independent image-only QA verdict for the old-vs-current report set: +Independent image-only QA verdict for the old-vs-current report set after the final retake: ```text Overall verdict: PASS for PM-facing old-vs-current comparison evidence, with careful wording that the current images are evaluator readback diagnostics, not official baseline acceptance proof. @@ -391,7 +406,7 @@ Old place: FAIL; only a white plane on black background, supports "placement rel Old open_door: FAIL; close-up of dark/white box corner, no readable door, handle, or action point. Current pick: PASS; centered blue bottle/flask-like target on tabletop. Current place: PASS; teal object and yellow placement square are both visible. -Current open_door: PASS/WARN; cabinet/drying-box-like structure, door panel, and orange handle/action point are clearly in frame, but this remains diagnostic evidence rather than official baseline acceptance. +Current open_door: PASS; cabinet/drying-box-like structure, door panel, and thin orange handle/action point are clearly in frame for the local task-render gate. ``` Updated interpretation: @@ -400,10 +415,10 @@ Updated interpretation: runtime_physics_stable=true runtime_joint_target_matches=true camera_readback_visible=true -single_handle_door_frame_visible=true -task_render_accepted=false +thin_handle_door_frame_visible=true +task_render_accepted=true official_baseline_evaluable=false -primary remaining blocker: render_validation_not_passed / formal task visual QA not signed off +primary remaining blocker: official Lift2 composite assets and official runner are not validated ``` ## Ranked Root-Cause Hypotheses @@ -412,8 +427,8 @@ primary remaining blocker: render_validation_not_passed / formal task visual QA - Static object placement and nested handle discovery have been repaired for the POC. - The old DryingBox USD/PhysX topology was a baseline-evaluability blocker: non-identity root scale, duplicate rigid link names, invalid inertial attributes, invalid joint body targets, and an extra prismatic DOF. - The POC runtime path now uses a sanitized surrogate that passes static topology validation and latest runtime joint sanity. - - The latest formal front-camera image makes the DryingBox frame, door panel, and single orange handle/action point visible for PM diagnosis. - - The remaining blocker is the formal gate: diagnostics still report `render_validation_not_passed`, so this cannot be upgraded to task render acceptance or baseline evaluability. + - The latest formal front-camera image makes the DryingBox frame, door panel, and thin orange handle/action point visible for the local task-render gate. + - The remaining blocker is official baseline scope: Lift2 composite assets and the official runner have not been validated, so this cannot be upgraded to official baseline evaluability. 2. Eval camera readback is black before recorder writing. - The first diagnostics proved the black frame existed immediately after `get_eval_camera_data()`. - P0a/P0b follow-up changed pick/place from `readback_black_before_recorder` to `readback_visible`. @@ -421,15 +436,15 @@ primary remaining blocker: render_validation_not_passed / formal task visual QA - Render product binding to `/Camera/LabUtopiaCamera2` appears valid in diagnostics. 3. Task layout is not yet a real LabUtopia reset layout. - LabUtopia source position ranges are in `task_semantics.yml`, and task-level visibility isolation now removes non-task objects for POC diagnosis. - - Full reset sampling and formal task render acceptance are still not signed off. + - Formal task render acceptance is signed off for the Franka POC gate; official baseline reset/runner validation is still separate. 4. Direct-render report screenshots used non-eval camera choices. - The report images were captured with changed lighting and viewpoint and no committed producer script. - This makes them unsuitable for proving task-scene correctness. 5. Overlay wrapper and task semantics are not yet aligned enough for visual or metric claims. - The wrapper payload exposes prims under GenManip-friendly names, but visible mesh bbox, wrapper pose, and semantic coordinates still need a measured consistency check. -6. The `open_door` PM diagnostic image is now usable, but the acceptance gate is still closed. - - Articulation state now matches the expected closed target and the formal camera shows the door/frame/single handle. - - The next step is not to overclaim the screenshot; it is to keep `render_validation_not_passed` as the hard blocker and run formal visual QA before any baseline statement. +6. The `open_door` PM diagnostic image is now usable for the local task-render gate, but the official baseline gate is still closed. + - Articulation state now matches the expected closed target and the formal camera shows the door/frame/thin handle. + - The next step is not to overclaim the screenshot as baseline evidence; it is to validate Lift2 composite assets and the official runner before any baseline statement. ## Claim Boundary @@ -438,19 +453,18 @@ Allowed now: ```text LabUtopia Franka POC can run through the local GenManip/EBench server-client smoke path and finalize 3/3 tasks with result files. P0a/P0b controlled diagnostics prove camera2 readback is no longer pure black for pick/place after camera_axes/pose and deterministic lighting fixes. -P1 visibility diagnostics make pick readable and place basically readable as PM-facing diagnostic frames. -The latest open_door runtime diagnostic has stable DryingBox joint positions, matches the expected closed target [0.0], only exposes RevoluteJoint, and shows the DryingBox frame, door panel, and one orange handle/action point in the same evaluator readback frame. +P1 formal diagnostics make pick, place, and open_door task frames pass render_validation. +The latest open_door runtime diagnostic has stable DryingBox joint positions, matches the expected closed target [0.0], only exposes RevoluteJoint, and shows the DryingBox frame, door panel, and a thin orange handle/action point in the same evaluator readback frame. +The three task render images are accepted for the Franka POC task-render gate. ``` Not allowed now: ```text -The three task render images are accepted. -The eval video path works. -Task reset layouts are visually verified. The official Lift2 baseline is evaluable. -The current open_door screenshot proves visual task correctness. -The current report display QA proves task visual acceptance. +The eval video path or policy success is proven. +The official Lift2 runner has been executed. +The current report display QA proves official baseline validation. ``` ## Required Next Diagnostics @@ -464,9 +478,32 @@ Run these in an isolated port/run_id so EOS or another engineer's run is not con 5. Done for POC runtime layer: extend static USD/PhysX validation for DryingBox articulation root scale, duplicate rigid-link basenames, non-finite COM, invalid principal axes, invalid joint body targets, and unexpected extra DOFs. 6. Done for POC runtime layer: build a sanitized DryingBox runtime asset and rerun `level1_open_door` diagnostics until runtime joint positions are finite. 7. Done for POC runtime layer: fix `open_door` closed-start joint initialization, move the handle to the non-hinge side, remove the duplicate orange marker, and formalize the front camera. -8. Next: run browser display QA and formal task visual QA on the updated report set; keep `task_render_accepted=false` until `render_validation` passes. -9. Write an evidence manifest for any future PM image. It must include `run_id`, task name, source eval frame path, report image path, sha256, camera config, asset root, commit hash, and `direct_render=false`. +8. Done for POC task render gate: rerun formal pick/place/open_door diagnostics; all three report `render_validation.passed=true` and `task_render_accepted=true`. +9. Next: run browser display QA on the updated report set and then move to the Lift2 composite asset root and official runner discovery. +10. Write an evidence manifest for any future PM image. It must include `run_id`, task name, source eval frame path, report image path, sha256, camera config, asset root, commit hash, and `direct_render=false`. ## Documentation Decision -The weekly report must keep the old JPGs as historical failed evidence and present the new PNGs as evaluator readback diagnostics. It may say `pick` is readable and `place` is basically readable for PM diagnosis. It may also say the latest `open_door` DryingBox articulation is diagnosable after removing the earlier joint explosion, that the closed-start target now matches `[0.0]`, and that the DryingBox frame, door panel, and one orange handle/action point are visible in the same evaluator frame. It must also say this is PM-facing diagnostic evidence only. It must not claim `task_render_accepted=true`, official Lift2 baseline evaluability, or open-door visual acceptance until formal task visual QA and `render_validation` pass. +The weekly report must keep the old JPGs as historical failed evidence and present the new PNGs as evaluator readback evidence. It may say `pick` is readable, `place` is relation-readable, and the latest `open_door` DryingBox articulation is stable after removing the earlier joint explosion. It may say the closed-start target matches `[0.0]`, the DryingBox frame, door panel, and thin orange handle/action point are visible in the same evaluator frame, and `task_render_accepted=true` for the three Franka POC render gates. It must not claim official Lift2 baseline evaluability, official runner execution, policy success, or score improvement. + +## 2026-06-24 P1 Render Gate Closure + +Final formal diagnostics: + +| Task | Run ID | Render gate | Key visible object metrics | Claim boundary | +| --- | --- | --- | --- | --- | +| `level1_pick` | `labutopia_p1_gate_pick_formal_20260624_0001` | PASS | bottle bbox `[236,222,275,299]`, 40x78 px | `task_render_accepted=true`, `official_baseline_evaluable=false` | +| `level1_place` | `labutopia_p1_gate_place_formal_20260624_0001` | PASS | beaker bbox `[251,208,289,263]`, target bbox `[215,398,296,459]` | `task_render_accepted=true`, `official_baseline_evaluable=false` | +| `level1_open_door` | `labutopia_p1_gate_open_door_formal_20260624_0002` | PASS | DryingBox bbox `[85,94,426,246]`, handle bbox `[246,146,273,230]` | `task_render_accepted=true`, `official_baseline_evaluable=false` | + +Old-image explanation for the HTML report: + +- `pick`: the old image did not clearly show the target bottle; fixed by camera/readback repair, workspace normalization, and task-level hiding. +- `place`: the old image did not show the beaker-to-target relation; fixed by workspace normalization, target color/placement, and hiding unrelated objects. +- `open_door`: the old image only showed a dark box corner, and an intermediate retake made the handle look like a broad orange panel. The final fix uses a sanitized DryingBox surrogate, closed-joint target replay, nested handle placement, duplicate marker removal, slimmer handle scale `[0.045, 0.075, 0.25]`, and the formal front camera. + +PM-safe wording: + +```text +The three task render images now pass the local GenManip/EBench task-render gate. They prove the LabUtopia POC can load and render task-readable scenes through evaluator readback. They do not prove policy success or official Lift2 baseline evaluability; that requires the Lift2 composite assets and official runner lane. +``` diff --git a/docs/records/2026-06-22-labutopia-ebench-weekly-report.md b/docs/records/2026-06-22-labutopia-ebench-weekly-report.md index 9d88555..bd1b173 100644 --- a/docs/records/2026-06-22-labutopia-ebench-weekly-report.md +++ b/docs/records/2026-06-22-labutopia-ebench-weekly-report.md @@ -7,7 +7,7 @@ HTML 版产品汇报页: 本周已经把 LabUtopia 的 Franka POC 跑通到端到端 smoke 阶段:任务可以提交、场景可以加载、三个 level-1 任务可以 reset/step、结果可以落盘、最终状态可以正常 complete。 -这说明当前最关键的“接入链路”已经打通。需要注意的是,这还不是任务求解成功,也不是官方 baseline 成绩;当前 smoke 使用默认动作,所以三个任务分数都是 `0.0`。2026-06-23 到 2026-06-24 最新复核补充:三任务现在都能通过 EBench/evaluator 正常读回非黑渲染图,资产静态坐标也已经从“明显导错”修到合理工作区;最新任务级隐藏和构图后,`pick` 已清楚,`place` 基本可读但还可再放大,`open_door` 已从关节爆值和黑箱角推进到关闭位正确、门板/框架/单个橙色把手可见的诊断状态,但自动 `render_validation` 仍未放行,所以仍不能说官方 baseline 可评已经闭环。 +这说明当前最关键的“接入链路”已经打通。需要注意的是,这还不是任务求解成功,也不是官方 baseline 成绩;当前 smoke 使用默认动作,所以三个任务分数都是 `0.0`。2026-06-23 到 2026-06-24 最新复核补充:三任务现在都能通过 EBench/evaluator 正常读回非黑渲染图,资产静态坐标也已经从“明显导错”修到合理工作区;最新正式诊断中 `pick`、`place`、`open_door` 均为 `render_validation.passed=true`、`task_render_accepted=true`。当前可以说 Franka POC 的任务渲染门禁已过,但官方 Lift2 baseline 的复合资产和官方 runner 还没验证,所以仍不能说官方 baseline 可评已经闭环。 ## 本周完成了什么 @@ -65,7 +65,7 @@ DryingBox_01 -> obj_DryingBox_01 table -> table ``` -这部分已经通过实际 smoke、静态 USD readback 和三任务 evaluator camera readback 验证。这里的“资产加载”仍然只表示 scene overlay、对象名映射和当前相机读回链路能进入 runtime;`pick/place` 当前诊断图已经可读,但还没有正式视觉 QA 签收,`open_door` 的底层物理读数已稳定且回到关闭位,当前正式相机图也能看出门板、框架和单个橙色把手,不过自动 `render_validation` 仍未通过。 +这部分已经通过实际 smoke、静态 USD readback 和三任务 evaluator camera readback 验证。这里的“资产加载”表示 scene overlay、对象名映射和当前相机读回链路能进入 runtime;`pick/place` 当前任务图已经可读并通过渲染门禁,`open_door` 的底层物理读数已稳定且回到关闭位,最终细把手正式相机图也能看出门板、框架和动作点,三任务最新正式诊断均为 `render_validation.passed=true`。 ### 3. 结果记录链路已修复 @@ -96,7 +96,7 @@ EOS/其他工程师任务: 8087 ## 有没有渲染图验证? -2026-06-23 到 2026-06-24 最新复核结论:现在已经有三张从 EBench/evaluator 路径读回的非黑图,说明“能不能通过评测链路拍到东西”这个问题已经推进了一大步。旧 JPG 保留为历史失败样例,新 PNG 是更可信的 eval-path 诊断图:`pick` 已清楚,`place` 基本可读,`open_door` 已从物理爆值和黑箱角推进到关闭位正确、门板/框架/单个橙色把手可识别,但自动 `render_validation` 仍未签收。 +2026-06-23 到 2026-06-24 最新复核结论:现在已经有三张从 EBench/evaluator 路径读回的非黑图,说明“能不能通过评测链路拍到东西”这个问题已经推进闭环。旧 JPG 保留为历史失败样例,新 PNG 是更可信的 eval-path 证据:`pick` 目标瓶清楚,`place` 烧杯和黄色托盘关系可读,`open_door` 已从物理爆值、黑箱角和大橙色块推进到关闭位正确、门板/框架/细橙色把手可识别。三任务最新正式诊断均为 `render_validation.passed=true`、`task_render_accepted=true`。 ```text docs/records/evidence/2026-06-22-labutopia-ebench-weekly-report/assets/labutopia-franka-level1-pick.jpg @@ -107,15 +107,15 @@ docs/records/evidence/2026-06-22-labutopia-ebench-weekly-report/assets/labutopia docs/records/evidence/2026-06-22-labutopia-ebench-weekly-report/assets/labutopia-franka-level1-open-door-eval-readback-p1.png ``` -旧的三张 JPG 保留为历史失败样例。新的三张 PNG 来自正常 evaluator camera readback,不是 direct-render 截图;它们更适合说明今天的真实状态:链路能拍到场景,pick/place 通过任务级隐藏后已经能让 PM 看懂任务目标,open_door 已从“只看到黑箱角/看不见把手”推进到“关闭位正确、门板/框架/单把手可识别但仍未验收”。 +旧的三张 JPG 保留为历史失败样例。新的三张 PNG 来自正常 evaluator camera readback,不是 direct-render 截图;它们更适合说明今天的真实状态:链路能拍到场景,pick/place 通过任务级隐藏后已经能让 PM 看懂任务目标,open_door 已从“只看到黑箱角/看不见把手/把手像一大片橙色面板”推进到“关闭位正确、门板/框架/细把手可识别”。这说明任务渲染门禁已经通过,但不等于官方 Lift2 baseline 已经可评。 给产品经理看的前后对照: | 任务 | 旧图问题 | 已经做的修复 | 现在还差什么 | | --- | --- | --- | --- | -| `level1_pick` | 抓取目标不明显,只看图无法判断“要抓哪个瓶子” | 修正 eval camera readback、相机朝向、光照,把瓶子归一到 Franka 工作区,并在 pick 任务里隐藏烧杯、托盘、干燥箱等非目标物体 | 当前新图已经能让 PM 看懂“抓这个蓝色瓶子”;还差正式 pixel/视觉 QA 签收 | -| `level1_place` | 看不出源物体和目标托盘的关系,不像一个放置任务 | 修正托盘、烧杯、瓶子坐标和颜色标记,并在 place 任务里隐藏瓶子和干燥箱,只保留烧杯与目标托盘 | 当前新图能看懂“把烧杯放到黄色托盘附近”;后续可以再拉近相机 | -| `level1_open_door` | 几乎只拍到黑色箱体角,门板、把手、铰链和动作目标都不清楚 | 把门把手恢复为干燥箱内部子部件,不再作为独立物体飞走;生成 DryingBox runtime surrogate,固定底座并只保留一个门关节;再补关节初始目标回放、把手位置修正、删除重复橙色块和任务专用正面相机 | 最新图中关节已回到期望关闭位 `0.0`,门板、框架和单个橙色把手可识别;但自动 `render_validation` 仍未通过,下一步要把视觉 QA 和 Lift2 baseline gate 接上 | +| `level1_pick` | 抓取目标不明显,只看图无法判断“要抓哪个瓶子” | 修正 eval camera readback、相机朝向、光照,把瓶子归一到 Franka 工作区,并在 pick 任务里隐藏烧杯、托盘、干燥箱等非目标物体 | 当前新图已经能让 PM 看懂“抓这个蓝色瓶子”,并已通过任务渲染门禁 | +| `level1_place` | 看不出源物体和目标托盘的关系,不像一个放置任务 | 修正托盘、烧杯、瓶子坐标和颜色标记,并在 place 任务里隐藏瓶子和干燥箱,只保留烧杯与目标托盘 | 当前新图能看懂“把烧杯放到黄色托盘附近”,并已通过任务渲染门禁 | +| `level1_open_door` | 几乎只拍到黑色箱体角,门板、把手、铰链和动作目标都不清楚;中间版本又出现把手像大橙色面板的问题 | 把门把手恢复为干燥箱内部子部件,不再作为独立物体飞走;生成 DryingBox runtime surrogate,固定底座并只保留一个门关节;再补关节初始目标回放、把手位置修正、删除重复橙色块、缩细把手和任务专用正面相机 | 最新图中关节已回到期望关闭位 `0.0`,门板、框架和细橙色把手可识别,并已通过任务渲染门禁;下一步接 Lift2 baseline gate | 需要特别说明: @@ -123,10 +123,10 @@ docs/records/evidence/2026-06-22-labutopia-ebench-weekly-report/assets/labutopia - 已推进:P0/P1 后,`level1_pick`、`level1_place`、`level1_open_door` 都是 `readback_visible`。 - 已推进:静态 USD readback 显示瓶子、烧杯、托盘、干燥箱、门把手的坐标和尺寸合理;门把手路径是 `/World/labutopia_level1_poc/obj_obj_DryingBox_01/handle`,不是独立飞走物体。 - 已推进:任务级隐藏后,`level1_pick` 当前图只保留目标瓶,`level1_place` 当前图只保留烧杯和目标托盘,PM 可以看懂任务目标和关系。 -- 已推进:`level1_open_door` 旧版本运行期关节读数曾出现 `1.573e13` 量级;最新诊断只暴露 `RevoluteJoint`,关节读数为 `0.0`,仿真能完成 readback,问题已经从“爆掉不可看/只看到黑箱角”收敛到“门板、框架和单个橙色把手可识别”。 -- 已复核:独立视觉审阅认为当前 old-vs-current 对照可用于 PM 汇报;旧图确实支持“目标不清、放置关系缺失、看不到门把手”的问题描述,当前 open_door 候选图可以作为诊断证据。 -- 仍 blocked:`level1_open_door` 当前图只能作为 PM 诊断图,不能作为官方 baseline 可评证据,因为 diagnostics claim boundary 仍是 `render_validation_not_passed`、`task_render_accepted=false`、`official_baseline_evaluable=false`。 -- 因此下一步不是继续宣传截图,而是给三任务补正式视觉 QA,并把 Lift2 baseline 所需的复合资产和官方 runner gate 接上。 +- 已推进:`level1_open_door` 旧版本运行期关节读数曾出现 `1.573e13` 量级;最新诊断只暴露 `RevoluteJoint`,关节读数为 `0.0`,仿真能完成 readback,问题已经从“爆掉不可看/只看到黑箱角/把手像大橙色面板”收敛到“门板、框架和细橙色把手可识别”。 +- 已复核:独立视觉审阅认为当前 old-vs-current 对照可用于 PM 汇报;旧图确实支持“目标不清、放置关系缺失、看不到门把手”的问题描述,当前三张新图均可作为任务渲染通过证据。 +- 仍 blocked:官方 Lift2 baseline 不能宣称可评,因为 Lift2 复合资产根目录和官方 runner 还没验证;当前 diagnostics claim boundary 是 `task_render_accepted=true`、`official_baseline_evaluable=false`。 +- 因此下一步不是继续修三张截图,而是把 Lift2 baseline 所需的复合资产和官方 runner gate 接上。 ## 本周遇到的问题和解决方式 @@ -136,7 +136,7 @@ docs/records/evidence/2026-06-22-labutopia-ebench-weekly-report/assets/labutopia | 缺少 `meta_info.pkl` | 传统 GenManip 任务依赖采集包里的元信息,LabUtopia POC 没有这份文件 | 针对 LabUtopia POC,从实时场景里自动生成最小可用元信息 | 已解决 | | camera cleanup 字段缺失 | 切换任务时 camera 清理报字段错误 | 补齐 camera 配置里的 cleanup flags | 已解决 | | eval recorder `camera2` 黑屏 | 保存过程帧时 `camera2` 输出纯黑 | 已修 camera axes/pose、deterministic lighting 和工作区相机;三任务当前 eval readback 均非黑 | 黑屏解除 | -| 当前新图仍未全部验收 | `pick` 已清楚,`place` 基本可读但还可放大;`open_door` 已能读回且关闭位正确,门板/框架/单把手可识别,但自动 `render_validation` 仍未放行 | pick/place 补正式视觉 QA;open_door 把当前正面图纳入同一验收门槛,并继续接 Lift2 baseline 所需的复合资产和官方 runner gate | 部分通过诊断 | +| 当前新图验收边界 | `pick` 已清楚,`place` 关系可读;`open_door` 已能读回且关闭位正确,门板/框架/细把手可识别 | 三任务最新正式诊断均为 `render_validation.passed=true`;继续接 Lift2 baseline 所需的复合资产和官方 runner gate | 任务渲染通过 | | `open_door` 运行期物理不稳定 | 旧版本 runtime articulation joint position 爆到 `1.573e13`,并伴随 PhysX transform warning | 已用 DryingBox runtime surrogate、固定底座、对齐后的门铰链和关节目标回放修复;最新读数为 `0.0 rad` 且只暴露 `RevoluteJoint` | 物理层已解决 | | 资产导入/layout 静态坐标 | 之前任务物体在源 lab 坐标,handle 变成异常放大的独立物体 | P1 已把对象归一到 Franka 工作区,并把 handle 保留为 DryingBox 内部子路径 | 静态层已推进 | | 最终进度不 complete | 任务已跑完,但进度没有写入,导致客户端最后等待超时 | 任务结束时写最小 `result_info.json`,并修复进度统计 | 已解决 | @@ -150,9 +150,9 @@ docs/records/evidence/2026-06-22-labutopia-ebench-weekly-report/assets/labutopia | LabUtopia 场景接入 | 已跑通 | 场景资产能加载,任务链路能跑完 | | Franka POC smoke | 已完成 | 3 个任务全部 complete | | 结果落盘 | 已完成 | per-task 和 final result 都能写出 | -| 渲染图/视频验收 | 部分通过诊断 | 三任务 eval readback 已非黑;pick 已清楚,place 基本可读,open_door 已可诊断且关闭位正确、门板/框架/单把手可识别,但自动 `render_validation` 仍未签收 | +| 渲染图/视频验收 | 任务渲染通过 | 三任务 eval readback 已非黑;pick 已清楚,place 关系可读,open_door 关闭位正确、门板/框架/细把手可识别;三任务最新正式诊断均为 `render_validation.passed=true` | | 任务求解能力 | 未验证 | 当前默认动作得分 0.0,不代表策略能力 | -| 官方 baseline | 不能宣称可评 | 三任务正式视觉验收、Lift2 复合资产和官方 runner 还没闭环,diagnostics claim boundary 仍是 `official_baseline_evaluable=false` | +| 官方 baseline | 不能宣称可评 | 任务渲染门禁已过,但 Lift2 复合资产和官方 runner 还没闭环,diagnostics claim boundary 仍是 `official_baseline_evaluable=false` | ## 验证证据 @@ -169,16 +169,16 @@ total_episodes=3 ```text python -m pytest tests/labutopia_poc -q -56 passed, 1 skipped +64 passed, 1 skipped python -m pytest tests/labutopia_poc/test_render_diagnostics_contract.py -q -2 passed +13 passed python standalone_tools/labutopia_poc/validate_task_package.py LabUtopia task package validation OK report display QA -Chromium headless desktop/mobile long screenshots passed; six report images exist and the DOM contains the old-image section, new eval readback section, latest single-handle `open_door`, and `render_validation_not_passed` boundary text. Evidence: `/tmp/labutopia_weekly_old_new_review_20260624/desktop_full_report.png`, `/tmp/labutopia_weekly_old_new_review_20260624/mobile_full_report_long.png`, `/tmp/labutopia_weekly_old_new_review_20260624/audit.json`. This checks report display only, not task visual acceptance. +Playwright/Chromium desktop/tablet/mobile full-page audits passed; six report images load, the DOM contains the old-image section, new eval readback section, latest thin-handle `open_door`, task-render-pass wording, and official-baseline-not-yet-validated boundary text. Evidence: `/tmp/labutopia_weekly_p1_gate_review_20260624/desktop_full_report.png`, `/tmp/labutopia_weekly_p1_gate_review_20260624/tablet_full_report.png`, `/tmp/labutopia_weekly_p1_gate_review_20260624/mobile_full_report.png`, plus matching `*_audit.json` files. This checks report display only, not official baseline evaluability. ``` 结果文件: @@ -196,8 +196,8 @@ saved/eval_results/ebench/labutopia_franka_smoke_clean8_20260622_100208/.../leve - eval recorder `camera2` frames: black, sampled RGB min/max/mean all 0 - runtime diagnostics: `level1_pick/place/open_door` all `readback_black_before_recorder` - evidence manifest: [docs/labutopia_lab_poc/evidence_manifests/render_diagnostics_20260623.json](../labutopia_lab_poc/evidence_manifests/render_diagnostics_20260623.json) -- P1 follow-up: `level1_pick/place/open_door` now `readback_visible`; latest task-level hiding makes pick readable and place basically readable, while open_door is diagnosable with closed joint target `[0.0]` and visible door/frame/single orange handle, but diagnostics still report `render_validation_not_passed` -- independent image-only review: old-vs-current comparison is PASS for PM-facing diagnostic evidence; open_door is PASS/WARN as diagnostic evidence and must not be described as official baseline acceptance +- P1 follow-up: `level1_pick/place/open_door` now `readback_visible`; latest task-level hiding makes pick readable and place relation readable, while open_door has closed joint target `[0.0]`, visible door/frame/thin orange handle, and `render_validation.passed=true` +- independent image-only review: old-vs-current comparison is PASS for PM-facing evidence; the final thin-handle open_door retake is PASS, but none of this should be described as official baseline execution - P0a/P0b evidence manifest: [docs/labutopia_lab_poc/evidence_manifests/render_p0a_p0b_20260623.json](../labutopia_lab_poc/evidence_manifests/render_p0a_p0b_20260623.json) - P1 evidence manifest: [docs/labutopia_lab_poc/evidence_manifests/render_p1_asset_layout_20260623.json](../labutopia_lab_poc/evidence_manifests/render_p1_asset_layout_20260623.json) - static direct-render evidence: visual QA failed on 2026-06-23 @@ -213,9 +213,9 @@ saved/eval_results/ebench/labutopia_franka_smoke_clean8_20260622_100208/.../leve 1. P0a/P0b:已修相机 readback 和 deterministic lighting,让 eval path 不再黑屏。 2. P1a:已把对象静态坐标和 nested handle 归一到 Franka 工作区。 3. P1b:已用 DryingBox surrogate 和关节目标回放修复 `open_door` runtime articulation/PhysX 爆值与关闭位问题。 -4. P1c:pick/place 补正式视觉 QA;open_door 使用最新单把手正式相机图继续验收,让门板、门面、把手和任务动作点在 evaluator 图里清楚可见。 -5. P2:用正常 eval-path 重新抓三任务关键帧,写 evidence manifest,并通过独立视觉 QA。 -6. P3:视觉和物理验收通过后,再进入 Lift2 复合资产根目录和官方 runner 发现。 +4. P1c:已完成任务级相机/构图复验,三任务 `render_validation.passed=true`。 +5. P2:已用正常 eval-path 重新抓三任务关键帧,写 evidence manifest,并完成独立视觉复核。 +6. P3:进入 Lift2 复合资产根目录和官方 runner 发现;这是官方 baseline 可评前的下一道硬门槛。 ## 新增调研和计划文档 diff --git a/docs/records/evidence/2026-06-22-labutopia-ebench-weekly-report/assets/labutopia-franka-level1-open-door-eval-readback-p1.png b/docs/records/evidence/2026-06-22-labutopia-ebench-weekly-report/assets/labutopia-franka-level1-open-door-eval-readback-p1.png index 3c5cbd66ea307dc7452b927ca878e4f8aa7c0127..081d582b933762b6821ed9272a88337e0aa1dbb8 100644 GIT binary patch literal 97117 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 Nj*?2lMdqh({l9)^5mf*H literal 98720 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

    }QB$Mj#|=H~Lhs!pbni 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≫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 diff --git a/docs/records/evidence/2026-06-22-labutopia-ebench-weekly-report/assets/labutopia-franka-level1-pick-eval-readback-p1.png b/docs/records/evidence/2026-06-22-labutopia-ebench-weekly-report/assets/labutopia-franka-level1-pick-eval-readback-p1.png index ea9ce2c2ca3ea9da50ed5e327e663bb7a6501e1d..a4f3c6329b7450d2079e56f4befb4974dd325966 100644 GIT binary patch literal 67000 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 Ws-Wk;Hm9wDpMnF$;z8`)U;jU?ycKBx literal 60884 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Π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 diff --git a/docs/records/evidence/2026-06-22-labutopia-ebench-weekly-report/index.html b/docs/records/evidence/2026-06-22-labutopia-ebench-weekly-report/index.html index 78eaceb..858f012 100644 --- a/docs/records/evidence/2026-06-22-labutopia-ebench-weekly-report/index.html +++ b/docs/records/evidence/2026-06-22-labutopia-ebench-weekly-report/index.html @@ -407,18 +407,19 @@

    Weekly report · 2026-06-22 · PM-facing evidence page

    LabUtopia x EBench接入周报

    -

    本周 Franka POC 已经跑通端到端 smoke:任务能提交,LabUtopia 场景能加载,三个 level-1 任务能 reset/step,结果能落盘并正常 complete。2026-06-23 到 2026-06-24 的 P1 复核后,三任务 eval camera2 都能读回非黑画面,静态资产坐标也已归一到 Franka 工作区;最新复核显示 pick 已经清楚,place 基本可读但还可再放大,open_door 已从关节爆值和黑箱角推进到“门板、框架、单个橙色把手在同一张图里可见”的可汇报诊断状态,但自动 render_validation 仍未放行,不能宣称官方 baseline 可评。

    +

    本周 Franka POC 已经跑通端到端 smoke:任务能提交,LabUtopia 场景能加载,三个 level-1 任务能 reset/step,结果能落盘并正常 complete。2026-06-23 到 2026-06-24 的 P1 复核后,三任务 eval camera2 都能读回非黑画面,静态资产坐标也已归一到 Franka 工作区;最新正式复核显示 pickplaceopen_door 三张任务图均已通过任务渲染门禁,open_door 也从“黑箱角/大橙色块”修到“门板、框架、细把手在同一张图里可见”。但这只代表任务渲染证据闭环,官方 Lift2 baseline 的复合资产和官方 runner 还没验证,仍不能宣称官方 baseline 可评。

    wiring smoke complete 3/3 episodes finalized 3/3 readback visible score=0.0 expected - open_door visual QA warn + 3/3 task render gate pass + official baseline blocked
    @@ -444,13 +445,13 @@

    当前进展总览

    测试
    -
    56 passed
    +
    64 passed
    1 skipped,validator 通过。
    渲染证据
    -
    3/3 可诊断
    -
    pick 已清楚,place 基本可读;open_door 关闭位正确且门板/单把手可见,但自动验收仍未签收。
    +
    3/3 通过门禁
    +
    pick/place/open_door 的任务渲染均已通过;只剩官方 baseline lane 未闭环。
    官方 Lift2 baseline
    @@ -499,9 +500,9 @@

    本周完成了什么

    渲染图验收 - 部分可读 - 当前三张 eval readback 图为非黑:pick 已清楚,place 基本可读但可再放大,open_door 关闭位已回到 0.0,门板、框架和单个橙色把手已可识别。 - 三张都可作为当前诊断图;正式验收仍要等自动 render_validation、三任务视觉 QA 和 Lift2 baseline gate 通过。 + 任务渲染通过 + 当前三张 eval readback 图为非黑,且三任务最新正式诊断均为 render_validation.passed=truepick 目标清楚,place 关系可读,open_door 关闭位为 0.0,门板、框架和细橙色把手可识别。 + 这证明 Franka POC 任务渲染门禁已过;下一道硬门槛是 Lift2 复合资产和官方 runner gate。 @@ -576,18 +577,18 @@

    资产加载是怎么做的

    已经闭环的部分

    -

    LabUtopia 场景 overlay 能被 Franka POC 加载,相关对象名能通过运行时对象映射进入 evaluator。P1 后,静态 USD 坐标也已经归一到工作区,门把手保留为干燥箱内部子路径。这证明接入路径和静态资产组织已经推进,不证明任务图视觉验收。

    +

    LabUtopia 场景 overlay 能被 Franka POC 加载,相关对象名能通过运行时对象映射进入 evaluator。P1 后,静态 USD 坐标已经归一到工作区,门把手保留为干燥箱内部子路径,三任务任务渲染门禁也已通过。这证明接入路径、静态资产组织和任务读图链路已经闭环;它不证明策略成功或官方 Lift2 baseline 可评。

    还没闭环的部分

    -

    pick/place 的任务级构图已经明显推进,但还没做正式 PASS 签收;open_door 的运行期 articulation 已从爆值推进到稳定关闭位,最新正面图里门板、外框和单个橙色把手已可识别,但自动 render_validation 仍保持阻断。Lift2 官方 baseline 还需要默认资产里的 robot_usds/lift2miscs/curobo,这一步必须排在视觉 gate 之后。

    +

    三任务任务渲染门禁已经通过,但官方 Lift2 baseline 还没闭环。下一步需要把 LabUtopia overlay 和默认 robot_usds/lift2miscs/curobo 组成官方候选资产根目录,再定位并验证官方 runner;在这些证据完成前,不能把当前图等同于官方 baseline 可评。

    EBench 加载后的三任务渲染图

    -

    2026-06-23 到 2026-06-24 P1 复核结论:旧图和新图都放在这里,方便做前后对照。旧图是历史失败样例,用来说明最开始“相机拍不到、资产位置不对、任务目标看不懂”的问题;新图来自正常 EBench/evaluator camera readback,用来说明我们已经把底层链路推进到“能拍到真实场景”。当前结论是 pick 已清楚、place 基本可读、open_door 已从黑箱角和物理爆值推进到关闭位正确,且门板、框架、单个橙色把手可识别;但自动验收仍未通过。

    +

    2026-06-23 到 2026-06-24 P1 复核结论:旧图和新图都放在这里,方便做前后对照。旧图是历史失败样例,用来说明最开始“相机拍不到、资产位置不对、任务目标看不懂”的问题;新图来自正常 EBench/evaluator camera readback,用来说明我们已经把底层链路推进到“能拍到真实场景、能看懂任务目标”。当前结论是三任务任务渲染门禁已通过;官方 Lift2 baseline 仍未验证,所以不能把这三张图直接说成 baseline 成绩证据。

    旧图要表达的问题

    @@ -600,7 +601,7 @@

    我们是怎么解决的

    渲染证据状态

    -

    当前三任务均为 readback_visible。最新任务级隐藏后,level1_pick 只保留抓取瓶子,目标清楚;level1_place 同时保留烧杯和黄色目标托盘,任务关系基本可读;level1_open_door 能完成 readback,关节位置已回到期望关闭位 0.0,正面图里箱体框架、门板和单个橙色把手已可识别;但诊断输出仍显示 render_validation_not_passed,正式视觉 QA 仍未通过。

    +

    当前三任务均为 readback_visiblerender_validation.passed=true。最新任务级隐藏后,level1_pick 只保留抓取瓶子,目标清楚;level1_place 同时保留烧杯和黄色目标托盘,任务关系可读;level1_open_door 关节位置已回到期望关闭位 0.0,正面图里箱体框架、门板和细橙色把手已可识别。诊断 claim boundary 已升级为 task_render_accepted=true,但 official_baseline_evaluable=false

    旧图:历史失败样例

    @@ -636,19 +637,19 @@

    旧图问题怎么解决

    level1_pick 旧图里抓取目标不明显,PM 只看图无法判断“要抓哪个瓶子”。 修正 eval camera readback、相机朝向、光照,把瓶子归一到 Franka 工作区,并在 pick 任务里隐藏烧杯、托盘、干燥箱等非目标物体。 - 当前新图已经能让 PM 看懂“抓这个蓝色瓶子”;还差正式 pixel/视觉 QA 签收,才能把 task_render_accepted 改成 true。 + 当前新图已经能让 PM 看懂“抓这个蓝色瓶子”,并已通过任务渲染门禁;后续进入 Lift2 baseline 资产和 runner 验证。 level1_place 旧图看不出源物体和目标托盘的关系,不像一个“把东西放到哪里”的任务。 修正托盘、烧杯、瓶子坐标和颜色标记,并在 place 任务里隐藏瓶子和干燥箱,只保留烧杯与目标托盘。 - 当前新图能看懂“把烧杯放到黄色托盘附近”;后续可以再拉近相机,让目标关系更大、更稳定。 + 当前新图能看懂“把烧杯放到黄色托盘附近”,并已通过任务渲染门禁;后续可再做展示级相机 polish,但这不是 baseline 阻断项。 level1_open_door 旧图看不清干燥箱门和门把手,无法判断开门动作目标。 把门把手恢复为干燥箱内部子部件,不再作为独立物体飞走;同时生成运行期干燥箱替身资产,固定底座并只保留一个门关节;再补关节初始目标回放、把手位置修正、删除重复橙色块和任务专用正面相机。 - 最新图中关节已回到期望关闭位 0.0,门板、框架和单个橙色把手可见;但自动 render_validation 仍未通过,下一步要把视觉 QA 和 Lift2 baseline gate 接上。 + 最新图中关节已回到期望关闭位 0.0,门板、框架和细橙色把手可见;任务渲染门禁已通过。下一步要接 Lift2 复合资产和官方 runner gate。 @@ -661,24 +662,24 @@

    已经解决了什么

    还没解决什么

    -

    pick/place 已经从“看不懂任务”推进到“PM 可以看懂当前诊断图”;但还没做正式验收签收。open_door 已从爆值和黑箱角推进到关闭位正确、门板/框架/单把手可见,不过自动验收仍未放行,所以仍不能说官方 baseline 可评。

    +

    任务渲染层已经过门禁,但官方 baseline lane 还没过。也就是说,我们已经解决“图能不能加载、能不能看懂任务”的问题;下一步要解决“能不能按官方 Lift2 方式评”的问题。

    新图:当前 EBench eval readback

    -

    这三张是当前更可信的诊断图,因为它们来自 evaluator camera readback。它们证明底层加载和相机读回已经推进,但也暴露了下一步要解决的正式视觉验收问题。

    +

    这三张是当前更可信的诊断图,因为它们来自 evaluator camera readback。它们证明底层加载、任务级构图和相机读回已经闭环到任务渲染门禁;剩余风险在官方 baseline 资产组合和 runner 验证。

    level1_pick eval readback after EBench LabUtopia asset loading -
    level1_pick · 当前可读任务级隐藏后只保留桌面和蓝色瓶子,抓取目标已经清楚;下一步补正式视觉 QA 后才能签收。source: saved/diagnostics/labutopia_p1_visibility_pick_20260623_175050/readback_after_get_eval_camera_data/camera2/00000.png
    +
    level1_pick · 任务渲染通过任务级隐藏后只保留桌面和蓝色瓶子,抓取目标清楚;诊断门禁 render_validation.passed=truesource: saved/diagnostics/labutopia_p1_gate_pick_formal_20260624_0001/readback_after_get_eval_camera_data/camera2/00000.png
    level1_place eval readback after EBench LabUtopia asset loading -
    level1_place · 基本可读烧杯和黄色目标托盘同时可见,放置关系可以解释;后续可把相机再拉近,提升 PM 截图质感。source: saved/diagnostics/labutopia_p1_visibility_place_20260623_175232/readback_after_get_eval_camera_data/camera2/00000.png
    +
    level1_place · 任务渲染通过烧杯和黄色目标托盘同时可见,放置关系可以解释;诊断门禁 render_validation.passed=truesource: saved/diagnostics/labutopia_p1_gate_place_formal_20260624_0001/readback_after_get_eval_camera_data/camera2/00000.png
    level1_open_door eval readback after EBench LabUtopia asset loading -
    level1_open_door · 可诊断 / 未验收最新正式配置诊断中只暴露 RevoluteJoint,关节位置已回到期望关闭位 0.0;门板、框架和单个橙色把手在同一 evaluator readback 图里可见,但诊断仍标记 render_validation_not_passed,不能作为官方 baseline 可评证据。source: saved/diagnostics/labutopia_p1_open_door_single_handle_formal_20260624_0001/readback_after_get_eval_camera_data/camera2/00000.png
    +
    level1_open_door · 任务渲染通过最新正式配置诊断中只暴露 RevoluteJoint,关节位置已回到期望关闭位 0.0;门板、框架和细橙色把手在同一 evaluator readback 图里可见,诊断门禁 render_validation.passed=truesource: saved/diagnostics/labutopia_p1_gate_open_door_formal_20260624_0002/readback_after_get_eval_camera_data/camera2/00000.png
    @@ -717,8 +718,8 @@

    验证证据

    render diagnostics - P1 advanced - 旧诊断:三个任务均为 readback_black_before_recorder。P1 诊断:三个任务均为 readback_visible;最新 open_door 运行期诊断已从爆值推进到关闭位正确、门板/框架/单把手可见,但 render_validation 仍未通过。 + P1 gate passed + 旧诊断:三个任务均为 readback_black_before_recorder。P1 最新正式诊断:三个任务均为 readback_visiblerender_validation.passed=true;open_door 已从爆值和大橙色块修到关闭位正确、门板/框架/细把手可见。 P0a/P0b manifest @@ -727,27 +728,27 @@

    验证证据

    P1 asset/layout manifest - 3/3 visible, 2 readable + 1 warn - render_p1_asset_layout_20260623.json 记录三任务 eval readback 图、静态 USD 坐标、任务级隐藏和 open_door 最新关闭位/单把手正面诊断状态。 + 3/3 task render pass + render_p1_asset_layout_20260623.json 记录三任务 eval readback 图、静态 USD 坐标、任务级隐藏、open_door 细把手修复和 official baseline 未验证边界。 visual QA - READABLE / READABLE / WARN - pick 已清楚,place 基本可读;open_door 可诊断且关闭位正确,门板/框架/单把手可识别,但自动验收仍未签收。 + PASS / PASS / PASS + pick 目标瓶清楚,place 烧杯和目标托盘关系可读;open_door 经细把手 retake 后独立图像审阅为 PASS。这个 PASS 只覆盖任务渲染图,不等于策略得分或官方 baseline 成绩。 report display QA passed - Chromium headless 桌面/移动端长截图已检查;6 张图片文件均存在,DOM 包含旧图、新图、单把手 open_door 和 render_validation_not_passed 说明。这个检查只证明页面展示没坏,不等于任务视觉验收通过。 + Playwright/Chromium 桌面、平板、移动端长截图已通过;6 张报告图片全部加载,旧图/新图/任务渲染通过/baseline 边界文案都存在,无请求失败或横向溢出。证据在 /tmp/labutopia_weekly_p1_gate_review_20260624/。 pytest - 56 passed, 1 skipped + 64 passed, 1 skipped 覆盖资产 override、fallback metadata、结果落盘、后处理 fail-fast 和 render diagnostic 合同等回归点。 diagnostic contract - 2 passed + 13 passed 纯 Python 帧统计和黑帧分类接口不依赖 Isaac,可快速回归。 @@ -810,9 +811,9 @@

    问题和处理结果

    当前新图仍未全部验收 - pick 已清楚,place 基本可读但还可放大;open_door 已能读回且关闭位正确,门板/框架/单把手可识别,但自动 render_validation 仍未放行。 - pick/place 补正式视觉 QA;open_door 把当前正面图纳入同一验收门槛,并继续接 Lift2 baseline 所需的复合资产和官方 runner gate。 - 部分通过诊断 + pick 已清楚,place 关系可读;open_door 已能读回且关闭位正确,门板/框架/细把手可识别。 + 已用正式任务渲染门禁签收三任务;继续接 Lift2 baseline 所需的复合资产和官方 runner gate。 + 任务渲染通过 资产导入/layout 静态坐标 @@ -857,6 +858,7 @@

    现在可以说

  • P1 后,三任务 eval camera2 都已从纯黑变为非黑 readback。
  • 静态 USD 坐标和门把手层级已经明显推进,handle 不再作为独立飞走物体。
  • 任务级隐藏后,pickplace 的当前诊断图已经能让 PM 看懂目标和关系。
  • +
  • P1 最新正式诊断中三任务 render_validation.passed=true,可以说任务渲染门禁已通过。
  • @@ -864,8 +866,8 @@

    现在不能说

    • 不能说任务已经求解成功,当前默认动作得分是 0.0
    • 不能说官方 Lift2 baseline 已经可评,Lift2 复合资产和官方 runner 还没闭环。
    • -
    • 不能说三任务画面/视频/任务布局已经全部验收;open_door 仍未通过。
    • -
    • 不能说 open_door 已经视觉可评;当前图可以做 PM 诊断说明,但诊断仍标记 render_validation_not_passed
    • +
    • 不能把当前任务渲染 PASS 等同于策略成功率、官方分数或官方 baseline 成绩。
    • +
    • 不能说已经跑过 EBench 官方 Lift2 baseline;当前只闭环了 GenManip/EBench 本地任务渲染门禁。
    @@ -887,17 +889,17 @@

    静态资产/layout 正规化

    P1b

    open_door 视角和门缝增强

    -

    物理爆值和关闭位问题已通过 DryingBox surrogate 与关节目标回放推进到可诊断;最新正面图已经让门板、框架和单个橙色把手可识别。下一步把这张图纳入正式视觉 QA,并继续让自动 render_validation 成为是否可评的硬门槛。

    +

    物理爆值和关闭位问题已通过 DryingBox surrogate 与关节目标回放解决;把手从“大橙色块”缩成细把手后,最新正面图已经让门板、框架和动作点可识别,且 render_validation.passed=true

    P1c

    任务级相机/构图

    -

    pick/place 先补正式视觉 QA;open_door 使用最新单把手正式相机图继续验收,要求门板、门面、把手和任务动作点在 eval 图中清楚可见。

    +

    pick/place/open_door 已完成任务级相机与构图复验,三任务 render_validation.passed=true;后续构图调整只作为展示 polish,不作为 P1 阻断项。

    P2

    Eval-path 重拍验收

    -

    用正常 evaluator/readback 路径重新抓关键帧,写 evidence manifest,并要求三任务独立视觉 QA 全部 PASS 后再签收。

    +

    已用正常 evaluator/readback 路径重新抓关键帧,写入 evidence manifest;三任务任务渲染门禁已通过。后续只做展示级 polish 或复验,不再作为 P1 阻断项。

    P3 @@ -951,7 +953,7 @@

    相关文档

    Render visual investigation - 6/23 多角度复核:旧图问题、P0 黑屏修复、P1 静态资产归一、任务级隐藏后的新 eval readback 图和 open_door 未验收边界。 + 6/23-6/24 多角度复核:旧图问题、P0 黑屏修复、P1 静态资产归一、任务级隐藏后的新 eval readback 图、open_door 细把手修复和 baseline 边界。 打开 @@ -966,7 +968,7 @@

    相关文档

    P1 asset/layout manifest - 三任务 eval readback 图、静态 USD 坐标、任务级隐藏、open_door 关闭位、单把手正面诊断和 render_validation_not_passed 边界。 + 三任务 eval readback 图、静态 USD 坐标、任务级隐藏、open_door 关闭位、细把手正面诊断和 official baseline 未验证边界。 打开 diff --git a/docs/superpowers/plans/2026-06-23-labutopia-ebench-render-layout-closure.md b/docs/superpowers/plans/2026-06-23-labutopia-ebench-render-layout-closure.md index 69e55c0..656c321 100644 --- a/docs/superpowers/plans/2026-06-23-labutopia-ebench-render-layout-closure.md +++ b/docs/superpowers/plans/2026-06-23-labutopia-ebench-render-layout-closure.md @@ -8,7 +8,7 @@ **Tech Stack:** Python 3.10, Isaac Sim 4.1 conda env, GenManip evaluator, EBench server/client, Pillow-based image stats, pytest, static GitHub Pages docs. -**Current status update, 2026-06-24 00:30 UTC:** P0 black-frame readback is resolved for all three Franka POC tasks. P1 static asset/layout normalization is partially resolved: required objects are in the Franka workspace and the DryingBox handle is a nested part again. Task-level visibility isolation now makes `level1_pick` readable and `level1_place` basically readable for PM diagnosis. The `open_door` runtime articulation has been stabilized with a sanitized DryingBox surrogate, aligned hinge, target replay, handle-side correction, duplicate marker removal, and a formal front-camera config; the latest formal retake starts at the expected closed joint target and shows the DryingBox frame, door panel, and one orange handle/action point. Independent image-only QA rates the old-vs-current PM comparison PASS, with open_door PASS/WARN as diagnostic evidence. Formal task render acceptance remains false because diagnostics still report `render_validation_not_passed`. +**Current status update, 2026-06-24 UTC:** P0 black-frame readback is resolved for all three Franka POC tasks. P1 static asset/layout normalization is closed for the Franka POC render gate: required objects are in the Franka workspace, the DryingBox handle remains a nested part, task-level visibility isolation is active, and task-specific cameras are in place. The `open_door` runtime articulation has been stabilized with a sanitized DryingBox surrogate, aligned hinge, target replay, handle-side correction, duplicate marker removal, a slimmer handle scale, and a formal front-camera config. The latest formal diagnostics for `level1_pick`, `level1_place`, and `level1_open_door` all report `render_validation.passed=true`; the claim boundary is now `task_render_accepted=true` and `official_baseline_evaluable=false`. The next lane is official Lift2 baseline readiness: composite assets plus official runner discovery/validation. --- @@ -59,7 +59,7 @@ Current status as of 2026-06-23: ```text Diagnostic helper and runtime capture script exist. -tests/labutopia_poc/test_render_diagnostics_contract.py: 2 passed +tests/labutopia_poc/test_render_diagnostics_contract.py: 13 passed after P1 gate updates LabUtopia POC regression tests: 34 passed, 1 skipped level1_pick: readback_black_before_recorder level1_place: readback_black_before_recorder @@ -69,12 +69,12 @@ level1_pick: readback_visible, low-texture frame, not task accepted level1_place: readback_visible, low-texture frame, not task accepted level1_open_door: not revalidated; remains blocked by asset/layout work after P1 asset/layout normalization: -level1_pick: readback_visible, target visible after task-level hiding, pending formal QA -level1_place: readback_visible, beaker/target relation basically readable after task-level hiding, pending formal QA -level1_open_door: readback_visible, runtime stable after sanitized surrogate and target replay, visual QA WARN/not accepted +level1_pick: readback_visible, render_validation.passed=true, task_render_accepted=true +level1_place: readback_visible, render_validation.passed=true, task_render_accepted=true +level1_open_door: readback_visible, runtime stable after sanitized surrogate/target replay/thin-handle retake, render_validation.passed=true, task_render_accepted=true ``` -Recorder writing is now ruled out as the primary black-frame source. P0a/P0b source fixes remove the pure-black readback failure. Task-level visibility isolation improves pick/place screenshots. The open-door physics blocker has been reduced to a visual framing blocker; task render acceptance remains blocked until formal visual QA passes. +Recorder writing is now ruled out as the primary black-frame source. P0a/P0b source fixes remove the pure-black readback failure. Task-level visibility isolation improves pick/place screenshots. The open-door physics and visual-handle blockers are closed for the Franka POC task-render gate. Official Lift2 baseline evaluability remains blocked until composite assets and the official runner are validated. **Files:** - Create: `standalone_tools/labutopia_poc/capture_eval_render_diagnostics.py` @@ -204,7 +204,7 @@ python -m pytest tests/labutopia_poc/test_render_diagnostics_contract.py -q Expected: ```text -2 passed +13 passed after the P1 render-gate contract expansion ``` - [x] **Step 5: Add Isaac runtime capture mode** @@ -341,11 +341,11 @@ python standalone_tools/labutopia_poc/validate_task_package.py -> OK 3. Asset/layout normalization: - Static layer is partially done: required objects are in the robot workspace and the nested handle preserves the DryingBox parent transform. - Task-level visibility isolation is done for POC diagnosis: pick hides beaker/target/DryingBox, place hides bottle/DryingBox, open_door hides bottle/beaker/target. - - Runtime physics is now diagnosable for the POC `open_door` diagnostic after the sanitized DryingBox surrogate, aligned hinge, and target replay; the closed-start joint now matches the expected `0.0`. - - Remaining acceptance gap: `open_door` now shows the DryingBox frame, door panel, and one orange handle/action point, but must not be upgraded beyond diagnostic evidence until `render_validation` and formal visual QA pass. + - Runtime physics is stable for the POC `open_door` diagnostic after the sanitized DryingBox surrogate, aligned hinge, and target replay; the closed-start joint now matches the expected `0.0`. + - The intermediate oversized orange handle/panel issue was fixed by reducing the nested handle scale to `[0.045, 0.075, 0.25]` and rerunning the formal front-camera gate. 4. Eval-path regeneration: - Three P1 diagnostics now produce non-black evaluator readback frames. - - Current P1 visibility images make pick readable and place basically readable, while the latest open_door single-handle front-camera image is PM-usable diagnostic evidence but still not accepted for baseline claims. + - Current P1 formal images pass the local task-render gate for pick, place, and open_door. They are not official Lift2 baseline evidence. ## P1: Reset-Time Task Layout Closure @@ -355,19 +355,19 @@ Current P1 status: static_usd_ok: true camera_readback_visible: true for level1_pick/place/open_door task_visibility_isolated: true for level1_pick/place/open_door -pick_place_pm_readable: true, pending formal visual QA +pick_place_pm_readable: true runtime_physics_stable: true for latest open_door diagnostic, joint_positions = [0.0], expected_joint_positions = [0.0] -open_door_visual_qa: PASS/WARN diagnostic/not accepted, DryingBox frame + door panel + single handle visible, render_validation_not_passed -task_render_accepted: false +open_door_visual_qa: PASS after thin-handle retake, DryingBox frame + door panel + thin handle visible, render_validation.passed=true +task_render_accepted: true official_baseline_evaluable: false ``` Evidence: ```text -saved/diagnostics/labutopia_p1_visibility_pick_20260623_175050/diagnostics.json -saved/diagnostics/labutopia_p1_visibility_place_20260623_175232/diagnostics.json -saved/diagnostics/labutopia_p1_open_door_single_handle_formal_20260624_0001/diagnostics.json +saved/diagnostics/labutopia_p1_gate_pick_formal_20260624_0001/diagnostics.json +saved/diagnostics/labutopia_p1_gate_place_formal_20260624_0001/diagnostics.json +saved/diagnostics/labutopia_p1_gate_open_door_formal_20260624_0002/diagnostics.json docs/labutopia_lab_poc/evidence_manifests/render_p1_asset_layout_20260623.json ``` @@ -376,8 +376,8 @@ Immediate next order: 1. Keep static validation so malformed DryingBox USD/PhysX topology fails before runtime. 2. Keep the sanitized DryingBox asset and runtime sanity gate in place; do not regress to source DryingBox physics. 3. Keep the runtime sanity gate for finite articulation joint positions and finite object transforms. -4. Run formal visual QA for pick/place; keep `task_render_accepted=false` until signed off. -5. Keep the formal single-handle front-camera open_door image as PM-facing diagnostic evidence only; rerun browser display QA and formal task visual QA, and keep `task_render_accepted=false` until `render_validation` passes. +4. Browser display QA is refreshed for the updated weekly report and evidence manifest. +5. Move to official Lift2 baseline lane: build the composite asset root, discover/hash official runner entrypoints, and run isolated dry smoke without claiming official baseline execution. **Files:** - Modify: `configs/tasks/ebench/labutopia_lab_poc/franka_poc/level1_pick.yml` @@ -493,21 +493,23 @@ Implemented `set_object_active` preprocessing so the runtime can hide non-task o ```text python -m pytest tests/labutopia_poc/test_scene_preprocess_rules.py -q -2 passed +13 passed after the P1 render-gate contract expansion python -m pytest tests/labutopia_poc/test_validate_task_package.py::test_franka_tasks_hide_non_task_objects_for_evidence_readability -q 1 passed python standalone_tools/labutopia_poc/validate_task_package.py LabUtopia task package validation OK ``` -Latest diagnostic outcome: +Superseded diagnostic outcome at this Step 4a stage: ```text level1_pick: readback_visible, PM-readable target bottle level1_place: readback_visible, beaker and target platform visible together -level1_open_door: readback_visible, runtime_stable, visual_QA_WARN_NOT_ACCEPTED +level1_open_door: readback_visible, runtime_stable, visual QA warning before the final thin-handle retake ``` +This was later superseded by `labutopia_p1_gate_open_door_formal_20260624_0002`, where `render_validation.passed=true` and `task_render_accepted=true`. + - [ ] **Step 4b: Add DryingBox articulation topology validation** Extend static validation to fail on: @@ -559,7 +561,7 @@ Expected: ```text camera frames are not black required objects are visible in at least one eval-path frame per task -diagnostics.json includes frame stats and claim_boundary.task_render_accepted=false until visual review passes +diagnostics.json includes frame stats and claim_boundary.task_render_accepted=true after render_validation passes ``` - [ ] **Step 2: Run visual QA review** @@ -613,7 +615,7 @@ Create `docs/labutopia_lab_poc/evidence_manifests/render_layout_closure_YYYYMMDD Reject the manifest if any image has `direct_render=true`, missing `source_frame`, missing `sha256`, or `visual_qa` not equal to `PASS`. -- [ ] **Step 4: Replace report images only after visual QA passes** +- [x] **Step 4: Replace report images only after visual QA passes** Keep old JPGs in the report as historical failed samples. Replace or add current diagnostic PNGs only with clear labels: @@ -625,12 +627,12 @@ docs/records/evidence/2026-06-22-labutopia-ebench-weekly-report/assets/labutopia Do not replace them with direct-render images unless the report clearly labels them as non-eval-path diagnostic images. -- [ ] **Step 5: Update PM report wording** +- [x] **Step 5: Update PM report wording** -Allowed wording after the partial P1 visibility update: +Historical wording before the full gate passed, retained only to document the earlier PM boundary: ```text -旧 JPG 是历史失败样例;新 PNG 来自 evaluator camera readback。当前 pick 已清楚、place 基本可读,open_door 已从物理爆值和黑箱角推进到关闭位正确、门板/框架/单个橙色把手可识别。该图可用于 PM 诊断汇报,但 diagnostics 仍是 render_validation_not_passed,不能作为 baseline 可评证据。 +旧 JPG 是历史失败样例;新 PNG 来自 evaluator camera readback。当前 pick 已清楚、place 基本可读,open_door 已从物理爆值和黑箱角推进到关闭位正确、门板/框架/单个橙色把手可识别。该图可用于 PM 诊断汇报,但当时仍有 render gate blocker,不能作为 baseline 可评证据。 ``` Allowed wording after the full gate passes: @@ -652,7 +654,7 @@ from pathlib import Path html = Path('docs/records/evidence/2026-06-22-labutopia-ebench-weekly-report/index.html').read_text(encoding='utf-8') for text in [ '旧图:历史失败样例', - 'level1_open_door · 可诊断 / 未验收', + 'level1_open_door · 任务渲染通过', 'render_visual_investigation_20260623.md', '2026-06-23-labutopia-ebench-render-layout-closure.md', ]: diff --git a/standalone_tools/labutopia_poc/build_asset_overlay.py b/standalone_tools/labutopia_poc/build_asset_overlay.py index 7ed2c93..eeefe7b 100644 --- a/standalone_tools/labutopia_poc/build_asset_overlay.py +++ b/standalone_tools/labutopia_poc/build_asset_overlay.py @@ -574,7 +574,7 @@ def _drying_box_surrogate_def(runtime_key: str) -> str: f"{_visual_cube_def('door_top_seam', translate=[0, -0.168, 0.225], scale=[0.52, 0.012, 0.012], color=seam_color, material_path=f'{root_path}/Looks/door_seam_mat')}\n" f"{_visual_cube_def('door_bottom_seam', translate=[0, -0.168, -0.205], scale=[0.52, 0.012, 0.012], color=seam_color, material_path=f'{root_path}/Looks/door_seam_mat')}\n" f"{_visual_cube_def('handle_mount_backplate', translate=[0.18, -0.174, 0.05], scale=[0.075, 0.014, 0.28], color=handle_mount_color, material_path=f'{root_path}/Looks/handle_mount_mat')}\n" - f"{_rigid_cube_def('handle', translate=[0.18, -0.22, 0.05], scale=[0.08, 0.06, 0.24], color=handle_color, mass=0.1, diagonal_inertia=[0.002, 0.002, 0.002], material_path=f'{root_path}/Looks/handle_mat')}\n" + f"{_rigid_cube_def('handle', translate=[0.18, -0.22, 0.05], scale=[0.045, 0.075, 0.25], color=handle_color, mass=0.1, diagonal_inertia=[0.002, 0.002, 0.002], material_path=f'{root_path}/Looks/handle_mat')}\n" f' def PhysicsFixedJoint "BaseFixedJoint"\n' f" {{\n" f" point3f physics:localPos0 = (0, 0, 0)\n" diff --git a/standalone_tools/labutopia_poc/capture_eval_render_diagnostics.py b/standalone_tools/labutopia_poc/capture_eval_render_diagnostics.py index d564dd3..1f67cfa 100644 --- a/standalone_tools/labutopia_poc/capture_eval_render_diagnostics.py +++ b/standalone_tools/labutopia_poc/capture_eval_render_diagnostics.py @@ -182,8 +182,10 @@ def build_claim_boundary( runtime_physics_stable: bool, diagnostic_completed: bool = True, diagnostic_error: dict[str, Any] | None = None, + official_baseline_validated: bool = False, ) -> dict[str, Any]: blockers: list[str] = [] + baseline_blockers: list[str] = [] if not diagnostic_completed: blockers.append("runtime_diagnostic_not_completed") if diagnostic_error is not None: @@ -196,11 +198,18 @@ def build_claim_boundary( if not runtime_physics_stable: blockers.append("runtime_physics_unstable") task_render_accepted = readback_visible and render_validation_passed - official_baseline_evaluable = task_render_accepted and runtime_physics_stable + if not official_baseline_validated: + baseline_blockers.append("official_baseline_not_validated") + official_baseline_evaluable = ( + task_render_accepted + and runtime_physics_stable + and official_baseline_validated + ) return { "task_render_accepted": task_render_accepted, "official_baseline_evaluable": official_baseline_evaluable, "blockers": blockers, + "baseline_blockers": baseline_blockers, } @@ -276,6 +285,256 @@ def frame_stats_from_png( ) +def _resolve_frame_path(frame_path: str | Path) -> Path: + path = Path(frame_path) + if path.exists() or path.is_absolute(): + return path + return REPO_ROOT / path + + +def _load_rgb_png(frame_path: str | Path) -> Any: + import cv2 + + resolved = _resolve_frame_path(frame_path) + image_bgr = cv2.imread(str(resolved), cv2.IMREAD_COLOR) + if image_bgr is None: + raise FileNotFoundError(resolved) + return cv2.cvtColor(image_bgr, cv2.COLOR_BGR2RGB) + + +def _primary_readback_frame( + camera_frames: list[dict[str, object]], + primary_camera: str, +) -> dict[str, object] | None: + matches = [ + frame + for frame in camera_frames + if frame.get("camera_name") == primary_camera + and frame.get("stage") == "readback_after_get_eval_camera_data" + ] + if matches: + return matches[0] + for frame in camera_frames: + if frame.get("camera_name") == primary_camera: + return frame + return None + + +def _rgb_channels(rgb: Any) -> tuple[Any, Any, Any]: + import numpy as np + + arr = _normalize_rgb_array(rgb).astype(np.int16) + return arr[:, :, 0], arr[:, :, 1], arr[:, :, 2] + + +def _object_visibility_mask(rgb: Any, uid: str) -> Any: + import numpy as np + + r, g, b = _rgb_channels(rgb) + chroma = np.maximum.reduce([r, g, b]) - np.minimum.reduce([r, g, b]) + if uid == "obj_conical_bottle02": + return ( + (r < 170) + & (g > 100) + & (b > 100) + & ((b - r) > 10) + & ((g - r) > 5) + & (chroma > 10) + ) + if uid == "obj_beaker2": + return ( + (r < 145) + & (g > 105) + & (b > 75) + & ((g - r) > 25) + & (chroma > 20) + ) + if uid == "obj_target_plat": + return ( + (r > 145) + & (g > 125) + & (b < 170) + & ((r - b) > 35) + & ((g - b) > 25) + ) + if uid == "obj_DryingBox_01_handle": + return ( + (r > 140) + & (g >= 40) + & (g < 175) + & (b < 115) + & ((r - b) > 70) + & ((r - g) > 35) + ) + if uid == "obj_DryingBox_01": + dark_frame = (r < 95) & (g < 100) & (b < 110) + blue_gray_panel = ( + (r > 70) + & (r < 185) + & (g > 75) + & (g < 195) + & (b > 80) + & (b < 210) + & ((b - r) >= 0) + & (chroma < 90) + & (chroma > 8) + ) + handle = _object_visibility_mask(rgb, "obj_DryingBox_01_handle") + return dark_frame | blue_gray_panel | handle + return np.zeros(_normalize_rgb_array(rgb).shape[:2], dtype=bool) + + +def _largest_component_metrics(mask: Any, *, image_width: int, image_height: int) -> dict[str, Any]: + import cv2 + import numpy as np + + mask_uint8 = np.asarray(mask, dtype=np.uint8) + component_count, _labels, stats, _centroids = cv2.connectedComponentsWithStats( + mask_uint8, + 8, + ) + if component_count <= 1: + return { + "present": False, + "bbox": None, + "width_px": 0, + "height_px": 0, + "mask_area_px": 0, + "mask_area_fraction": 0.0, + "bbox_area_fraction": 0.0, + "severe_clipping": False, + } + largest_index = max( + range(1, component_count), + key=lambda index: int(stats[index, cv2.CC_STAT_AREA]), + ) + x = int(stats[largest_index, cv2.CC_STAT_LEFT]) + y = int(stats[largest_index, cv2.CC_STAT_TOP]) + width = int(stats[largest_index, cv2.CC_STAT_WIDTH]) + height = int(stats[largest_index, cv2.CC_STAT_HEIGHT]) + area = int(stats[largest_index, cv2.CC_STAT_AREA]) + frame_area = float(image_width * image_height) + clipped = ( + x <= 1 + or y <= 1 + or (x + width) >= image_width - 1 + or (y + height) >= image_height - 1 + ) + return { + "present": area > 0, + "bbox": [x, y, x + width - 1, y + height - 1], + "width_px": width, + "height_px": height, + "mask_area_px": area, + "mask_area_fraction": area / frame_area, + "bbox_area_fraction": (width * height) / frame_area, + "severe_clipping": clipped, + } + + +def _passes_object_thresholds( + metrics: dict[str, Any], + thresholds: dict[str, Any], +) -> list[str]: + failures: list[str] = [] + if not metrics["present"]: + return ["required_object_missing"] + for key in ("min_width_px", "min_height_px"): + expected = thresholds.get(key) + if expected is not None: + metric_key = key.replace("min_", "") + if float(metrics[metric_key]) < float(expected): + failures.append(key) + min_bbox_area_fraction = thresholds.get("min_bbox_area_fraction") + if min_bbox_area_fraction is not None and float(metrics["bbox_area_fraction"]) < float( + min_bbox_area_fraction + ): + failures.append("min_bbox_area_fraction") + if metrics["severe_clipping"]: + failures.append("severe_clipping") + return failures + + +def evaluate_render_validation( + eval_config: dict[str, Any], + camera_frames: list[dict[str, object]], +) -> dict[str, Any]: + raw_validation = eval_config.get("labutopia_render_validation") or {} + validation = raw_validation if isinstance(raw_validation, dict) else {} + primary_camera = str(validation.get("primary_camera", "camera2")) + report: dict[str, Any] = { + "passed": False, + "schema_version": validation.get("schema_version"), + "primary_camera": primary_camera, + "frame_path": None, + "reject_rule_results": {}, + "required_objects": {}, + "failures": [], + } + if not isinstance(raw_validation, dict): + report["failures"].append("missing_render_validation_config") + return report + if validation.get("evidence_policy") != {"direct_render": False}: + report["failures"].append("direct_render_evidence_policy_not_disabled") + frame = _primary_readback_frame(camera_frames, primary_camera) + if frame is None: + report["failures"].append("primary_camera_readback_missing") + return report + report["frame_path"] = frame.get("frame_path") + if classify_frame_stats(frame) != "visible_frame": + report["reject_rule_results"]["black_frame"] = True + report["failures"].append("black_frame") + return report + report["reject_rule_results"]["black_frame"] = False + channel_min = frame.get("channel_min") + channel_max = frame.get("channel_max") + low_texture = False + if isinstance(channel_min, list) and isinstance(channel_max, list): + channel_range = max( + float(high) - float(low) + for low, high in zip(channel_min, channel_max) + ) + low_texture = channel_range < 30.0 + report["reject_rule_results"]["low_texture"] = low_texture + if low_texture: + report["failures"].append("low_texture") + + try: + rgb = _load_rgb_png(str(frame["frame_path"])) + except Exception as exc: + report["failures"].append("primary_camera_frame_load_failed") + report["frame_load_error"] = repr(exc) + return report + height, width = rgb.shape[:2] + thresholds = validation.get("object_pixel_thresholds") or {} + for uid in validation.get("required_visible_objects") or []: + uid = str(uid) + object_thresholds = thresholds.get(uid, {}) + metrics = _largest_component_metrics( + _object_visibility_mask(rgb, uid), + image_width=width, + image_height=height, + ) + failed_thresholds = _passes_object_thresholds(metrics, object_thresholds) + object_report = { + **metrics, + "thresholds": object_thresholds, + "failed_thresholds": failed_thresholds, + "passed": not failed_thresholds, + } + report["required_objects"][uid] = object_report + if "required_object_missing" in failed_thresholds: + report["reject_rule_results"]["required_object_missing"] = True + if "severe_clipping" in failed_thresholds: + report["reject_rule_results"]["severe_clipping"] = True + for failure in failed_thresholds: + report["failures"].append(f"{uid}:{failure}") + report["reject_rule_results"].setdefault("required_object_missing", False) + report["reject_rule_results"].setdefault("severe_clipping", False) + report["passed"] = not report["failures"] + return report + + def _save_rgb_png(rgb: Any, frame_path: Path) -> None: from genmanip.utils.standalone.frame_utils import save_image @@ -578,6 +837,10 @@ def run_runtime_diagnostics(args: argparse.Namespace) -> dict[str, Any]: required_articulations=required_articulations, expected_joint_positions=expected_joint_positions, ), + "render_validation": { + "passed": False, + "failures": ["runtime_diagnostic_not_completed"], + }, "boundary_classification": None, "diagnostic_error": None, "claim_boundary": build_claim_boundary( @@ -666,9 +929,13 @@ def capture_get_eval_camera_data(camera_list: dict[str, Any]) -> dict[str, Any]: required_articulations=required_articulations, expected_joint_positions=expected_joint_positions, ) + diagnostics["render_validation"] = evaluate_render_validation( + eval_config, + diagnostics["camera_frames"], + ) diagnostics["claim_boundary"] = build_claim_boundary( boundary_classification=diagnostics["boundary_classification"], - render_validation_passed=False, + render_validation_passed=bool(diagnostics["render_validation"]["passed"]), runtime_physics_stable=bool( diagnostics["runtime_sanity"]["runtime_physics_stable"] ), @@ -689,6 +956,10 @@ def capture_get_eval_camera_data(camera_list: dict[str, Any]) -> dict[str, Any]: required_articulations=required_articulations, expected_joint_positions=expected_joint_positions, ) + diagnostics["render_validation"] = { + "passed": False, + "failures": ["runtime_diagnostic_exception"], + } diagnostics["claim_boundary"] = build_claim_boundary( boundary_classification=diagnostics["boundary_classification"], render_validation_passed=False, diff --git a/standalone_tools/labutopia_poc/validate_task_package.py b/standalone_tools/labutopia_poc/validate_task_package.py index b172791..f9e8098 100644 --- a/standalone_tools/labutopia_poc/validate_task_package.py +++ b/standalone_tools/labutopia_poc/validate_task_package.py @@ -70,17 +70,21 @@ "position": [0.28, -0.55, 1.2], "orientation": [0.87184, 0.4898, 0.0, 0.0], "resolution": [512, 512], + "focal_length": 5.6, + "horizontal_aperture": 10.0, }, "level1_place": { "position": [0.26, -0.7, 1.32], "orientation": [0.87184, 0.4898, 0.0, 0.0], "resolution": [512, 512], + "focal_length": 10.0, + "horizontal_aperture": 10.0, }, "level1_open_door": { "position": [0.75, -0.56, 1.02], "orientation": [0.87184, 0.4898, 0.0, 0.0], "resolution": [512, 512], - "focal_length": 5.0, + "focal_length": 5.4, "horizontal_aperture": 10.0, }, } @@ -89,31 +93,31 @@ "obj_conical_bottle02": { "min_width_px": 36, "min_height_px": 48, - "min_area_fraction": 0.01, + "min_bbox_area_fraction": 0.01, }, }, "level1_place": { "obj_beaker2": { "min_width_px": 34, "min_height_px": 34, - "min_area_fraction": 0.008, + "min_bbox_area_fraction": 0.008, }, "obj_target_plat": { "min_width_px": 42, "min_height_px": 24, - "min_area_fraction": 0.006, + "min_bbox_area_fraction": 0.006, }, }, "level1_open_door": { "obj_DryingBox_01": { "min_width_px": 160, "min_height_px": 150, - "min_area_fraction": 0.12, + "min_bbox_area_fraction": 0.12, }, "obj_DryingBox_01_handle": { "min_width_px": 18, "min_height_px": 64, - "min_area_fraction": 0.004, + "min_bbox_area_fraction": 0.004, }, }, } @@ -167,6 +171,7 @@ "double3 xformOp:translate = (0.18, -0.174, 0.05)", "double3 xformOp:translate = (0.18, -0.22, 0.05)", "double3 xformOp:scale = (0.075, 0.014, 0.28)", + "double3 xformOp:scale = (0.045, 0.075, 0.25)", "point3f physics:localPos0 = (0.18, -0.10, 0.04)", ] EXPECTED_FRANKA_CAMERA_AXES = { diff --git a/tests/labutopia_poc/test_build_asset_overlay.py b/tests/labutopia_poc/test_build_asset_overlay.py index 9df422d..46061a8 100644 --- a/tests/labutopia_poc/test_build_asset_overlay.py +++ b/tests/labutopia_poc/test_build_asset_overlay.py @@ -76,6 +76,8 @@ def test_build_asset_overlay_writes_scene_wrapper_manifest_and_cleans_reruns( assert "double3 xformOp:translate = (0.18, -0.174, 0.05)" in scene_usda assert "double3 xformOp:scale = (0.075, 0.014, 0.28)" in scene_usda assert "double3 xformOp:translate = (0.18, -0.22, 0.05)" in scene_usda + assert "double3 xformOp:scale = (0.045, 0.075, 0.25)" in scene_usda + assert "double3 xformOp:scale = (0.08, 0.06, 0.24)" not in scene_usda assert 'def Cube "handle_visual_marker"' not in scene_usda assert "double3 xformOp:translate = (0.18, -0.165, 0.05)" not in scene_usda assert "double3 xformOp:scale = (0.035, 0.06, 0.18)" not in scene_usda diff --git a/tests/labutopia_poc/test_render_diagnostics_contract.py b/tests/labutopia_poc/test_render_diagnostics_contract.py index 19eb36d..391b631 100644 --- a/tests/labutopia_poc/test_render_diagnostics_contract.py +++ b/tests/labutopia_poc/test_render_diagnostics_contract.py @@ -4,9 +4,41 @@ build_claim_boundary, classify_articulation_runtime_state, classify_frame_stats, + evaluate_render_validation, + frame_stats_from_png, ) +def _write_test_png(path, rectangles): + import cv2 + import numpy as np + + image = np.full((512, 512, 3), [170, 170, 170], dtype=np.uint8) + for x1, y1, x2, y2, color in rectangles: + image[y1:y2, x1:x2] = color + cv2.imwrite(str(path), cv2.cvtColor(image, cv2.COLOR_RGB2BGR)) + + +def _render_validation_config(task_name, required_objects, thresholds): + return { + "task_name": f"ebench/labutopia_lab_poc/franka_poc/{task_name}", + "labutopia_render_validation": { + "schema_version": 1, + "primary_camera": "camera2", + "required_camera_names": ["camera2"], + "required_visible_objects": required_objects, + "object_pixel_thresholds": thresholds, + "reject_frame_if": [ + "black_frame", + "low_texture", + "required_object_missing", + "severe_clipping", + ], + "evidence_policy": {"direct_render": False}, + }, + } + + def test_classify_black_frame_as_failed(): stats = build_camera_frame_stats( camera_name="camera2", @@ -37,6 +69,102 @@ def test_classify_visible_frame_as_pass(): assert classify_frame_stats(stats) == "visible_frame" +def test_render_validation_accepts_required_object_pixel_thresholds(tmp_path): + frame_path = tmp_path / "place.png" + _write_test_png( + frame_path, + [ + (210, 150, 258, 195, [26, 184, 138]), + (260, 270, 314, 301, [242, 199, 31]), + ], + ) + stats = frame_stats_from_png(camera_name="camera2", frame_path=frame_path) + stats["stage"] = "readback_after_get_eval_camera_data" + config = _render_validation_config( + "level1_place", + ["obj_beaker2", "obj_target_plat"], + { + "obj_beaker2": { + "min_width_px": 34, + "min_height_px": 34, + "min_bbox_area_fraction": 0.008, + }, + "obj_target_plat": { + "min_width_px": 42, + "min_height_px": 24, + "min_bbox_area_fraction": 0.006, + }, + }, + ) + + report = evaluate_render_validation(config, [stats]) + + assert report["passed"] is True + assert report["required_objects"]["obj_beaker2"]["passed"] is True + assert report["required_objects"]["obj_target_plat"]["passed"] is True + + +def test_render_validation_rejects_required_object_below_threshold(tmp_path): + frame_path = tmp_path / "pick_too_small.png" + _write_test_png(frame_path, [(240, 230, 260, 290, [26, 122, 242])]) + stats = frame_stats_from_png(camera_name="camera2", frame_path=frame_path) + stats["stage"] = "readback_after_get_eval_camera_data" + config = _render_validation_config( + "level1_pick", + ["obj_conical_bottle02"], + { + "obj_conical_bottle02": { + "min_width_px": 36, + "min_height_px": 48, + "min_bbox_area_fraction": 0.01, + } + }, + ) + + report = evaluate_render_validation(config, [stats]) + + assert report["passed"] is False + bottle = report["required_objects"]["obj_conical_bottle02"] + assert bottle["passed"] is False + assert "min_width_px" in bottle["failed_thresholds"] + + +def test_render_validation_accepts_open_door_nested_handle_visuals(tmp_path): + frame_path = tmp_path / "open_door.png" + _write_test_png( + frame_path, + [ + (120, 120, 360, 290, [45, 54, 62]), + (180, 135, 330, 280, [112, 135, 162]), + (245, 165, 270, 245, [255, 46, 10]), + ], + ) + stats = frame_stats_from_png(camera_name="camera2", frame_path=frame_path) + stats["stage"] = "readback_after_get_eval_camera_data" + config = _render_validation_config( + "level1_open_door", + ["obj_DryingBox_01", "obj_DryingBox_01_handle"], + { + "obj_DryingBox_01": { + "min_width_px": 160, + "min_height_px": 150, + "min_bbox_area_fraction": 0.12, + }, + "obj_DryingBox_01_handle": { + "min_width_px": 18, + "min_height_px": 64, + "min_bbox_area_fraction": 0.004, + }, + }, + ) + + report = evaluate_render_validation(config, [stats]) + + assert report["passed"] is True + assert report["required_objects"]["obj_DryingBox_01"]["passed"] is True + assert report["required_objects"]["obj_DryingBox_01_handle"]["passed"] is True + + def test_classify_huge_articulation_joint_position_as_unstable(): report = classify_articulation_runtime_state( { @@ -100,6 +228,32 @@ def test_claim_boundary_requires_runtime_physics_for_baseline(): assert "runtime_physics_unstable" in claim_boundary["blockers"] +def test_claim_boundary_keeps_official_baseline_separate_from_task_render(): + claim_boundary = build_claim_boundary( + boundary_classification="readback_visible", + render_validation_passed=True, + runtime_physics_stable=True, + ) + + assert claim_boundary["task_render_accepted"] is True + assert claim_boundary["official_baseline_evaluable"] is False + assert claim_boundary["blockers"] == [] + assert "official_baseline_not_validated" in claim_boundary["baseline_blockers"] + + +def test_claim_boundary_allows_official_baseline_when_explicitly_validated(): + claim_boundary = build_claim_boundary( + boundary_classification="readback_visible", + render_validation_passed=True, + runtime_physics_stable=True, + official_baseline_validated=True, + ) + + assert claim_boundary["task_render_accepted"] is True + assert claim_boundary["official_baseline_evaluable"] is True + assert claim_boundary["baseline_blockers"] == [] + + def test_claim_boundary_marks_incomplete_diagnostic_not_evaluable(): claim_boundary = build_claim_boundary( boundary_classification=None, diff --git a/tests/labutopia_poc/test_validate_task_package.py b/tests/labutopia_poc/test_validate_task_package.py index 8e0770e..8cd6b16 100644 --- a/tests/labutopia_poc/test_validate_task_package.py +++ b/tests/labutopia_poc/test_validate_task_package.py @@ -429,7 +429,7 @@ def test_franka_render_validation_declares_object_pixel_readability_thresholds() actual = thresholds[uid] assert actual["min_width_px"] >= minimums["min_width_px"] assert actual["min_height_px"] >= minimums["min_height_px"] - assert actual["min_area_fraction"] > 0.0 + assert actual["min_bbox_area_fraction"] > 0.0 def test_franka_tasks_hide_non_task_objects_for_evidence_readability(): @@ -646,7 +646,7 @@ def test_validate_camera_configs_rejects_open_door_lens_regression( _write_yaml(open_door_camera_path, open_door_cameras) monkeypatch.setattr(validate_task_package, "ROOT", tmp_path) - with pytest.raises(AssertionError, match="camera2 focal_length must be 5.0"): + with pytest.raises(AssertionError, match="camera2 focal_length must be 5.4"): validate_task_package._validate_camera_configs() From 4bf56e559c3966fa019a7d4d9841f7bf20c9923f Mon Sep 17 00:00:00 2001 From: Codex Date: Wed, 24 Jun 2026 10:39:30 +0800 Subject: [PATCH 19/30] docs: add USD articulation door tutorial --- ...26-06-22-labutopia-ebench-weekly-report.md | 4 +- .../index.html | 5 +- .../index.html | 897 ++++++++++++++++++ 3 files changed, 903 insertions(+), 3 deletions(-) create mode 100644 docs/records/evidence/2026-06-24-usd-articulation-dryingbox-tutorial/index.html diff --git a/docs/records/2026-06-22-labutopia-ebench-weekly-report.md b/docs/records/2026-06-22-labutopia-ebench-weekly-report.md index bd1b173..1766051 100644 --- a/docs/records/2026-06-22-labutopia-ebench-weekly-report.md +++ b/docs/records/2026-06-22-labutopia-ebench-weekly-report.md @@ -109,13 +109,15 @@ docs/records/evidence/2026-06-22-labutopia-ebench-weekly-report/assets/labutopia 旧的三张 JPG 保留为历史失败样例。新的三张 PNG 来自正常 evaluator camera readback,不是 direct-render 截图;它们更适合说明今天的真实状态:链路能拍到场景,pick/place 通过任务级隐藏后已经能让 PM 看懂任务目标,open_door 已从“只看到黑箱角/看不见把手/把手像一大片橙色面板”推进到“关闭位正确、门板/框架/细把手可识别”。这说明任务渲染门禁已经通过,但不等于官方 Lift2 baseline 已经可评。 +`open_door` 的 USD 铰接物体问题已经单独整理成解释性教学页:`docs/records/evidence/2026-06-24-usd-articulation-dryingbox-tutorial/index.html`。这篇页面面向产品经理解释 `USD articulation`、`ArticulationRootAPI`、`RevoluteJoint`、`runtime surrogate`、门把手层级和我们这次的修复路径。 + 给产品经理看的前后对照: | 任务 | 旧图问题 | 已经做的修复 | 现在还差什么 | | --- | --- | --- | --- | | `level1_pick` | 抓取目标不明显,只看图无法判断“要抓哪个瓶子” | 修正 eval camera readback、相机朝向、光照,把瓶子归一到 Franka 工作区,并在 pick 任务里隐藏烧杯、托盘、干燥箱等非目标物体 | 当前新图已经能让 PM 看懂“抓这个蓝色瓶子”,并已通过任务渲染门禁 | | `level1_place` | 看不出源物体和目标托盘的关系,不像一个放置任务 | 修正托盘、烧杯、瓶子坐标和颜色标记,并在 place 任务里隐藏瓶子和干燥箱,只保留烧杯与目标托盘 | 当前新图能看懂“把烧杯放到黄色托盘附近”,并已通过任务渲染门禁 | -| `level1_open_door` | 几乎只拍到黑色箱体角,门板、把手、铰链和动作目标都不清楚;中间版本又出现把手像大橙色面板的问题 | 把门把手恢复为干燥箱内部子部件,不再作为独立物体飞走;生成 DryingBox runtime surrogate,固定底座并只保留一个门关节;再补关节初始目标回放、把手位置修正、删除重复橙色块、缩细把手和任务专用正面相机 | 最新图中关节已回到期望关闭位 `0.0`,门板、框架和细橙色把手可识别,并已通过任务渲染门禁;下一步接 Lift2 baseline gate | +| `level1_open_door` | 几乎只拍到黑色箱体角,门板、把手、铰链和动作目标都不清楚;中间版本又出现把手像大橙色面板的问题 | 把门把手恢复为干燥箱内部子部件,不再作为独立物体飞走;生成 DryingBox runtime surrogate,固定底座并只保留一个门关节;再补关节初始目标回放、把手位置修正、删除重复橙色块、缩细把手和任务专用正面相机;背景解释见 `docs/records/evidence/2026-06-24-usd-articulation-dryingbox-tutorial/index.html` | 最新图中关节已回到期望关闭位 `0.0`,门板、框架和细橙色把手可识别,并已通过任务渲染门禁;下一步接 Lift2 baseline gate | 需要特别说明: diff --git a/docs/records/evidence/2026-06-22-labutopia-ebench-weekly-report/index.html b/docs/records/evidence/2026-06-22-labutopia-ebench-weekly-report/index.html index 858f012..bef6a73 100644 --- a/docs/records/evidence/2026-06-22-labutopia-ebench-weekly-report/index.html +++ b/docs/records/evidence/2026-06-22-labutopia-ebench-weekly-report/index.html @@ -397,6 +397,7 @@ 总览 资产加载 渲染图 + 门教学 证据 下一步 @@ -596,7 +597,7 @@

    旧图要表达的问题

    我们是怎么解决的

    -

    先修相机和光照,让评测相机真的能读回画面;再把瓶子、烧杯、托盘、干燥箱从原始实验室坐标搬回 Franka 工作区;然后按任务隐藏无关物体。对 open_door 还额外做了干燥箱运行期替身资产:固定底座、只保留一个门关节、对齐铰链;随后补关节初始目标回放,把把手移到非铰链侧,删除额外橙色块,并换成正面任务相机,让门板、框架和单把手能同时看见。

    +

    先修相机和光照,让评测相机真的能读回画面;再把瓶子、烧杯、托盘、干燥箱从原始实验室坐标搬回 Franka 工作区;然后按任务隐藏无关物体。对 open_door 还额外做了干燥箱运行期替身资产:固定底座、只保留一个门关节、对齐铰链;随后补关节初始目标回放,把把手移到非铰链侧,删除额外橙色块,并换成正面任务相机,让门板、框架和单把手能同时看见。更系统的解释见 USD articulation 与 DryingBox 门问题教学页

    @@ -648,7 +649,7 @@

    旧图问题怎么解决

    level1_open_door 旧图看不清干燥箱门和门把手,无法判断开门动作目标。 - 把门把手恢复为干燥箱内部子部件,不再作为独立物体飞走;同时生成运行期干燥箱替身资产,固定底座并只保留一个门关节;再补关节初始目标回放、把手位置修正、删除重复橙色块和任务专用正面相机。 + 把门把手恢复为干燥箱内部子部件,不再作为独立物体飞走;同时生成运行期干燥箱替身资产,固定底座并只保留一个门关节;再补关节初始目标回放、把手位置修正、删除重复橙色块和任务专用正面相机。背景解释见 门问题教学页。 最新图中关节已回到期望关闭位 0.0,门板、框架和细橙色把手可见;任务渲染门禁已通过。下一步要接 Lift2 复合资产和官方 runner gate。 diff --git a/docs/records/evidence/2026-06-24-usd-articulation-dryingbox-tutorial/index.html b/docs/records/evidence/2026-06-24-usd-articulation-dryingbox-tutorial/index.html new file mode 100644 index 0000000..5e08342 --- /dev/null +++ b/docs/records/evidence/2026-06-24-usd-articulation-dryingbox-tutorial/index.html @@ -0,0 +1,897 @@ + + + + + + + USD Articulation 教学:DryingBox 门问题复盘 + + + + + +
    +
    +
    +

    Teaching Note · USD Physics · DryingBox

    +

    为什么一个门会让 open_door 变难

    +

    这篇页面给产品经理和评测侧解释:USD articulation 不是普通模型导入,它同时包含层级、刚体、关节、驱动、材质和运行期状态。我们遇到的 DryingBox 问题,本质是“一个看起来像门的资产,进入 Isaac runtime 后没有成为一个稳定、可读、可评的门”。

    + +
    +
    +
    +
    Articulation Root: obj_obj_DryingBox_01
    +
    Joint: RevoluteJoint
    +
    +
    +
    +
    +
    +
    +
    +
    Handle path: /handle
    +
    +
    +
    + +
    +
    +

    一句话结论

    +
    +
    + Before +

    旧门不是稳定任务对象

    +

    旧图只看到黑箱角,门板、把手、铰链和动作点不可读;运行期还出现过 joint position ~= 1.573e13 这类异常状态。

    +
    +
    + Root Cause +

    USD 层级和 PhysX articulation 不干净

    +

    门是 articulated object,不是普通 mesh。ArticulationRootAPIRigidBodyAPIJointbody0/body1scalehandle 层级都要同时正确。

    +
    +
    + After +

    用 runtime surrogate 收敛风险

    +

    我们改成稳定的 DryingBox runtime surrogate:固定底座、只保留一个 RevoluteJoint、把门回放到 [0.0]、把手保留在内部 /handle 路径。

    +
    +
    +
    + +
    +

    先把基础讲清楚:什么是 USD articulation

    +
    +
    +

    USD 不是单个模型文件

    +

    USD 更像一个“可组合的场景数据库”。一个最终场景通常由很多 layerreferencepayload 和 override 合成。产品上看到的是一个门,工程上看到的是一棵 prim hierarchy 加很多属性。

    +

    这也是为什么“资产加载成功”不等于“评测可用”:mesh 可能出现了,但坐标、材质、物理、关节、相机读取任一环节不对,任务图就可能不可读。

    +
    +
    +

    Articulation 是带关节的刚体系统

    +

    UsdPhysics / PhysX 里,articulation 由多个 rigid links 和 joints 组成。joint position 是每个 DOF 的状态;如果是旋转轴,它就是弧度角。

    +

    门最小可用结构可以理解成:箱体是固定 base,门板是一个 link,门板和箱体之间有一个 RevoluteJoint,把手应该挂在门板上。

    +
    +
    +
    +
    +
    Root
    +
    ArticulationRootAPI
    +
    +
    +
    Base
    +
    PhysicsFixedJoint
    +
    +
    +
    Door DOF
    +
    RevoluteJoint
    +
    +
    +
    Reset Target
    +
    [0.0]
    +
    +
    +
    + 对产品经理的比喻:普通 mesh 像一块静态道具;USD articulation 像一个带合页、门轴、限位、质量和物理状态的舞台机关。道具放进场景只要位置对;机关还必须能稳定转、能复位、能被相机看清。 +
    +
    + +
    +

    互动:切换失败模式,看门为什么会坏

    +

    下面这个小动画不是 Isaac 渲染结果,而是用于教学的结构示意。切换按钮可以看到我们这次实际排查过的几类风险。

    +
    + + + + +
    +
    +
    +
    +
    Root scale: [1,1,1]
    +
    Joint state: 0.0 rad
    +
    +
    +
    +
    +
    +
    +
    +
    Nested handle: /World/.../obj_obj_DryingBox_01/handle
    +
    +
    + Fixed +

    最终修复态:稳定、可读、可验证

    +

    当前正式诊断里,DryingBox 只暴露 RevoluteJointjoint_positions=[0.0]runtime_physics_stable=true。门板、框架和细橙色把手都在同一张 camera2 readback 图里。

    +
    + 门角度演示 + + 28 deg +
    +
    +
    +
    + +
    +

    我们这次遇到的门问题

    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    问题产品侧表现工程侧原因风险
    旧图只看到黑箱角看不出门在哪里,也看不出要抓哪个把手。camera2 构图、任务对象显隐、LabUtopia 坐标归一都还没收敛。PM 无法判断任务是否成立,评测侧也无法把它当作有效视觉证据。
    joint position 爆值门状态不可解释,reset 后可能不是可评的关闭态。原始 DryingBox 的 USD physics topology 对 Isaac runtime 不稳定,出现过 1.573e13 量级关节读数和 PhysX transform warning策略还没开始评,场景状态已经不可信。
    把手像独立物体把手位置异常,或者中间版本变成一大片橙色面板。把手原本应该是 DryingBox 内部子路径 /handle,但导入/封装时曾出现 top-level duplicate 或过大 display marker。动作点不可读,视觉 QA 会把图判为 WARN 或 FAIL。
    原生材质/结构不等于 eval 可读原生视角可能更像 LabUtopia,但不一定能让任务目标清楚。P1 目标是 eval-path evidence,所以用了任务专用 camera2、任务级隐藏和高对比 displayColor如果混淆“展示图”和“评测图”,会误判当前 readiness。
    +
    +
    + +
    +

    我们怎么解决:从原始资产到稳定 runtime surrogate

    +
    +
    +
    +
    + +
    /World/labutopia_level1_poc/obj_obj_DryingBox_01
    PhysicsArticulationRootAPI, root scale [1,1,1]
    + Root +
    +
    + +
    body_link
    fixed base, mass and inertia are finite
    + Base +
    +
    + +
    door_link
    connected by RevoluteJoint, reset target [0.0]
    + Door +
    +
    + +
    /handle
    nested under DryingBox, fixed to door_link, scale [0.045,0.075,0.25]
    + Handle +
    +
    +
    +
    +

    关键策略

    +
      +
    • 不用不稳定的原始复杂 articulation 直接进评测,而是生成 DryingBox runtime surrogate
    • +
    • BaseFixedJoint 把箱体固定住,避免整个干燥箱在物理里漂移。
    • +
    • 只保留一个明确的 RevoluteJoint,让门的状态可以用一个角度解释。
    • +
    • 在 reset 后回放 target_positions=[0.0],保证任务从关闭态开始。
    • +
    • handle 保持为 DryingBox 内部子部件,而不是 top-level object。
    • +
    • 给门板、门缝、把手加 displayColor 和 material binding,让 camera2 读图可解释。
    • +
    +
    +
    +
    +
    +

    代码层证据

    +
    root: /World/labutopia_level1_poc/obj_obj_DryingBox_01
    +joint: RevoluteJoint
    +target_positions: [0.0]
    +handle: /World/labutopia_level1_poc/obj_obj_DryingBox_01/handle
    +runtime_joint_positions: [0.0]
    +runtime_physics_stable: true
    +task_render_accepted: true
    +official_baseline_evaluable: false
    +
    +
    +

    可读性结果

    +

    最终图不是为了还原 LabUtopia 原生 viewer,而是为了证明 evaluator 能看到任务:门板、框架、细橙色把手在同一张图里;render_validation 能检测到 obj_DryingBox_01obj_DryingBox_01_handle 的 bbox 尺寸达标。

    +

    当前边界是:task_render_accepted=true,但 official_baseline_evaluable=false,因为官方 Lift2 baseline 的复合资产和 runner 还没验证。

    +
    +
    +
    + +
    +

    旧图和最终图怎么读

    +
    +
    + Old open door failed render +
    旧图:只看到黑色箱体角,门板、把手、铰链和动作点都不明确。
    +
    +
    + Current open door evaluator readback +
    最终 eval readback:正面可见 DryingBox frame、door panel 和 thin orange handle;正式诊断里 render_validation.passed=true
    +
    +
    +
    + +
    +

    资料来源和我们如何使用

    +

    中文解释是面向 PM 的二次整理;英文术语保留原样,便于工程和论文阅读时对齐官方文档。

    +
    +
    + OpenUSD Introduction + 用于解释 USD compositionlayerreference 和 scene graph 不是单个模型文件。 +
    +
    + UsdPhysics Schema + 用于说明 UsdPhysics 是 USD 中表达 physics simulation representation 的 schema 集合。 +
    +
    + UsdPhysicsArticulationRootAPI + 用于解释 ArticulationRootAPI 标记 reduced-coordinate articulation subtree,以及 fixed-base root 的放置规则。 +
    +
    + UsdPhysicsJoint + 用于解释 Joint 如何约束两个 rigid bodies,或约束 rigid body 与 world。 +
    +
    + Omni Physics Articulations + 用于对齐 Isaac/Omniverse 对 USD hierarchy、fixed-base articulation 和 root API 的实践要求。 +
    +
    + PhysX Articulations + 用于解释 reduced-coordinate articulation 维护 joint positions / velocities / accelerations,旋转轴 joint position 是 radians。 +
    +
    + NVIDIA OpenUSD for Developers + 用于 PM 口径解释 OpenUSD 是建模、分类、组合多数据源的生态,不只是文件格式。 +
    +
    + AOUSD Explainer: What Is OpenUSD? + 用于补充 layeringcomposition 和协作式资产组织的非官方解读。 +
    +
    +

    项目证据来自 saved/diagnostics/labutopia_p1_gate_open_door_formal_20260624_0002/diagnostics.jsonstandalone_tools/labutopia_poc/build_asset_overlay.py 和周报 evidence manifest。

    +
    +
    + + + + From 9f054cb518d6e5dd6317ee05494bf1aec090e7ae Mon Sep 17 00:00:00 2001 From: Codex Date: Wed, 24 Jun 2026 12:16:53 +0800 Subject: [PATCH 20/30] docs: plan native DryingBox gate --- ...26-06-22-labutopia-ebench-weekly-report.md | 25 +- .../index.html | 56 ++- .../index.html | 269 ++++++++++- .../plans/2026-06-22-ebench-labutopia-poc.md | 30 +- .../2026-06-24-ebench-native-dryingbox.md | 442 ++++++++++++++++++ 5 files changed, 757 insertions(+), 65 deletions(-) create mode 100644 docs/superpowers/plans/2026-06-24-ebench-native-dryingbox.md diff --git a/docs/records/2026-06-22-labutopia-ebench-weekly-report.md b/docs/records/2026-06-22-labutopia-ebench-weekly-report.md index 1766051..f6221c1 100644 --- a/docs/records/2026-06-22-labutopia-ebench-weekly-report.md +++ b/docs/records/2026-06-22-labutopia-ebench-weekly-report.md @@ -7,7 +7,7 @@ HTML 版产品汇报页: 本周已经把 LabUtopia 的 Franka POC 跑通到端到端 smoke 阶段:任务可以提交、场景可以加载、三个 level-1 任务可以 reset/step、结果可以落盘、最终状态可以正常 complete。 -这说明当前最关键的“接入链路”已经打通。需要注意的是,这还不是任务求解成功,也不是官方 baseline 成绩;当前 smoke 使用默认动作,所以三个任务分数都是 `0.0`。2026-06-23 到 2026-06-24 最新复核补充:三任务现在都能通过 EBench/evaluator 正常读回非黑渲染图,资产静态坐标也已经从“明显导错”修到合理工作区;最新正式诊断中 `pick`、`place`、`open_door` 均为 `render_validation.passed=true`、`task_render_accepted=true`。当前可以说 Franka POC 的任务渲染门禁已过,但官方 Lift2 baseline 的复合资产和官方 runner 还没验证,所以仍不能说官方 baseline 可评已经闭环。 +这说明当前最关键的“接入链路”已经打通。需要注意的是,这还不是任务求解成功,也不是官方 baseline 成绩;当前 smoke 使用默认动作,所以三个任务分数都是 `0.0`。2026-06-23 到 2026-06-24 最新复核补充:三任务现在都能通过 EBench/evaluator 正常读回非黑渲染图,资产静态坐标也已经从“明显导错”修到合理工作区;最新正式诊断中 `pick`、`place`、`open_door` 均为 `render_validation.passed=true`、`task_render_accepted=true`。当前可以说 Franka POC 的任务渲染门禁已过,但 `open_door` 证据仍是 P1 `sanitized_surrogate` 对照组,不是 LabUtopia native complex `DryingBox` 已调通;官方 Lift2 baseline 的复合资产和官方 runner 也还没验证。 ## 本周完成了什么 @@ -109,7 +109,7 @@ docs/records/evidence/2026-06-22-labutopia-ebench-weekly-report/assets/labutopia 旧的三张 JPG 保留为历史失败样例。新的三张 PNG 来自正常 evaluator camera readback,不是 direct-render 截图;它们更适合说明今天的真实状态:链路能拍到场景,pick/place 通过任务级隐藏后已经能让 PM 看懂任务目标,open_door 已从“只看到黑箱角/看不见把手/把手像一大片橙色面板”推进到“关闭位正确、门板/框架/细把手可识别”。这说明任务渲染门禁已经通过,但不等于官方 Lift2 baseline 已经可评。 -`open_door` 的 USD 铰接物体问题已经单独整理成解释性教学页:`docs/records/evidence/2026-06-24-usd-articulation-dryingbox-tutorial/index.html`。这篇页面面向产品经理解释 `USD articulation`、`ArticulationRootAPI`、`RevoluteJoint`、`runtime surrogate`、门把手层级和我们这次的修复路径。 +`open_door` 的 USD 铰接物体问题已经单独整理成解释性教学页:`docs/records/evidence/2026-06-24-usd-articulation-dryingbox-tutorial/index.html`。这篇页面面向产品经理解释 `USD articulation`、`ArticulationRootAPI`、`RevoluteJoint`、P1 `sanitized_surrogate` 对照组、P2 native complex `DryingBox` 七步 gate、门把手层级和 claim boundary。 给产品经理看的前后对照: @@ -117,7 +117,7 @@ docs/records/evidence/2026-06-22-labutopia-ebench-weekly-report/assets/labutopia | --- | --- | --- | --- | | `level1_pick` | 抓取目标不明显,只看图无法判断“要抓哪个瓶子” | 修正 eval camera readback、相机朝向、光照,把瓶子归一到 Franka 工作区,并在 pick 任务里隐藏烧杯、托盘、干燥箱等非目标物体 | 当前新图已经能让 PM 看懂“抓这个蓝色瓶子”,并已通过任务渲染门禁 | | `level1_place` | 看不出源物体和目标托盘的关系,不像一个放置任务 | 修正托盘、烧杯、瓶子坐标和颜色标记,并在 place 任务里隐藏瓶子和干燥箱,只保留烧杯与目标托盘 | 当前新图能看懂“把烧杯放到黄色托盘附近”,并已通过任务渲染门禁 | -| `level1_open_door` | 几乎只拍到黑色箱体角,门板、把手、铰链和动作目标都不清楚;中间版本又出现把手像大橙色面板的问题 | 把门把手恢复为干燥箱内部子部件,不再作为独立物体飞走;生成 DryingBox runtime surrogate,固定底座并只保留一个门关节;再补关节初始目标回放、把手位置修正、删除重复橙色块、缩细把手和任务专用正面相机;背景解释见 `docs/records/evidence/2026-06-24-usd-articulation-dryingbox-tutorial/index.html` | 最新图中关节已回到期望关闭位 `0.0`,门板、框架和细橙色把手可识别,并已通过任务渲染门禁;下一步接 Lift2 baseline gate | +| `level1_open_door` | 几乎只拍到黑色箱体角,门板、把手、铰链和动作目标都不清楚;中间版本又出现把手像大橙色面板的问题 | 把门把手恢复为干燥箱内部子部件,不再作为独立物体飞走;生成 P1 `sanitized_surrogate` 对照组,固定底座并只保留一个门关节;再补关节初始目标回放、把手位置修正、删除重复橙色块、缩细把手和任务专用正面相机;背景解释见 `docs/records/evidence/2026-06-24-usd-articulation-dryingbox-tutorial/index.html` | 最新图中关节已回到期望关闭位 `0.0`,门板、框架和细橙色把手可识别,并已通过任务渲染门禁;下一步先调通 LabUtopia native complex `DryingBox`,再接 Lift2 baseline gate | 需要特别说明: @@ -125,10 +125,11 @@ docs/records/evidence/2026-06-22-labutopia-ebench-weekly-report/assets/labutopia - 已推进:P0/P1 后,`level1_pick`、`level1_place`、`level1_open_door` 都是 `readback_visible`。 - 已推进:静态 USD readback 显示瓶子、烧杯、托盘、干燥箱、门把手的坐标和尺寸合理;门把手路径是 `/World/labutopia_level1_poc/obj_obj_DryingBox_01/handle`,不是独立飞走物体。 - 已推进:任务级隐藏后,`level1_pick` 当前图只保留目标瓶,`level1_place` 当前图只保留烧杯和目标托盘,PM 可以看懂任务目标和关系。 -- 已推进:`level1_open_door` 旧版本运行期关节读数曾出现 `1.573e13` 量级;最新诊断只暴露 `RevoluteJoint`,关节读数为 `0.0`,仿真能完成 readback,问题已经从“爆掉不可看/只看到黑箱角/把手像大橙色面板”收敛到“门板、框架和细橙色把手可识别”。 +- 已推进:`level1_open_door` 旧版本运行期关节读数曾出现 `1.573e13` 量级;P1 `sanitized_surrogate` 对照组最新诊断只暴露 `RevoluteJoint`,关节读数为 `0.0`,仿真能完成 readback,问题已经从“爆掉不可看/只看到黑箱角/把手像大橙色面板”收敛到“门板、框架和细橙色把手可识别”。 - 已复核:独立视觉审阅认为当前 old-vs-current 对照可用于 PM 汇报;旧图确实支持“目标不清、放置关系缺失、看不到门把手”的问题描述,当前三张新图均可作为任务渲染通过证据。 +- 仍 blocked:LabUtopia native complex `DryingBox` 还没调通;下一步必须做 `asset audit`、native-only Isaac smoke、EBench wrapper、`additive physics override` 和 `open_door` eval readback gate。 - 仍 blocked:官方 Lift2 baseline 不能宣称可评,因为 Lift2 复合资产根目录和官方 runner 还没验证;当前 diagnostics claim boundary 是 `task_render_accepted=true`、`official_baseline_evaluable=false`。 -- 因此下一步不是继续修三张截图,而是把 Lift2 baseline 所需的复合资产和官方 runner gate 接上。 +- 因此下一步不是继续修三张截图,而是先把 native complex `DryingBox` 接入 gate 做完,再把 Lift2 baseline 所需的复合资产和官方 runner gate 接上。 ## 本周遇到的问题和解决方式 @@ -138,8 +139,8 @@ docs/records/evidence/2026-06-22-labutopia-ebench-weekly-report/assets/labutopia | 缺少 `meta_info.pkl` | 传统 GenManip 任务依赖采集包里的元信息,LabUtopia POC 没有这份文件 | 针对 LabUtopia POC,从实时场景里自动生成最小可用元信息 | 已解决 | | camera cleanup 字段缺失 | 切换任务时 camera 清理报字段错误 | 补齐 camera 配置里的 cleanup flags | 已解决 | | eval recorder `camera2` 黑屏 | 保存过程帧时 `camera2` 输出纯黑 | 已修 camera axes/pose、deterministic lighting 和工作区相机;三任务当前 eval readback 均非黑 | 黑屏解除 | -| 当前新图验收边界 | `pick` 已清楚,`place` 关系可读;`open_door` 已能读回且关闭位正确,门板/框架/细把手可识别 | 三任务最新正式诊断均为 `render_validation.passed=true`;继续接 Lift2 baseline 所需的复合资产和官方 runner gate | 任务渲染通过 | -| `open_door` 运行期物理不稳定 | 旧版本 runtime articulation joint position 爆到 `1.573e13`,并伴随 PhysX transform warning | 已用 DryingBox runtime surrogate、固定底座、对齐后的门铰链和关节目标回放修复;最新读数为 `0.0 rad` 且只暴露 `RevoluteJoint` | 物理层已解决 | +| 当前新图验收边界 | `pick` 已清楚,`place` 关系可读;`open_door` 已能读回且关闭位正确,门板/框架/细把手可识别 | 三任务最新正式诊断均为 `render_validation.passed=true`;继续接 native complex `DryingBox` gate 和 Lift2 baseline 所需的复合资产/官方 runner gate | 任务渲染通过 | +| `open_door` 运行期物理不稳定 | 旧版本 runtime articulation joint position 爆到 `1.573e13`,并伴随 PhysX transform warning | P1 已用 DryingBox `sanitized_surrogate`、固定底座、对齐后的门铰链和关节目标回放修复;最新读数为 `0.0 rad` 且只暴露 `RevoluteJoint`。P2 要把同样稳定性迁移到 LabUtopia native complex `DryingBox` | P1 已解决;native 待验证 | | 资产导入/layout 静态坐标 | 之前任务物体在源 lab 坐标,handle 变成异常放大的独立物体 | P1 已把对象归一到 Franka 工作区,并把 handle 保留为 DryingBox 内部子路径 | 静态层已推进 | | 最终进度不 complete | 任务已跑完,但进度没有写入,导致客户端最后等待超时 | 任务结束时写最小 `result_info.json`,并修复进度统计 | 已解决 | | 后处理异常可能被误记为完成 | 如果后处理报错,不能用 0 分兜底伪装成成功 | 增加 fail-fast 逻辑和回归测试 | 已解决 | @@ -153,6 +154,7 @@ docs/records/evidence/2026-06-22-labutopia-ebench-weekly-report/assets/labutopia | Franka POC smoke | 已完成 | 3 个任务全部 complete | | 结果落盘 | 已完成 | per-task 和 final result 都能写出 | | 渲染图/视频验收 | 任务渲染通过 | 三任务 eval readback 已非黑;pick 已清楚,place 关系可读,open_door 关闭位正确、门板/框架/细把手可识别;三任务最新正式诊断均为 `render_validation.passed=true` | +| Native complex DryingBox | 待验证 | 当前 open_door 证据来自 P1 `sanitized_surrogate` 对照组;下一步必须保留 LabUtopia native visual/hierarchy/handle,只用 `additive physics override` 修 runtime 物理 | | 任务求解能力 | 未验证 | 当前默认动作得分 0.0,不代表策略能力 | | 官方 baseline | 不能宣称可评 | 任务渲染门禁已过,但 Lift2 复合资产和官方 runner 还没闭环,diagnostics claim boundary 仍是 `official_baseline_evaluable=false` | @@ -214,13 +216,16 @@ saved/eval_results/ebench/labutopia_franka_smoke_clean8_20260622_100208/.../leve 1. P0a/P0b:已修相机 readback 和 deterministic lighting,让 eval path 不再黑屏。 2. P1a:已把对象静态坐标和 nested handle 归一到 Franka 工作区。 -3. P1b:已用 DryingBox surrogate 和关节目标回放修复 `open_door` runtime articulation/PhysX 爆值与关闭位问题。 +3. P1b:已用 DryingBox `sanitized_surrogate` 对照组和关节目标回放修复 `open_door` runtime articulation/PhysX 爆值与关闭位问题。 4. P1c:已完成任务级相机/构图复验,三任务 `render_validation.passed=true`。 -5. P2:已用正常 eval-path 重新抓三任务关键帧,写 evidence manifest,并完成独立视觉复核。 -6. P3:进入 Lift2 复合资产根目录和官方 runner 发现;这是官方 baseline 可评前的下一道硬门槛。 +5. P1d:已用正常 eval-path 重新抓三任务关键帧,写 evidence manifest,并完成独立视觉复核。 +6. P2:进入 LabUtopia native complex `DryingBox` gate:asset audit、native-only Isaac smoke、EBench wrapper、additive physics override、open_door eval readback。 +7. P3:进入 Lift2 复合资产根目录和官方 runner 发现;这是官方 baseline 可评前的下一道硬门槛。 ## 新增调研和计划文档 - [docs/labutopia_lab_poc/render_visual_investigation_20260623.md](../labutopia_lab_poc/render_visual_investigation_20260623.md) - [docs/superpowers/plans/2026-06-23-labutopia-ebench-render-layout-closure.md](../superpowers/plans/2026-06-23-labutopia-ebench-render-layout-closure.md) +- [docs/superpowers/plans/2026-06-24-ebench-native-dryingbox.md](../superpowers/plans/2026-06-24-ebench-native-dryingbox.md) +- [docs/records/evidence/2026-06-24-usd-articulation-dryingbox-tutorial/index.html](evidence/2026-06-24-usd-articulation-dryingbox-tutorial/index.html) - [docs/labutopia_lab_poc/evidence_manifests/render_p1_asset_layout_20260623.json](../labutopia_lab_poc/evidence_manifests/render_p1_asset_layout_20260623.json) diff --git a/docs/records/evidence/2026-06-22-labutopia-ebench-weekly-report/index.html b/docs/records/evidence/2026-06-22-labutopia-ebench-weekly-report/index.html index bef6a73..7c83a55 100644 --- a/docs/records/evidence/2026-06-22-labutopia-ebench-weekly-report/index.html +++ b/docs/records/evidence/2026-06-22-labutopia-ebench-weekly-report/index.html @@ -408,19 +408,20 @@

    Weekly report · 2026-06-22 · PM-facing evidence page

    LabUtopia x EBench接入周报

    -

    本周 Franka POC 已经跑通端到端 smoke:任务能提交,LabUtopia 场景能加载,三个 level-1 任务能 reset/step,结果能落盘并正常 complete。2026-06-23 到 2026-06-24 的 P1 复核后,三任务 eval camera2 都能读回非黑画面,静态资产坐标也已归一到 Franka 工作区;最新正式复核显示 pickplaceopen_door 三张任务图均已通过任务渲染门禁,open_door 也从“黑箱角/大橙色块”修到“门板、框架、细把手在同一张图里可见”。但这只代表任务渲染证据闭环,官方 Lift2 baseline 的复合资产和官方 runner 还没验证,仍不能宣称官方 baseline 可评。

    +

    本周 Franka POC 已经跑通端到端 smoke:任务能提交,LabUtopia 场景能加载,三个 level-1 任务能 reset/step,结果能落盘并正常 complete。2026-06-23 到 2026-06-24 的 P1 复核后,三任务 eval camera2 都能读回非黑画面,静态资产坐标也已归一到 Franka 工作区;最新正式复核显示 pickplaceopen_door 三张任务图均已通过任务渲染门禁,open_door 也从“黑箱角/大橙色块”修到“门板、框架、细把手在同一张图里可见”。但这只代表 P1 任务渲染证据闭环;open_door 的 LabUtopia native complex DryingBox 还没调通,官方 Lift2 baseline 的复合资产和官方 runner 也还没验证。

    wiring smoke complete 3/3 episodes finalized 3/3 readback visible score=0.0 expected 3/3 task render gate pass + native DryingBox pending official baseline blocked
    @@ -452,12 +453,12 @@

    当前进展总览

    渲染证据
    3/3 通过门禁
    -
    pick/place/open_door 的任务渲染均已通过;只剩官方 baseline lane 未闭环。
    +
    pick/place/open_door 的任务渲染均已通过;native DryingBox 和官方 baseline lane 未闭环。
    官方 Lift2 baseline
    不可宣称
    -
    视觉验收、Lift2 复合资产和官方 runner 还没闭环。
    +
    native DryingBox、Lift2 复合资产和官方 runner 还没闭环。
    @@ -503,7 +504,7 @@

    本周完成了什么

    渲染图验收 任务渲染通过 当前三张 eval readback 图为非黑,且三任务最新正式诊断均为 render_validation.passed=truepick 目标清楚,place 关系可读,open_door 关闭位为 0.0,门板、框架和细橙色把手可识别。 - 这证明 Franka POC 任务渲染门禁已过;下一道硬门槛是 Lift2 复合资产和官方 runner gate。 + 这证明 Franka POC 任务渲染门禁已过;下一道硬门槛是 native complex DryingBox gate,然后才是 Lift2 复合资产和官方 runner gate。 @@ -582,7 +583,7 @@

    已经闭环的部分

    还没闭环的部分

    -

    三任务任务渲染门禁已经通过,但官方 Lift2 baseline 还没闭环。下一步需要把 LabUtopia overlay 和默认 robot_usds/lift2miscs/curobo 组成官方候选资产根目录,再定位并验证官方 runner;在这些证据完成前,不能把当前图等同于官方 baseline 可评。

    +

    三任务任务渲染门禁已经通过,但 open_door 目前是 P1 sanitized_surrogate 对照组证据,不是 LabUtopia native complex DryingBox 调通证据。下一步先做 native DryingBox 的 asset audit、native-only Isaac smoke、EBench wrapper 和 eval readback gate;之后再把 LabUtopia overlay 和默认 robot_usds/lift2miscs/curobo 组成官方候选资产根目录,并验证官方 runner。

    @@ -597,12 +598,12 @@

    旧图要表达的问题

    我们是怎么解决的

    -

    先修相机和光照,让评测相机真的能读回画面;再把瓶子、烧杯、托盘、干燥箱从原始实验室坐标搬回 Franka 工作区;然后按任务隐藏无关物体。对 open_door 还额外做了干燥箱运行期替身资产:固定底座、只保留一个门关节、对齐铰链;随后补关节初始目标回放,把把手移到非铰链侧,删除额外橙色块,并换成正面任务相机,让门板、框架和单把手能同时看见。更系统的解释见 USD articulation 与 DryingBox 门问题教学页

    +

    先修相机和光照,让评测相机真的能读回画面;再把瓶子、烧杯、托盘、干燥箱从原始实验室坐标搬回 Franka 工作区;然后按任务隐藏无关物体。对 open_door,P1 额外做了 sanitized_surrogate 对照组:固定底座、只保留一个门关节、对齐铰链;随后补关节初始目标回放,把把手移到非铰链侧,删除额外橙色块,并换成正面任务相机,让门板、框架和单把手能同时看见。新的硬约束是 P2 必须回到 LabUtopia native complex DryingBox,保留原生 visual/hierarchy/handle,只用 additive physics override 修 runtime 物理。更系统的解释见 USD articulation 与 DryingBox 门问题教学页

    渲染证据状态

    -

    当前三任务均为 readback_visiblerender_validation.passed=true。最新任务级隐藏后,level1_pick 只保留抓取瓶子,目标清楚;level1_place 同时保留烧杯和黄色目标托盘,任务关系可读;level1_open_door 关节位置已回到期望关闭位 0.0,正面图里箱体框架、门板和细橙色把手已可识别。诊断 claim boundary 已升级为 task_render_accepted=true,但 official_baseline_evaluable=false

    +

    当前三任务均为 readback_visiblerender_validation.passed=true。最新任务级隐藏后,level1_pick 只保留抓取瓶子,目标清楚;level1_place 同时保留烧杯和黄色目标托盘,任务关系可读;level1_open_door 在 P1 surrogate baseline 中关节位置已回到期望关闭位 0.0,正面图里箱体框架、门板和细橙色把手已可识别。诊断 claim boundary 已升级为 task_render_accepted=true,但 native_complex_dryingbox_ready=falseofficial_baseline_evaluable=false

    旧图:历史失败样例

    @@ -649,8 +650,8 @@

    旧图问题怎么解决

    level1_open_door 旧图看不清干燥箱门和门把手,无法判断开门动作目标。 - 把门把手恢复为干燥箱内部子部件,不再作为独立物体飞走;同时生成运行期干燥箱替身资产,固定底座并只保留一个门关节;再补关节初始目标回放、把手位置修正、删除重复橙色块和任务专用正面相机。背景解释见 门问题教学页。 - 最新图中关节已回到期望关闭位 0.0,门板、框架和细橙色把手可见;任务渲染门禁已通过。下一步要接 Lift2 复合资产和官方 runner gate。 + 把门把手恢复为干燥箱内部子部件,不再作为独立物体飞走;同时生成 P1 sanitized_surrogate 对照组,固定底座并只保留一个门关节;再补关节初始目标回放、把手位置修正、删除重复橙色块和任务专用正面相机。背景解释见 门问题教学页。 + 最新图中关节已回到期望关闭位 0.0,门板、框架和细橙色把手可见;任务渲染门禁已通过。下一步先调通 LabUtopia native complex DryingBox,再接 Lift2 复合资产和官方 runner gate。 @@ -663,7 +664,7 @@

    已经解决了什么

    还没解决什么

    -

    任务渲染层已经过门禁,但官方 baseline lane 还没过。也就是说,我们已经解决“图能不能加载、能不能看懂任务”的问题;下一步要解决“能不能按官方 Lift2 方式评”的问题。

    +

    任务渲染层已经过门禁,但 open_door native complex DryingBox 和官方 baseline lane 都还没过。也就是说,我们已经解决“图能不能加载、能不能看懂任务”的 P1 问题;下一步先解决“原生复杂门能不能稳定进入 EBench”,再解决“能不能按官方 Lift2 方式评”。

    @@ -813,7 +814,7 @@

    问题和处理结果

    当前新图仍未全部验收 pick 已清楚,place 关系可读;open_door 已能读回且关闭位正确,门板/框架/细把手可识别。 - 已用正式任务渲染门禁签收三任务;继续接 Lift2 baseline 所需的复合资产和官方 runner gate。 + 已用正式任务渲染门禁签收三任务;继续接 native complex DryingBox gate,再接 Lift2 baseline 所需的复合资产和官方 runner gate。 任务渲染通过 @@ -824,9 +825,9 @@

    问题和处理结果

    open_door runtime 物理不稳定 - 旧版本 runtime articulation joint position 爆到 1.573e13,并伴随 PhysX transform warning。 - 已用稳定的 DryingBox runtime surrogate、固定底座、对齐后的门铰链和关节目标回放修复;最新读数为 0.0 rad 且只暴露 RevoluteJoint。 - 物理层已解决 + 旧版本 runtime articulation joint position 爆到 1.573e13,并伴随 PhysX transform warning。 + P1 已用稳定的 DryingBox sanitized_surrogate、固定底座、对齐后的门铰链和关节目标回放修复;最新读数为 0.0 rad 且只暴露 RevoluteJoint。P2 要把同样稳定性迁移到 LabUtopia native complex DryingBox。 + P1 已解决;native 待验证 最终进度不 complete @@ -860,6 +861,7 @@

    现在可以说

  • 静态 USD 坐标和门把手层级已经明显推进,handle 不再作为独立飞走物体。
  • 任务级隐藏后,pickplace 的当前诊断图已经能让 PM 看懂目标和关系。
  • P1 最新正式诊断中三任务 render_validation.passed=true,可以说任务渲染门禁已通过。
  • +
  • 可以说 open_door 的 P1 sanitized_surrogate 对照组稳定了关节读数和 readback 证据。
  • @@ -868,6 +870,7 @@

    现在不能说

  • 不能说任务已经求解成功,当前默认动作得分是 0.0
  • 不能说官方 Lift2 baseline 已经可评,Lift2 复合资产和官方 runner 还没闭环。
  • 不能把当前任务渲染 PASS 等同于策略成功率、官方分数或官方 baseline 成绩。
  • +
  • 不能说 LabUtopia native complex DryingBox 已经调通;当前 open_door 正式证据仍是 P1 surrogate baseline。
  • 不能说已经跑过 EBench 官方 Lift2 baseline;当前只闭环了 GenManip/EBench 本地任务渲染门禁。
  • @@ -890,7 +893,7 @@

    静态资产/layout 正规化

    P1b

    open_door 视角和门缝增强

    -

    物理爆值和关闭位问题已通过 DryingBox surrogate 与关节目标回放解决;把手从“大橙色块”缩成细把手后,最新正面图已经让门板、框架和动作点可识别,且 render_validation.passed=true

    +

    物理爆值和关闭位问题已在 P1 sanitized_surrogate 对照组里通过关节目标回放解决;把手从“大橙色块”缩成细把手后,最新正面图已经让门板、框架和动作点可识别,且 render_validation.passed=true。下一步不是继续打磨 surrogate,而是让 LabUtopia native complex DryingBox 通过同一套 gate。

    P1c @@ -898,14 +901,19 @@

    任务级相机/构图

    pick/place/open_door 已完成任务级相机与构图复验,三任务 render_validation.passed=true;后续构图调整只作为展示 polish,不作为 P1 阻断项。

    - P2 + P1d

    Eval-path 重拍验收

    已用正常 evaluator/readback 路径重新抓关键帧,写入 evidence manifest;三任务任务渲染门禁已通过。后续只做展示级 polish 或复验,不再作为 P1 阻断项。

    +
    + P2 +

    Native complex DryingBox gate

    +

    新增原生复杂资产路线:先做 asset audit,再做 native-only Isaac smoke,然后把原生 DryingBox 接回 EBench wrapper,只允许 additive physics override 修 root scale、mass/inertia、body rel、joint frame、reset target 和非任务 DOF。

    +
    P3

    Lift2 资产预检

    -

    camera readback、资产/layout 和三图视觉 QA 通过后,再构建复合资产根目录:LabUtopia scene overlay + 默认 robot_usds/lift2 + 默认 miscs/curobo

    +

    Native complex DryingBox gate 通过后,再构建复合资产根目录:LabUtopia scene overlay + 默认 robot_usds/lift2 + 默认 miscs/curobo

    P4 @@ -977,6 +985,16 @@

    相关文档

    P0-P2 修复计划:camera 诊断、reset 布局、eval-path 重拍和视觉 QA。 打开 + + Native DryingBox plan + P2 七步 gate:asset audit、native-only Isaac smoke、EBench wrapper、additive physics override、open_door eval readback、文档证据和 Lift2 gate。 + 打开 + + + USD articulation 门教学页 + 面向 PM 解释 surrogate baseline、native complex DryingBox、USD articulation 和 claim boundary。 + 打开 + Lift2 baseline lane plan 仿 EOS 的 P0-P4 证据门推进计划。 @@ -992,7 +1010,7 @@

    相关文档

    - + diff --git a/docs/records/evidence/2026-06-24-usd-articulation-dryingbox-tutorial/index.html b/docs/records/evidence/2026-06-24-usd-articulation-dryingbox-tutorial/index.html index 5e08342..ab69e90 100644 --- a/docs/records/evidence/2026-06-24-usd-articulation-dryingbox-tutorial/index.html +++ b/docs/records/evidence/2026-06-24-usd-articulation-dryingbox-tutorial/index.html @@ -466,6 +466,40 @@ background: var(--blue-soft); color: #17304f; } + .gate-stepper { + display: grid; + grid-template-columns: 260px minmax(0, 1fr); + gap: 14px; + margin-top: 14px; + } + .gate-list { + display: grid; + gap: 8px; + align-content: start; + } + .gate-btn { + width: 100%; + min-height: 44px; + text-align: left; + } + .gate-btn[aria-pressed="true"] { + border-color: var(--green); + background: var(--green); + color: #ffffff; + } + .gate-panel { + min-height: 220px; + } + .status-strip { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 10px; + margin-top: 12px; + } + .status-strip .metric .value { + font-size: 15px; + line-height: 1.35; + } .footnote { color: var(--muted); font-size: 13px; @@ -480,7 +514,9 @@ .grid-3, .render-compare, .source-list, - .metric-row { + .metric-row, + .gate-stepper, + .status-strip { grid-template-columns: 1fr; } .hero-stage, @@ -504,6 +540,7 @@ LabUtopia EBench