Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
34a2a15
Add graph review app for UnresolvedArenaEnvGraphSpec
qianl-nv Jun 11, 2026
0807491
fix copyright years
qianl-nv Jun 15, 2026
07c9d7f
Move folder
qianl-nv Jun 15, 2026
f84bb1c
Simplify setup
qianl-nv Jun 15, 2026
19ab3df
Refactor graph review tool into review_gui package.
qianl-nv Jun 15, 2026
8d5ecab
Rename review_gui render module to dashboard.
qianl-nv Jun 15, 2026
2989b7e
Add SimApp sidecar for fast registry-backed YAML validation in review…
qianl-nv Jun 15, 2026
56b9127
Add live USD snapshot rendering to the review GUI sidecar.
qianl-nv Jun 15, 2026
e69503a
Add prompt-driven generation to the review GUI and defer pxr on asset…
qianl-nv Jun 15, 2026
8aee26c
Reorder review GUI dashboard: nodes, graph with unary sidebar, tasks.
qianl-nv Jun 15, 2026
8bd64e8
Add 16-env sim preview rollout to the review GUI sidecar.
qianl-nv Jun 15, 2026
ab6f54a
Fix viewer look-at when relation solver sets PosePerEnv poses.
qianl-nv Jun 15, 2026
adab971
Tune sim preview: 10 steps and default scene viewport camera.
qianl-nv Jun 15, 2026
91ab7d1
Fix repeat sim preview by sharing eval-runner env teardown.
qianl-nv Jun 16, 2026
8d4601d
Run review GUI SimApp sidecar with Kit visualizer enabled.
qianl-nv Jun 16, 2026
9730161
Frame sim preview captures on the full multi-env grid overview.
qianl-nv Jun 16, 2026
5f1bce1
Fix repeat sim preview by closing env before resetting the USD stage.
qianl-nv Jun 16, 2026
1af0d55
Log sim preview phase timings before Kit scene spawn.
qianl-nv Jun 16, 2026
ce1d94b
Skip redundant sidecar validation when launching sim preview.
qianl-nv Jun 16, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,33 @@ def generate_spec(
A ``(EnvironmentIntentSpec, raw_response)`` tuple. The raw text is
useful for debugging.
"""
data, text = self.fetch_intent_from_prompt(
prompt,
asset_catalog=asset_catalog,
relation_catalog=relation_catalog,
task_catalog=task_catalog,
temperature=temperature,
max_tokens=max_tokens,
max_retries=max_retries,
)
spec = EnvironmentIntentSpec.model_validate(data)
return spec, text

def fetch_intent_from_prompt(
self,
prompt: str,
asset_catalog: AssetCatalogue | None = None,
relation_catalog: RelationCatalogue | None = None,
task_catalog: TaskCatalogue | None = None,
temperature: float = 0.2,
max_tokens: int = 4096,
max_retries: int = 3,
) -> tuple[dict[str, Any], str]:
"""Call the model and return parsed intent JSON without registry validation.

Registry-backed :class:`EnvironmentIntentSpec` validation should run in a
SimApp process (e.g. the review GUI sidecar) where registries are warm.
"""
asset_catalog = asset_catalog or build_asset_catalogue()
relation_catalog = relation_catalog or build_relation_catalogue()
task_catalog = task_catalog or build_task_catalogue()
Expand All @@ -268,7 +295,7 @@ def generate_spec(
last_exc: Exception | None = None
for attempt in range(1 + max_retries):
if attempt > 0:
print(f"[generate_spec] retry {attempt}/{max_retries} after: {last_exc}", flush=True)
print(f"[fetch_intent_from_prompt] retry {attempt}/{max_retries} after: {last_exc}", flush=True)

try:
resp = self.client.chat.completions.create(
Expand Down Expand Up @@ -299,8 +326,7 @@ def generate_spec(
# (e.g. literal tabs) inside JSON strings — DeepSeek-v4-flash is known
# to emit these.
data = json.loads(text, strict=False)
spec = EnvironmentIntentSpec.model_validate(data)
return spec, text
return data, text
except Exception as exc:
last_exc = exc

Expand Down
13 changes: 10 additions & 3 deletions isaaclab_arena/assets/object.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,9 @@
from isaaclab.sim.spawners.spawner_cfg import SpawnerCfg

from isaaclab_arena.assets.object_base import ObjectBase, ObjectType
from isaaclab_arena.assets.object_utils import detect_object_type
from isaaclab_arena.relations.relations import RelationBase
from isaaclab_arena.utils.bounding_box import AxisAlignedBoundingBox, quaternion_to_90_deg_z_quarters
from isaaclab_arena.utils.pose import Pose
from isaaclab_arena.utils.usd.rigid_bodies import find_shallowest_rigid_body
from isaaclab_arena.utils.usd_helpers import compute_local_bounding_box_from_usd, has_light, open_stage


class Object(ObjectBase):
Expand Down Expand Up @@ -49,6 +46,8 @@ def __init__(
"object_type is None (indicating auto-detect) but usd_path is also None. usd_path is required to detect"
" object type"
)
from isaaclab_arena.assets.object_utils import detect_object_type

object_type = detect_object_type(usd_path=usd_path)
super().__init__(name=name, prim_path=prim_path, object_type=object_type, **kwargs)
self.usd_path = usd_path
Expand All @@ -69,6 +68,8 @@ def add_relation(self, relation: RelationBase) -> None:

def get_bounding_box(self) -> AxisAlignedBoundingBox:
"""Get local bounding box (relative to object origin)."""
from isaaclab_arena.utils.usd_helpers import compute_local_bounding_box_from_usd

assert self.usd_path is not None
if self.bounding_box is None:
self.bounding_box = compute_local_bounding_box_from_usd(self.usd_path, self.scale)
Expand All @@ -88,6 +89,8 @@ def get_world_bounding_box(self) -> AxisAlignedBoundingBox:
return local_bbox.rotated_90_around_z(quarters).translated(self.initial_pose.position_xyz)

def get_corners(self, pos: torch.Tensor) -> torch.Tensor:
from isaaclab_arena.utils.usd_helpers import compute_local_bounding_box_from_usd

assert self.usd_path is not None
if self.bounding_box is None:
self.bounding_box = compute_local_bounding_box_from_usd(self.usd_path, self.scale)
Expand All @@ -107,6 +110,8 @@ def enable_reset_pose(self) -> None:
def get_contact_sensor_cfg(
self, contact_against_object: ObjectBase | None = None, usd_path: str | None = None
) -> ContactSensorCfg:
from isaaclab_arena.utils.usd.rigid_bodies import find_shallowest_rigid_body

assert self.object_type == ObjectType.RIGID, "Contact sensor is only supported for rigid objects"
# We override this function from the parent class because in some assets, the rigid body
# is not at the root of the USD file. To be robust to this, we find the shallowest rigid body
Expand Down Expand Up @@ -177,6 +182,8 @@ def _generate_articulation_cfg(self) -> ArticulationCfg:
return self._add_initial_pose_to_cfg(object_cfg)

def _generate_base_cfg(self) -> AssetBaseCfg:
from isaaclab_arena.utils.usd_helpers import has_light, open_stage

assert self.object_type == ObjectType.BASE
if self.spawner_cfg is None:
with open_stage(self.usd_path) as stage:
Expand Down
2 changes: 1 addition & 1 deletion isaaclab_arena/assets/object_library.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
from isaaclab_arena.assets.lightwheel_lazy import LightwheelLazyPath
from isaaclab_arena.assets.object import Object
from isaaclab_arena.assets.object_base import ObjectType
from isaaclab_arena.assets.object_utils import (
from isaaclab_arena.assets.object_spawn_defaults import (
EMPTY_ARTICULATION_INIT_STATE_CFG,
RIGID_BODY_PROPS_HIGH_PRECISION,
RIGID_BODY_PROPS_MEDIUM_PRECISION,
Expand Down
46 changes: 46 additions & 0 deletions isaaclab_arena/assets/object_spawn_defaults.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# Copyright (c) 2025-2026, The Isaac Lab Arena Project Developers (https://github.com/isaac-sim/IsaacLab-Arena/blob/main/CONTRIBUTORS.md).
# All rights reserved.
#
# SPDX-License-Identifier: Apache-2.0

"""Spawn/physics defaults for library objects (no USD / pxr imports)."""

import isaaclab.sim as sim_utils
from isaaclab.assets import ArticulationCfg

# Predefined rigid body property configurations for assembly tasks
# High iteration count for precision tasks (peg/hole insertion)
RIGID_BODY_PROPS_HIGH_PRECISION = sim_utils.RigidBodyPropertiesCfg(
disable_gravity=False,
max_depenetration_velocity=5.0,
linear_damping=0.0,
angular_damping=0.0,
max_linear_velocity=1000.0,
max_angular_velocity=3666.0,
enable_gyroscopic_forces=True,
solver_position_iteration_count=192,
solver_velocity_iteration_count=1,
max_contact_impulse=1e32,
)

# Standard iteration count for gear mesh tasks
RIGID_BODY_PROPS_MEDIUM_PRECISION = sim_utils.RigidBodyPropertiesCfg(
disable_gravity=False,
max_depenetration_velocity=5.0,
linear_damping=0.0,
angular_damping=0.0,
max_linear_velocity=1000.0,
max_angular_velocity=3666.0,
enable_gyroscopic_forces=True,
solver_position_iteration_count=32,
solver_velocity_iteration_count=32,
max_contact_impulse=1e32,
)

# Initial state configuration for articulations without joints (e.g., rigid bodies treated as articulations).
# We explicitly set joint_pos and joint_vel to empty dicts to avoid the default pattern {".*": 0.0} in ArticulationCfg.InitialStateCfg,
# which would fail to match when there are no joints in the articulation.
EMPTY_ARTICULATION_INIT_STATE_CFG = ArticulationCfg.InitialStateCfg(
joint_pos={},
joint_vel={},
)
65 changes: 22 additions & 43 deletions isaaclab_arena/assets/object_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,28 @@
# SPDX-License-Identifier: Apache-2.0


import isaaclab.sim as sim_utils
from isaaclab.assets import ArticulationCfg
from pxr import Usd
from typing import TYPE_CHECKING

from isaaclab_arena.assets.object_base import ObjectType
from isaaclab_arena.utils.usd_helpers import get_prim_depth, is_articulation_root, is_rigid_body
from isaaclab_arena.assets.object_spawn_defaults import (
EMPTY_ARTICULATION_INIT_STATE_CFG,
RIGID_BODY_PROPS_HIGH_PRECISION,
RIGID_BODY_PROPS_MEDIUM_PRECISION,
)

if TYPE_CHECKING:
from pxr import Usd

# Re-export spawn defaults for backward compatibility.
__all__ = [
"EMPTY_ARTICULATION_INIT_STATE_CFG",
"RIGID_BODY_PROPS_HIGH_PRECISION",
"RIGID_BODY_PROPS_MEDIUM_PRECISION",
"detect_object_type",
]


def detect_object_type(usd_path: str | None = None, stage: Usd.Stage | None = None) -> ObjectType:
def detect_object_type(usd_path: str | None = None, stage: "Usd.Stage | None" = None) -> ObjectType:
"""Detect the object type of the asset

Goes through the USD tree and detects the object type. The detection is based
Expand All @@ -28,6 +41,10 @@ def detect_object_type(usd_path: str | None = None, stage: Usd.Stage | None = No
Returns:
The object type of the asset.
"""
from pxr import Usd

from isaaclab_arena.utils.usd_helpers import get_prim_depth, is_articulation_root, is_rigid_body

assert usd_path is not None or stage is not None, "Either usd_path or stage must be provided"
assert usd_path is None or stage is None, "Either usd_path or stage must be provided"
if usd_path is not None:
Expand Down Expand Up @@ -62,41 +79,3 @@ def detect_object_type(usd_path: str | None = None, stage: Usd.Stage | None = No
return ObjectType.ARTICULATION
else:
raise ValueError("This should not happen. There is an unknown USD type in the tree.")


# Predefined rigid body property configurations for assembly tasks
# High iteration count for precision tasks (peg/hole insertion)
RIGID_BODY_PROPS_HIGH_PRECISION = sim_utils.RigidBodyPropertiesCfg(
disable_gravity=False,
max_depenetration_velocity=5.0,
linear_damping=0.0,
angular_damping=0.0,
max_linear_velocity=1000.0,
max_angular_velocity=3666.0,
enable_gyroscopic_forces=True,
solver_position_iteration_count=192,
solver_velocity_iteration_count=1,
max_contact_impulse=1e32,
)

# Standard iteration count for gear mesh tasks
RIGID_BODY_PROPS_MEDIUM_PRECISION = sim_utils.RigidBodyPropertiesCfg(
disable_gravity=False,
max_depenetration_velocity=5.0,
linear_damping=0.0,
angular_damping=0.0,
max_linear_velocity=1000.0,
max_angular_velocity=3666.0,
enable_gyroscopic_forces=True,
solver_position_iteration_count=32,
solver_velocity_iteration_count=32,
max_contact_impulse=1e32,
)

# Initial state configuration for articulations without joints (e.g., rigid bodies treated as articulations).
# We explicitly set joint_pos and joint_vel to empty dicts to avoid the default pattern {".*": 0.0} in ArticulationCfg.InitialStateCfg,
# which would fail to match when there are no joints in the articulation.
EMPTY_ARTICULATION_INIT_STATE_CFG = ArticulationCfg.InitialStateCfg(
joint_pos={},
joint_vel={},
)
18 changes: 16 additions & 2 deletions isaaclab_arena/environments/arena_env_graph_spec.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from pathlib import Path
from typing import TYPE_CHECKING, Any, Self

from pydantic import BaseModel, Field, SerializeAsAny, field_validator, model_validator
from pydantic import BaseModel, Field, SerializeAsAny, ValidationInfo, field_validator, model_validator

from isaaclab_arena.assets.registries import TaskRegistry
from isaaclab_arena.environments.arena_env_graph_types import (
Expand Down Expand Up @@ -83,6 +83,18 @@ def write_yaml(self, path: str | Path) -> None:
with Path(path).open("w", encoding="utf-8") as f:
yaml.safe_dump(self.to_dict(), f, sort_keys=False)

def to_yaml(self, path: str | Path) -> Path:
"""Write this spec to ``path`` as YAML. Creates parent dirs as needed.

Returns the resolved :class:`Path` written. Symmetric with
:meth:`from_yaml`.
"""
out_path = Path(path)
out_path.parent.mkdir(parents=True, exist_ok=True)
with out_path.open("w", encoding="utf-8") as f:
yaml.safe_dump(self.to_dict(), f, sort_keys=False)
return out_path

@property
def nodes_by_id(self) -> dict[str, ArenaEnvGraphNodeSpec]:
return {node.id: node for node in self.nodes}
Expand Down Expand Up @@ -158,8 +170,10 @@ class ArenaEnvInitialGraphSpec(ArenaEnvGraphSpecBase):
initial_state_spec: ArenaEnvGraphStateSpec

@model_validator(mode="after")
def validate(self) -> Self:
def validate(self, info: ValidationInfo) -> Self:
"""Check unique IDs, constraint references, and spatial constraint shapes."""
if info.context and info.context.get("skip_registry"):
return self
assert_unique_ids(self.nodes, [], [self.initial_state_spec])
assert_constraint_references(self.nodes, [self.initial_state_spec])
assert_spatial_constraint_shapes([self.initial_state_spec])
Expand Down
10 changes: 7 additions & 3 deletions isaaclab_arena/environments/arena_env_graph_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
from enum import Enum
from typing import Any

from pydantic import BaseModel, Field, field_validator, model_validator
from pydantic import BaseModel, Field, ValidationInfo, field_validator, model_validator

from isaaclab_arena.assets.object_type import ObjectType
from isaaclab_arena.assets.registries import ObjectRelationLibraryRegistry, TaskRegistry
Expand Down Expand Up @@ -115,7 +115,9 @@ class TaskSpec(BaseModel):

@field_validator("kind")
@classmethod
def _validate_registered_task_type(cls, value: str) -> str:
def _validate_registered_task_type(cls, value: str, info: ValidationInfo) -> str:
if info.context and info.context.get("skip_registry"):
return value
registry = TaskRegistry()
assert registry.is_registered(value), f"Unknown task kind '{value}'"
return value
Expand Down Expand Up @@ -169,7 +171,9 @@ class SpatialRelationSpec(BaseModel):
)

@model_validator(mode="after")
def _validate_kind_and_arity(self) -> SpatialRelationSpec:
def _validate_kind_and_arity(self, info: ValidationInfo) -> SpatialRelationSpec:
if info.context and info.context.get("skip_registry"):
return self
registry = ObjectRelationLibraryRegistry()
assert registry.is_registered(self.kind), f"Unknown relation kind '{self.kind}'"
relation_cls = registry.get_object_relation_by_name(self.kind)
Expand Down
27 changes: 8 additions & 19 deletions isaaclab_arena/evaluation/eval_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,12 @@

import argparse
import dataclasses
import gc
import json
import math
import os
import subprocess
import sys
import tempfile
import torch
import traceback
from gymnasium.wrappers import RecordVideo
from pathlib import Path
Expand All @@ -24,7 +22,11 @@
from isaaclab_arena.evaluation.policy_runner import get_policy_cls, rollout_policy
from isaaclab_arena.metrics.aggregate_metrics import aggregate_metrics
from isaaclab_arena.metrics.metrics_logger import MetricsLogger
from isaaclab_arena.utils.isaaclab_utils.simulation_app import SimulationAppContext, teardown_simulation_app
from isaaclab_arena.utils.isaaclab_utils.simulation_app import (
SimulationAppContext,
close_env_and_reset_sim,
collect_garbage_and_clear_cuda_cache,
)
from isaaclab_arena_environments.cli import get_arena_builder_from_cli, get_isaaclab_arena_environments_cli_parser

if TYPE_CHECKING:
Expand Down Expand Up @@ -111,31 +113,18 @@ def get_policy_from_job(job: Job) -> "PolicyBase":
return policy


def _collect_garbage_and_clear_cuda_cache() -> None:
gc.collect()
if torch.cuda.is_available():
torch.cuda.empty_cache()


def _close_policy(policy: "PolicyBase | None") -> None:
try:
if policy is not None:
policy.close()
finally:
_collect_garbage_and_clear_cuda_cache()
collect_garbage_and_clear_cuda_cache()


def _close_env(env) -> None:
if env is None:
return
try:
teardown_simulation_app(suppress_exceptions=False, make_new_stage=True)
finally:
try:
# cleanup managers, including recorder manager closing hdf5 file
env.close()
finally:
_collect_garbage_and_clear_cuda_cache()
close_env_and_reset_sim(env)


def _close_job_resources(policy: "PolicyBase | None", env) -> None:
Expand Down Expand Up @@ -326,7 +315,7 @@ def main():
finally:
policy = None
env = None
_collect_garbage_and_clear_cuda_cache()
collect_garbage_and_clear_cuda_cache()

# Aggregate the metrics from the different experiments into a single view.
if metrics_per_run:
Expand Down
Loading
Loading