From 59e0b6648023a06bca71ba15d97ec652cfbf57a4 Mon Sep 17 00:00:00 2001 From: Alex Millane Date: Wed, 22 Jul 2026 14:27:08 +0200 Subject: [PATCH 01/10] Support graph-spec YAML environments in typed YAML experiment configs The typed YAML experiment frontend previously resolved environment.type only against registered environment names; graph-spec YAML environments (e.g. the robolab tasks) were reachable only through the legacy JSON format or the CLI. An environment.type ending in .yaml/.yml now routes through the same LegacyGraphEnvironmentCfg compatibility path the JSON frontend uses, and the camera pre-launch guard recognizes such runs. Signed-off-by: Alex Millane --- .../arena_experiment_config_loader.py | 26 ++++++++ .../evaluation/experiment_runner.py | 14 ++++- .../hydra/typed_experiment_loader.py | 56 ++++++++++++++--- .../test_arena_experiment_config_loader.py | 61 +++++++++++++++++++ isaaclab_arena/tests/test_experiment_hydra.py | 48 +++++++++++++++ 5 files changed, 194 insertions(+), 11 deletions(-) diff --git a/isaaclab_arena/evaluation/arena_experiment_config_loader.py b/isaaclab_arena/evaluation/arena_experiment_config_loader.py index da6d99c0c5..3d6464213c 100644 --- a/isaaclab_arena/evaluation/arena_experiment_config_loader.py +++ b/isaaclab_arena/evaluation/arena_experiment_config_loader.py @@ -11,12 +11,15 @@ from dataclasses import replace from importlib import import_module from pathlib import Path +from typing import Any from isaaclab_arena.assets.registries import EnvironmentRegistry, PolicyRegistry from isaaclab_arena.environments.arena_environment_factory import ArenaEnvironmentCfg from isaaclab_arena.evaluation.arena_experiment import ArenaExperimentCfg from isaaclab_arena.evaluation.arena_run import ArenaRunCfg +from isaaclab_arena.evaluation.legacy_environment_cli_args import legacy_environment_args_to_cli_args from isaaclab_arena.evaluation.legacy_eval_config import run_cfgs_from_legacy_eval_config +from isaaclab_arena.evaluation.legacy_graph_environment_cli import LegacyGraphEnvironmentCfg from isaaclab_arena.hydra.typed_experiment_loader import load_arena_experiment_from_yaml from isaaclab_arena.policy.policy_base import PolicyCfg from isaaclab_arena_environments.cli import ensure_environments_registered @@ -63,6 +66,7 @@ def load_arena_experiment_from_config_file( path, environment_cfg_types=_registered_environment_cfg_types(), policy_cfg_type_resolver=_resolve_policy_cfg_type_from_name_or_class_path, + graph_environment_cfg_factory=_graph_environment_cfg_from_yaml_values, overrides=overrides, ) @@ -80,6 +84,28 @@ def load_arena_experiment_from_config_file( return ArenaExperimentCfg(runs=runs_with_process_device) +# TODO(cvolk, 2026-07-07): [typed-config-migration] Delete this factory when graph-YAML +# environments have a typed configuration and no longer use the argparse compatibility path. +def _graph_environment_cfg_from_yaml_values( + env_graph_spec_yaml: str, + environment_values: dict[str, Any], + environment_builder_values: dict[str, Any], +) -> LegacyGraphEnvironmentCfg: + """Create the temporary graph-YAML compatibility config from typed YAML Run values. + + The environment and environment_builder values are rendered as CLI tokens for the + existing graph-environment argparse path, mirroring the legacy JSON frontend. The + environment_builder section additionally composes into the Run's typed builder + config as usual. + """ + arena_env_args: dict[str, Any] = { + "environment": env_graph_spec_yaml, + **environment_builder_values, + **environment_values, + } + return LegacyGraphEnvironmentCfg(arena_env_args=legacy_environment_args_to_cli_args(arena_env_args)) + + def _registered_environment_cfg_types() -> dict[str, type[ArenaEnvironmentCfg]]: """Return registered environment selector names and their config types.""" ensure_environments_registered() diff --git a/isaaclab_arena/evaluation/experiment_runner.py b/isaaclab_arena/evaluation/experiment_runner.py index 80f21da086..2fa73b96bb 100644 --- a/isaaclab_arena/evaluation/experiment_runner.py +++ b/isaaclab_arena/evaluation/experiment_runner.py @@ -10,13 +10,14 @@ load_arena_experiment_from_config_file, validate_experiment_config_path, ) -from isaaclab_arena.evaluation.arena_run import build_runs_info_table +from isaaclab_arena.evaluation.arena_run import ArenaRunCfg, build_runs_info_table from isaaclab_arena.evaluation.experiment_runner_cli import parse_experiment_runner_args from isaaclab_arena.evaluation.legacy_experiment_runner import ( legacy_json_experiment_requires_cameras, load_legacy_json_experiment_config, run_legacy_json_in_chunks, ) +from isaaclab_arena.evaluation.legacy_graph_environment_cli import LegacyGraphEnvironmentCfg from isaaclab_arena.evaluation.run_execution import build_arena_builder_from_run_cfg, execute_experiment from isaaclab_arena.hydra.typed_experiment_yaml_search import typed_experiment_requires_cameras from isaaclab_arena.metrics.metrics_logger import MetricsLogger @@ -48,13 +49,22 @@ def _experiment_requires_cameras( def _assert_camera_support_enabled(experiment_cfg: ArenaExperimentCfg, enable_cameras: bool) -> None: """Check that AppLauncher enabled camera support requested by typed Runs.""" - camera_run_names = [run_cfg.name for run_cfg in experiment_cfg.runs.values() if run_cfg.environment.enable_cameras] + camera_run_names = [ + run_cfg.name for run_cfg in experiment_cfg.runs.values() if _run_environment_requires_cameras(run_cfg) + ] assert not camera_run_names or enable_cameras, ( f"Runs {camera_run_names} enable environment cameras but AppLauncher started without camera support. " "The camera requirements read from the Experiment before startup disagree with the composed Experiment." ) +def _run_environment_requires_cameras(run_cfg: ArenaRunCfg) -> bool: + """Return whether a Run's environment enables cameras, including graph-YAML environments.""" + if isinstance(run_cfg.environment, LegacyGraphEnvironmentCfg): + return "--enable_cameras" in run_cfg.environment.arena_env_args + return run_cfg.environment.enable_cameras + + def _assert_exact_experiment_output_directory_is_available(experiment_output_directory: Path) -> None: """Check that an exact Experiment output path is missing or empty.""" if experiment_output_directory.exists(): diff --git a/isaaclab_arena/hydra/typed_experiment_loader.py b/isaaclab_arena/hydra/typed_experiment_loader.py index 164cdcf349..91af49cbb8 100644 --- a/isaaclab_arena/hydra/typed_experiment_loader.py +++ b/isaaclab_arena/hydra/typed_experiment_loader.py @@ -42,6 +42,7 @@ def load_arena_experiment_from_yaml( *, environment_cfg_types: dict[str, type[ArenaEnvironmentCfg]], policy_cfg_type_resolver: Callable[[str], type[PolicyCfg]], + graph_environment_cfg_factory: Callable[[str, dict[str, Any], dict[str, Any]], ArenaEnvironmentCfg] | None = None, overrides: list[str] | None = None, ) -> ArenaExperimentCfg: """Load a YAML Arena Experiment Definition as a typed named-Run mapping. @@ -55,6 +56,10 @@ def load_arena_experiment_from_yaml( yaml_path: Path to the Arena Experiment YAML file. environment_cfg_types: Environment selector names mapped to typed configuration classes. policy_cfg_type_resolver: Function returning the PolicyCfg subclass for a policy.type value. + graph_environment_cfg_factory: Function building an environment config when + environment.type is a graph-spec YAML path instead of a selector name. It + receives the path, the remaining environment values, and the Run's + environment_builder values. overrides: Hydra field overrides for Runs already declared in YAML. Returns: @@ -76,6 +81,7 @@ def load_arena_experiment_from_yaml( run_values, environment_cfg_types, policy_cfg_type_resolver, + graph_environment_cfg_factory, ) for index, (run_name, run_values) in enumerate(run_values_by_name.items()) } @@ -151,6 +157,7 @@ def _build_arena_run_cfg_from_yaml_values( run_values: dict[str, Any], environment_cfg_types: dict[str, type[ArenaEnvironmentCfg]], policy_cfg_type_resolver: Callable[[str], type[PolicyCfg]], + graph_environment_cfg_factory: Callable[[str, dict[str, Any], dict[str, Any]], ArenaEnvironmentCfg] | None, ) -> ArenaRunCfg: """Build one typed Arena Run from its unresolved YAML values. @@ -162,6 +169,8 @@ def _build_arena_run_cfg_from_yaml_values( run_values: Unresolved values declared for the Run. environment_cfg_types: Environment selectors mapped to typed configuration classes. policy_cfg_type_resolver: Function returning the PolicyCfg subclass for a policy.type value. + graph_environment_cfg_factory: Function building an environment config from a + graph-spec YAML environment.type selector, or None if unsupported. Returns: The fully composed typed Run configuration. @@ -173,15 +182,34 @@ def _build_arena_run_cfg_from_yaml_values( hydra_run_config_name = f"{hydra_config_namespace}_run_{index}" hydra_environment_config_name = f"{hydra_run_config_name}_environment" hydra_policy_config_name = f"{hydra_run_config_name}_policy" - environment = _compose_typed_config_from_yaml_selector( - config_store, - hydra_environment_config_name, - run_name, - "environment", - environment_values, - environment_cfg_types, - ArenaEnvironmentCfg, - ) + graph_spec_yaml = _graph_spec_yaml_selector(environment_values) + if graph_spec_yaml is not None: + assert graph_environment_cfg_factory is not None, ( + f"Run '{run_name}' selects graph-spec YAML environment '{graph_spec_yaml}', " + "but this loader was not given graph-YAML environment support" + ) + environment_values_without_selector = { + field_name: value for field_name, value in environment_values.items() if field_name != "type" + } + environment_builder_values = remaining_values.get("environment_builder") or {} + assert isinstance( + environment_builder_values, dict + ), f"Run '{run_name}' must define 'environment_builder' as a mapping" + environment = graph_environment_cfg_factory( + graph_spec_yaml, + environment_values_without_selector, + environment_builder_values, + ) + else: + environment = _compose_typed_config_from_yaml_selector( + config_store, + hydra_environment_config_name, + run_name, + "environment", + environment_values, + environment_cfg_types, + ArenaEnvironmentCfg, + ) policy_cfg_types: dict[str, type[PolicyCfg]] = {} if isinstance(policy_values, dict): policy_selector = policy_values.get("type") @@ -208,6 +236,16 @@ def _build_arena_run_cfg_from_yaml_values( return run +def _graph_spec_yaml_selector(environment_values: Any) -> str | None: + """Return the environment.type value when it selects a graph-spec YAML path.""" + if not isinstance(environment_values, dict): + return None + selector = environment_values.get("type") + if isinstance(selector, str) and selector.lower().endswith((".yaml", ".yml")): + return selector + return None + + def _compose_typed_config_from_yaml_selector( config_store: ConfigStore, hydra_config_name: str, diff --git a/isaaclab_arena/tests/test_arena_experiment_config_loader.py b/isaaclab_arena/tests/test_arena_experiment_config_loader.py index 372033d07c..a41e8457b4 100644 --- a/isaaclab_arena/tests/test_arena_experiment_config_loader.py +++ b/isaaclab_arena/tests/test_arena_experiment_config_loader.py @@ -18,6 +18,7 @@ from isaaclab_arena.evaluation.arena_run import ArenaRunCfg from isaaclab_arena.evaluation.experiment_runner import _assert_camera_support_enabled from isaaclab_arena.evaluation.legacy_experiment_runner import legacy_json_experiment_requires_cameras +from isaaclab_arena.evaluation.legacy_graph_environment_cli import LegacyGraphEnvironmentCfg from isaaclab_arena.policy.zero_action_policy import ZeroActionPolicyCfg from isaaclab_arena.tests.utils.constants import TestConstants from isaaclab_arena_environments.pick_and_place_maple_table_environment import PickAndPlaceMapleTableEnvironmentCfg @@ -76,6 +77,66 @@ def test_load_typed_yaml_experiment_applies_overrides_and_device(monkeypatch): assert all(run.environment_builder.device == "cuda:1" for run in runs.values()) +def test_load_typed_yaml_experiment_with_graph_spec_environment(tmp_path, monkeypatch): + monkeypatch.setattr(arena_experiment_config_loader, "_registered_environment_cfg_types", lambda: {}) + monkeypatch.setattr( + arena_experiment_config_loader, + "_resolve_policy_cfg_type_from_name_or_class_path", + lambda policy_name_or_class_path: {"zero_action": ZeroActionPolicyCfg}[policy_name_or_class_path], + ) + config_path = tmp_path / "experiment.yaml" + config_path.write_text( + """ +runs: + graph_run: + environment: + type: robolab/tasks/banana_in_bowl.yaml + enable_cameras: true + pick_up_object: banana + policy: + type: zero_action + environment_builder: + num_envs: 2 + rollout_limit: + num_episodes: 4 +""", + encoding="utf-8", + ) + + experiment_cfg = load_arena_experiment_from_config_file(config_path, device="cuda:1") + run = experiment_cfg.runs["graph_run"] + + assert isinstance(run.environment, LegacyGraphEnvironmentCfg) + assert run.environment.arena_env_args == [ + "--num_envs", + "2", + "--enable_cameras", + "--env_graph_spec_yaml", + "robolab/tasks/banana_in_bowl.yaml", + "--pick_up_object", + "banana", + ] + assert run.environment_builder.num_envs == 2 + assert run.environment_builder.device == "cuda:1" + assert run.rollout_limit.num_episodes == 4 + + +def test_typed_graph_camera_run_requires_prelaunch_camera_flag(): + run_cfg = ArenaRunCfg( + name="graph_run", + environment=LegacyGraphEnvironmentCfg( + arena_env_args=["--enable_cameras", "--env_graph_spec_yaml", "robolab/tasks/banana_in_bowl.yaml"] + ), + policy=ZeroActionPolicyCfg(), + ) + experiment_cfg = ArenaExperimentCfg(runs={run_cfg.name: run_cfg}) + + with pytest.raises(AssertionError, match="enable environment cameras"): + _assert_camera_support_enabled(experiment_cfg, enable_cameras=False) + + _assert_camera_support_enabled(experiment_cfg, enable_cameras=True) + + def test_policy_config_type_resolves_from_dotted_class_path(): policy_cfg_type = arena_experiment_config_loader._resolve_policy_cfg_type_from_name_or_class_path( "isaaclab_arena.policy.zero_action_policy.ZeroActionPolicy" diff --git a/isaaclab_arena/tests/test_experiment_hydra.py b/isaaclab_arena/tests/test_experiment_hydra.py index b417587202..5ef7c5fe95 100644 --- a/isaaclab_arena/tests/test_experiment_hydra.py +++ b/isaaclab_arena/tests/test_experiment_hydra.py @@ -15,6 +15,7 @@ from hydra.core.global_hydra import GlobalHydra from isaaclab_arena.evaluation.arena_experiment import ArenaExperimentCfg +from isaaclab_arena.evaluation.legacy_graph_environment_cli import LegacyGraphEnvironmentCfg from isaaclab_arena.hydra.typed_experiment_loader import load_arena_experiment_from_yaml from isaaclab_arena.hydra.typed_experiment_serializer import serialize_arena_experiment_to_yaml from isaaclab_arena.policy.zero_action_policy import ZeroActionPolicyCfg @@ -195,6 +196,53 @@ def test_effective_experiment_serializes_to_reloadable_yaml(tmp_path): assert _load_experiment(serialized_path) == experiment_cfg +GRAPH_SPEC_EXPERIMENT_CONTENTS = """ +runs: + graph_run: + environment: + type: robolab/tasks/banana_in_bowl.yaml + pick_up_object: banana + policy: + type: zero_action + environment_builder: + num_envs: 2 + rollout_limit: + num_steps: 5 +""" + + +def test_graph_spec_yaml_environment_uses_injected_factory(tmp_path): + config_path = _write_experiment(tmp_path, GRAPH_SPEC_EXPERIMENT_CONTENTS) + factory_calls = [] + + def graph_environment_cfg_factory(graph_spec_yaml, environment_values, environment_builder_values): + factory_calls.append((graph_spec_yaml, environment_values, environment_builder_values)) + return LegacyGraphEnvironmentCfg(arena_env_args=["--env_graph_spec_yaml", graph_spec_yaml]) + + experiment_cfg = load_arena_experiment_from_yaml( + config_path, + environment_cfg_types={}, + policy_cfg_type_resolver=_policy_cfg_type_for_name_or_class_path, + graph_environment_cfg_factory=graph_environment_cfg_factory, + ) + run = experiment_cfg.runs["graph_run"] + + assert factory_calls == [("robolab/tasks/banana_in_bowl.yaml", {"pick_up_object": "banana"}, {"num_envs": 2})] + assert run.environment == LegacyGraphEnvironmentCfg( + arena_env_args=["--env_graph_spec_yaml", "robolab/tasks/banana_in_bowl.yaml"] + ) + assert run.policy == ZeroActionPolicyCfg() + assert run.environment_builder.num_envs == 2 + assert run.rollout_limit.num_steps == 5 + + +def test_graph_spec_yaml_environment_requires_factory(tmp_path): + config_path = _write_experiment(tmp_path, GRAPH_SPEC_EXPERIMENT_CONTENTS) + + with pytest.raises(AssertionError, match="not given graph-YAML environment support"): + _load_experiment(config_path) + + @pytest.mark.parametrize( ("run_contents", "exception_type", "error"), [ From 8ede2cf4a4cdd3990e041146f2796f29c8f60e7f Mon Sep 17 00:00:00 2001 From: Alex Millane Date: Wed, 22 Jul 2026 16:14:55 +0200 Subject: [PATCH 02/10] Keep device and language_instruction out of graph-environment CLI tokens language_instruction is not a flag on the graph-environment parser (it is injected from the typed builder config after parsing, as is device), so rendering it as a token made argparse swallow the value as the example-environment positional. Signed-off-by: Alex Millane --- .../evaluation/arena_experiment_config_loader.py | 10 +++++++++- .../tests/test_arena_experiment_config_loader.py | 4 ++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/isaaclab_arena/evaluation/arena_experiment_config_loader.py b/isaaclab_arena/evaluation/arena_experiment_config_loader.py index 3d6464213c..261a281692 100644 --- a/isaaclab_arena/evaluation/arena_experiment_config_loader.py +++ b/isaaclab_arena/evaluation/arena_experiment_config_loader.py @@ -98,9 +98,17 @@ def _graph_environment_cfg_from_yaml_values( environment_builder section additionally composes into the Run's typed builder config as usual. """ + # device and language_instruction are not rendered as tokens: language_instruction is + # not a parser flag, and both are injected from the Run's typed builder config after + # parsing (see build_arena_builder_from_legacy_graph), matching the JSON frontend. + builder_cli_values = { + field_name: value + for field_name, value in environment_builder_values.items() + if field_name not in ("device", "language_instruction") + } arena_env_args: dict[str, Any] = { "environment": env_graph_spec_yaml, - **environment_builder_values, + **builder_cli_values, **environment_values, } return LegacyGraphEnvironmentCfg(arena_env_args=legacy_environment_args_to_cli_args(arena_env_args)) diff --git a/isaaclab_arena/tests/test_arena_experiment_config_loader.py b/isaaclab_arena/tests/test_arena_experiment_config_loader.py index a41e8457b4..07839c068e 100644 --- a/isaaclab_arena/tests/test_arena_experiment_config_loader.py +++ b/isaaclab_arena/tests/test_arena_experiment_config_loader.py @@ -97,6 +97,7 @@ def test_load_typed_yaml_experiment_with_graph_spec_environment(tmp_path, monkey type: zero_action environment_builder: num_envs: 2 + language_instruction: Pick up the banana. rollout_limit: num_episodes: 4 """, @@ -107,6 +108,8 @@ def test_load_typed_yaml_experiment_with_graph_spec_environment(tmp_path, monkey run = experiment_cfg.runs["graph_run"] assert isinstance(run.environment, LegacyGraphEnvironmentCfg) + # language_instruction and device reach the argparse path through the typed builder + # config, not as CLI tokens. assert run.environment.arena_env_args == [ "--num_envs", "2", @@ -118,6 +121,7 @@ def test_load_typed_yaml_experiment_with_graph_spec_environment(tmp_path, monkey ] assert run.environment_builder.num_envs == 2 assert run.environment_builder.device == "cuda:1" + assert run.environment_builder.language_instruction == "Pick up the banana." assert run.rollout_limit.num_episodes == 4 From eb420ac115dfa4af4afaeaeb0972c7443ccf4cd4 Mon Sep 17 00:00:00 2001 From: Alex Millane Date: Wed, 22 Jul 2026 17:25:06 +0200 Subject: [PATCH 03/10] Serialize graph-YAML environments for OSMO Experiment submission osmo/submit_arena_experiment.py embeds the effective Experiment by re-serializing it, which failed for graph-YAML environments because the serializer only resolves registry-registered configs. The compatibility config now records its graph-spec path and source environment values, and the serializer emits them as the environment section. Graph runs also now execute with the Run's typed environment_builder config instead of one re-derived from CLI tokens, so post-load Hydra overrides (e.g. environment_builder.num_envs on an OSMO submission) take effect; tokens carry only environment values. Signed-off-by: Alex Millane --- .../arena_experiment_config_loader.py | 28 ++++------ .../evaluation/legacy_eval_config.py | 4 ++ .../legacy_graph_environment_cli.py | 27 +++++++--- isaaclab_arena/evaluation/run_execution.py | 3 +- .../hydra/typed_experiment_loader.py | 17 ++---- .../hydra/typed_experiment_serializer.py | 21 +++++++- .../test_arena_experiment_config_loader.py | 51 ++++++++++++++++-- isaaclab_arena/tests/test_experiment_hydra.py | 6 +-- .../tests/test_legacy_eval_config.py | 29 +++++++--- .../tests/test_osmo_experiment_workflow.py | 53 +++++++++++++++++++ isaaclab_arena_environments/cli.py | 4 +- 11 files changed, 183 insertions(+), 60 deletions(-) diff --git a/isaaclab_arena/evaluation/arena_experiment_config_loader.py b/isaaclab_arena/evaluation/arena_experiment_config_loader.py index 261a281692..bb9a83db7d 100644 --- a/isaaclab_arena/evaluation/arena_experiment_config_loader.py +++ b/isaaclab_arena/evaluation/arena_experiment_config_loader.py @@ -89,29 +89,19 @@ def load_arena_experiment_from_config_file( def _graph_environment_cfg_from_yaml_values( env_graph_spec_yaml: str, environment_values: dict[str, Any], - environment_builder_values: dict[str, Any], ) -> LegacyGraphEnvironmentCfg: """Create the temporary graph-YAML compatibility config from typed YAML Run values. - The environment and environment_builder values are rendered as CLI tokens for the - existing graph-environment argparse path, mirroring the legacy JSON frontend. The - environment_builder section additionally composes into the Run's typed builder - config as usual. + The environment values are rendered as CLI tokens for the existing graph-environment + argparse path; the Run's environment_builder section stays typed and is applied + directly at execution (see build_arena_builder_from_legacy_graph). """ - # device and language_instruction are not rendered as tokens: language_instruction is - # not a parser flag, and both are injected from the Run's typed builder config after - # parsing (see build_arena_builder_from_legacy_graph), matching the JSON frontend. - builder_cli_values = { - field_name: value - for field_name, value in environment_builder_values.items() - if field_name not in ("device", "language_instruction") - } - arena_env_args: dict[str, Any] = { - "environment": env_graph_spec_yaml, - **builder_cli_values, - **environment_values, - } - return LegacyGraphEnvironmentCfg(arena_env_args=legacy_environment_args_to_cli_args(arena_env_args)) + arena_env_args: dict[str, Any] = {"environment": env_graph_spec_yaml, **environment_values} + return LegacyGraphEnvironmentCfg( + arena_env_args=legacy_environment_args_to_cli_args(arena_env_args), + env_graph_spec_yaml=env_graph_spec_yaml, + environment_values=dict(environment_values), + ) def _registered_environment_cfg_types() -> dict[str, type[ArenaEnvironmentCfg]]: diff --git a/isaaclab_arena/evaluation/legacy_eval_config.py b/isaaclab_arena/evaluation/legacy_eval_config.py index 8bd1566225..f86dd61ead 100644 --- a/isaaclab_arena/evaluation/legacy_eval_config.py +++ b/isaaclab_arena/evaluation/legacy_eval_config.py @@ -158,6 +158,10 @@ def _graph_environment_cfg_from_legacy_args( """Create the temporary graph-YAML compatibility config from legacy arguments.""" return LegacyGraphEnvironmentCfg( arena_env_args=legacy_environment_args_to_cli_args(arena_env_args), + env_graph_spec_yaml=str(arena_env_args["environment"]), + environment_values={ + field_name: value for field_name, value in arena_env_args.items() if field_name != "environment" + }, ) diff --git a/isaaclab_arena/evaluation/legacy_graph_environment_cli.py b/isaaclab_arena/evaluation/legacy_graph_environment_cli.py index 6d7060a3a0..b0e603be04 100644 --- a/isaaclab_arena/evaluation/legacy_graph_environment_cli.py +++ b/isaaclab_arena/evaluation/legacy_graph_environment_cli.py @@ -8,13 +8,14 @@ from __future__ import annotations from dataclasses import dataclass, field -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Any from isaaclab_arena.environments.arena_environment_factory import ArenaEnvironmentCfg -from isaaclab_arena_environments.cli import get_arena_builder_from_cli, get_isaaclab_arena_environments_cli_parser +from isaaclab_arena_environments.cli import arena_env_from_graph_spec, get_isaaclab_arena_environments_cli_parser if TYPE_CHECKING: from isaaclab_arena.environments.arena_env_builder import ArenaEnvBuilder + from isaaclab_arena.environments.arena_env_builder_cfg import ArenaEnvBuilderCfg # TODO(cvolk, 2026-07-07): [typed-config-migration] Delete this module when graph-YAML environments have a # typed configuration and factory. Until then, only graph construction crosses the @@ -29,17 +30,27 @@ class LegacyGraphEnvironmentCfg(ArenaEnvironmentCfg): arena_env_args: list[str] = field(kw_only=True) """Arguments consumed by the existing graph-environment parser.""" + env_graph_spec_yaml: str = "" + """Graph-spec YAML path the environment was loaded from.""" + + environment_values: dict[str, Any] = field(default_factory=dict) + """Environment values (without the type selector) used to re-serialize the Run.""" + def build_arena_builder_from_legacy_graph( cfg: LegacyGraphEnvironmentCfg, - device: str, - language_instruction: str | None, + environment_builder: ArenaEnvBuilderCfg, hydra_overrides: list[str], ) -> ArenaEnvBuilder: - """Build a graph-YAML environment through the existing argparse adapter.""" + """Build a graph-YAML environment through the existing argparse adapter. + + Only environment construction crosses the argparse boundary; the Run's typed + builder configuration is used directly, so Hydra overrides on it take effect. + """ + from isaaclab_arena.environments.arena_env_builder import ArenaEnvBuilder + assert "--env_graph_spec_yaml" in cfg.arena_env_args, "legacy graph config must select a graph YAML" parser = get_isaaclab_arena_environments_cli_parser() args_cli = parser.parse_args(cfg.arena_env_args) - args_cli.device = device - args_cli.language_instruction = language_instruction - return get_arena_builder_from_cli(args_cli, hydra_overrides=hydra_overrides) + arena_env = arena_env_from_graph_spec(args_cli.env_graph_spec_yaml, args_cli) + return ArenaEnvBuilder(arena_env, environment_builder, hydra_overrides=hydra_overrides) diff --git a/isaaclab_arena/evaluation/run_execution.py b/isaaclab_arena/evaluation/run_execution.py index 7a3f8fec14..d1bdb257e6 100644 --- a/isaaclab_arena/evaluation/run_execution.py +++ b/isaaclab_arena/evaluation/run_execution.py @@ -148,8 +148,7 @@ def build_arena_builder_from_run_cfg(cfg: ArenaRunCfg) -> ArenaEnvBuilder: return ( build_arena_builder_from_legacy_graph( cfg.environment, - device=cfg.environment_builder.device, - language_instruction=cfg.environment_builder.language_instruction, + environment_builder=cfg.environment_builder, hydra_overrides=hydra_overrides, ) if isinstance(cfg.environment, LegacyGraphEnvironmentCfg) diff --git a/isaaclab_arena/hydra/typed_experiment_loader.py b/isaaclab_arena/hydra/typed_experiment_loader.py index 91af49cbb8..541d437c75 100644 --- a/isaaclab_arena/hydra/typed_experiment_loader.py +++ b/isaaclab_arena/hydra/typed_experiment_loader.py @@ -42,7 +42,7 @@ def load_arena_experiment_from_yaml( *, environment_cfg_types: dict[str, type[ArenaEnvironmentCfg]], policy_cfg_type_resolver: Callable[[str], type[PolicyCfg]], - graph_environment_cfg_factory: Callable[[str, dict[str, Any], dict[str, Any]], ArenaEnvironmentCfg] | None = None, + graph_environment_cfg_factory: Callable[[str, dict[str, Any]], ArenaEnvironmentCfg] | None = None, overrides: list[str] | None = None, ) -> ArenaExperimentCfg: """Load a YAML Arena Experiment Definition as a typed named-Run mapping. @@ -58,8 +58,7 @@ def load_arena_experiment_from_yaml( policy_cfg_type_resolver: Function returning the PolicyCfg subclass for a policy.type value. graph_environment_cfg_factory: Function building an environment config when environment.type is a graph-spec YAML path instead of a selector name. It - receives the path, the remaining environment values, and the Run's - environment_builder values. + receives the path and the remaining environment values. overrides: Hydra field overrides for Runs already declared in YAML. Returns: @@ -157,7 +156,7 @@ def _build_arena_run_cfg_from_yaml_values( run_values: dict[str, Any], environment_cfg_types: dict[str, type[ArenaEnvironmentCfg]], policy_cfg_type_resolver: Callable[[str], type[PolicyCfg]], - graph_environment_cfg_factory: Callable[[str, dict[str, Any], dict[str, Any]], ArenaEnvironmentCfg] | None, + graph_environment_cfg_factory: Callable[[str, dict[str, Any]], ArenaEnvironmentCfg] | None, ) -> ArenaRunCfg: """Build one typed Arena Run from its unresolved YAML values. @@ -191,15 +190,7 @@ def _build_arena_run_cfg_from_yaml_values( environment_values_without_selector = { field_name: value for field_name, value in environment_values.items() if field_name != "type" } - environment_builder_values = remaining_values.get("environment_builder") or {} - assert isinstance( - environment_builder_values, dict - ), f"Run '{run_name}' must define 'environment_builder' as a mapping" - environment = graph_environment_cfg_factory( - graph_spec_yaml, - environment_values_without_selector, - environment_builder_values, - ) + environment = graph_environment_cfg_factory(graph_spec_yaml, environment_values_without_selector) else: environment = _compose_typed_config_from_yaml_selector( config_store, diff --git a/isaaclab_arena/hydra/typed_experiment_serializer.py b/isaaclab_arena/hydra/typed_experiment_serializer.py index b50049d242..723e32f9f6 100644 --- a/isaaclab_arena/hydra/typed_experiment_serializer.py +++ b/isaaclab_arena/hydra/typed_experiment_serializer.py @@ -15,6 +15,7 @@ from isaaclab_arena.assets.registries import EnvironmentRegistry, PolicyRegistry from isaaclab_arena.evaluation.arena_experiment import ArenaExperimentCfg from isaaclab_arena.evaluation.arena_run import ArenaRunCfg +from isaaclab_arena.evaluation.legacy_graph_environment_cli import LegacyGraphEnvironmentCfg def serialize_arena_experiment_to_yaml(experiment_cfg: ArenaExperimentCfg) -> str: @@ -43,9 +44,8 @@ def serialize_arena_experiment_to_yaml(experiment_cfg: ArenaExperimentCfg) -> st assert isinstance(run_values, dict) assert run_values.pop("name") == run_name - environment_type = environment_registry.get_factory_type_for_cfg(run_cfg.environment) + run_values["environment"] = _environment_yaml_values(environment_registry, run_cfg, run_values["environment"]) policy_type = policy_registry.get_policy_type_for_cfg(run_cfg.policy) - run_values["environment"] = {"type": environment_type.name, **run_values["environment"]} policy_selector = policy_type.name if not policy_type.__module__.startswith("isaaclab_arena.policy."): policy_selector = f"{policy_type.__module__}.{policy_type.__qualname__}" @@ -54,6 +54,23 @@ def serialize_arena_experiment_to_yaml(experiment_cfg: ArenaExperimentCfg) -> st return yaml.safe_dump({"runs": run_values_by_name}, sort_keys=False) +def _environment_yaml_values( + environment_registry: EnvironmentRegistry, + run_cfg: ArenaRunCfg, + dumped_environment_values: dict[str, Any], +) -> dict[str, Any]: + """Return one Run's environment section with the type selector the loader expects.""" + if isinstance(run_cfg.environment, LegacyGraphEnvironmentCfg): + # Graph-YAML environments serialize from their original source values; the derived + # arena_env_args tokens are an execution detail the loader rebuilds on reload. + assert ( + run_cfg.environment.env_graph_spec_yaml + ), "Graph-YAML environment cannot be serialized because it does not record its graph-spec YAML path" + return {"type": run_cfg.environment.env_graph_spec_yaml, **run_cfg.environment.environment_values} + environment_type = environment_registry.get_factory_type_for_cfg(run_cfg.environment) + return {"type": environment_type.name, **dumped_environment_values} + + def _to_yaml_values(value: Any) -> Any: """Convert structured-config leaf values into safe YAML primitives.""" if isinstance(value, dict): diff --git a/isaaclab_arena/tests/test_arena_experiment_config_loader.py b/isaaclab_arena/tests/test_arena_experiment_config_loader.py index 07839c068e..f6bd59151c 100644 --- a/isaaclab_arena/tests/test_arena_experiment_config_loader.py +++ b/isaaclab_arena/tests/test_arena_experiment_config_loader.py @@ -5,6 +5,7 @@ """Test loading Arena Experiments at the evaluation boundary.""" +import yaml from pathlib import Path import pytest @@ -19,6 +20,7 @@ from isaaclab_arena.evaluation.experiment_runner import _assert_camera_support_enabled from isaaclab_arena.evaluation.legacy_experiment_runner import legacy_json_experiment_requires_cameras from isaaclab_arena.evaluation.legacy_graph_environment_cli import LegacyGraphEnvironmentCfg +from isaaclab_arena.hydra.typed_experiment_serializer import serialize_arena_experiment_to_yaml from isaaclab_arena.policy.zero_action_policy import ZeroActionPolicyCfg from isaaclab_arena.tests.utils.constants import TestConstants from isaaclab_arena_environments.pick_and_place_maple_table_environment import PickAndPlaceMapleTableEnvironmentCfg @@ -108,23 +110,64 @@ def test_load_typed_yaml_experiment_with_graph_spec_environment(tmp_path, monkey run = experiment_cfg.runs["graph_run"] assert isinstance(run.environment, LegacyGraphEnvironmentCfg) - # language_instruction and device reach the argparse path through the typed builder - # config, not as CLI tokens. + # Only environment values become CLI tokens; the environment_builder section stays + # typed and reaches the argparse path directly at execution. assert run.environment.arena_env_args == [ - "--num_envs", - "2", "--enable_cameras", "--env_graph_spec_yaml", "robolab/tasks/banana_in_bowl.yaml", "--pick_up_object", "banana", ] + assert run.environment.env_graph_spec_yaml == "robolab/tasks/banana_in_bowl.yaml" + assert run.environment.environment_values == {"enable_cameras": True, "pick_up_object": "banana"} assert run.environment_builder.num_envs == 2 assert run.environment_builder.device == "cuda:1" assert run.environment_builder.language_instruction == "Pick up the banana." assert run.rollout_limit.num_episodes == 4 +def test_graph_spec_environment_serializes_to_reloadable_yaml(tmp_path, monkeypatch): + monkeypatch.setattr(arena_experiment_config_loader, "_registered_environment_cfg_types", lambda: {}) + monkeypatch.setattr( + arena_experiment_config_loader, + "_resolve_policy_cfg_type_from_name_or_class_path", + lambda policy_name_or_class_path: {"zero_action": ZeroActionPolicyCfg}[policy_name_or_class_path], + ) + config_path = tmp_path / "experiment.yaml" + config_path.write_text( + """ +runs: + graph_run: + environment: + type: robolab/tasks/banana_in_bowl.yaml + enable_cameras: true + pick_up_object: banana + policy: + type: zero_action + environment_builder: + num_envs: 2 + rollout_limit: + num_episodes: 4 +""", + encoding="utf-8", + ) + experiment_cfg = load_arena_experiment_from_config_file(config_path, device="cuda:1") + + serialized_experiment = serialize_arena_experiment_to_yaml(experiment_cfg) + serialized_values = yaml.safe_load(serialized_experiment) + serialized_environment = serialized_values["runs"]["graph_run"]["environment"] + assert serialized_environment == { + "type": "robolab/tasks/banana_in_bowl.yaml", + "enable_cameras": True, + "pick_up_object": "banana", + } + + serialized_path = tmp_path / "serialized_experiment.yaml" + serialized_path.write_text(serialized_experiment, encoding="utf-8") + assert load_arena_experiment_from_config_file(serialized_path, device="cuda:1") == experiment_cfg + + def test_typed_graph_camera_run_requires_prelaunch_camera_flag(): run_cfg = ArenaRunCfg( name="graph_run", diff --git a/isaaclab_arena/tests/test_experiment_hydra.py b/isaaclab_arena/tests/test_experiment_hydra.py index 5ef7c5fe95..543ef8f29d 100644 --- a/isaaclab_arena/tests/test_experiment_hydra.py +++ b/isaaclab_arena/tests/test_experiment_hydra.py @@ -215,8 +215,8 @@ def test_graph_spec_yaml_environment_uses_injected_factory(tmp_path): config_path = _write_experiment(tmp_path, GRAPH_SPEC_EXPERIMENT_CONTENTS) factory_calls = [] - def graph_environment_cfg_factory(graph_spec_yaml, environment_values, environment_builder_values): - factory_calls.append((graph_spec_yaml, environment_values, environment_builder_values)) + def graph_environment_cfg_factory(graph_spec_yaml, environment_values): + factory_calls.append((graph_spec_yaml, environment_values)) return LegacyGraphEnvironmentCfg(arena_env_args=["--env_graph_spec_yaml", graph_spec_yaml]) experiment_cfg = load_arena_experiment_from_yaml( @@ -227,7 +227,7 @@ def graph_environment_cfg_factory(graph_spec_yaml, environment_values, environme ) run = experiment_cfg.runs["graph_run"] - assert factory_calls == [("robolab/tasks/banana_in_bowl.yaml", {"pick_up_object": "banana"}, {"num_envs": 2})] + assert factory_calls == [("robolab/tasks/banana_in_bowl.yaml", {"pick_up_object": "banana"})] assert run.environment == LegacyGraphEnvironmentCfg( arena_env_args=["--env_graph_spec_yaml", "robolab/tasks/banana_in_bowl.yaml"] ) diff --git a/isaaclab_arena/tests/test_legacy_eval_config.py b/isaaclab_arena/tests/test_legacy_eval_config.py index b74eed5a6b..18f5fb1fd6 100644 --- a/isaaclab_arena/tests/test_legacy_eval_config.py +++ b/isaaclab_arena/tests/test_legacy_eval_config.py @@ -109,7 +109,8 @@ def test_legacy_graph_builder_keeps_namespace_inside_graph_compatibility(monkeyp }, device="cuda:1", ) - parsed_args = SimpleNamespace() + parsed_args = SimpleNamespace(env_graph_spec_yaml=str(graph_path)) + expected_arena_env = object() expected_builder = object() captured = {} @@ -120,24 +121,38 @@ def parse_args(self, arguments): monkeypatch.setattr(legacy_graph_environment_cli, "get_isaaclab_arena_environments_cli_parser", lambda: _Parser()) - def get_builder(args_cli, hydra_overrides): + def get_arena_env(env_graph_spec_yaml, args_cli): + captured["env_graph_spec_yaml"] = env_graph_spec_yaml captured["args_cli"] = args_cli + return expected_arena_env + + monkeypatch.setattr(legacy_graph_environment_cli, "arena_env_from_graph_spec", get_arena_env) + + def get_builder(arena_env, builder_cfg, hydra_overrides): + captured["arena_env"] = arena_env + captured["builder_cfg"] = builder_cfg captured["hydra_overrides"] = hydra_overrides return expected_builder - monkeypatch.setattr(legacy_graph_environment_cli, "get_arena_builder_from_cli", get_builder) + # Patched by name so ArenaEnvBuilder is imported when the test runs rather than at + # collection, which would pull Isaac Lab modules in before SimulationApp starts. + monkeypatch.setattr("isaaclab_arena.environments.arena_env_builder.ArenaEnvBuilder", get_builder) builder = legacy_graph_environment_cli.build_arena_builder_from_legacy_graph( run.environment, - device=run.environment_builder.device, - language_instruction=run.environment_builder.language_instruction, + environment_builder=run.environment_builder, hydra_overrides=overrides_from_dict(run.variations), ) assert builder is expected_builder assert captured["arguments"] == run.environment.arena_env_args - assert parsed_args.device == "cuda:1" - assert parsed_args.language_instruction is None + assert captured["args_cli"] is parsed_args + assert captured["env_graph_spec_yaml"] == str(graph_path) + # The Run's typed builder config crosses the boundary directly, so device and + # language_instruction never round-trip through the argparse namespace. + assert captured["builder_cfg"] is run.environment_builder + assert captured["builder_cfg"].device == "cuda:1" + assert captured["arena_env"] is expected_arena_env assert captured["hydra_overrides"] == ["light.intensity.enabled=true"] diff --git a/isaaclab_arena/tests/test_osmo_experiment_workflow.py b/isaaclab_arena/tests/test_osmo_experiment_workflow.py index 487a3a9589..81b7c48b16 100644 --- a/isaaclab_arena/tests/test_osmo_experiment_workflow.py +++ b/isaaclab_arena/tests/test_osmo_experiment_workflow.py @@ -16,6 +16,7 @@ from isaaclab_arena.evaluation.arena_experiment import ArenaExperimentCfg from isaaclab_arena.evaluation.arena_experiment_config_loader import load_arena_experiment_from_config_file from isaaclab_arena.evaluation.arena_run import ArenaRunCfg +from isaaclab_arena.evaluation.legacy_graph_environment_cli import LegacyGraphEnvironmentCfg from isaaclab_arena.policy.zero_action_policy import ZeroActionPolicyCfg from isaaclab_arena_environments.pick_and_place_maple_table_environment import PickAndPlaceMapleTableEnvironmentCfg from isaaclab_arena_openpi.policy import pi0_remote_policy # noqa: F401 @@ -421,6 +422,58 @@ def test_embedded_openpi_experiment_composes_through_experiment_runner_loader(tm assert run_cfg.policy.ping_timeout == Pi0ServerTaskCfg.client_ping_timeout_s +def test_embedded_graph_environment_experiment_composes_through_experiment_runner_loader(tmp_path): + """Embed graph-YAML environment Runs and keep the handoff loadable by the runner.""" + experiment_path = tmp_path / "graph_experiment.yaml" + experiment_path.write_text( + """runs: + graph_run: + environment: + type: isaaclab_arena/tests/test_data/pick_and_place_maple_table_env_graph.yaml + enable_cameras: true + policy: + type: isaaclab_arena_openpi.policy.pi0_remote_policy.Pi0RemotePolicy + environment_builder: + num_envs: 2 + rollout_limit: + num_episodes: 3 +""", + encoding="utf-8", + ) + submission_cfg = _compose_submission( + ["experiment_cfg.runs.graph_run.environment_builder.num_envs=4"], + experiment_path, + ) + workflow = Pi0ArenaExperimentWorkflow( + workflow_cfg=submission_cfg.osmo, + experiment_cfg=submission_cfg.experiment_cfg, + server_task_cfg=submission_cfg.policy_server, + task_cfg=submission_cfg.experiment_runner, + ) + rendered_workflow = workflow.generate_workflow() + experiment_runner_task = _workflow_tasks(rendered_workflow)[0] + embedded_run = _embedded_experiment(experiment_runner_task)["runs"]["graph_run"] + assert embedded_run["environment"] == { + "type": "isaaclab_arena/tests/test_data/pick_and_place_maple_table_env_graph.yaml", + "enable_cameras": True, + } + # The post-load Hydra override must land in the typed builder the runner executes with. + assert embedded_run["environment_builder"]["num_envs"] == 4 + + embedded_path = tmp_path / "embedded_experiment.yaml" + embedded_path.write_text(_task_file(experiment_runner_task, REMOTE_EXPERIMENT_PATH)["contents"], encoding="utf-8") + experiment_cfg = load_arena_experiment_from_config_file(embedded_path, device="cuda:0") + run_cfg = experiment_cfg.runs["graph_run"] + assert isinstance(run_cfg.environment, LegacyGraphEnvironmentCfg) + assert run_cfg.environment.arena_env_args == [ + "--enable_cameras", + "--env_graph_spec_yaml", + "isaaclab_arena/tests/test_data/pick_and_place_maple_table_env_graph.yaml", + ] + assert run_cfg.environment_builder.num_envs == 4 + assert run_cfg.policy.remote_host == Pi0ServerTask.host_token("policy-server-0") + + def test_submission_overrides_osmo_resources(monkeypatch): """Apply scheduler overrides after the typed workflow defaults.""" submitted_command = None diff --git a/isaaclab_arena_environments/cli.py b/isaaclab_arena_environments/cli.py index 387601b248..ae07bd87dd 100644 --- a/isaaclab_arena_environments/cli.py +++ b/isaaclab_arena_environments/cli.py @@ -217,7 +217,7 @@ def get_arena_builder_from_cli( # Either env graph spec yaml OR example env name arena_env = ( - _arena_env_from_graph_spec(env_graph_spec_yaml, args_cli) + arena_env_from_graph_spec(env_graph_spec_yaml, args_cli) if env_graph_spec_yaml is not None else _arena_env_from_example_name(example_environment, args_cli) ) @@ -225,7 +225,7 @@ def get_arena_builder_from_cli( return ArenaEnvBuilder(arena_env, builder_cfg, hydra_overrides=hydra_overrides) -def _arena_env_from_graph_spec(env_graph_spec_yaml: str, args_cli: argparse.Namespace) -> IsaacLabArenaEnvironment: +def arena_env_from_graph_spec(env_graph_spec_yaml: str, args_cli: argparse.Namespace) -> IsaacLabArenaEnvironment: """Build the arena env from a graph spec YAML, applying any CLI node overrides.""" spec = ArenaEnvGraphSpec.from_yaml(env_graph_spec_yaml) spec.apply_cli_override_args(args_cli) From e3fd43534bb33c7915da0dc1b9bc5a61a39b44d8 Mon Sep 17 00:00:00 2001 From: alex Date: Fri, 31 Jul 2026 16:15:28 +0200 Subject: [PATCH 04/10] Respond to review. --- .../arena_experiment_config_loader.py | 24 ----- .../evaluation/experiment_runner.py | 14 +-- .../hydra/typed_experiment_loader.py | 93 ++++++++++++------- isaaclab_arena/tests/test_experiment_hydra.py | 47 ++++++---- 4 files changed, 93 insertions(+), 85 deletions(-) diff --git a/isaaclab_arena/evaluation/arena_experiment_config_loader.py b/isaaclab_arena/evaluation/arena_experiment_config_loader.py index bb9a83db7d..da6d99c0c5 100644 --- a/isaaclab_arena/evaluation/arena_experiment_config_loader.py +++ b/isaaclab_arena/evaluation/arena_experiment_config_loader.py @@ -11,15 +11,12 @@ from dataclasses import replace from importlib import import_module from pathlib import Path -from typing import Any from isaaclab_arena.assets.registries import EnvironmentRegistry, PolicyRegistry from isaaclab_arena.environments.arena_environment_factory import ArenaEnvironmentCfg from isaaclab_arena.evaluation.arena_experiment import ArenaExperimentCfg from isaaclab_arena.evaluation.arena_run import ArenaRunCfg -from isaaclab_arena.evaluation.legacy_environment_cli_args import legacy_environment_args_to_cli_args from isaaclab_arena.evaluation.legacy_eval_config import run_cfgs_from_legacy_eval_config -from isaaclab_arena.evaluation.legacy_graph_environment_cli import LegacyGraphEnvironmentCfg from isaaclab_arena.hydra.typed_experiment_loader import load_arena_experiment_from_yaml from isaaclab_arena.policy.policy_base import PolicyCfg from isaaclab_arena_environments.cli import ensure_environments_registered @@ -66,7 +63,6 @@ def load_arena_experiment_from_config_file( path, environment_cfg_types=_registered_environment_cfg_types(), policy_cfg_type_resolver=_resolve_policy_cfg_type_from_name_or_class_path, - graph_environment_cfg_factory=_graph_environment_cfg_from_yaml_values, overrides=overrides, ) @@ -84,26 +80,6 @@ def load_arena_experiment_from_config_file( return ArenaExperimentCfg(runs=runs_with_process_device) -# TODO(cvolk, 2026-07-07): [typed-config-migration] Delete this factory when graph-YAML -# environments have a typed configuration and no longer use the argparse compatibility path. -def _graph_environment_cfg_from_yaml_values( - env_graph_spec_yaml: str, - environment_values: dict[str, Any], -) -> LegacyGraphEnvironmentCfg: - """Create the temporary graph-YAML compatibility config from typed YAML Run values. - - The environment values are rendered as CLI tokens for the existing graph-environment - argparse path; the Run's environment_builder section stays typed and is applied - directly at execution (see build_arena_builder_from_legacy_graph). - """ - arena_env_args: dict[str, Any] = {"environment": env_graph_spec_yaml, **environment_values} - return LegacyGraphEnvironmentCfg( - arena_env_args=legacy_environment_args_to_cli_args(arena_env_args), - env_graph_spec_yaml=env_graph_spec_yaml, - environment_values=dict(environment_values), - ) - - def _registered_environment_cfg_types() -> dict[str, type[ArenaEnvironmentCfg]]: """Return registered environment selector names and their config types.""" ensure_environments_registered() diff --git a/isaaclab_arena/evaluation/experiment_runner.py b/isaaclab_arena/evaluation/experiment_runner.py index 2fa73b96bb..80f21da086 100644 --- a/isaaclab_arena/evaluation/experiment_runner.py +++ b/isaaclab_arena/evaluation/experiment_runner.py @@ -10,14 +10,13 @@ load_arena_experiment_from_config_file, validate_experiment_config_path, ) -from isaaclab_arena.evaluation.arena_run import ArenaRunCfg, build_runs_info_table +from isaaclab_arena.evaluation.arena_run import build_runs_info_table from isaaclab_arena.evaluation.experiment_runner_cli import parse_experiment_runner_args from isaaclab_arena.evaluation.legacy_experiment_runner import ( legacy_json_experiment_requires_cameras, load_legacy_json_experiment_config, run_legacy_json_in_chunks, ) -from isaaclab_arena.evaluation.legacy_graph_environment_cli import LegacyGraphEnvironmentCfg from isaaclab_arena.evaluation.run_execution import build_arena_builder_from_run_cfg, execute_experiment from isaaclab_arena.hydra.typed_experiment_yaml_search import typed_experiment_requires_cameras from isaaclab_arena.metrics.metrics_logger import MetricsLogger @@ -49,22 +48,13 @@ def _experiment_requires_cameras( def _assert_camera_support_enabled(experiment_cfg: ArenaExperimentCfg, enable_cameras: bool) -> None: """Check that AppLauncher enabled camera support requested by typed Runs.""" - camera_run_names = [ - run_cfg.name for run_cfg in experiment_cfg.runs.values() if _run_environment_requires_cameras(run_cfg) - ] + camera_run_names = [run_cfg.name for run_cfg in experiment_cfg.runs.values() if run_cfg.environment.enable_cameras] assert not camera_run_names or enable_cameras, ( f"Runs {camera_run_names} enable environment cameras but AppLauncher started without camera support. " "The camera requirements read from the Experiment before startup disagree with the composed Experiment." ) -def _run_environment_requires_cameras(run_cfg: ArenaRunCfg) -> bool: - """Return whether a Run's environment enables cameras, including graph-YAML environments.""" - if isinstance(run_cfg.environment, LegacyGraphEnvironmentCfg): - return "--enable_cameras" in run_cfg.environment.arena_env_args - return run_cfg.environment.enable_cameras - - def _assert_exact_experiment_output_directory_is_available(experiment_output_directory: Path) -> None: """Check that an exact Experiment output path is missing or empty.""" if experiment_output_directory.exists(): diff --git a/isaaclab_arena/hydra/typed_experiment_loader.py b/isaaclab_arena/hydra/typed_experiment_loader.py index 541d437c75..fa9ad14a34 100644 --- a/isaaclab_arena/hydra/typed_experiment_loader.py +++ b/isaaclab_arena/hydra/typed_experiment_loader.py @@ -23,6 +23,8 @@ from isaaclab_arena.environments.arena_environment_factory import ArenaEnvironmentCfg from isaaclab_arena.evaluation.arena_experiment import ArenaExperimentCfg from isaaclab_arena.evaluation.arena_run import ArenaRunCfg +from isaaclab_arena.evaluation.legacy_environment_cli_args import legacy_environment_args_to_cli_args +from isaaclab_arena.evaluation.legacy_graph_environment_cli import LegacyGraphEnvironmentCfg from isaaclab_arena.policy.policy_base import PolicyCfg @@ -42,23 +44,19 @@ def load_arena_experiment_from_yaml( *, environment_cfg_types: dict[str, type[ArenaEnvironmentCfg]], policy_cfg_type_resolver: Callable[[str], type[PolicyCfg]], - graph_environment_cfg_factory: Callable[[str, dict[str, Any]], ArenaEnvironmentCfg] | None = None, overrides: list[str] | None = None, ) -> ArenaExperimentCfg: """Load a YAML Arena Experiment Definition as a typed named-Run mapping. Each entry in the runs mapping declares one Run using its key as the Run - name. The environment.type selector chooses from the supplied mapping, - policy.type is resolved when its Run is built. Hydra overrides can update - fields on Runs declared in YAML, but cannot add Runs. + name. The environment.type selector chooses from the supplied mapping, or + names a graph-spec YAML path; policy.type is resolved when its Run is built. + Hydra overrides can update fields on Runs declared in YAML, but cannot add Runs. Args: yaml_path: Path to the Arena Experiment YAML file. environment_cfg_types: Environment selector names mapped to typed configuration classes. policy_cfg_type_resolver: Function returning the PolicyCfg subclass for a policy.type value. - graph_environment_cfg_factory: Function building an environment config when - environment.type is a graph-spec YAML path instead of a selector name. It - receives the path and the remaining environment values. overrides: Hydra field overrides for Runs already declared in YAML. Returns: @@ -80,7 +78,6 @@ def load_arena_experiment_from_yaml( run_values, environment_cfg_types, policy_cfg_type_resolver, - graph_environment_cfg_factory, ) for index, (run_name, run_values) in enumerate(run_values_by_name.items()) } @@ -156,7 +153,6 @@ def _build_arena_run_cfg_from_yaml_values( run_values: dict[str, Any], environment_cfg_types: dict[str, type[ArenaEnvironmentCfg]], policy_cfg_type_resolver: Callable[[str], type[PolicyCfg]], - graph_environment_cfg_factory: Callable[[str, dict[str, Any]], ArenaEnvironmentCfg] | None, ) -> ArenaRunCfg: """Build one typed Arena Run from its unresolved YAML values. @@ -168,8 +164,6 @@ def _build_arena_run_cfg_from_yaml_values( run_values: Unresolved values declared for the Run. environment_cfg_types: Environment selectors mapped to typed configuration classes. policy_cfg_type_resolver: Function returning the PolicyCfg subclass for a policy.type value. - graph_environment_cfg_factory: Function building an environment config from a - graph-spec YAML environment.type selector, or None if unsupported. Returns: The fully composed typed Run configuration. @@ -181,26 +175,13 @@ def _build_arena_run_cfg_from_yaml_values( hydra_run_config_name = f"{hydra_config_namespace}_run_{index}" hydra_environment_config_name = f"{hydra_run_config_name}_environment" hydra_policy_config_name = f"{hydra_run_config_name}_policy" - graph_spec_yaml = _graph_spec_yaml_selector(environment_values) - if graph_spec_yaml is not None: - assert graph_environment_cfg_factory is not None, ( - f"Run '{run_name}' selects graph-spec YAML environment '{graph_spec_yaml}', " - "but this loader was not given graph-YAML environment support" - ) - environment_values_without_selector = { - field_name: value for field_name, value in environment_values.items() if field_name != "type" - } - environment = graph_environment_cfg_factory(graph_spec_yaml, environment_values_without_selector) - else: - environment = _compose_typed_config_from_yaml_selector( - config_store, - hydra_environment_config_name, - run_name, - "environment", - environment_values, - environment_cfg_types, - ArenaEnvironmentCfg, - ) + environment = _build_environment_cfg_from_yaml_values( + config_store, + hydra_environment_config_name, + run_name, + environment_values, + environment_cfg_types, + ) policy_cfg_types: dict[str, type[PolicyCfg]] = {} if isinstance(policy_values, dict): policy_selector = policy_values.get("type") @@ -227,6 +208,56 @@ def _build_arena_run_cfg_from_yaml_values( return run +def _build_environment_cfg_from_yaml_values( + config_store: ConfigStore, + hydra_environment_config_name: str, + run_name: str, + environment_values: Any, + environment_cfg_types: dict[str, type[ArenaEnvironmentCfg]], +) -> ArenaEnvironmentCfg: + """Build a Run's environment from a graph-spec YAML path or a typed selector. + + When environment.type names a graph-spec YAML file it is built on the temporary + argparse compatibility path; otherwise the type selects a registered typed config. + """ + graph_spec_yaml = _graph_spec_yaml_selector(environment_values) + if graph_spec_yaml is not None: + environment_values_without_selector = { + field_name: value for field_name, value in environment_values.items() if field_name != "type" + } + return _graph_environment_cfg_from_yaml_values(graph_spec_yaml, environment_values_without_selector) + return _compose_typed_config_from_yaml_selector( + config_store, + hydra_environment_config_name, + run_name, + "environment", + environment_values, + environment_cfg_types, + ArenaEnvironmentCfg, + ) + + +# TODO(cvolk, 2026-07-07): [typed-config-migration] Delete this factory when graph-YAML +# environments have a typed configuration and no longer use the argparse compatibility path. +def _graph_environment_cfg_from_yaml_values( + env_graph_spec_yaml: str, + environment_values: dict[str, Any], +) -> LegacyGraphEnvironmentCfg: + """Create the temporary graph-YAML compatibility config from typed YAML Run values. + + The environment values are rendered as CLI tokens for the existing graph-environment + argparse path; the Run's environment_builder section stays typed and is applied + directly at execution (see build_arena_builder_from_legacy_graph). + """ + arena_env_args: dict[str, Any] = {"environment": env_graph_spec_yaml, **environment_values} + return LegacyGraphEnvironmentCfg( + arena_env_args=legacy_environment_args_to_cli_args(arena_env_args), + enable_cameras=bool(environment_values.get("enable_cameras", False)), + env_graph_spec_yaml=env_graph_spec_yaml, + environment_values=dict(environment_values), + ) + + def _graph_spec_yaml_selector(environment_values: Any) -> str | None: """Return the environment.type value when it selects a graph-spec YAML path.""" if not isinstance(environment_values, dict): diff --git a/isaaclab_arena/tests/test_experiment_hydra.py b/isaaclab_arena/tests/test_experiment_hydra.py index 543ef8f29d..3543dae79c 100644 --- a/isaaclab_arena/tests/test_experiment_hydra.py +++ b/isaaclab_arena/tests/test_experiment_hydra.py @@ -211,36 +211,47 @@ def test_effective_experiment_serializes_to_reloadable_yaml(tmp_path): """ -def test_graph_spec_yaml_environment_uses_injected_factory(tmp_path): +def test_graph_spec_yaml_environment_builds_legacy_cfg(tmp_path): config_path = _write_experiment(tmp_path, GRAPH_SPEC_EXPERIMENT_CONTENTS) - factory_calls = [] - def graph_environment_cfg_factory(graph_spec_yaml, environment_values): - factory_calls.append((graph_spec_yaml, environment_values)) - return LegacyGraphEnvironmentCfg(arena_env_args=["--env_graph_spec_yaml", graph_spec_yaml]) - - experiment_cfg = load_arena_experiment_from_yaml( - config_path, - environment_cfg_types={}, - policy_cfg_type_resolver=_policy_cfg_type_for_name_or_class_path, - graph_environment_cfg_factory=graph_environment_cfg_factory, - ) + experiment_cfg = _load_experiment(config_path) run = experiment_cfg.runs["graph_run"] - assert factory_calls == [("robolab/tasks/banana_in_bowl.yaml", {"pick_up_object": "banana"})] assert run.environment == LegacyGraphEnvironmentCfg( - arena_env_args=["--env_graph_spec_yaml", "robolab/tasks/banana_in_bowl.yaml"] + arena_env_args=[ + "--env_graph_spec_yaml", + "robolab/tasks/banana_in_bowl.yaml", + "--pick_up_object", + "banana", + ], + env_graph_spec_yaml="robolab/tasks/banana_in_bowl.yaml", + environment_values={"pick_up_object": "banana"}, ) assert run.policy == ZeroActionPolicyCfg() assert run.environment_builder.num_envs == 2 assert run.rollout_limit.num_steps == 5 -def test_graph_spec_yaml_environment_requires_factory(tmp_path): - config_path = _write_experiment(tmp_path, GRAPH_SPEC_EXPERIMENT_CONTENTS) +def test_graph_spec_yaml_environment_populates_enable_cameras(tmp_path): + config_path = _write_experiment( + tmp_path, + """ +runs: + graph_run: + environment: + type: robolab/tasks/banana_in_bowl.yaml + enable_cameras: true + policy: + type: zero_action +""", + ) - with pytest.raises(AssertionError, match="not given graph-YAML environment support"): - _load_experiment(config_path) + run = _load_experiment(config_path).runs["graph_run"] + + # The typed enable_cameras field drives pre-startup camera detection; the CLI token + # drives the graph-environment argparse path. Both must reflect the YAML value. + assert run.environment.enable_cameras is True + assert "--enable_cameras" in run.environment.arena_env_args @pytest.mark.parametrize( From 9cbc5d9be260c1e5ae1221b98aea455e86c8128f Mon Sep 17 00:00:00 2001 From: alex Date: Mon, 3 Aug 2026 11:28:36 +0200 Subject: [PATCH 05/10] More self review. --- .../evaluation/legacy_eval_config.py | 6 ++--- .../legacy_graph_environment_cli.py | 26 ++++++++++++------- .../hydra/typed_experiment_loader.py | 14 +++++----- .../hydra/typed_experiment_serializer.py | 4 +-- .../test_arena_experiment_config_loader.py | 18 +++++-------- isaaclab_arena/tests/test_experiment_hydra.py | 17 +++++------- .../tests/test_legacy_eval_config.py | 9 ++++--- .../tests/test_osmo_experiment_workflow.py | 10 +++---- 8 files changed, 48 insertions(+), 56 deletions(-) diff --git a/isaaclab_arena/evaluation/legacy_eval_config.py b/isaaclab_arena/evaluation/legacy_eval_config.py index f86dd61ead..81de228f8d 100644 --- a/isaaclab_arena/evaluation/legacy_eval_config.py +++ b/isaaclab_arena/evaluation/legacy_eval_config.py @@ -14,7 +14,6 @@ from isaaclab_arena.environments.arena_env_builder_cfg import ArenaEnvBuilderCfg from isaaclab_arena.environments.arena_environment_factory import ArenaEnvironmentCfg from isaaclab_arena.evaluation.arena_run import ArenaRunCfg, RolloutLimitCfg -from isaaclab_arena.evaluation.legacy_environment_cli_args import legacy_environment_args_to_cli_args from isaaclab_arena.evaluation.legacy_graph_environment_cli import LegacyGraphEnvironmentCfg from isaaclab_arena.evaluation.policy_runner import get_policy_cls from isaaclab_arena.policy.policy_base import PolicyCfg @@ -157,9 +156,8 @@ def _graph_environment_cfg_from_legacy_args( ) -> LegacyGraphEnvironmentCfg: """Create the temporary graph-YAML compatibility config from legacy arguments.""" return LegacyGraphEnvironmentCfg( - arena_env_args=legacy_environment_args_to_cli_args(arena_env_args), - env_graph_spec_yaml=str(arena_env_args["environment"]), - environment_values={ + env_graph_spec_yaml_path=str(arena_env_args["environment"]), + per_run_overrides={ field_name: value for field_name, value in arena_env_args.items() if field_name != "environment" }, ) diff --git a/isaaclab_arena/evaluation/legacy_graph_environment_cli.py b/isaaclab_arena/evaluation/legacy_graph_environment_cli.py index b0e603be04..641dfb513d 100644 --- a/isaaclab_arena/evaluation/legacy_graph_environment_cli.py +++ b/isaaclab_arena/evaluation/legacy_graph_environment_cli.py @@ -11,6 +11,7 @@ from typing import TYPE_CHECKING, Any from isaaclab_arena.environments.arena_environment_factory import ArenaEnvironmentCfg +from isaaclab_arena.evaluation.legacy_environment_cli_args import legacy_environment_args_to_cli_args from isaaclab_arena_environments.cli import arena_env_from_graph_spec, get_isaaclab_arena_environments_cli_parser if TYPE_CHECKING: @@ -24,17 +25,19 @@ @dataclass class LegacyGraphEnvironmentCfg(ArenaEnvironmentCfg): - """Carry a graph-YAML environment through its temporary CLI construction path.""" + """Environment config for graph-YAML environments - # Keyword-only so this required field can follow the defaulted fields of ArenaEnvironmentCfg. - arena_env_args: list[str] = field(kw_only=True) - """Arguments consumed by the existing graph-environment parser.""" + The environment is stored as env_graph_spec_yaml_path and the per-run overrides. + """ - env_graph_spec_yaml: str = "" - """Graph-spec YAML path the environment was loaded from.""" + env_graph_spec_yaml_path: str = "" + """Graph-spec YAML path the environment was loaded from; serialized as the environment ``type``.""" - environment_values: dict[str, Any] = field(default_factory=dict) - """Environment values (without the type selector) used to re-serialize the Run.""" + per_run_overrides: dict[str, Any] = field(default_factory=dict) + """The Run's ``environment`` YAML values minus the environment path itself + i.e. the per-run overrides e.g. {"pick_up_object": "banana"}. Combined with the path to + re-serialize the run and build the graph-environment CLI tokens at execution time. + """ def build_arena_builder_from_legacy_graph( @@ -49,8 +52,11 @@ def build_arena_builder_from_legacy_graph( """ from isaaclab_arena.environments.arena_env_builder import ArenaEnvBuilder - assert "--env_graph_spec_yaml" in cfg.arena_env_args, "legacy graph config must select a graph YAML" + assert cfg.env_graph_spec_yaml_path.endswith((".yaml", ".yml")), "legacy graph config must select a graph YAML" + arena_env_args = legacy_environment_args_to_cli_args( + {"environment": cfg.env_graph_spec_yaml_path, **cfg.per_run_overrides} + ) parser = get_isaaclab_arena_environments_cli_parser() - args_cli = parser.parse_args(cfg.arena_env_args) + args_cli = parser.parse_args(arena_env_args) arena_env = arena_env_from_graph_spec(args_cli.env_graph_spec_yaml, args_cli) return ArenaEnvBuilder(arena_env, environment_builder, hydra_overrides=hydra_overrides) diff --git a/isaaclab_arena/hydra/typed_experiment_loader.py b/isaaclab_arena/hydra/typed_experiment_loader.py index fa9ad14a34..7e631cac2f 100644 --- a/isaaclab_arena/hydra/typed_experiment_loader.py +++ b/isaaclab_arena/hydra/typed_experiment_loader.py @@ -23,7 +23,6 @@ from isaaclab_arena.environments.arena_environment_factory import ArenaEnvironmentCfg from isaaclab_arena.evaluation.arena_experiment import ArenaExperimentCfg from isaaclab_arena.evaluation.arena_run import ArenaRunCfg -from isaaclab_arena.evaluation.legacy_environment_cli_args import legacy_environment_args_to_cli_args from isaaclab_arena.evaluation.legacy_graph_environment_cli import LegacyGraphEnvironmentCfg from isaaclab_arena.policy.policy_base import PolicyCfg @@ -245,16 +244,15 @@ def _graph_environment_cfg_from_yaml_values( ) -> LegacyGraphEnvironmentCfg: """Create the temporary graph-YAML compatibility config from typed YAML Run values. - The environment values are rendered as CLI tokens for the existing graph-environment - argparse path; the Run's environment_builder section stays typed and is applied - directly at execution (see build_arena_builder_from_legacy_graph). + The path and environment values are stored structured and rendered into CLI tokens for + the existing graph-environment argparse path on demand at execution; the Run's + environment_builder section stays typed and is applied directly (see + build_arena_builder_from_legacy_graph). """ - arena_env_args: dict[str, Any] = {"environment": env_graph_spec_yaml, **environment_values} return LegacyGraphEnvironmentCfg( - arena_env_args=legacy_environment_args_to_cli_args(arena_env_args), enable_cameras=bool(environment_values.get("enable_cameras", False)), - env_graph_spec_yaml=env_graph_spec_yaml, - environment_values=dict(environment_values), + env_graph_spec_yaml_path=env_graph_spec_yaml, + per_run_overrides=dict(environment_values), ) diff --git a/isaaclab_arena/hydra/typed_experiment_serializer.py b/isaaclab_arena/hydra/typed_experiment_serializer.py index 723e32f9f6..056514a4e8 100644 --- a/isaaclab_arena/hydra/typed_experiment_serializer.py +++ b/isaaclab_arena/hydra/typed_experiment_serializer.py @@ -64,9 +64,9 @@ def _environment_yaml_values( # Graph-YAML environments serialize from their original source values; the derived # arena_env_args tokens are an execution detail the loader rebuilds on reload. assert ( - run_cfg.environment.env_graph_spec_yaml + run_cfg.environment.env_graph_spec_yaml_path ), "Graph-YAML environment cannot be serialized because it does not record its graph-spec YAML path" - return {"type": run_cfg.environment.env_graph_spec_yaml, **run_cfg.environment.environment_values} + return {"type": run_cfg.environment.env_graph_spec_yaml_path, **run_cfg.environment.per_run_overrides} environment_type = environment_registry.get_factory_type_for_cfg(run_cfg.environment) return {"type": environment_type.name, **dumped_environment_values} diff --git a/isaaclab_arena/tests/test_arena_experiment_config_loader.py b/isaaclab_arena/tests/test_arena_experiment_config_loader.py index f6bd59151c..cbef7f6f92 100644 --- a/isaaclab_arena/tests/test_arena_experiment_config_loader.py +++ b/isaaclab_arena/tests/test_arena_experiment_config_loader.py @@ -110,17 +110,10 @@ def test_load_typed_yaml_experiment_with_graph_spec_environment(tmp_path, monkey run = experiment_cfg.runs["graph_run"] assert isinstance(run.environment, LegacyGraphEnvironmentCfg) - # Only environment values become CLI tokens; the environment_builder section stays - # typed and reaches the argparse path directly at execution. - assert run.environment.arena_env_args == [ - "--enable_cameras", - "--env_graph_spec_yaml", - "robolab/tasks/banana_in_bowl.yaml", - "--pick_up_object", - "banana", - ] - assert run.environment.env_graph_spec_yaml == "robolab/tasks/banana_in_bowl.yaml" - assert run.environment.environment_values == {"enable_cameras": True, "pick_up_object": "banana"} + # Only environment values are stored (and later become CLI tokens at execution); the + # environment_builder section stays typed and reaches the argparse path directly. + assert run.environment.env_graph_spec_yaml_path == "robolab/tasks/banana_in_bowl.yaml" + assert run.environment.per_run_overrides == {"enable_cameras": True, "pick_up_object": "banana"} assert run.environment_builder.num_envs == 2 assert run.environment_builder.device == "cuda:1" assert run.environment_builder.language_instruction == "Pick up the banana." @@ -172,7 +165,8 @@ def test_typed_graph_camera_run_requires_prelaunch_camera_flag(): run_cfg = ArenaRunCfg( name="graph_run", environment=LegacyGraphEnvironmentCfg( - arena_env_args=["--enable_cameras", "--env_graph_spec_yaml", "robolab/tasks/banana_in_bowl.yaml"] + env_graph_spec_yaml_path="robolab/tasks/banana_in_bowl.yaml", + enable_cameras=True, ), policy=ZeroActionPolicyCfg(), ) diff --git a/isaaclab_arena/tests/test_experiment_hydra.py b/isaaclab_arena/tests/test_experiment_hydra.py index 3543dae79c..8869b6693f 100644 --- a/isaaclab_arena/tests/test_experiment_hydra.py +++ b/isaaclab_arena/tests/test_experiment_hydra.py @@ -218,14 +218,8 @@ def test_graph_spec_yaml_environment_builds_legacy_cfg(tmp_path): run = experiment_cfg.runs["graph_run"] assert run.environment == LegacyGraphEnvironmentCfg( - arena_env_args=[ - "--env_graph_spec_yaml", - "robolab/tasks/banana_in_bowl.yaml", - "--pick_up_object", - "banana", - ], - env_graph_spec_yaml="robolab/tasks/banana_in_bowl.yaml", - environment_values={"pick_up_object": "banana"}, + env_graph_spec_yaml_path="robolab/tasks/banana_in_bowl.yaml", + per_run_overrides={"pick_up_object": "banana"}, ) assert run.policy == ZeroActionPolicyCfg() assert run.environment_builder.num_envs == 2 @@ -248,10 +242,11 @@ def test_graph_spec_yaml_environment_populates_enable_cameras(tmp_path): run = _load_experiment(config_path).runs["graph_run"] - # The typed enable_cameras field drives pre-startup camera detection; the CLI token - # drives the graph-environment argparse path. Both must reflect the YAML value. + # The typed enable_cameras field drives pre-startup camera detection; the same value is + # retained in per_run_overrides, from which the graph-environment CLI tokens are built at + # execution. Both must reflect the YAML value. assert run.environment.enable_cameras is True - assert "--enable_cameras" in run.environment.arena_env_args + assert run.environment.per_run_overrides["enable_cameras"] is True @pytest.mark.parametrize( diff --git a/isaaclab_arena/tests/test_legacy_eval_config.py b/isaaclab_arena/tests/test_legacy_eval_config.py index 18f5fb1fd6..e08e506075 100644 --- a/isaaclab_arena/tests/test_legacy_eval_config.py +++ b/isaaclab_arena/tests/test_legacy_eval_config.py @@ -90,9 +90,8 @@ def test_legacy_graph_environment_stays_in_the_existing_cli_path(): (run,) = run_cfgs_from_legacy_eval_config(legacy_config, device="cpu") assert isinstance(run.environment, LegacyGraphEnvironmentCfg) - assert run.environment.arena_env_args == legacy_environment_args_to_cli_args( - legacy_config["jobs"][0]["arena_env_args"] - ) + assert run.environment.env_graph_spec_yaml_path == str(graph_path) + assert run.environment.per_run_overrides == {"enable_cameras": True, "object": "dex_cube"} def test_legacy_graph_builder_keeps_namespace_inside_graph_compatibility(monkeypatch): @@ -145,7 +144,9 @@ def get_builder(arena_env, builder_cfg, hydra_overrides): ) assert builder is expected_builder - assert captured["arguments"] == run.environment.arena_env_args + assert captured["arguments"] == legacy_environment_args_to_cli_args( + {"environment": run.environment.env_graph_spec_yaml_path, **run.environment.per_run_overrides} + ) assert captured["args_cli"] is parsed_args assert captured["env_graph_spec_yaml"] == str(graph_path) # The Run's typed builder config crosses the boundary directly, so device and diff --git a/isaaclab_arena/tests/test_osmo_experiment_workflow.py b/isaaclab_arena/tests/test_osmo_experiment_workflow.py index 81b7c48b16..d4ca9db436 100644 --- a/isaaclab_arena/tests/test_osmo_experiment_workflow.py +++ b/isaaclab_arena/tests/test_osmo_experiment_workflow.py @@ -465,11 +465,11 @@ def test_embedded_graph_environment_experiment_composes_through_experiment_runne experiment_cfg = load_arena_experiment_from_config_file(embedded_path, device="cuda:0") run_cfg = experiment_cfg.runs["graph_run"] assert isinstance(run_cfg.environment, LegacyGraphEnvironmentCfg) - assert run_cfg.environment.arena_env_args == [ - "--enable_cameras", - "--env_graph_spec_yaml", - "isaaclab_arena/tests/test_data/pick_and_place_maple_table_env_graph.yaml", - ] + assert ( + run_cfg.environment.env_graph_spec_yaml_path + == "isaaclab_arena/tests/test_data/pick_and_place_maple_table_env_graph.yaml" + ) + assert run_cfg.environment.per_run_overrides == {"enable_cameras": True} assert run_cfg.environment_builder.num_envs == 4 assert run_cfg.policy.remote_host == Pi0ServerTask.host_token("policy-server-0") From c64d1c7d05e92e0e369f28f7a4e22d3e3d53fef9 Mon Sep 17 00:00:00 2001 From: alex Date: Mon, 3 Aug 2026 11:44:38 +0200 Subject: [PATCH 06/10] Address self-review. --- .../legacy_graph_environment_cli.py | 6 +-- .../hydra/typed_experiment_loader.py | 52 +++++++++++-------- .../hydra/typed_experiment_serializer.py | 12 ++--- 3 files changed, 35 insertions(+), 35 deletions(-) diff --git a/isaaclab_arena/evaluation/legacy_graph_environment_cli.py b/isaaclab_arena/evaluation/legacy_graph_environment_cli.py index 641dfb513d..3d9ad87bd4 100644 --- a/isaaclab_arena/evaluation/legacy_graph_environment_cli.py +++ b/isaaclab_arena/evaluation/legacy_graph_environment_cli.py @@ -45,11 +45,7 @@ def build_arena_builder_from_legacy_graph( environment_builder: ArenaEnvBuilderCfg, hydra_overrides: list[str], ) -> ArenaEnvBuilder: - """Build a graph-YAML environment through the existing argparse adapter. - - Only environment construction crosses the argparse boundary; the Run's typed - builder configuration is used directly, so Hydra overrides on it take effect. - """ + """Build a graph-YAML environment through the existing argparse adapter.""" from isaaclab_arena.environments.arena_env_builder import ArenaEnvBuilder assert cfg.env_graph_spec_yaml_path.endswith((".yaml", ".yml")), "legacy graph config must select a graph YAML" diff --git a/isaaclab_arena/hydra/typed_experiment_loader.py b/isaaclab_arena/hydra/typed_experiment_loader.py index 7e631cac2f..ec5002305a 100644 --- a/isaaclab_arena/hydra/typed_experiment_loader.py +++ b/isaaclab_arena/hydra/typed_experiment_loader.py @@ -219,28 +219,29 @@ def _build_environment_cfg_from_yaml_values( When environment.type names a graph-spec YAML file it is built on the temporary argparse compatibility path; otherwise the type selects a registered typed config. """ - graph_spec_yaml = _graph_spec_yaml_selector(environment_values) - if graph_spec_yaml is not None: - environment_values_without_selector = { + if _is_environment_graph_yaml_spec(environment_values): + env_graph_spec_yaml_path = _graph_spec_yaml_path(environment_values) + per_run_overrides = { field_name: value for field_name, value in environment_values.items() if field_name != "type" } - return _graph_environment_cfg_from_yaml_values(graph_spec_yaml, environment_values_without_selector) - return _compose_typed_config_from_yaml_selector( - config_store, - hydra_environment_config_name, - run_name, - "environment", - environment_values, - environment_cfg_types, - ArenaEnvironmentCfg, - ) + return _graph_environment_cfg_from_yaml_values(env_graph_spec_yaml_path, per_run_overrides) + else: + return _compose_typed_config_from_yaml_selector( + config_store, + hydra_environment_config_name, + run_name, + "environment", + environment_values, + environment_cfg_types, + ArenaEnvironmentCfg, + ) # TODO(cvolk, 2026-07-07): [typed-config-migration] Delete this factory when graph-YAML # environments have a typed configuration and no longer use the argparse compatibility path. def _graph_environment_cfg_from_yaml_values( - env_graph_spec_yaml: str, - environment_values: dict[str, Any], + env_graph_spec_yaml_path: str, + per_run_overrides: dict[str, Any], ) -> LegacyGraphEnvironmentCfg: """Create the temporary graph-YAML compatibility config from typed YAML Run values. @@ -250,19 +251,24 @@ def _graph_environment_cfg_from_yaml_values( build_arena_builder_from_legacy_graph). """ return LegacyGraphEnvironmentCfg( - enable_cameras=bool(environment_values.get("enable_cameras", False)), - env_graph_spec_yaml_path=env_graph_spec_yaml, - per_run_overrides=dict(environment_values), + enable_cameras=bool(per_run_overrides.get("enable_cameras", False)), + env_graph_spec_yaml_path=env_graph_spec_yaml_path, + per_run_overrides=dict(per_run_overrides), ) -def _graph_spec_yaml_selector(environment_values: Any) -> str | None: - """Return the environment.type value when it selects a graph-spec YAML path.""" +def _is_environment_graph_yaml_spec(environment_values: Any) -> bool: + """Return whether a Run's environment.type names a graph-spec YAML path.""" + return _graph_spec_yaml_path(environment_values) is not None + + +def _graph_spec_yaml_path(environment_values: Any) -> str | None: + """Return the environment.type value when it names a graph-spec YAML path, else None.""" if not isinstance(environment_values, dict): return None - selector = environment_values.get("type") - if isinstance(selector, str) and selector.lower().endswith((".yaml", ".yml")): - return selector + environment_type = environment_values.get("type") + if isinstance(environment_type, str) and environment_type.lower().endswith((".yaml", ".yml")): + return environment_type return None diff --git a/isaaclab_arena/hydra/typed_experiment_serializer.py b/isaaclab_arena/hydra/typed_experiment_serializer.py index 056514a4e8..ad88cba591 100644 --- a/isaaclab_arena/hydra/typed_experiment_serializer.py +++ b/isaaclab_arena/hydra/typed_experiment_serializer.py @@ -59,16 +59,14 @@ def _environment_yaml_values( run_cfg: ArenaRunCfg, dumped_environment_values: dict[str, Any], ) -> dict[str, Any]: - """Return one Run's environment section with the type selector the loader expects.""" + """Return one Run's environment section.""" if isinstance(run_cfg.environment, LegacyGraphEnvironmentCfg): # Graph-YAML environments serialize from their original source values; the derived - # arena_env_args tokens are an execution detail the loader rebuilds on reload. - assert ( - run_cfg.environment.env_graph_spec_yaml_path - ), "Graph-YAML environment cannot be serialized because it does not record its graph-spec YAML path" + # CLI tokens are an execution detail the loader rebuilds on reload. return {"type": run_cfg.environment.env_graph_spec_yaml_path, **run_cfg.environment.per_run_overrides} - environment_type = environment_registry.get_factory_type_for_cfg(run_cfg.environment) - return {"type": environment_type.name, **dumped_environment_values} + else: + environment_type = environment_registry.get_factory_type_for_cfg(run_cfg.environment) + return {"type": environment_type.name, **dumped_environment_values} def _to_yaml_values(value: Any) -> Any: From d7f4ea7f265aadc288333a2b6cb138797c84b496 Mon Sep 17 00:00:00 2001 From: alex Date: Mon, 3 Aug 2026 11:55:11 +0200 Subject: [PATCH 07/10] Address self-review. --- isaaclab_arena/hydra/typed_experiment_loader.py | 2 +- isaaclab_arena/hydra/typed_experiment_serializer.py | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/isaaclab_arena/hydra/typed_experiment_loader.py b/isaaclab_arena/hydra/typed_experiment_loader.py index ec5002305a..3ad064c07c 100644 --- a/isaaclab_arena/hydra/typed_experiment_loader.py +++ b/isaaclab_arena/hydra/typed_experiment_loader.py @@ -211,7 +211,7 @@ def _build_environment_cfg_from_yaml_values( config_store: ConfigStore, hydra_environment_config_name: str, run_name: str, - environment_values: Any, + environment_values: dict[str, Any], environment_cfg_types: dict[str, type[ArenaEnvironmentCfg]], ) -> ArenaEnvironmentCfg: """Build a Run's environment from a graph-spec YAML path or a typed selector. diff --git a/isaaclab_arena/hydra/typed_experiment_serializer.py b/isaaclab_arena/hydra/typed_experiment_serializer.py index ad88cba591..ee5df87fd1 100644 --- a/isaaclab_arena/hydra/typed_experiment_serializer.py +++ b/isaaclab_arena/hydra/typed_experiment_serializer.py @@ -61,8 +61,6 @@ def _environment_yaml_values( ) -> dict[str, Any]: """Return one Run's environment section.""" if isinstance(run_cfg.environment, LegacyGraphEnvironmentCfg): - # Graph-YAML environments serialize from their original source values; the derived - # CLI tokens are an execution detail the loader rebuilds on reload. return {"type": run_cfg.environment.env_graph_spec_yaml_path, **run_cfg.environment.per_run_overrides} else: environment_type = environment_registry.get_factory_type_for_cfg(run_cfg.environment) From 12e0e9c9aa3636ddd5c1ce2268c0130534e4b8bf Mon Sep 17 00:00:00 2001 From: alex Date: Mon, 3 Aug 2026 13:27:43 +0200 Subject: [PATCH 08/10] Add an experiment file for running pi on 2 easy robolab tasks. --- .../robolab_2_tasks_pi0.yaml | 31 +++++++++++++++++++ .../policy/gr00t_remote_closedloop_policy.py | 6 ++-- 2 files changed, 34 insertions(+), 3 deletions(-) create mode 100644 isaaclab_arena_environments/robolab/experiment_configs/robolab_2_tasks_pi0.yaml diff --git a/isaaclab_arena_environments/robolab/experiment_configs/robolab_2_tasks_pi0.yaml b/isaaclab_arena_environments/robolab/experiment_configs/robolab_2_tasks_pi0.yaml new file mode 100644 index 0000000000..412f81c08b --- /dev/null +++ b/isaaclab_arena_environments/robolab/experiment_configs/robolab_2_tasks_pi0.yaml @@ -0,0 +1,31 @@ +# Copyright (c) 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 + +# Runs the two robolab tasks with both the OpenPI (pi0) and GR00T policies. On OSMO the +# inference server for each Run is derived from its policy type, so this launches a pi0 +# server and a GR00T server, each wired to the Runs that use it. +runs: + + banana_on_plate_pi0: + environment: &banana_in_bowl_env + type: isaaclab_arena_environments/robolab/tasks/banana_in_bowl.yaml + enable_cameras: true + policy: &openpi_policy + type: isaaclab_arena_openpi.policy.pi0_remote_policy.Pi0RemotePolicy + policy_variant: pi05 + policy_device: cuda:0 + remote_host: 127.0.0.1 + remote_port: 8000 + openpi_embodiment_adapter: droid + rollout_limit: + num_episodes: 1 + + banana_in_bowl_pi0: + environment: &banana_on_plate_env + type: isaaclab_arena_environments/robolab/tasks/banana_on_plate.yaml + enable_cameras: true + policy: *openpi_policy + rollout_limit: + num_episodes: 1 diff --git a/isaaclab_arena_gr00t/policy/gr00t_remote_closedloop_policy.py b/isaaclab_arena_gr00t/policy/gr00t_remote_closedloop_policy.py index 63e4cab6f9..7524d83260 100644 --- a/isaaclab_arena_gr00t/policy/gr00t_remote_closedloop_policy.py +++ b/isaaclab_arena_gr00t/policy/gr00t_remote_closedloop_policy.py @@ -14,7 +14,7 @@ import gymnasium as gym import torch from dataclasses import dataclass -from typing import Any, Literal +from typing import Any from gr00t.policy.server_client import PolicyClient as Gr00tPolicyClient @@ -54,8 +54,8 @@ class Gr00tRemoteClosedloopPolicyCfg(Gr00tBasePolicyCfg): remote_api_token: str | None = None """Optional policy-server API token.""" - scheduler: Literal["chunk", "synced_batch"] = "chunk" - """Action scheduler used to consume inference chunks.""" + scheduler: str = "chunk" + """Action scheduler used to consume inference chunks: "chunk" or "synced_batch".""" @register_policy From c301b4db7bbd99a1f76c813b8bbb75fd0e39abd0 Mon Sep 17 00:00:00 2001 From: alex Date: Mon, 3 Aug 2026 14:14:55 +0200 Subject: [PATCH 09/10] Remvoe yaml tag. --- .../robolab/experiment_configs/robolab_2_tasks_pi0.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/isaaclab_arena_environments/robolab/experiment_configs/robolab_2_tasks_pi0.yaml b/isaaclab_arena_environments/robolab/experiment_configs/robolab_2_tasks_pi0.yaml index 412f81c08b..531dbc3ff5 100644 --- a/isaaclab_arena_environments/robolab/experiment_configs/robolab_2_tasks_pi0.yaml +++ b/isaaclab_arena_environments/robolab/experiment_configs/robolab_2_tasks_pi0.yaml @@ -9,7 +9,7 @@ runs: banana_on_plate_pi0: - environment: &banana_in_bowl_env + environment: type: isaaclab_arena_environments/robolab/tasks/banana_in_bowl.yaml enable_cameras: true policy: &openpi_policy @@ -23,7 +23,7 @@ runs: num_episodes: 1 banana_in_bowl_pi0: - environment: &banana_on_plate_env + environment: type: isaaclab_arena_environments/robolab/tasks/banana_on_plate.yaml enable_cameras: true policy: *openpi_policy From 066dc92f657634d4252e4161a226e6ec5d19d91c Mon Sep 17 00:00:00 2001 From: alex Date: Mon, 3 Aug 2026 15:26:01 +0200 Subject: [PATCH 10/10] Fix config file. --- .../robolab/experiment_configs/robolab_2_tasks_pi0.yaml | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/isaaclab_arena_environments/robolab/experiment_configs/robolab_2_tasks_pi0.yaml b/isaaclab_arena_environments/robolab/experiment_configs/robolab_2_tasks_pi0.yaml index 531dbc3ff5..ea04e1d4cd 100644 --- a/isaaclab_arena_environments/robolab/experiment_configs/robolab_2_tasks_pi0.yaml +++ b/isaaclab_arena_environments/robolab/experiment_configs/robolab_2_tasks_pi0.yaml @@ -3,14 +3,11 @@ # # SPDX-License-Identifier: Apache-2.0 -# Runs the two robolab tasks with both the OpenPI (pi0) and GR00T policies. On OSMO the -# inference server for each Run is derived from its policy type, so this launches a pi0 -# server and a GR00T server, each wired to the Runs that use it. runs: banana_on_plate_pi0: environment: - type: isaaclab_arena_environments/robolab/tasks/banana_in_bowl.yaml + type: isaaclab_arena_environments/robolab/tasks/banana_on_plate.yaml enable_cameras: true policy: &openpi_policy type: isaaclab_arena_openpi.policy.pi0_remote_policy.Pi0RemotePolicy @@ -24,7 +21,7 @@ runs: banana_in_bowl_pi0: environment: - type: isaaclab_arena_environments/robolab/tasks/banana_on_plate.yaml + type: isaaclab_arena_environments/robolab/tasks/banana_in_bowl.yaml enable_cameras: true policy: *openpi_policy rollout_limit: