Skip to content

Commit 3f1491e

Browse files
committed
Move typed Arena job configs into core
Keep the reusable environment and evaluation Cfg models out of the example package. Resolve concrete environment schemas through EnvironmentRegistry while retaining the Hydra runner as an example frontend. Signed-off-by: Clemens Volk <cvolk@nvidia.com>
1 parent 40d60fd commit 3f1491e

14 files changed

Lines changed: 637 additions & 350 deletions

CONTEXT.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# Isaac Lab-Arena
2+
3+
Isaac Lab-Arena defines composable robotics environments and evaluates policies against them.
4+
5+
## Language
6+
7+
**Environment Configuration**:
8+
Declarative values that specialize one environment provider for a job.
9+
_Avoid_: Hydra environment, environment wrapper
10+
11+
**Environment Provider**:
12+
A named recipe that turns an environment configuration into an assembled Arena environment.
13+
_Avoid_: Runtime environment, Hydra environment
14+
15+
**Arena Environment**:
16+
An assembled embodiment, scene, and task ready to be passed to an environment builder.
17+
_Avoid_: Environment configuration, environment provider
18+
19+
**Gym Environment**:
20+
The instantiated simulation interface used for reset and step operations.
21+
_Avoid_: Arena environment configuration
22+
23+
**Evaluation Job**:
24+
A portable specification of an environment, policy, and rollout that can be dispatched independently.
25+
_Avoid_: Suite, simulation application
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# Copyright (c) 2026, The Isaac Lab Arena Project Developers (https://github.com/isaac-sim/IsaacLab-Arena/blob/main/CONTRIBUTORS.md).
2+
# All rights reserved.
3+
#
4+
# SPDX-License-Identifier: Apache-2.0
5+
6+
"""Base configuration for registered Arena environment providers."""
7+
8+
from dataclasses import dataclass
9+
10+
from omegaconf import MISSING
11+
12+
13+
@dataclass
14+
class ArenaEnvironmentCfg:
15+
"""Configure the environment provider selected for an Arena job."""
16+
17+
name: str = MISSING
18+
"""Name used to resolve the environment through ``EnvironmentRegistry``."""
19+
enable_cameras: bool = False
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
# Copyright (c) 2026, The Isaac Lab Arena Project Developers (https://github.com/isaac-sim/IsaacLab-Arena/blob/main/CONTRIBUTORS.md).
2+
# All rights reserved.
3+
#
4+
# SPDX-License-Identifier: Apache-2.0
5+
6+
"""Arena policy-evaluation configuration and runners."""
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
# Copyright (c) 2026, The Isaac Lab Arena Project Developers (https://github.com/isaac-sim/IsaacLab-Arena/blob/main/CONTRIBUTORS.md).
2+
# All rights reserved.
3+
#
4+
# SPDX-License-Identifier: Apache-2.0
5+
6+
"""Typed configuration for Arena evaluation jobs."""
7+
8+
from __future__ import annotations
9+
10+
from dataclasses import dataclass, field
11+
from typing import Any
12+
13+
from omegaconf import MISSING
14+
15+
from isaaclab_arena.environments.arena_environment_cfg import ArenaEnvironmentCfg
16+
17+
18+
@dataclass
19+
class EnvironmentBuilderCfg:
20+
"""Configure how Arena builds the selected environment."""
21+
22+
num_envs: int = 1
23+
env_spacing: float = 30.0
24+
seed: int = 42
25+
solve_relations: bool = True
26+
placement_seed: int | None = None
27+
resolve_on_reset: bool | None = None
28+
random_yaw_init: bool = False
29+
disable_fabric: bool = False
30+
mimic: bool = False
31+
presets: str | None = None
32+
33+
def __post_init__(self) -> None:
34+
assert self.num_envs > 0, "num_envs must be greater than zero"
35+
36+
37+
@dataclass
38+
class RolloutCfg:
39+
"""Configure an evaluation rollout."""
40+
41+
num_steps: int = 2
42+
43+
def __post_init__(self) -> None:
44+
assert self.num_steps > 0, "num_steps must be greater than zero"
45+
46+
47+
@dataclass
48+
class PolicyCfg:
49+
"""Select and configure an evaluation policy."""
50+
51+
type: str = "zero_action"
52+
parameters: dict[str, Any] = field(default_factory=dict)
53+
54+
def __post_init__(self) -> None:
55+
assert self.type, "policy type must not be empty"
56+
57+
58+
@dataclass
59+
class ArenaJobCfg:
60+
"""Configure one independently dispatchable Arena evaluation job."""
61+
62+
name: str = MISSING
63+
environment: ArenaEnvironmentCfg = MISSING
64+
"""Concrete registered environment configuration selected during composition."""
65+
environment_builder: EnvironmentBuilderCfg = field(default_factory=EnvironmentBuilderCfg)
66+
policy: PolicyCfg = field(default_factory=PolicyCfg)
67+
rollout: RolloutCfg = field(default_factory=RolloutCfg)
68+
num_rebuilds: int = 1
69+
variations: dict[str, Any] = field(default_factory=dict)
70+
71+
def __post_init__(self) -> None:
72+
assert self.name, "job name must not be empty"
73+
assert self.num_rebuilds > 0, "num_rebuilds must be greater than zero"

isaaclab_arena/tests/test_hydra_configuration_example.py

Lines changed: 184 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -5,58 +5,207 @@
55

66
"""Tests for the minimal Hydra environment-configuration example."""
77

8+
from pathlib import Path
9+
810
import pytest
911
from hydra.errors import ConfigCompositionException
1012

11-
from isaaclab_arena_examples.hydra_configuration.config import ArenaRunConfiguration, compose_hydra_example_suite
12-
from isaaclab_arena_examples.hydra_configuration.pick_and_place_maple_table import (
13-
PickAndPlaceMapleTableEnvironmentConfiguration,
13+
from isaaclab_arena.assets.registries import EnvironmentRegistry
14+
from isaaclab_arena.evaluation.arena_job_cfg import ArenaJobCfg
15+
from isaaclab_arena_environments.pick_and_place_maple_table_environment import (
16+
PickAndPlaceMapleTableEnvironment,
17+
PickAndPlaceMapleTableEnvironmentCfg,
18+
)
19+
from isaaclab_arena_examples.hydra_configuration.run import (
20+
_evaluation_runner_arguments,
21+
_job_from_configuration,
22+
compose_from_command_line,
23+
compose_hydra_example_jobs,
1424
)
15-
from isaaclab_arena_examples.hydra_configuration.run import _job_from_configuration, compose_from_command_line
1625

26+
EXAMPLE_CONFIG_PATH = (
27+
Path(__file__).parents[2] / "isaaclab_arena_examples" / "hydra_configuration" / "hydra_example_suite.yaml"
28+
)
1729

18-
def test_hydra_example_suite_composes_to_concrete_environment_configuration():
19-
configuration = compose_hydra_example_suite()
2030

21-
assert isinstance(configuration, ArenaRunConfiguration)
22-
assert isinstance(configuration.environment, PickAndPlaceMapleTableEnvironmentConfiguration)
23-
assert configuration.environment.embodiment_asset_name == "droid_abs_joint_pos"
24-
assert configuration.environment.pick_up_object_asset_name == "rubiks_cube_hot3d_robolab"
25-
assert configuration.policy.type == "zero_action"
26-
assert configuration.rollout.num_steps > 0
31+
def test_hydra_example_jobs_port_variations_job_and_control():
32+
jobs = compose_hydra_example_jobs(EXAMPLE_CONFIG_PATH)
33+
34+
assert [job.name for job in jobs] == ["variations_demo", "baseline_no_variations"]
35+
assert all(isinstance(job, ArenaJobCfg) for job in jobs)
36+
assert all(isinstance(job.environment, PickAndPlaceMapleTableEnvironmentCfg) for job in jobs)
37+
assert all(job.environment.name == "pick_and_place_maple_table" for job in jobs)
38+
assert all(job.environment.enable_cameras for job in jobs)
39+
assert all(job.environment.embodiment_asset_name == "droid_rel_joint_pos" for job in jobs)
40+
assert all(job.environment.high_dynamic_range_image_name == "home_office_robolab" for job in jobs)
41+
assert all(job.environment.pick_up_object_asset_name == "rubiks_cube_hot3d_robolab" for job in jobs)
42+
assert all(job.environment.destination_location_asset_name == "bowl_ycb_robolab" for job in jobs)
43+
assert jobs[0].environment is not jobs[1].environment
44+
assert all(job.policy.type == "zero_action" for job in jobs)
45+
assert all(job.rollout.num_steps == 10 for job in jobs)
46+
assert all(job.num_rebuilds == 1 for job in jobs)
47+
assert jobs[0].variations == {
48+
"light": {
49+
"hdr_image": {"enabled": True},
50+
"intensity": {"enabled": True},
51+
},
52+
"droid_rel_joint_pos": {
53+
"camera_extrinsics_wrist_camera": {"enabled": True},
54+
},
55+
}
56+
assert jobs[1].variations == {}
57+
58+
59+
def test_hydra_example_jobs_accept_typed_shared_environment_override():
60+
jobs = compose_hydra_example_jobs(EXAMPLE_CONFIG_PATH, ["environment.light_intensity=825"])
61+
62+
assert [job.environment.light_intensity for job in jobs] == [825.0, 825.0]
63+
64+
65+
def test_hydra_example_jobs_preserve_add_operator_for_shared_override():
66+
jobs = compose_hydra_example_jobs(EXAMPLE_CONFIG_PATH, ["+policy.parameters.checkpoint=/tmp/model"])
67+
68+
assert [job.policy.parameters["checkpoint"] for job in jobs] == ["/tmp/model", "/tmp/model"]
69+
70+
71+
def test_hydra_example_cli_keeps_dispatcher_flags_out_of_job_configuration():
72+
jobs, launcher_arguments = compose_from_command_line([
73+
str(EXAMPLE_CONFIG_PATH),
74+
"--device",
75+
"cuda:1",
76+
"--viz",
77+
"kit",
78+
"rollout.num_steps=1",
79+
])
80+
eval_arguments = _evaluation_runner_arguments(
81+
jobs,
82+
launcher_arguments.device,
83+
launcher_arguments.visualizer,
84+
)
85+
86+
assert launcher_arguments.device == "cuda:1"
87+
assert launcher_arguments.visualizer == "kit"
88+
assert [job.rollout.num_steps for job in jobs] == [1, 1]
89+
assert eval_arguments.device == "cuda:1"
90+
assert eval_arguments.visualizer == "kit"
91+
assert eval_arguments.enable_cameras
2792

2893

29-
def test_hydra_example_suite_accepts_typed_environment_override():
30-
configuration = compose_hydra_example_suite(["environment.light_intensity=750"])
94+
def test_hydra_example_cli_requires_a_yaml_path():
95+
with pytest.raises(SystemExit):
96+
compose_from_command_line([])
3197

32-
assert configuration.environment.light_intensity == 750.0
3398

99+
def test_dispatcher_aggregates_camera_requirements_without_mutating_jobs():
100+
no_cameras = ArenaJobCfg(
101+
name="no_cameras",
102+
environment=PickAndPlaceMapleTableEnvironmentCfg(),
103+
)
104+
with_cameras = ArenaJobCfg(
105+
name="with_cameras",
106+
environment=PickAndPlaceMapleTableEnvironmentCfg(enable_cameras=True),
107+
)
34108

35-
def test_hydra_example_cli_maps_visualizer_and_forwards_hydra_overrides():
36-
configuration = compose_from_command_line(["--viz", "kit", "rollout.num_steps=1"])
109+
eval_arguments = _evaluation_runner_arguments([no_cameras, with_cameras], "cuda:0", None)
37110

38-
assert configuration.simulation_app.visualizer == "kit"
39-
assert configuration.rollout.num_steps == 1
111+
assert eval_arguments.enable_cameras
112+
assert not no_cameras.environment.enable_cameras
113+
assert with_cameras.environment.enable_cameras
40114

41115

42-
def test_hydra_example_suite_rejects_unknown_environment_option():
116+
def test_hydra_example_jobs_reject_unknown_environment_option():
43117
with pytest.raises(ConfigCompositionException, match="unknown_option"):
44-
compose_hydra_example_suite(["environment.unknown_option=true"])
118+
compose_hydra_example_jobs(EXAMPLE_CONFIG_PATH, ["environment.unknown_option=true"])
45119

46120

47-
def test_hydra_run_maps_to_eval_job_without_environment_cli_round_trip():
48-
configuration = compose_hydra_example_suite([
49-
"name=mapped_run",
50-
"environment_builder.num_envs=3",
51-
"policy.type=zero_action",
52-
"rollout.num_steps=7",
53-
])
121+
def test_hydra_example_jobs_apply_python_defaults_to_minimal_yaml(tmp_path):
122+
config_path = tmp_path / "minimal_jobs.yaml"
123+
config_path.write_text("""\
124+
jobs:
125+
- name: maple_table_job
126+
environment:
127+
name: pick_and_place_maple_table
128+
""")
129+
130+
jobs = compose_hydra_example_jobs(config_path)
131+
132+
assert [job.name for job in jobs] == ["maple_table_job"]
133+
assert isinstance(jobs[0].environment, PickAndPlaceMapleTableEnvironmentCfg)
134+
54135

55-
job = _job_from_configuration(configuration)
136+
def test_hydra_example_jobs_resolve_environment_configuration_through_registry():
137+
jobs = compose_hydra_example_jobs(EXAMPLE_CONFIG_PATH)
56138

57-
assert job.name == "mapped_run"
58-
assert job.num_envs == 3
59-
assert job.num_steps == 7
60-
assert job.policy_type == "zero_action"
61-
assert job.policy_config_dict == {}
62-
assert job.arena_env_args == []
139+
provider = EnvironmentRegistry().get_component_by_name(jobs[0].environment.name)
140+
141+
assert provider is PickAndPlaceMapleTableEnvironment
142+
assert provider.cfg_type is PickAndPlaceMapleTableEnvironmentCfg
143+
144+
145+
def test_hydra_example_jobs_require_an_environment_name(tmp_path):
146+
config_path = tmp_path / "missing_environment_name.yaml"
147+
config_path.write_text("""\
148+
jobs:
149+
- name: maple_table_job
150+
environment: {}
151+
""")
152+
153+
with pytest.raises(AssertionError, match="environment.name must be a string"):
154+
compose_hydra_example_jobs(config_path)
155+
156+
157+
def test_hydra_example_jobs_reject_unknown_yaml_environment_option(tmp_path):
158+
config_path = tmp_path / "invalid_jobs.yaml"
159+
config_path.write_text("""\
160+
jobs:
161+
- name: maple_table_job
162+
environment:
163+
name: pick_and_place_maple_table
164+
unknown_option: true
165+
""")
166+
167+
with pytest.raises(ConfigCompositionException, match="unknown_option"):
168+
compose_hydra_example_jobs(config_path)
169+
170+
171+
def test_hydra_example_jobs_reject_duplicate_names(tmp_path):
172+
config_path = tmp_path / "duplicate_jobs.yaml"
173+
config_path.write_text("""\
174+
jobs:
175+
- name: duplicate
176+
environment:
177+
name: pick_and_place_maple_table
178+
- name: duplicate
179+
environment:
180+
name: pick_and_place_maple_table
181+
""")
182+
183+
with pytest.raises(AssertionError, match="job names must be unique"):
184+
compose_hydra_example_jobs(config_path)
185+
186+
187+
def test_hydra_jobs_map_to_eval_jobs_without_environment_cli_round_trip():
188+
configurations = compose_hydra_example_jobs(
189+
EXAMPLE_CONFIG_PATH,
190+
[
191+
"environment_builder.num_envs=3",
192+
"policy.type=zero_action",
193+
"rollout.num_steps=7",
194+
],
195+
)
196+
197+
jobs = [_job_from_configuration(configuration) for configuration in configurations]
198+
199+
assert [job.name for job in jobs] == ["variations_demo", "baseline_no_variations"]
200+
assert all(job.num_envs == 3 for job in jobs)
201+
assert all(job.num_steps == 7 for job in jobs)
202+
assert all(job.num_rebuilds == 1 for job in jobs)
203+
assert all(job.policy_type == "zero_action" for job in jobs)
204+
assert all(job.policy_config_dict == {} for job in jobs)
205+
assert all(job.arena_env_args == [] for job in jobs)
206+
assert set(jobs[0].variations) == {
207+
"light.hdr_image.enabled=true",
208+
"light.intensity.enabled=true",
209+
"droid_rel_joint_pos.camera_extrinsics_wrist_camera.enabled=true",
210+
}
211+
assert jobs[1].variations == []

isaaclab_arena/tests/test_hydra_configuration_example_subprocess.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,13 @@
1212

1313

1414
@pytest.mark.with_subprocess
15-
def test_hydra_configuration_example_runs():
15+
def test_hydra_configuration_example_runs_two_jobs():
1616
result = run_subprocess(
1717
[
1818
TestConstants.python_path,
1919
"-m",
2020
"isaaclab_arena_examples.hydra_configuration.run",
21+
"isaaclab_arena_examples/hydra_configuration/hydra_example_suite.yaml",
2122
"--viz",
2223
"none",
2324
"rollout.num_steps=1",
@@ -26,4 +27,7 @@ def test_hydra_configuration_example_runs():
2627
)
2728

2829
assert result is not None
29-
assert "[hydra-example] completed 'maple_table_zero_action'" in result.stdout
30+
assert result.stdout.index("Running job variations_demo") < result.stdout.index(
31+
"Running job baseline_no_variations"
32+
)
33+
assert "[hydra-example] completed jobs=['variations_demo', 'baseline_no_variations']" in result.stdout

0 commit comments

Comments
 (0)