Add EnvGraphSpec to ArenaEnv method#724
Conversation
There was a problem hiding this comment.
Code Review
Thanks for adding the to_arena_env() method and the comprehensive conversion utilities! This is a substantial addition that bridges the gap between declarative graph specs and runtime environment instances. Below are my observations:
✅ Strengths
-
Clean API Design: The
to_arena_env()method onArenaEnvGraphSpecprovides a natural, discoverable entry point. TheTYPE_CHECKINGguard for the import keeps the module dependency graph clean. -
Extensible Task Resolution: The task factories mechanism allows custom task types while still supporting class name and module discovery fallbacks. The resolution order (import path → class name → module stem) is intuitive.
-
Well-Structured Utilities: The conversion logic is broken into focused helper functions (
_select_state_spec,_instantiate_node_assets,_apply_state_spatial_constraints, etc.), making the code readable and testable. -
Good Test Coverage: The new test case exercises sequential task composition and validates the full conversion pipeline through a realistic YAML fixture.
🔍 Suggestions & Questions
1. Consider documenting the task_args aliasing behavior
The _align_task_kwargs function does clever fuzzy matching of YAML keys to constructor parameters (lines 357-390). While useful for YAML ergonomics, this implicit behavior could surprise users when debugging. Consider either:
- Adding a note in docstrings about which aliases are supported, or
- Logging/warning when a fuzzy match is used
2. _discover_task_classes does package-wide scanning
If the direct module candidates fail, the fallback scans all modules under isaaclab_arena.tasks. This could become slow as the task library grows. Is caching viable here, or is this path rarely hit in practice?
3. Potential edge case in _match_task_arg_to_parameter
The substring matching (normalized_key in _normalize_identifier(name)) could yield unexpected results for short keys. For example, a task arg "x" would match any parameter containing the letter "x". The narrowing to required params helps, but it might be worth documenting or tightening this.
4. The IN spatial constraint raises NotImplementedError
Noticed that ArenaEnvGraphSpatialConstraintType.IN is defined but raises at conversion time. The TODO comment references May 21—is there a tracking issue for this, or should one be created?
5. Minor import ordering
In arena_env_graph_spec.py, the diff shows yaml import moved below from collections.abc which deviates from the typical stdlib-first ordering in this codebase. Not blocking, just a style nit.
🧪 Testing
The new test test_arena_env_graph_conversion_builds_sequential_pick_and_place_task is well-structured. One suggestion: consider adding a negative test for the IN constraint NotImplementedError to lock in the expected behavior until solver support lands.
Overall, this is a solid PR that adds valuable functionality. The suggestions above are minor refinements. Happy to discuss any of these points!
Update (53a0a44)
Thanks for the follow-up commit! This update addresses several important areas:
✅ What's Improved
-
API Simplification: Removed the
task_factoriesparameter fromto_arena_env()andbuild_arena_env_from_graph_spec(). This simplifies the public API—users now rely on task class discovery rather than custom factories. -
Validation Before Instantiation: Added comprehensive pre-conversion validation via
_validate_graph_spec_for_conversion():_validate_spatial_constraint_shapes()ensures proper child node arity for each constraint type_validate_task_arg_node_references()catches dangling node references in task args early_validate_initial_state_supported_for_conversion()prevents unsupportedINconstraints from reaching the runtime
-
Expanded Test Coverage: Three new validation tests added:
test_arena_env_graph_conversion_validates_references_before_instantiationtest_arena_env_graph_conversion_validates_relationship_shape_before_instantiationtest_arena_env_graph_conversion_validates_task_arg_node_references_before_instantiation
-
Code Consolidation:
- Unified
_normalize_task_class_nameand_normalize_task_module_nameinto single_normalize_task_name() - Cleaned up the test data by consolidating
two_pick_and_place_maple_table_env_graph.yamlinto the existing test fixture - Tightened error messages for better debugging
- Unified
-
TYPE_CHECKING Guards: Expanded use of
TYPE_CHECKINGimports in the conversion utils to keep the module lean at runtime.
🔍 Minor Observations
- The validation now catches my earlier suggestion #4 (the
INconstraint issue) with a clear assertion before hitting the runtimeNotImplementedError. - Good use of early-exit validation pattern—fail fast with descriptive errors rather than cryptic runtime failures during asset instantiation.
🧪 Test Fixture Updates
The pick_and_place_maple_table_env_graph.yaml now includes the second task and third state spec inline, which makes the sequential task test self-contained. The removed two_pick_and_place_maple_table_env_graph.yaml was redundant.
No blocking concerns. This is a nice cleanup that improves both usability and debuggability. 👍
Update (f28226a): Reviewed 4 new commits (03410c8 → f28226a).
Changes Since Last Review
These commits are a significant cleanup and simplification pass:
-
API Simplification: Removed
state_spec_idparameter fromto_arena_env()— now always uses the first state spec. This is cleaner but removes flexibility; the TODO notes a future aggregation approach. -
Refactored
is_unary()in Relations: Movedis_unary()fromRelationBaseto individual subclasses (UnaryRelation,IsAnchor,RandomAroundSolution,RotateAroundSolution).⚠️ Note: If external code subclassesRelationBasedirectly without implementingis_unary(), it will now fail withAttributeError. Since these relations are likely internal, this is probably fine, but worth a note in release/migration docs if public. -
Removed Helper Indirection:
_select_state_spec(),_select_embodiment(), and_relation_params_for_constraint()are gone — logic is inlined or moved upstream. The upstream contract now expectsconstraint.paramsto arrive in constructor-ready form (no more Side enum conversion or position_xyz unpacking). -
Hoisted Imports: Several imports moved from inside functions to module-level. Trades laziness for clarity; should be fine given these modules are already in use when conversion runs.
-
Extensive Docstring Improvements:
arena_env_graph_task_conversion_utils.pyandgraph_spec_utils.pynow have detailed docstrings with examples. Nice for discoverability. -
Test Adjustment: Removed
test_arena_env_graph_conversion_validates_selected_initial_state_before_instantiationsince thestate_spec_idparam no longer exists.
Assessment
No new bugs spotted. The simplification is reasonable — removing state_spec_id reduces API surface while the TODO signals the intended future direction. The is_unary() change is the most notable from a compatibility standpoint but appears safe for internal usage.
✅ New commits look good.
Update (4d10198): Reviewed incremental changes.
Changes
- Inlined helper functions: Removed
_child_asset(),_add_relation(),_set_initial_pose()— logic now directly calls asset methods. Cleaner code, relies on upstream validation to catch issues early. - Single-pass
OBJECT_REFERENCEinstantiation: Consolidated into main loop with documented upstream contract (nodes must be ordered parent-first). Avoids second iteration. - Module-level imports: Hoisted
isaaclab_arena.tasks,SequentialTaskBase,TaskBaseimports. - Minor: Removed unused
ArenaEnvGraphSpatialConstraintSpecimport and redundanttuple()wrapping.
Assessment
Continuation of the cleanup theme. No new issues introduced. ✅
Update (b0ae704): Reviewed incremental changes.
Changes
This commit is a major API simplification that removes the fuzzy/loose matching behavior:
-
Removed fuzzy
task_argsmatching —_align_task_kwargs()and related helpers are gone. Task arg keys must now exactly match constructor parameter names. -
Removed task type loose resolution —
_normalize_task_name(),_task_module_candidates(), and module-stem matching removed. Tasktype:in YAML must now be the exact class name (e.g.,PickAndPlaceTask) or a qualified import path. -
Removed auto-injection of
instance_name— upstream must now emitinstance_nameexplicitly innode.paramswhen needed. -
Removed
assert_task_arg_node_references_exist()validation — node reference validation now happens at runtime rather than spec construction. -
Test fixtures updated to use exact names (
pick_up_objectinstead ofobject,PickAndPlaceTaskinstead ofpick_and_place).
Assessment
This directly addresses my earlier suggestions (#1-#3 from the original review) by removing the complexity entirely rather than documenting it. The strict upstream contract approach is cleaner and more predictable.
No new issues. ✅
Update (ced545a): Reviewed incremental changes.
Changes
This commit is a refactoring to break circular imports by extracting types into a dedicated module:
-
New
arena_env_graph_types.pymodule: Contains all the pure-data schema types (enums and dataclasses) that were previously inarena_env_graph_spec.py:- Enums:
ArenaEnvGraphNodeType,ArenaEnvGraphSpatialConstraintType,ArenaEnvGraphTaskConstraintType - Dataclasses:
ArenaEnvGraphNodeSpec,ArenaEnvGraphObjectReferenceNodeSpec,ArenaEnvGraphSpatialConstraintSpec,ArenaEnvGraphTaskConstraintSpec,ArenaEnvGraphStateSpec,ArenaEnvGraphTaskSpec
- Enums:
-
arena_env_graph_spec.pyslimmed down: Now imports/re-exports types via__all__for backward compatibility. Thebuild_arena_env_from_graph_specimport is hoisted to module level (removing the lazy import into_arena_env()). -
Import path updates:
arena_env_graph_conversion_utils.py,arena_env_graph_task_conversion_utils.py, andgraph_spec_utils.pynow import types from the newarena_env_graph_typesmodule. -
Test fixture adjustments: Node indices updated in validation tests due to node reordering (background now at index 1, object_reference at index 2). Task type updated to exact name
PickAndPlaceTask.
Assessment
Clean separation of concerns:
- Types module: Pure data (no behavior, no dependencies on conversion logic)
- Spec module: Parsing, validation, and conversion entry point
This breaks the potential circular import chain (conversion_utils → spec → conversion_utils) while maintaining backward compatibility via re-exports. No new issues. ✅
There was a problem hiding this comment.
Thanks @xyao-nv for the draft. Overall looks ok, but isaaclab_arena/environments/arena_env_graph_conversion_utils.py is a long file and could potentially be clean up to make it simpler. Especially in terms of spec validation, many checks should happen at the spec construction step, not at the spec->arena env conversion step.
| """Validate relationship endpoints and arity.""" | ||
| from isaaclab_arena.environments.arena_env_graph_spec import ArenaEnvGraphSpatialConstraintType | ||
|
|
||
| child_required_types = { |
There was a problem hiding this comment.
should we build a map from ArenaEnvGraphSpatialConstraintType to the actual relation class (in the relation.py files), and for the relation classes to have a static method in_unary(). would be easier to keep track then keeping a map here.
this might also solve the not_supported types in _validate_initial_state_supported_for_conversion cleaner.
|
Changes made:
|
| subtasks = [_build_task(task_spec, assets_by_id) for task_spec in task_specs] | ||
| if len(subtasks) == 1: | ||
| return subtasks[0] | ||
| return SequentialTaskBase(subtasks=subtasks, desired_subtask_success_state=[True for _ in subtasks]) |
There was a problem hiding this comment.
self review: maybe it will change based on what subtask tracking brings
| def _instantiate_node_assets(nodes: list[ArenaEnvGraphNodeSpec], asset_registry: Any) -> dict[str, Any]: | ||
| """Create concrete asset entities for graph nodes and wire object-reference nodes. | ||
|
|
||
| Upstream contract: nodes are ordered so an OBJECT_REFERENCE appears after its parent — |
There was a problem hiding this comment.
self review: think about how to enforce those contract in upstream
|
|
||
| Two forms are supported: | ||
| * Qualified import path — ``pkg.module:Class`` or ``pkg.module.Class``. | ||
| * Bare class name — searched for in the tasks package by exact ``__name__``. |
There was a problem hiding this comment.
self review: I wonder if it shall be enforced to only use bare class name, as import path may be changed
| @@ -0,0 +1,137 @@ | |||
| # Copyright (c) 2026, The Isaac Lab Arena Project Developers (https://github.com/isaac-sim/IsaacLab-Arena/blob/main/CONTRIBUTORS.md). | |||
There was a problem hiding this comment.
note: purely mv stuffs around, so this file only contains data, no orchestration/method.
| """ | ||
| # TODO(xinjieyao, 2026-05-26): once `build_arena_env_from_graph_spec` aggregates across all state_specs, | ||
| # this wrapper stays single-arg — no caller-side selection is needed. | ||
| return build_arena_env_from_graph_spec(self) |
There was a problem hiding this comment.
note: there are too many detailed fun calls, so I avoid spamming the class method.
alexmillane
left a comment
There was a problem hiding this comment.
A couple of nits, but my main comment is that we should add registries for tasks and constraints to avoid some stuff in here that makes the graph decoding not extensible out-of-source.
| if node.type == ArenaEnvGraphNodeType.EMBODIMENT: | ||
| assert embodiment is None, "Only one embodiment node can be converted to an IsaacLabArenaEnvironment" | ||
| embodiment = assets_by_id[node.id] | ||
| elif node.type in (ArenaEnvGraphNodeType.OBJECT, ArenaEnvGraphNodeType.OBJECT_REFERENCE): | ||
| scene_assets.append(assets_by_id[node.id]) |
There was a problem hiding this comment.
Shall we add a final else?
Perhaps right now the left over nodes are not handled? We could add a warning for now?
| if node.type == ArenaEnvGraphNodeType.OBJECT_REFERENCE: | ||
| assert isinstance(node, ArenaEnvGraphObjectReferenceNodeSpec) | ||
| assets_by_id[node.id] = ObjectReference( | ||
| name=node.name, | ||
| prim_path=node.prim_path, | ||
| parent_asset=assets_by_id[node.parent], | ||
| object_type=node.object_type, | ||
| **node.params, | ||
| ) | ||
| continue | ||
| asset_cls = asset_registry.get_asset_by_name(node.name) | ||
| assets_by_id[node.id] = asset_cls(**node.params) |
There was a problem hiding this comment.
nit: suggestion to use if..else, rather than if...continue.
For some reason claude likes to do this, but I (perhaps personally) feel that if...else is a far more normal construction.
| Upstream contract: | ||
| * Nodes are ordered so an OBJECT_REFERENCE appears after its parent — a single pass is | ||
| enough; the parent lookup in `assets_by_id` would `KeyError` otherwise. | ||
| * `node.params` is emitted in the asset constructor's exact kwarg form, including any | ||
| `instance_name` needed to disambiguate duplicate assets. | ||
| """ |
There was a problem hiding this comment.
Suggestion to check this (probably in a function).
| assert embodiment is None, "Only one embodiment node can be converted to an IsaacLabArenaEnvironment" | ||
| embodiment = assets_by_id[node.id] | ||
| elif node.type in (ArenaEnvGraphNodeType.OBJECT, ArenaEnvGraphNodeType.OBJECT_REFERENCE): | ||
| scene_assets.append(assets_by_id[node.id]) |
There was a problem hiding this comment.
nit: move to a function so this function (assets_to_scene_and_embodiment)
| _apply_state_spatial_constraints(state_spec, assets_by_id) | ||
|
|
||
| embodiment = None | ||
| scene_assets: list[Any] = [] |
| assert position_xyz is not None, f"at_pose constraint '{constraint.id}' requires params.position_xyz" | ||
| assert not params, f"Unsupported at_pose params for constraint '{constraint.id}': {sorted(params)}" | ||
| parent_asset.set_initial_pose(Pose(position_xyz=position_xyz, rotation_xyzw=rotation_xyzw)) | ||
| continue |
There was a problem hiding this comment.
prefer if...else over if...continue.
| @cache | ||
| def spatial_constraint_relation_classes() -> dict[Any, type[Any]]: | ||
| """Map graph spatial constraint types to the relation classes that implement them.""" | ||
| from isaaclab_arena.environments.arena_env_graph_types import ArenaEnvGraphSpatialConstraintType | ||
| from isaaclab_arena.relations.relations import ( | ||
| AtPosition, | ||
| IsAnchor, | ||
| NextTo, | ||
| On, | ||
| PositionLimits, | ||
| RandomAroundSolution, | ||
| RotateAroundSolution, | ||
| ) | ||
|
|
||
| return { | ||
| ArenaEnvGraphSpatialConstraintType.IS_ANCHOR: IsAnchor, | ||
| ArenaEnvGraphSpatialConstraintType.NEXT_TO: NextTo, | ||
| ArenaEnvGraphSpatialConstraintType.ON: On, | ||
| ArenaEnvGraphSpatialConstraintType.AT_POSITION: AtPosition, | ||
| ArenaEnvGraphSpatialConstraintType.POSITION_LIMITS: PositionLimits, | ||
| ArenaEnvGraphSpatialConstraintType.RANDOM_AROUND_SOLUTION: RandomAroundSolution, | ||
| ArenaEnvGraphSpatialConstraintType.ROTATE_AROUND_SOLUTION: RotateAroundSolution, | ||
| } |
There was a problem hiding this comment.
Is there a reason to not make this a module level constant? I believe that would be more normal
SPATIAL_CONSTRAINT_RELATION_CLASSES={
ArenaEnvGraphSpatialConstraintType.IS_ANCHOR: IsAnchor,
...
}
| Upstream contract: `task_args` keys are emitted in the task constructor's exact parameter | ||
| names — no loose / case-insensitive matching is performed here. | ||
| """ | ||
| task_cls = _resolve_task_class(task_spec.type) |
There was a problem hiding this comment.
Elsewhere in arena we deal with the "string to object" problem via a registry.
We have a few registries in registries.py.
If you compare your code for decoding the scene to the code here for decoding the tasks, there's a big difference in complexity. Furthermore, the code here is not extendable by a user who wants to add tasks from outside the source-tree, because we only search for tasks effectively in a single folder.
I suggest we move to a registry for tasks. Effectively this just associates a string name to each task, and gives us a nice interface for looking them up.
What do you think?
| IS_ANCHOR = "is_anchor" | ||
| NEXT_TO = "next_to" | ||
| ON = "on" | ||
| AT_POSE = "at_pose" # through set_initial_pose() |
There was a problem hiding this comment.
At some point we'll have to clean this up and add a relation for this 😅
Let's do that later.
| def spatial_constraint_relation_classes() -> dict[Any, type[Any]]: | ||
| """Map graph spatial constraint types to the relation classes that implement them.""" | ||
| from isaaclab_arena.environments.arena_env_graph_types import ArenaEnvGraphSpatialConstraintType | ||
| from isaaclab_arena.relations.relations import ( | ||
| AtPosition, | ||
| IsAnchor, | ||
| NextTo, | ||
| On, | ||
| PositionLimits, | ||
| RandomAroundSolution, | ||
| RotateAroundSolution, | ||
| ) | ||
|
|
||
| return { | ||
| ArenaEnvGraphSpatialConstraintType.IS_ANCHOR: IsAnchor, | ||
| ArenaEnvGraphSpatialConstraintType.NEXT_TO: NextTo, | ||
| ArenaEnvGraphSpatialConstraintType.ON: On, | ||
| ArenaEnvGraphSpatialConstraintType.AT_POSITION: AtPosition, | ||
| ArenaEnvGraphSpatialConstraintType.POSITION_LIMITS: PositionLimits, | ||
| ArenaEnvGraphSpatialConstraintType.RANDOM_AROUND_SOLUTION: RandomAroundSolution, | ||
| ArenaEnvGraphSpatialConstraintType.ROTATE_AROUND_SOLUTION: RotateAroundSolution, |
There was a problem hiding this comment.
One issue here is that it requires a user to maintain the relationship between constraints in the relations folder and this static struct here.
That's not ideal, although not a huge problem for us. The issue is that if someone wants to extend arena with new relations. They can do that on the relations side, but they can't do it here, because this mapping is hard coded.
I think the correction solution here is to have a constraint registry. Such a registry provides a mapping between a string name, and a constrain object. Such a registry is extensible by the user. It also eliminates the need to maintain this mapping between a name and an object - constraint declare their names.
Summary
Convert EnvGraph to ArenaEnv
Detailed description
ArenaEnvGraphSpec.to_arena_env()so YAML graph specs can be converted intoIsaacLabArenaEnvironment.