Skip to content
24 changes: 24 additions & 0 deletions isaaclab_arena/evaluation/arena_experiment_config_loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
)

Expand All @@ -80,6 +84,26 @@ 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()
Expand Down
14 changes: 12 additions & 2 deletions isaaclab_arena/evaluation/experiment_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Comment thread
alexmillane marked this conversation as resolved.
Outdated


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():
Expand Down
4 changes: 4 additions & 0 deletions isaaclab_arena/evaluation/legacy_eval_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
},
)


Expand Down
27 changes: 19 additions & 8 deletions isaaclab_arena/evaluation/legacy_graph_environment_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.
Comment thread
alexmillane marked this conversation as resolved.
Outdated
"""
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)
3 changes: 1 addition & 2 deletions isaaclab_arena/evaluation/run_execution.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
47 changes: 38 additions & 9 deletions isaaclab_arena/hydra/typed_experiment_loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]], ArenaEnvironmentCfg] | None = None,
overrides: list[str] | None = None,
) -> ArenaExperimentCfg:
"""Load a YAML Arena Experiment Definition as a typed named-Run mapping.
Expand All @@ -55,6 +56,9 @@ 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 and the remaining environment values.
overrides: Hydra field overrides for Runs already declared in YAML.

Returns:
Expand All @@ -76,6 +80,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())
}
Expand Down Expand Up @@ -151,6 +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]], ArenaEnvironmentCfg] | None,
) -> ArenaRunCfg:
"""Build one typed Arena Run from its unresolved YAML values.

Expand All @@ -162,6 +168,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.
Expand All @@ -173,15 +181,26 @@ 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 = graph_environment_cfg_factory(graph_spec_yaml, environment_values_without_selector)
Comment thread
alexmillane marked this conversation as resolved.
Outdated
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")
Expand All @@ -208,6 +227,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,
Expand Down
21 changes: 19 additions & 2 deletions isaaclab_arena/hydra/typed_experiment_serializer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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__}"
Expand All @@ -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."""
Comment thread
alexmillane marked this conversation as resolved.
Outdated
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"
Comment thread
alexmillane marked this conversation as resolved.
Outdated
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}
Comment thread
alexmillane marked this conversation as resolved.
Outdated


def _to_yaml_values(value: Any) -> Any:
"""Convert structured-config leaf values into safe YAML primitives."""
if isinstance(value, dict):
Expand Down
Loading
Loading