diff --git a/isaaclab_arena/evaluation/legacy_eval_config.py b/isaaclab_arena/evaluation/legacy_eval_config.py index 8bd1566225..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,7 +156,10 @@ 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_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 6d7060a3a0..3d9ad87bd4 100644 --- a/isaaclab_arena/evaluation/legacy_graph_environment_cli.py +++ b/isaaclab_arena/evaluation/legacy_graph_environment_cli.py @@ -8,13 +8,15 @@ 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.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: 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 @@ -23,23 +25,34 @@ @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_path: str = "" + """Graph-spec YAML path the environment was loaded from; serialized as the environment ``type``.""" + + 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( 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.""" - assert "--env_graph_spec_yaml" in cfg.arena_env_args, "legacy graph config must select a graph YAML" + 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" + 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.device = device - args_cli.language_instruction = language_instruction - return get_arena_builder_from_cli(args_cli, hydra_overrides=hydra_overrides) + 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/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 164cdcf349..3ad064c07c 100644 --- a/isaaclab_arena/hydra/typed_experiment_loader.py +++ b/isaaclab_arena/hydra/typed_experiment_loader.py @@ -23,6 +23,7 @@ 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_graph_environment_cli import LegacyGraphEnvironmentCfg from isaaclab_arena.policy.policy_base import PolicyCfg @@ -47,9 +48,9 @@ def load_arena_experiment_from_yaml( """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. @@ -173,14 +174,12 @@ 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( + environment = _build_environment_cfg_from_yaml_values( 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): @@ -208,6 +207,71 @@ 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: 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. + + 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. + """ + 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(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_path: str, + per_run_overrides: dict[str, Any], +) -> LegacyGraphEnvironmentCfg: + """Create the temporary graph-YAML compatibility config from typed YAML Run values. + + 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). + """ + return LegacyGraphEnvironmentCfg( + 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 _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 + environment_type = environment_values.get("type") + if isinstance(environment_type, str) and environment_type.lower().endswith((".yaml", ".yml")): + return environment_type + return None + + def _compose_typed_config_from_yaml_selector( config_store: ConfigStore, hydra_config_name: str, diff --git a/isaaclab_arena/hydra/typed_experiment_serializer.py b/isaaclab_arena/hydra/typed_experiment_serializer.py index b50049d242..ee5df87fd1 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,19 @@ 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.""" + if isinstance(run_cfg.environment, LegacyGraphEnvironmentCfg): + 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) + 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 372033d07c..cbef7f6f92 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 @@ -18,6 +19,8 @@ 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.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 @@ -76,6 +79,105 @@ 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 + language_instruction: Pick up the banana. + 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) + # 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." + 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", + environment=LegacyGraphEnvironmentCfg( + env_graph_spec_yaml_path="robolab/tasks/banana_in_bowl.yaml", + enable_cameras=True, + ), + 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..8869b6693f 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,59 @@ 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_builds_legacy_cfg(tmp_path): + config_path = _write_experiment(tmp_path, GRAPH_SPEC_EXPERIMENT_CONTENTS) + + experiment_cfg = _load_experiment(config_path) + run = experiment_cfg.runs["graph_run"] + + assert run.environment == LegacyGraphEnvironmentCfg( + 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 + assert run.rollout_limit.num_steps == 5 + + +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 +""", + ) + + run = _load_experiment(config_path).runs["graph_run"] + + # 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 run.environment.per_run_overrides["enable_cameras"] is True + + @pytest.mark.parametrize( ("run_contents", "exception_type", "error"), [ diff --git a/isaaclab_arena/tests/test_legacy_eval_config.py b/isaaclab_arena/tests/test_legacy_eval_config.py index b74eed5a6b..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): @@ -109,7 +108,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 +120,40 @@ 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["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 + # 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..d4ca9db436 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.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") + + 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) 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..ea04e1d4cd --- /dev/null +++ b/isaaclab_arena_environments/robolab/experiment_configs/robolab_2_tasks_pi0.yaml @@ -0,0 +1,28 @@ +# 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: + + banana_on_plate_pi0: + environment: + 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 + 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: + type: isaaclab_arena_environments/robolab/tasks/banana_in_bowl.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