Skip to content

Configs and Task Registry

A task is an EdenConfig subclass: one validated Pydantic object that holds every option group an environment is built from. This page documents the config hierarchy, its serialization helpers, and the task registry that finds configs by name. For how to write a task config, read the task configuration guide; for variants and their CLI flags, read the TaskMods guide.

from eden.tasks import TASK_REGISTRY, TaskMod
from eden.utils.configs import EdenConfig, EdenMimicConfig, EdenRLConfig

Each config class pairs with exactly one environment class (EdenConfig with EnvBase, EdenRLConfig with RLEnvBase, EdenMimicConfig with MimicRLEnvBase); a mismatch raises TypeError at construction. See Environments.

Config hierarchy

EdenConfig

EdenConfig(**data)

Bases: ConfigurableOptions

Base configuration for all Eden environments (EnvBase).

PARAMETER DESCRIPTION
env_options

Environment options (sim dt, num_envs, etc.).

scene_options

Scene options (entities, attachments).

observation_options

Observation manager options.

action_options

Action manager options.

event_options

Event manager options.

cameras_options

Cameras options.

sensors_options

Sensors options.

metric_options

Metric manager options.

recorder_options

Recorder manager options.

renderer_options

Renderer options.

language_instructions

Language instructions for the task.

Source code in eden/options/options.py
def __init__(self, **data):
    # Seed type metadata as construction data so BaseModel validates it into
    # __pydantic_extra__ (no-op for extra="forbid" classes, which use
    # the serialization envelope instead).
    data.update(option_metadata(self))
    # NOTE: all of the option modifications/validations should be done in the __init__ method
    try:
        BaseModel.__init__(self, **data)
    except ValidationError as exc:
        # A misspelled field on an extra="forbid" class should teach the field set, not just
        # name the typo. Only rewritten when every error is extra_forbidden.
        enriched = _enrich_extra_forbidden(type(self), exc)
        if enriched is None:
            raise
        raise enriched from None

Attributes

env_options class-attribute instance-attribute

env_options: EnvOptions = EnvOptions()

scene_options class-attribute instance-attribute

scene_options: SceneOptions = SceneOptions()

observation_options class-attribute instance-attribute

action_options class-attribute instance-attribute

action_options: ActionManagerOptions | None = ActionManagerOptions()

event_options class-attribute instance-attribute

cameras_options class-attribute instance-attribute

cameras_options: CamerasOptions | None = CamerasOptions()

sensors_options class-attribute instance-attribute

sensors_options: SensorsOptions | None = SensorsOptions()

metric_options class-attribute instance-attribute

metric_options: MetricManagerOptions | None = MetricManagerOptions()

recorder_options class-attribute instance-attribute

recorder_options: RecorderManagerOptions | None = RecorderManagerOptions()

renderer_options class-attribute instance-attribute

renderer_options: RendererOptions | None = RendererOptions()

language_instructions class-attribute instance-attribute

language_instructions: list[str] = []

Methods:

check_bindings

check_bindings() -> None

Raise if a field this config leaves for a :class:~eden.tasks.registry.TaskMod to fill is unset.

:meth:eden.envs.base.EnvBase.from_config calls this before it constructs the env. A task whose robot (or data) binding lives in a TaskMod ships the robot-specific fields as empty sentinels, and a config that reaches an env without the mod -- TASK_REGISTRY.get(name)(), a subclass registered with modifiers=(), a subclass of an unregistered base -- otherwise fails deep in env.build() on a pattern error that names neither the field nor the mod, or does not fail at all where an empty link list resolves to no links. The base checks nothing; a config with mod-bound fields overrides this to name them and the mod (VelocityTrackingBaseConfig is the worked example).

Source code in eden/utils/configs.py
def check_bindings(self) -> None:
    """Raise if a field this config leaves for a :class:`~eden.tasks.registry.TaskMod` to fill is unset.

    :meth:`eden.envs.base.EnvBase.from_config` calls this before it constructs the env. A task
    whose robot (or data) binding lives in a TaskMod ships the robot-specific fields as empty
    sentinels, and a config that reaches an env without the mod -- ``TASK_REGISTRY.get(name)()``,
    a subclass registered with ``modifiers=()``, a subclass of an unregistered base -- otherwise
    fails deep in ``env.build()`` on a pattern error that names neither the field nor the mod,
    or does not fail at all where an empty link list resolves to no links. The base checks
    nothing; a config with mod-bound fields overrides this to name them and the mod
    (``VelocityTrackingBaseConfig`` is the worked example).
    """

recording_camera

recording_camera() -> CameraOptions

The camera a recording of this task renders, added as rec by :meth:add_recording_camera.

eden inference --rec adds it; so does any script that records the task and calls :meth:add_recording_camera. A method rather than a cameras_options entry so that only a recording run builds it: a camera in the training config costs every training run its setup (and whatever its overlays allocate) though nothing renders it, and a checkpoint replays the config it was trained with. The default follows the robot entity from 1.5 m out, which suits a locomoting robot; a task whose subject is elsewhere, or whose recording paints state the scene does not render (a camera overlay, :mod:eden.entities.camera_overlays), overrides it.

Source code in eden/utils/configs.py
def recording_camera(self) -> CameraOptions:
    """The camera a recording of this task renders, added as ``rec`` by :meth:`add_recording_camera`.

    ``eden inference --rec`` adds it; so does any script that records the task and calls
    :meth:`add_recording_camera`. A method rather than a ``cameras_options`` entry so that only a recording run
    builds it: a camera in the training config costs every training run its setup (and whatever its overlays
    allocate) though nothing renders it, and a checkpoint replays the config it was trained with. The default
    follows the ``robot`` entity from 1.5 m out, which suits a locomoting robot; a task whose subject is
    elsewhere, or whose recording paints state the scene does not render (a camera overlay,
    :mod:`eden.entities.camera_overlays`), overrides it.
    """
    return CameraOptions(cam_pos=(1.5, 1.0, 2.0), cam_lookat=(0.0, 0.0, 1.0), follow_entity_name="robot")

add_recording_camera

add_recording_camera() -> None

Add :meth:recording_camera to this config as its rec camera, in place, unless it already has one.

A rec camera the config already has (a cameras_options: {rec: {...}} override) is kept: replacing it would silently discard the override. It replaces the task's camera whole, overlays included, so the overlays it lacks are named in a warning.

Source code in eden/utils/configs.py
def add_recording_camera(self) -> None:
    """Add :meth:`recording_camera` to this config as its ``rec`` camera, in place, unless it already has one.

    A ``rec`` camera the config already has (a ``cameras_options: {rec: {...}}`` override) is kept: replacing it
    would silently discard the override. It replaces the task's camera whole, overlays included, so the overlays
    it lacks are named in a warning.
    """
    if self.cameras_options is None:
        self.cameras_options = CamerasOptions()
    declared = getattr(self.cameras_options, "rec", None)
    if declared is None:
        self.cameras_options.rec = self.recording_camera()
        return
    # By value: an overlay read back from a saved config is equal to the one written (its parameters are given their
    # declared types again, so a tuple is not a list).
    dropped = [overlay for overlay in self.recording_camera().overlays if overlay not in declared.overlays]
    if dropped:
        en.logger.warning(
            f"The config declares its own `rec` camera, which replaces {type(self).__name__}.recording_camera() "
            f"whole, so the recording leaves out that camera's overlays {[overlay.name for overlay in dropped]} "
            "(task state the scene does not render, such as ink). To keep them, add them to "
            "cameras_options.rec.overlays, e.g. the entries of config.recording_camera().overlays."
        )

save_as_file

save_as_file(path: str, lock_config: bool = True)

Save the config as either JSON or YAML file with metadata envelope.

PARAMETER DESCRIPTION
path

The path to save the config.

TYPE: str

lock_config

Whether to lock the config after saving to prevent further modifications.

TYPE: bool DEFAULT: True

Source code in eden/utils/configs.py
def save_as_file(self, path: str, lock_config: bool = True):
    """
    Save the config as either JSON or YAML file with metadata envelope.

    Parameters
    ----------
    path: str
        The path to save the config.
    lock_config: bool
        Whether to lock the config after saving to prevent further modifications.
    """
    file_type = path.split(".")[-1].lower()
    if file_type not in ["json", "yaml", "yml"]:
        raise ValueError(f"Unsupported config file extension: {file_type}. Use .json, .yaml, or .yml")

    result = self._serialize()

    if lock_config:
        self.lock()

    dirpath = os.path.dirname(path)
    if dirpath:
        os.makedirs(dirpath, exist_ok=True)
    with open(path, "w") as f:
        if file_type == "json":
            json.dump(result, f, indent=4)
        else:
            yaml.safe_dump(result, f, sort_keys=False)

save_as_json

save_as_json(path: str | None = None, lock_config: bool = True)

Save the config as a JSON file with metadata envelope.

PARAMETER DESCRIPTION
path

The path to save the config. If not provided, the config will be saved to the log directory.

TYPE: str | None DEFAULT: None

lock_config

Whether to lock the config after saving to prevent further modifications.

TYPE: bool DEFAULT: True

RAISES DESCRIPTION
ValueError

If path does not end in .json (case-insensitive). Nothing is written and the config is left unlocked.

Source code in eden/utils/configs.py
def save_as_json(self, path: str | None = None, lock_config: bool = True):
    """
    Save the config as a JSON file with metadata envelope.

    Parameters
    ----------
    path: str | None
        The path to save the config. If not provided, the config will be saved to the log directory.
    lock_config: bool
        Whether to lock the config after saving to prevent further modifications.

    Raises
    ------
    ValueError
        If ``path`` does not end in ``.json`` (case-insensitive). Nothing is written and the
        config is left unlocked.
    """
    path = path or f"{en.log_dir}/config.json"
    # Not an ``assert``: ``python -O`` strips those, and ``save_as_file`` dispatches on the
    # suffix — so ``save_as_json("cfg.txt")`` would silently write a file whose extension
    # ``load_from_file`` refuses. Matched case-insensitively, as ``save_as_file`` is.
    if not path.lower().endswith(".json"):
        raise ValueError(
            f"save_as_json expects a path ending in '.json', got '{path}'. "
            f"Use save_as_yaml for YAML, or save_as_file to dispatch on the extension."
        )
    self.save_as_file(path, lock_config)

save_as_yaml

save_as_yaml(path: str | None = None, lock_config: bool = True)

Save the config as a YAML file with metadata envelope.

PARAMETER DESCRIPTION
path

The path to save the config. If not provided, the config will be saved to the log directory.

TYPE: str | None DEFAULT: None

lock_config

Whether to lock the config after saving to prevent further modifications.

TYPE: bool DEFAULT: True

RAISES DESCRIPTION
ValueError

If path does not end in .yaml or .yml (case-insensitive). Nothing is written and the config is left unlocked.

Source code in eden/utils/configs.py
def save_as_yaml(self, path: str | None = None, lock_config: bool = True):
    """
    Save the config as a YAML file with metadata envelope.

    Parameters
    ----------
    path: str | None
        The path to save the config. If not provided, the config will be saved to the log directory.
    lock_config: bool
        Whether to lock the config after saving to prevent further modifications.

    Raises
    ------
    ValueError
        If ``path`` does not end in ``.yaml`` or ``.yml`` (case-insensitive). Nothing is
        written and the config is left unlocked.
    """
    path = path or f"{en.log_dir}/config.yaml"
    # See ``save_as_json`` — same ``-O``-stripped assert, same silent-wrong-extension outcome.
    if not path.lower().endswith((".yaml", ".yml")):
        raise ValueError(
            f"save_as_yaml expects a path ending in '.yaml' or '.yml', got '{path}'. "
            f"Use save_as_json for JSON, or save_as_file to dispatch on the extension."
        )
    self.save_as_file(path, lock_config)

load_from_file classmethod

load_from_file(path: str) -> EdenConfig

Load the config from a JSON or YAML file, reconstructing its saved class.

The class the file was saved from is recorded in its metadata, so EdenConfig.load_from_file("logs/.../config.json") returns the concrete task config (ReacherConfig) rather than a bare EdenConfig — which is what makes a generic loader like eden train --config <a saved config.json> work at all: a base EdenConfig has nowhere to put reward_options and rejects the file outright.

cls still constrains the result: the saved class must be cls or a subclass of it, so EdenMimicConfig.load_from_file(<a non-mimic config>) fails loud instead of quietly handing back something that is not an EdenMimicConfig. Files with no class metadata (legacy saves) load as cls.

PARAMETER DESCRIPTION
path

The path to load the config from.

TYPE: str

RETURNS DESCRIPTION
EdenConfig

An instance of the saved class (a cls instance when the file records no class).

Source code in eden/utils/configs.py
@classmethod
def load_from_file(cls, path: str) -> "EdenConfig":
    """
    Load the config from a JSON or YAML file, reconstructing its saved class.

    The class the file was saved from is recorded in its metadata, so
    ``EdenConfig.load_from_file("logs/.../config.json")`` returns the concrete task config
    (``ReacherConfig``) rather than a bare ``EdenConfig`` — which is what makes a generic
    loader like ``eden train --config <a saved config.json>`` work at all: a base
    ``EdenConfig`` has nowhere to put ``reward_options`` and rejects the file outright.

    ``cls`` still constrains the result: the saved class must be ``cls`` or a subclass of it,
    so ``EdenMimicConfig.load_from_file(<a non-mimic config>)`` fails loud instead of quietly
    handing back something that is not an ``EdenMimicConfig``. Files with no class metadata
    (legacy saves) load as ``cls``.

    Parameters
    ----------
    path: str
        The path to load the config from.

    Returns
    -------
    EdenConfig
        An instance of the saved class (a ``cls`` instance when the file records no class).
    """
    return cls.load_from_data(read_config_file(path), path)

load_from_data classmethod

load_from_data(data: dict, source: str = '<data>') -> EdenConfig

Reconstruct a saved config from already-read data — see :meth:load_from_file.

Split out so a caller that has to inspect the data before deciding what to do with it (eden train --config, which reads :func:is_full_config to tell a full save from a partial override) can reconstruct without reading the file a second time.

PARAMETER DESCRIPTION
data

The file's contents, in either the _meta/config envelope or the legacy flat layout. Consumed destructively — pass a copy to keep it.

TYPE: dict

source

Where data came from, for error messages.

TYPE: str DEFAULT: '<data>'

RETURNS DESCRIPTION
EdenConfig

An instance of the saved class (a cls instance when the data records no class).

Source code in eden/utils/configs.py
@classmethod
def load_from_data(cls, data: dict, source: str = "<data>") -> "EdenConfig":
    """Reconstruct a saved config from already-read data — see :meth:`load_from_file`.

    Split out so a caller that has to *inspect* the data before deciding what to do with it
    (``eden train --config``, which reads :func:`is_full_config` to tell a full save from a
    partial override) can reconstruct without reading the file a second time.

    Parameters
    ----------
    data: dict
        The file's contents, in either the ``_meta``/``config`` envelope or the legacy flat
        layout. Consumed destructively — pass a copy to keep it.
    source: str
        Where ``data`` came from, for error messages.

    Returns
    -------
    EdenConfig
        An instance of the saved class (a ``cls`` instance when the data records no class).
    """
    meta = data.pop("_meta", None)
    if meta is not None and "config" in data:
        # Envelope format: {"_meta": {...}, "config": {...}}
        data = data["config"]
    else:
        # Legacy flat format: metadata sat alongside the fields. It carries the same
        # class tags, so it resolves the saved class the same way.
        meta = {key: data.pop(key, None) for key in (*OPTION_METADATA_KEYS, "eden_version", "genesis_version")}

    options = to_options(data, cls._saved_config_class(meta, source))

    for key, name, current in (
        ("eden", "Eden", get_eden_provenance()),
        ("genesis", "Genesis", get_package_provenance("genesis-world")),
    ):
        if change := describe_provenance_change(meta.get(key), current):
            en.logger.warning(f"{name} differs from the one that saved {source}: {change}.")

    return options

with_overrides_from_file

with_overrides_from_file(path: str) -> EdenConfig

Create a new config by merging this config with values from a partial config file.

The current config is not modified.

PARAMETER DESCRIPTION
path

The path to the override file (.json, .yaml, or .yml).

TYPE: str

RETURNS DESCRIPTION
EdenConfig

A new config instance based on this config with overrides applied.

Source code in eden/utils/configs.py
def with_overrides_from_file(self, path: str) -> "EdenConfig":
    """Create a new config by merging this config with values from a partial config file.

    The current config is not modified.

    Parameters
    ----------
    path: str
        The path to the override file (.json, .yaml, or .yml).

    Returns
    -------
    EdenConfig
        A new config instance based on this config with overrides applied.
    """
    return self.with_overrides_from_dict(read_config_file(path))

with_overrides_from_dict

with_overrides_from_dict(data_dict: dict) -> EdenConfig
Source code in eden/utils/configs.py
def with_overrides_from_dict(self, data_dict: dict) -> "EdenConfig":
    merged = _deep_merge(self._to_dict(), data_dict)
    return to_options(merged, self)

EdenRLConfig

EdenRLConfig(**data)

Bases: EdenConfig

Configuration for RL environments (RLEnvBase).

Extends EdenConfig with reward, termination, command, curriculum, and runner options needed for reinforcement learning.

PARAMETER DESCRIPTION
reward_options

Reward manager options.

termination_options

Termination manager options.

command_options

Command manager options.

curriculum_options

Curriculum manager options.

runner_options

Runner/training algorithm options. Framework-agnostic base type; use RslRlOnPolicyRunnerOptions, RlGamesPpoRunnerOptions, etc.

Source code in eden/options/options.py
def __init__(self, **data):
    # Seed type metadata as construction data so BaseModel validates it into
    # __pydantic_extra__ (no-op for extra="forbid" classes, which use
    # the serialization envelope instead).
    data.update(option_metadata(self))
    # NOTE: all of the option modifications/validations should be done in the __init__ method
    try:
        BaseModel.__init__(self, **data)
    except ValidationError as exc:
        # A misspelled field on an extra="forbid" class should teach the field set, not just
        # name the typo. Only rewritten when every error is extra_forbidden.
        enriched = _enrich_extra_forbidden(type(self), exc)
        if enriched is None:
            raise
        raise enriched from None

Attributes

reward_options class-attribute instance-attribute

reward_options: RewardManagerOptions | None = RewardManagerOptions()

termination_options class-attribute instance-attribute

termination_options: TerminationManagerOptions | None = TerminationManagerOptions()

command_options class-attribute instance-attribute

command_options: CommandManagerOptions | None = CommandManagerOptions()

curriculum_options class-attribute instance-attribute

curriculum_options: CurriculumManagerOptions | None = CurriculumManagerOptions()

runner_options class-attribute instance-attribute

runner_options: ConfigurableOptions | None = None

EdenMimicConfig

EdenMimicConfig(**data)

Bases: EdenRLConfig

Configuration for motion imitation environments (MimicRLEnvBase).

Extends EdenRLConfig with motion manager options for motion imitation tasks.

PARAMETER DESCRIPTION
motion_options

Motion manager options.

Source code in eden/options/options.py
def __init__(self, **data):
    # Seed type metadata as construction data so BaseModel validates it into
    # __pydantic_extra__ (no-op for extra="forbid" classes, which use
    # the serialization envelope instead).
    data.update(option_metadata(self))
    # NOTE: all of the option modifications/validations should be done in the __init__ method
    try:
        BaseModel.__init__(self, **data)
    except ValidationError as exc:
        # A misspelled field on an extra="forbid" class should teach the field set, not just
        # name the typo. Only rewritten when every error is extra_forbidden.
        enriched = _enrich_extra_forbidden(type(self), exc)
        if enriched is None:
            raise
        raise enriched from None

Attributes

motion_options class-attribute instance-attribute

motion_options: MotionManagerOptions | None = None

Gaussian-splat configs

Configs for environments that render a Gaussian-splat background (see Gaussian-splat environments).

EdenSplatConfig

EdenSplatConfig(**data)

Bases: EdenConfig

Configuration for non-RL gaussian-splat environments (:class:~eden.envs.splat.SplatEnvBase).

A sibling of :class:EdenRLConfig (like :class:EdenNyxSplatConfig): a splat scene with no reward/termination/command/curriculum surface — viewer / teleop / inference / data collection. Whether the splat is rendered per-env or broadcast is a camera choice, not a config-leaf one (see :class:~eden.options.splat.SplatOptions), so this leaf covers both single-env and batched non-RL rendering. For RL training on a splat scene, use :class:EdenSplatRLConfig.

splat_options is a root field rather than a member of scene_options because it selects the environment class — :func:~eden.envs.base.env_class_for_config dispatches on this config type, and a field buried in the scene would leave the dispatcher unable to see it.

PARAMETER DESCRIPTION
splat_options

Splat renderer options for photorealistic background rendering (shared / per-env Gaussians, minibatch, camera mode).

Source code in eden/options/options.py
def __init__(self, **data):
    # Seed type metadata as construction data so BaseModel validates it into
    # __pydantic_extra__ (no-op for extra="forbid" classes, which use
    # the serialization envelope instead).
    data.update(option_metadata(self))
    # NOTE: all of the option modifications/validations should be done in the __init__ method
    try:
        BaseModel.__init__(self, **data)
    except ValidationError as exc:
        # A misspelled field on an extra="forbid" class should teach the field set, not just
        # name the typo. Only rewritten when every error is extra_forbidden.
        enriched = _enrich_extra_forbidden(type(self), exc)
        if enriched is None:
            raise
        raise enriched from None

Attributes

splat_options class-attribute instance-attribute

splat_options: SplatOptions | None = None

Methods:

recording_camera

recording_camera() -> CameraOptions

:meth:EdenConfig.recording_camera with the splat composited behind empty space. A task whose splat stands in for a sim surface (a table plane) overrides it to name that entity in the SplatComposite.

Source code in eden/utils/configs.py
def recording_camera(self) -> CameraOptions:
    """:meth:`EdenConfig.recording_camera` with the splat composited behind empty space. A task whose splat
    stands in for a sim surface (a table plane) overrides it to name that entity in the ``SplatComposite``.
    """
    return _with_splat(super().recording_camera())

EdenSplatRLConfig

EdenSplatRLConfig(**data)

Bases: EdenRLConfig

Configuration for RL gaussian-splat environments (:class:~eden.envs.splat.SplatRLEnvBase).

The RL counterpart of :class:EdenSplatConfig: the same gsplat splat background plus the full reward/termination/command/curriculum/runner surface of :class:EdenRLConfig. The env↔env split mirrors EnvBase / RLEnvBase; the single-vs-batched rendering choice is orthogonal and lives on the camera (:class:~eden.options.splat.SplatOptions.camera_mode).

PARAMETER DESCRIPTION
splat_options

Splat renderer options for photorealistic background rendering (shared / per-env Gaussians, minibatch, camera mode).

Source code in eden/options/options.py
def __init__(self, **data):
    # Seed type metadata as construction data so BaseModel validates it into
    # __pydantic_extra__ (no-op for extra="forbid" classes, which use
    # the serialization envelope instead).
    data.update(option_metadata(self))
    # NOTE: all of the option modifications/validations should be done in the __init__ method
    try:
        BaseModel.__init__(self, **data)
    except ValidationError as exc:
        # A misspelled field on an extra="forbid" class should teach the field set, not just
        # name the typo. Only rewritten when every error is extra_forbidden.
        enriched = _enrich_extra_forbidden(type(self), exc)
        if enriched is None:
            raise
        raise enriched from None

Attributes

splat_options class-attribute instance-attribute

splat_options: SplatOptions | None = None

Methods:

recording_camera

recording_camera() -> CameraOptions

:meth:EdenConfig.recording_camera with the splat composited behind empty space, as for :class:EdenSplatConfig.

Source code in eden/utils/configs.py
def recording_camera(self) -> CameraOptions:
    """:meth:`EdenConfig.recording_camera` with the splat composited behind empty space, as for
    :class:`EdenSplatConfig`.
    """
    return _with_splat(super().recording_camera())

EdenNyxSplatConfig

EdenNyxSplatConfig(**data)

Bases: EdenConfig

Configuration for Nyx path-traced splat environments (NyxSplatEnvBase).

A sibling of :class:EdenSplatConfig, mirroring the env classes: both render splats, but through different backends with different options types. (Nyx has no batched path.)

PARAMETER DESCRIPTION
splat_options

Nyx splat renderer options for path-traced background rendering.

Source code in eden/options/options.py
def __init__(self, **data):
    # Seed type metadata as construction data so BaseModel validates it into
    # __pydantic_extra__ (no-op for extra="forbid" classes, which use
    # the serialization envelope instead).
    data.update(option_metadata(self))
    # NOTE: all of the option modifications/validations should be done in the __init__ method
    try:
        BaseModel.__init__(self, **data)
    except ValidationError as exc:
        # A misspelled field on an extra="forbid" class should teach the field set, not just
        # name the typo. Only rewritten when every error is extra_forbidden.
        enriched = _enrich_extra_forbidden(type(self), exc)
        if enriched is None:
            raise
        raise enriched from None

Attributes

splat_options class-attribute instance-attribute

splat_options: NyxSplatOptions | None = None

Reading and converting config files

Eden config hierarchy (EdenConfig/EdenRLConfig/EdenMimicConfig) and serialization.

  • :class:EdenConfig — base config for all environments (EnvBase).
  • :class:EdenRLConfig — adds reward, termination, command, curriculum, and runner options.
  • :class:EdenMimicConfig — adds motion options for motion-imitation tasks.

Root config classes use Pydantic extra="forbid" (so misspelled fields raise), while inner ManagerOptions / SceneOptions keep extra="allow" to hold dynamic term/entity children. Saved configs use a _meta/config envelope; nested Options recover their type from _option_module_ / _option_class_ keys.

The root config recovers its type the same way: :meth:EdenConfig.load_from_file reads the saved class out of _meta and reconstructs that class, so a generic EdenConfig.load_from_file(path) returns the concrete task config it was saved from. The requested class still constrains the result — the saved class must be a subclass of it — and files with no class metadata load as the requested class. Round-tripping a config through save/load is therefore type-preserving and idempotent (pinned by tests/utils/test_configs.py::test_every_task_config_round_trips_to_a_fixed_point).

Functions:

read_config_file

read_config_file(path: str) -> dict

Read a .json / .yaml / .yml config file into a raw dict.

The single file-reading primitive behind :meth:EdenConfig.load_from_file and :meth:EdenConfig.with_overrides_from_file, so both dispatch on the extension the same way and report an unsupported one identically.

PARAMETER DESCRIPTION
path

The path to read.

TYPE: str

RETURNS DESCRIPTION
dict

The file's contents, exactly as parsed — envelope and all, if it has one.

RAISES DESCRIPTION
ValueError

If the extension is not .json, .yaml, or .yml, or if the file does not hold a mapping (a bare scalar or list is never a config).

Source code in eden/utils/configs.py
def read_config_file(path: str) -> dict:
    """Read a ``.json`` / ``.yaml`` / ``.yml`` config file into a raw dict.

    The single file-reading primitive behind :meth:`EdenConfig.load_from_file` and
    :meth:`EdenConfig.with_overrides_from_file`, so both dispatch on the extension the
    same way and report an unsupported one identically.

    Parameters
    ----------
    path: str
        The path to read.

    Returns
    -------
    dict
        The file's contents, exactly as parsed — envelope and all, if it has one.

    Raises
    ------
    ValueError
        If the extension is not ``.json``, ``.yaml``, or ``.yml``, or if the file does not
        hold a mapping (a bare scalar or list is never a config).
    """
    path_lower = path.lower()
    if path_lower.endswith(".json"):
        with open(path, "r") as f:
            data = json.load(f)
    elif path_lower.endswith((".yaml", ".yml")):
        with open(path, "r") as f:
            data = yaml.safe_load(f)
    else:
        raise ValueError(f"Unsupported config file extension: {path}. Use .json, .yaml, or .yml")

    if not isinstance(data, dict):
        raise ValueError(f"{path} does not hold a config mapping — parsed as {type(data).__name__}.")
    return data

is_full_config

is_full_config(data: dict) -> bool

Whether data is a saved config (replaces a base) rather than a partial override.

A save from :meth:EdenConfig.save_as_file carries its class and version tags — under a _meta envelope, or (legacy flat saves) alongside the fields. A hand-written partial override never does, which is what makes the two forms distinguishable by content instead of by flag: eden train --config reads this to decide whether the file replaces the task's config or layers onto it.

Source code in eden/utils/configs.py
def is_full_config(data: dict) -> bool:
    """Whether ``data`` is a *saved* config (replaces a base) rather than a partial override.

    A save from :meth:`EdenConfig.save_as_file` carries its class and version tags — under a
    ``_meta`` envelope, or (legacy flat saves) alongside the fields. A hand-written partial
    override never does, which is what makes the two forms distinguishable by content instead
    of by flag: ``eden train --config`` reads this to decide whether the file replaces the
    task's config or layers onto it.
    """
    if "_meta" in data and "config" in data:
        return True
    return any(key in data for key in (*OPTION_METADATA_KEYS, "eden_version", "genesis_version"))

saved_config_qualname

saved_config_qualname(data: dict) -> str | None

The module.Class a saved config records, or None when it records no class.

Reads the same tags :meth:EdenConfig.load_from_data reconstructs from, without importing anything — so a caller can compare identities (eden train --config, checking that a saved config is the one its --task builds) and report a mismatch by name even when the saved class is not importable here.

PARAMETER DESCRIPTION
data

A saved config's contents, in either the _meta/config envelope or the legacy flat layout. Not modified.

TYPE: dict

RETURNS DESCRIPTION
str | None

The dotted qualname, or None for a file with no class metadata (legacy saves and hand-written full configs, which load as whatever class is asked for).

Source code in eden/utils/configs.py
def saved_config_qualname(data: dict) -> str | None:
    """The ``module.Class`` a saved config records, or ``None`` when it records no class.

    Reads the same tags :meth:`EdenConfig.load_from_data` reconstructs from, without importing
    anything — so a caller can compare identities (``eden train --config``, checking that a
    saved config is the one its ``--task`` builds) and report a mismatch by name even when the
    saved class is not importable here.

    Parameters
    ----------
    data: dict
        A saved config's contents, in either the ``_meta``/``config`` envelope or the legacy
        flat layout. Not modified.

    Returns
    -------
    str | None
        The dotted qualname, or ``None`` for a file with no class metadata (legacy saves and
        hand-written full configs, which load as whatever class is asked for).
    """
    meta = data.get("_meta") if isinstance(data.get("_meta"), dict) and "config" in data else data
    module_name, class_name = (meta.get(key) for key in OPTION_METADATA_KEYS)
    return f"{module_name}.{class_name}" if module_name and class_name else None

overrides_from_str

overrides_from_str(text: str) -> dict

Parse one --override argument — an inline YAML (or JSON) mapping — into a dict.

YAML flow style keeps the shell-facing form short and quote-free ('env_options: {num_envs: 64}'); JSON is a subset, so the explicit form works too. Note that PyYAML implements YAML 1.1 implicit typing, where off / no / yes parse as booleans — quote any value that should stay a string.

PARAMETER DESCRIPTION
text

The inline mapping.

TYPE: str

RETURNS DESCRIPTION
dict

The parsed override data, ready for :meth:EdenConfig.with_overrides_from_dict.

RAISES DESCRIPTION
ValueError

If text is not parseable, or parses to something other than a mapping — the common case being a file path, which belongs to --config.

Source code in eden/utils/configs.py
def overrides_from_str(text: str) -> dict:
    """Parse one ``--override`` argument — an inline YAML (or JSON) mapping — into a dict.

    YAML flow style keeps the shell-facing form short and quote-free
    (``'env_options: {num_envs: 64}'``); JSON is a subset, so the explicit form works too.
    Note that PyYAML implements YAML 1.1 implicit typing, where ``off`` / ``no`` / ``yes``
    parse as booleans — quote any value that should stay a string.

    Parameters
    ----------
    text: str
        The inline mapping.

    Returns
    -------
    dict
        The parsed override data, ready for :meth:`EdenConfig.with_overrides_from_dict`.

    Raises
    ------
    ValueError
        If ``text`` is not parseable, or parses to something other than a mapping — the
        common case being a file path, which belongs to ``--config``.
    """
    try:
        data = yaml.safe_load(text)
    except yaml.YAMLError as e:
        raise ValueError(f"Could not parse override {text!r} as an inline YAML mapping: {e}") from e

    if not isinstance(data, dict):
        raise ValueError(
            f"Override {text!r} is not a mapping — it parsed as {type(data).__name__}. Write it as "
            f"'env_options: {{num_envs: 64}}'. To load overrides from a .json/.yaml file, use --config."
        )
    return data

serialize_obj_with_metadata

serialize_obj_with_metadata(obj)

Recursively serialize an object, capturing Options type metadata.

Captures _option_module_ and _option_class_ metadata from Options instances. This works by using model_dump() for data but accessing actual Option objects to extract their metadata.

NamedTuple values (e.g. Genesis TemperatureProperties) subclass tuple and are serialized as plain lists of fields in field order; to_options reconstructs them using the surrounding Pydantic field annotations on load.

Source code in eden/utils/configs.py
def serialize_obj_with_metadata(obj):
    """Recursively serialize an object, capturing Options type metadata.

    Captures ``_option_module_`` and ``_option_class_`` metadata from Options
    instances. This works by using ``model_dump()`` for data but accessing actual
    Option objects to extract their metadata.

    NamedTuple values (e.g. Genesis ``TemperatureProperties``) subclass ``tuple`` and are
    serialized as plain lists of fields in field order; ``to_options`` reconstructs them
    using the surrounding Pydantic field annotations on load.
    """
    if isinstance(obj, Options):
        # A field left at a None default is dumped as absent, not as an explicit None: loading would
        # otherwise mark it set, and Genesis reads set-ness — a solver whose options merely list `dt`
        # among `model_fields_set` divides the sim dt by it (`MochiOptions(dt=None)` would crash the build).
        unset_none = {name for name in type(obj).model_fields if name not in obj.model_fields_set}
        unset_none = {name for name in unset_none if getattr(obj, name, None) is None}
        result: dict = obj.model_dump(exclude=unset_none)

        # Stamp type metadata at dump time. The getattr fallback covers extra="forbid" options
        # (which don't carry it in __pydantic_extra__), so entities stored as unannotated scene
        # children still recover their concrete class on load even though they forbid extras.
        result["_option_module_"] = getattr(obj, "_option_module_", obj.__module__)
        result["_option_class_"] = getattr(obj, "_option_class_", obj.__class__.__name__)

        # Now recursively process each field value to capture nested Options metadata
        for field_name in list(result.keys()):
            if field_name not in OPTION_METADATA_KEYS:
                # Try to get the actual object using getattr to handle all cases
                # (including fields not in __dict__ due to defaults, properties, etc)
                try:
                    actual_value = getattr(obj, field_name)
                    # If the actual value is an Options object, recursively serialize it
                    if isinstance(actual_value, Options):
                        result[field_name] = serialize_obj_with_metadata(actual_value)
                    else:
                        # Recurse over the live field value so container fields
                        # (for example list[Union[...Options]]) keep subclass data
                        # that may be erased by model_dump() schema narrowing.
                        result[field_name] = serialize_obj_with_metadata(actual_value)
                except AttributeError:
                    # Field doesn't exist, keep the dumped value and recurse
                    result[field_name] = serialize_obj_with_metadata(result[field_name])

        return result
    elif isinstance(obj, BaseModel):
        # For other BaseModel instances, serialize and recurse
        result = obj.model_dump()
        for key, value in list(result.items()):
            result[key] = serialize_obj_with_metadata(value)
        return result
    elif isinstance(obj, dict):
        return {key: serialize_obj_with_metadata(value) for key, value in obj.items()}
    elif isinstance(obj, (list, tuple)):
        return [serialize_obj_with_metadata(item) for item in obj]
    elif isinstance(obj, Enum):
        return obj.value
    elif isinstance(obj, np.ndarray):
        return obj.tolist()
    elif isinstance(obj, torch.Tensor):
        return obj.detach().cpu().numpy().tolist()
    # in case of np/torch scalars, cast to plain numeric types
    elif isinstance(obj, bool):
        return bool(obj)
    elif isinstance(obj, int):
        return int(obj)
    elif isinstance(obj, float):
        return float(obj)
    return obj

get_options_class_from_qualname

get_options_class_from_qualname(qualname: str)

Dynamically import and retrieve an Options class from its fully qualified name.

PARAMETER DESCRIPTION
qualname

The fully qualified name of the Options class (e.g., "eden.options.managers.actions.ActionTermOptions")

TYPE: str

RETURNS DESCRIPTION
Type[Options]

The Options class

RAISES DESCRIPTION
ImportError

If the module cannot be imported

AttributeError

If the class cannot be found in the module

Source code in eden/utils/configs.py
def get_options_class_from_qualname(qualname: str):
    """
    Dynamically import and retrieve an Options class from its fully qualified name.

    Parameters
    ----------
    qualname : str
        The fully qualified name of the Options class (e.g., "eden.options.managers.actions.ActionTermOptions")

    Returns
    -------
    Type[Options]
        The Options class

    Raises
    ------
    ImportError
        If the module cannot be imported
    AttributeError
        If the class cannot be found in the module
    """
    import importlib

    # Split the qualname into module path and class name
    parts = qualname.rsplit(".", 1)
    if len(parts) != 2:
        raise ValueError(f"Invalid qualified name: {qualname}. Expected format: 'module.path.ClassName'")

    module_path, class_name = parts

    # Import the module
    try:
        module = importlib.import_module(module_path)
    except ImportError as e:
        raise ImportError(f"Could not import module '{module_path}': {e}")

    # Get the class from the module
    try:
        return getattr(module, class_name)
    except AttributeError:
        raise AttributeError(f"Class '{class_name}' not found in module '{module_path}'")

to_options

to_options(config: dict, base_options: Options | type[Options])

Reconstruct the original Options instance from serialized data.

Eden options (config root classes included) are rebuilt with validation, so an unknown key on an extra="forbid" class raises, while extra="allow" classes (ManagerOptions, SceneOptions, etc.) keep unknown keys as their dynamic term/entity children. Genesis-native options skip validation, and an extra="forbid" one drops unknown keys with a warning — see :func:_safe_model_construct.

PARAMETER DESCRIPTION
config

The serialized field data.

TYPE: dict

base_options

The target type, as either a class or an instance to take the class of. Only the type is read, so pass the class when there is nothing to instantiate it from — a task config's __init__ can be expensive and its result is discarded here.

TYPE: Options | type[Options]

Source code in eden/utils/configs.py
def to_options(config: dict, base_options: Options | type[Options]):
    """
    Reconstruct the original Options instance from serialized data.

    Eden options (config root classes included) are rebuilt with validation, so an unknown key on
    an ``extra="forbid"`` class raises, while ``extra="allow"`` classes (ManagerOptions,
    SceneOptions, etc.) keep unknown keys as their dynamic term/entity children. Genesis-native
    options skip validation, and an ``extra="forbid"`` one drops unknown keys with a warning — see
    :func:`_safe_model_construct`.

    Parameters
    ----------
    config: dict
        The serialized field data.
    base_options: Options | type[Options]
        The target type, as either a class or an instance to take the class of. Only the
        *type* is read, so pass the class when there is nothing to instantiate it from —
        a task config's ``__init__`` can be expensive and its result is discarded here.
    """
    options_cls = base_options if isinstance(base_options, type) else type(base_options)

    # Convert all config attributes
    res = {}
    for key, value in config.items():
        field_info = options_cls.model_fields.get(key)
        field_annotation = field_info.annotation if field_info is not None else None
        res[key] = _convert_value(value, field_annotation)

    return _safe_model_construct(options_cls, res)

Task registry

Tasks register themselves with @TASK_REGISTRY.register() on the config class. The registry scans the source of eden.tasks (and of every package an installed distribution names under the eden.tasks entry-point group) on first lookup without importing it, so there is no central index to update. TASK_REGISTRY.build(name, **kwargs) returns a configured instance with the task's TaskMods applied, which is what eden train does; TASK_REGISTRY.get(name) returns the bare class, and instantiating that yourself skips every mod.

TaskRegistry

TaskRegistry()

Task registry with two lookup paths, tried in order.

  1. Eager registry — populated by @TASK_REGISTRY.register() decorators or explicit register(cls, ...) calls.
  2. Auto-discovery — an AST scan of the configured search packages (default ["eden.tasks"]) finds @TASK_REGISTRY.register() decorators in every *.py file without importing them. On first :meth:get miss we import only the one file that matches the requested name; that triggers the decorator and populates the eager registry.

Tasks self-register at the definition site — no central index in eden/tasks/__init__.py to keep in sync. Out-of-tree tasks join the scan through the :data:TASK_ENTRY_POINT_GROUP entry point of the distribution that ships them, so eden list / eden train --task see them without importing anything; a script can also call :meth:add_search_path before its first lookup.

Auto-discovery rules (files that are NOT scanned):

  • __init__.py is skipped — put decorated classes in a sibling module such as config.py.
  • files whose name starts with _ are skipped (e.g. _meta.py) — private modules are treated as implementation details.

Auto-discovery rules (decorator recognition):

  • The scan recognizes @<name>.register where <name> is bound, in the same file, to TASK_REGISTRY via an absolute import from eden.tasks or eden.tasks.registry (from ... import TASK_REGISTRY or ... as X). Both forms work.
  • Using the literal name TASK_REGISTRY without importing it from a recognized location emits a warning — the scan will not silently miss the registration.

Usage::

# Preferred: decorator at definition site; modifiers passed to the
# decorator so they live on the registry (not on the config class,
# which is a Pydantic model and should only hold serializable schema).
@TASK_REGISTRY.register(modifiers=(ReacherDifficultyMod,))
class ReacherConfig(EdenRLConfig):
    ...

cfg = TASK_REGISTRY.build("reacher", difficulty="hard")  # a configured instance, mods applied
cls = TASK_REGISTRY.get("reacher")                       # the bare class; cls() runs no mod
Source code in eden/tasks/registry.py
def __init__(self) -> None:
    self._registry: dict[str, Any] = {}
    # Modifiers attached at registration time (override class attr). ``_INHERITED`` stands
    # for "take the nearest registered base's", resolved on read by _resolve_modifiers.
    self._modifiers: dict[str, tuple[TaskMod, ...] | _Inherited] = {}
    # Memo for that walk, dropped whenever a registration changes what it would answer.
    self._resolved_modifiers: dict[str, tuple[TaskMod, ...]] = {}
    # Packages add_search_path added to the scan (eden.tasks always), each mapped to the source
    # task_source() reports for its tasks.
    self._search_packages: dict[str, str] = {"eden.tasks": "eden"}
    # The registrations _do_register set aside because their name clashes, by "module:Class",
    # as (name, class, modifiers entry): the class answers to no name, but a subclass still
    # inherits its modifiers through it, and a rescan that ends the clash registers it.
    self._clashing_registrations: dict[str, tuple[str, Any, tuple[TaskMod, ...] | _Inherited]] = {}

    # What the scan found. _ensure_discovered recomputes all of it, entry points included, so a
    # rescan sees a plugin fixed or installed since; nothing else writes it.
    # name -> "module.path:ClassName". None means "not yet scanned".
    self._discovered: dict[str, str] | None = None
    # name -> modifier display-names, read from the decorators.
    self._discovered_modifiers: dict[str, list[str]] = {}
    # Every package scanned, eden.tasks first, mapped to its source label (task_source()).
    self._package_sources: dict[str, str] = {}
    # What the scan could not answer, kept so that only a lookup it could change raises it (one
    # broken plugin must not stop every built-in task): entry points that name no scannable
    # package, as (the package it names, message, cause), and names registered in several
    # scanned files, as name -> [(module:Class, file path)].
    self._broken_entry_points: list[tuple[str, str, BaseException | None]] = []
    self._collisions: dict[str, list[tuple[str, str]]] = {}

Methods:

register

register(obj: Any = None, *, name: str | None = None, override: bool = False, modifiers: tuple[ModifierEntry, ...] | None = None) -> Any

Register a config class eagerly. Can be used as a decorator or function call.

PARAMETER DESCRIPTION
obj

The config class to register. When None, returns a decorator.

TYPE: Any DEFAULT: None

name

Lookup name. Defaults to _default_task_name(cls.__name__) (to_snake_case with any trailing _config stripped).

TYPE: str | None DEFAULT: None

override

If True, silently replace an existing registration with the same name.

TYPE: bool DEFAULT: False

modifiers

Sequence of TaskMod instances (preferred) or bare TaskMod subclasses (auto-instantiated with no args). Applied in order by :meth:build. None (the default) inherits the modifiers of the nearest registered class in obj's MRO, so a subclass of a task keeps the mods that bind it — its robot, its data — instead of building against the parent's unset sentinels. That inheritance is live: it re-resolves on every read, so re-registering the class the mods come from — or registering it after this one — updates this task too, whichever order the two ran in. The registration override=True replaces counts, so shadowing a built-in with a subclass keeps the built-in's mods; that one is a snapshot, since the registration it came from no longer exists. () opts out; a class with no registered ancestor gets none either way.

TYPE: tuple[ModifierEntry, ...] | None DEFAULT: None

RAISES DESCRIPTION
ValueError

If modifiers is None and the nearest registered ancestor is registered under several names with different modifiers, none of them the name being replaced — pass modifiers= explicitly to pick. A later registration that makes an already-registered task ambiguous the same way raises from :meth:get_modifiers / :meth:build instead — :meth:modifier_names, a display path, falls back to the names the decorators declare.

Source code in eden/tasks/registry.py
def register(
    self,
    obj: Any = None,
    *,
    name: str | None = None,
    override: bool = False,
    modifiers: tuple[ModifierEntry, ...] | None = None,
) -> Any:
    """Register a config class eagerly. Can be used as a decorator or function call.

    Parameters
    ----------
    obj: Any
        The config class to register. When ``None``, returns a decorator.
    name: str | None
        Lookup name. Defaults to ``_default_task_name(cls.__name__)``
        (``to_snake_case`` with any trailing ``_config`` stripped).
    override: bool
        If ``True``, silently replace an existing registration with the
        same name.
    modifiers: tuple[ModifierEntry, ...] | None
        Sequence of TaskMod **instances** (preferred) or bare TaskMod
        subclasses (auto-instantiated with no args). Applied in order by
        :meth:`build`. ``None`` (the default) inherits the modifiers of the
        nearest registered class in ``obj``'s MRO, so a subclass of a task
        keeps the mods that bind it — its robot, its data — instead of
        building against the parent's unset sentinels. That inheritance is
        **live**: it re-resolves on every read, so re-registering the class
        the mods come from — or registering it after this one — updates this
        task too, whichever order the two ran in. The registration
        ``override=True`` replaces counts, so shadowing a built-in with a
        subclass keeps the built-in's mods; that one is a snapshot, since the
        registration it came from no longer exists. ``()`` opts out; a class
        with no registered ancestor gets none either way.

    Raises
    ------
    ValueError
        If ``modifiers`` is ``None`` and the nearest registered ancestor is
        registered under several names with different modifiers, none of them
        the ``name`` being replaced — pass ``modifiers=`` explicitly to pick.
        A later registration that makes an already-registered task ambiguous
        the same way raises from :meth:`get_modifiers` / :meth:`build` instead
        — :meth:`modifier_names`, a display path, falls back to the names the
        decorators declare.
    """

    def deco(cls: Any) -> Any:
        if not isinstance(cls, type):
            # Every lookup reads the class's module and name, so anything else would register
            # here and then fail far from this line.
            raise TypeError(
                f"TASK_REGISTRY.register() takes a config class; got {cls!r} ({type(cls).__name__}). "
                "Register the class itself, and apply what a factory would set through modifiers=(...)."
            )
        self._do_register(name or _default_task_name(cls.__name__), cls, override=override, modifiers=modifiers)
        return cls

    return deco if obj is None else deco(obj)

add_search_path

add_search_path(package: str) -> None

Register an additional package root to AST-scan for decorated tasks.

Only the process that calls it sees the package, so it suits a script that does its own lookups::

from eden.tasks import TASK_REGISTRY
TASK_REGISTRY.add_search_path("my_project.tasks")

For the eden CLI to see an installed project's tasks, name the package under the :data:TASK_ENTRY_POINT_GROUP entry-point group in the project's pyproject.toml instead. A package that cannot be found is skipped, unlike a broken entry point.

Source code in eden/tasks/registry.py
def add_search_path(self, package: str) -> None:
    """Register an additional package root to AST-scan for decorated tasks.

    Only the process that calls it sees the package, so it suits a script that does its own
    lookups::

        from eden.tasks import TASK_REGISTRY
        TASK_REGISTRY.add_search_path("my_project.tasks")

    For the ``eden`` CLI to see an installed project's tasks, name the package under the
    :data:`TASK_ENTRY_POINT_GROUP` entry-point group in the project's ``pyproject.toml``
    instead. A package that cannot be found is skipped, unlike a broken entry point.
    """
    if package in self._search_packages:
        return
    self._search_packages[package] = package
    # Invalidate the discovery cache so the new package is scanned on the next miss.
    self._discovered = None

get

get(name: str) -> Any

Get a config CLASS by name.

Resolution order: eager registry → auto-discovery. The returned object is the bare config class, so get(name)() runs none of the task's registered TaskMods. For a task whose robot binding lives in a mod, that yields a config that constructs and serializes without complaint and then fails in env.build() on an empty link name — an error that says nothing about modifiers. Use :meth:build to obtain a configured instance; reach for get only when the class itself is what you want (issubclass checks, subclassing, matching a loaded config's type).

RAISES DESCRIPTION
ValueError

If name is registered in several scanned files.

RuntimeError

If name is not found and an installed :data:TASK_ENTRY_POINT_GROUP entry point names no scannable package, which could hold it.

KeyError

If name is not found anywhere.

Source code in eden/tasks/registry.py
def get(self, name: str) -> Any:
    """Get a config CLASS by name.

    Resolution order: eager registry → auto-discovery. The returned object
    is the bare config class, so ``get(name)()`` runs **none** of the task's
    registered TaskMods. For a task whose robot binding lives in a mod,
    that yields a config that constructs and serializes without
    complaint and then fails in ``env.build()`` on an empty link name — an
    error that says nothing about modifiers.
    Use :meth:`build` to obtain a configured instance; reach for ``get``
    only when the class itself is what you want (``issubclass`` checks,
    subclassing, matching a loaded config's type).

    Raises
    ------
    ValueError
        If ``name`` is registered in several scanned files.
    RuntimeError
        If ``name`` is not found and an installed :data:`TASK_ENTRY_POINT_GROUP` entry point
        names no scannable package, which could hold it.
    KeyError
        If ``name`` is not found anywhere.
    """
    qualname = self._lookup(name)
    if qualname is None:
        raise KeyError(self.missing_task_message(name))
    if name not in self._registry:
        # Importing the module triggers @TASK_REGISTRY.register(), which
        # populates self._registry[name].
        importlib.import_module(qualname.rsplit(":", 1)[0])
    if name in self._registry:
        return self._registry[name]
    # Import succeeded but the decorator didn't fire (e.g. the class
    # is gated behind `if __name__ == "__main__":`). Report clearly.
    raise RuntimeError(
        f"Auto-discovered task '{name}' at '{qualname}' but importing its module "
        "did not register it. Check that @TASK_REGISTRY.register() runs at import time."
    )

get_modifiers

get_modifiers(name: str) -> tuple[TaskMod, ...]

Resolve and return the TaskMod instances associated with name.

Modifiers live on the registry itself (not on the config class), attached via register(modifiers=...). For decorator-registered tasks the decorator's modifiers= tuple is populated when the module is imported, so we call :meth:get first to ensure that import has happened. A registration that inherits its modifiers resolves them here, against the registry as it stands now.

Source code in eden/tasks/registry.py
def get_modifiers(self, name: str) -> tuple[TaskMod, ...]:
    """Resolve and return the TaskMod **instances** associated with ``name``.

    Modifiers live on the registry itself (not on the config class),
    attached via ``register(modifiers=...)``. For decorator-registered
    tasks the decorator's ``modifiers=`` tuple is populated when the
    module is imported, so we call :meth:`get` first to ensure that
    import has happened. A registration that inherits its modifiers
    resolves them here, against the registry as it stands now.
    """
    if name not in self:
        raise KeyError(f"No task named '{name}' found in task registry.")
    # Ensure the module is loaded so the decorator has had a chance to
    # attach modifiers. No-op if already loaded.
    self.get(name)
    return self._resolve_modifiers(name)

modifier_names

modifier_names(name: str) -> list[str]

Modifier display-names for name without importing its module.

Unlike :meth:get_modifiers (which imports the task to return live instances), this is purely informational — used by eden list so a read-only listing never triggers a task-module import and the asset downloads that can ride along. Prefers modifier instances already attached (eager registrations / already-loaded tasks) for exactness, an inherited registration included — resolving one reads the other live registrations, which needs no import either. Otherwise falls back to the identifier names captured from the @register(modifiers=...) decorator during AST auto-discovery — for a decorator without modifiers=, the ones its base class's decorator names, since that is what registering it inherits. A listing renders rather than raises, so an inherited registration left ambiguous by a later one falls back here too; the error still belongs to :meth:get_modifiers and :meth:build, which are what a caller acts on.

Source code in eden/tasks/registry.py
def modifier_names(self, name: str) -> list[str]:
    """Modifier display-names for ``name`` **without importing** its module.

    Unlike :meth:`get_modifiers` (which imports the task to return live
    instances), this is purely informational — used by ``eden list`` so a
    read-only listing never triggers a task-module import and the asset
    downloads that can ride along. Prefers modifier instances already
    attached (eager registrations / already-loaded tasks) for exactness,
    an inherited registration included — resolving one reads the other live
    registrations, which needs no import either. Otherwise falls back to the
    identifier names captured from the ``@register(modifiers=...)`` decorator
    during AST auto-discovery — for a decorator without ``modifiers=``, the
    ones its base class's decorator names, since that is what registering it
    inherits. A listing renders rather than raises, so an inherited
    registration left ambiguous by a later one falls back here too; the
    error still belongs to :meth:`get_modifiers` and :meth:`build`, which
    are what a caller acts on.
    """
    try:
        attached = self._resolve_modifiers(name)
    except ValueError:
        attached = ()
    if attached:
        return [type(m).__name__ for m in attached]
    self._ensure_discovered()  # populates self._discovered_modifiers
    return list(self._discovered_modifiers.get(name, []))

build

build(name: str, **modifier_kwargs: Any) -> Any

Instantiate the config and apply registered TaskMods.

Modifier kwargs are flat and use each mod's prefixed field names (see :meth:TaskMod.flag_for). For an unprefixed mod that exposes difficulty, pass difficulty=...; for an instance registered with prefix="left" that exposes robot, pass left_robot=....

Collisions — two mods that would claim the same flag — raise ValueError at build time with both owners listed. Unknown kwargs raise ValueError with the list of accepted flags.

Source code in eden/tasks/registry.py
def build(self, name: str, **modifier_kwargs: Any) -> Any:
    """Instantiate the config and apply registered TaskMods.

    Modifier kwargs are flat and use each mod's **prefixed** field names
    (see :meth:`TaskMod.flag_for`). For an unprefixed mod that exposes
    ``difficulty``, pass ``difficulty=...``; for an instance registered
    with ``prefix="left"`` that exposes ``robot``, pass ``left_robot=...``.

    Collisions — two mods that would claim the same flag — raise
    ``ValueError`` at build time with both owners listed. Unknown kwargs
    raise ``ValueError`` with the list of accepted flags.
    """
    cls = self.get(name)
    config = cls()
    mods = self._resolve_modifiers(name)

    # Map each prefixed flag to (mod index, field_name, annotation).
    flag_map: dict[str, tuple[int, str, Any]] = {}
    for i, mod in enumerate(mods):
        for field_name, annotation, _default, _help in cli_fields(type(mod)):
            flag = mod.flag_for(field_name)
            if flag in flag_map:
                j, other_field, _ = flag_map[flag]
                raise ValueError(
                    f"Modifier flag collision on task '{name}': '{flag}' is claimed by "
                    f"both mod[{j}] ({type(mods[j]).__name__}.{other_field}) and "
                    f"mod[{i}] ({type(mod).__name__}.{field_name}). "
                    "Give the TaskMod instances distinct prefix values via .with_prefix(...)."
                )
            flag_map[flag] = (i, field_name, annotation)

    unknown = set(modifier_kwargs) - set(flag_map)
    if unknown:
        # Surface a targeted hint when the unknown kwarg matches a
        # keyword-only init param on a registered mod — that's the most
        # common adoption mistake (declaring a CLI knob after `*` so it
        # never reaches the CLI surface). Match on the prefixed flag form
        # so the hint also fires for prefixed mods.
        kwonly_index: dict[str, tuple[TaskMod, str]] = {}
        for mod in mods:
            for field in _keyword_only_fields(type(mod)):
                kwonly_index[mod.flag_for(field)] = (mod, field)
        kwonly_hits: list[str] = []
        for u in sorted(unknown):
            if u in kwonly_index:
                mod, field = kwonly_index[u]
                kwonly_hits.append(
                    f"'{u}' is keyword-only on {type(mod).__name__} (as '{field}') — "
                    f"keyword-only params are non-CLI ctor state. Set it at registration "
                    f"with modifiers=({type(mod).__name__}({field}=...),); if you own "
                    f"{type(mod).__name__} and want a CLI flag, move '{field}' before "
                    f"`*` in __init__."
                )
        hint = ("\n" + textwrap.indent("Hint:\n" + "\n".join(kwonly_hits), "  ")) if kwonly_hits else ""
        raise ValueError(
            f"Unknown modifier kwargs for task '{name}': {sorted(unknown)}. "
            f"Known: {sorted(flag_map) or 'none'}{hint}"
        )

    # Group kwargs per mod, stripping the prefix and validating each value
    # against its declared annotation (Literal membership + primitive type).
    updates_per_mod: list[dict[str, Any]] = [{} for _ in mods]
    for flag, value in modifier_kwargs.items():
        i, field_name, annotation = flag_map[flag]
        _check_cli_value(field_name, annotation, value)
        updates_per_mod[i][field_name] = value

    for mod, updates in zip(mods, updates_per_mod):
        if updates:
            # Deep copy so per-build overrides don't mutate the registered
            # instance; deep so mutable instance state (lists/dicts a
            # subclass might keep) is not aliased across builds.
            applied = copy.deepcopy(mod)
            for field_name, value in updates.items():
                setattr(applied, field_name, value)
        else:
            applied = mod
        config = applied.apply(config)
    return config

list_tasks

list_tasks() -> list[str]

List all registered task names (eager + discovered).

RAISES DESCRIPTION
ValueError

If a task name is registered in several scanned files.

RuntimeError

If an installed :data:TASK_ENTRY_POINT_GROUP entry point names no scannable package — the list would be missing that package's tasks.

Source code in eden/tasks/registry.py
def list_tasks(self) -> list[str]:
    """List all registered task names (eager + discovered).

    Raises
    ------
    ValueError
        If a task name is registered in several scanned files.
    RuntimeError
        If an installed :data:`TASK_ENTRY_POINT_GROUP` entry point names no scannable package
        — the list would be missing that package's tasks.
    """
    discovered = self._ensure_discovered()
    if self._collisions:
        raise ValueError("\n".join(_collision_message(n, where) for n, where in sorted(self._collisions.items())))
    self._raise_if_scan_incomplete("Cannot list every task.")
    return sorted(set(self._registry) | set(discovered))

config_qualname_for_task

config_qualname_for_task(name: str) -> str | None

The module.Class of the config name builds, or None if no such task.

The forward direction of :meth:task_for_config_qualname, and import-free for the same reason — eager entries are already in memory, discovered-only entries carry the module:Class string the AST scan recorded — so a caller can ask "is this saved config the one --task builds?" without importing the task module. The replay path (eden inference / eden deploy) never needs that module: it loads the checkpoint's own config, and only wants the identity.

Prefer this over reversing :meth:task_for_config_qualname whenever the question is about a named task. Nothing stops one config class being registered under two names (aliases are ordinary here — :meth:register keys on name alone), and the reverse lookup returns only the first of them, so an alias would read as a mismatch.

PARAMETER DESCRIPTION
name

Registered task name.

TYPE: str

RETURNS DESCRIPTION
str | None

The dotted qualname, or None when no registered or discoverable task has that name.

Source code in eden/tasks/registry.py
def config_qualname_for_task(self, name: str) -> str | None:
    """The ``module.Class`` of the config ``name`` builds, or ``None`` if no such task.

    The forward direction of :meth:`task_for_config_qualname`, and import-free for the same
    reason — eager entries are already in memory, discovered-only entries carry the
    ``module:Class`` string the AST scan recorded — so a caller can ask "is this saved config
    the one ``--task`` builds?" without importing the task module. The replay path
    (``eden inference`` / ``eden deploy``) never needs that module: it loads the checkpoint's
    own config, and only wants the identity.

    Prefer this over reversing :meth:`task_for_config_qualname` whenever the question is about
    a *named* task. Nothing stops one config class being registered under two names (aliases
    are ordinary here — :meth:`register` keys on ``name`` alone), and the reverse lookup
    returns only the first of them, so an alias would read as a mismatch.

    Parameters
    ----------
    name: str
        Registered task name.

    Returns
    -------
    str | None
        The dotted qualname, or ``None`` when no registered or discoverable task has that name.
    """
    qualname = self._lookup(name)
    return None if qualname is None else qualname.replace(":", ".", 1)

task_for_config_qualname

task_for_config_qualname(qualname: str) -> str | None

The task whose config class is module.Class, or None if no task owns it.

The reverse of :meth:get, and deliberately import-free: eager entries are already in memory, and discovered-only entries are matched against the module:Class string the AST scan recorded. Calling :meth:get in a loop instead would import every task module in the tree — and could raise out of an unrelated one — which is the wrong trade for a lookup whose caller is building an error message.

PARAMETER DESCRIPTION
qualname

Dotted module.Class name, as :func:eden.utils.configs.saved_config_qualname reads it out of a saved config.

TYPE: str

RETURNS DESCRIPTION
str | None

The task name, or None when no registered or discoverable task declares it.

Source code in eden/tasks/registry.py
def task_for_config_qualname(self, qualname: str) -> str | None:
    """The task whose config class is ``module.Class``, or ``None`` if no task owns it.

    The reverse of :meth:`get`, and deliberately **import-free**: eager entries are already
    in memory, and discovered-only entries are matched against the ``module:Class`` string
    the AST scan recorded. Calling :meth:`get` in a loop instead would import every task
    module in the tree — and could raise out of an unrelated one — which is the wrong trade
    for a lookup whose caller is building an error message.

    Parameters
    ----------
    qualname: str
        Dotted ``module.Class`` name, as :func:`eden.utils.configs.saved_config_qualname`
        reads it out of a saved config.

    Returns
    -------
    str | None
        The task name, or ``None`` when no registered or discoverable task declares it.
    """
    discovered = self._ensure_discovered()
    module, _, cls_name = qualname.rpartition(".")
    key = f"{module}:{cls_name}"  # the "module:Class" form the scan and _class_key use
    for name, cls in self._registry.items():
        if _class_key(cls) == key:
            self._raise_if_clashing(name, cls)
            return name
    for name, found in discovered.items():
        if found == key:
            return name
    for name, where in self._collisions.items():
        if any(found == key for found, _ in where):
            raise ValueError(_collision_message(name, where))
    self._raise_if_scan_incomplete(
        f"No task declares '{qualname}', and its module lies under a task package the scan could not read.",
        module=module,
    )
    return None

task_source

task_source(name: str) -> str | None

Where the config name builds comes from, as eden list shows it.

"eden" for a built-in, the distribution's name for a task found through its :data:TASK_ENTRY_POINT_GROUP entry point, the package for one under a package passed to :meth:add_search_path, and the defining module for a class registered from anywhere else — so a built-in name shadowed by a register(..., override=True) reads as the module that shadowed it. Import-free, like :meth:config_qualname_for_task, which it reads.

PARAMETER DESCRIPTION
name

Registered task name.

TYPE: str

RETURNS DESCRIPTION
str | None

The source label, or None when no registered or discoverable task has that name.

Source code in eden/tasks/registry.py
def task_source(self, name: str) -> str | None:
    """Where the config ``name`` builds comes from, as ``eden list`` shows it.

    ``"eden"`` for a built-in, the distribution's name for a task found through its
    :data:`TASK_ENTRY_POINT_GROUP` entry point, the package for one under a package passed to
    :meth:`add_search_path`, and the defining module for a class registered from anywhere else
    — so a built-in name shadowed by a ``register(..., override=True)`` reads as the module
    that shadowed it. Import-free, like :meth:`config_qualname_for_task`, which it reads.

    Parameters
    ----------
    name: str
        Registered task name.

    Returns
    -------
    str | None
        The source label, or ``None`` when no registered or discoverable task has that name.
    """
    qualname = self.config_qualname_for_task(name)
    if qualname is None:
        return None
    module = qualname.rpartition(".")[0]
    # The innermost search package holding the module, so a package scanned inside another's tree wins.
    owners = [package for package in self._package_sources if _is_under(module, package)]
    return self._package_sources[max(owners, key=len)] if owners else module

missing_task_message

missing_task_message(name: str) -> str

The message for a miss on name: a close match, every task there is, and where to declare one.

The text of :meth:get's KeyError (so one line), for a caller that reports a miss its own way, as eden's --task check does. Unlike :meth:list_tasks, it lists what the scan resolved even while another name clashes or a plugin is broken, since those are reported where they are looked up, and the reader here needs the name they mistyped. It raises only what the scan itself does (a search-path package whose parents fail to import), and nothing once a lookup has scanned.

Source code in eden/tasks/registry.py
def missing_task_message(self, name: str) -> str:
    """The message for a miss on ``name``: a close match, every task there is, and where to declare one.

    The text of :meth:`get`'s ``KeyError`` (so one line), for a caller that reports a miss its own way,
    as ``eden``'s ``--task`` check does. Unlike :meth:`list_tasks`, it lists what the scan resolved
    even while another name clashes or a plugin is broken, since those are reported where they
    are looked up, and the reader here needs the name they mistyped. It raises only what the scan
    itself does (a search-path package whose parents fail to import), and nothing once a lookup
    has scanned.
    """
    available = sorted(set(self._registry) | set(self._ensure_discovered()))
    close = difflib.get_close_matches(name, available, n=1)
    did_you_mean = f" Did you mean '{close[0]}'?" if close else ""
    return (
        f"No task named '{name}' found in task registry.{did_you_mean} "
        f"Available: {', '.join(available) or 'none'}. Note: {OUT_OF_TREE_TASK_HINT}"
    )

TaskMod

Tool that modifies an EdenConfig instance.

TaskMod is a plain Python class (not a Pydantic model) because it is logic with a few parameters, not user-facing data. EdenConfig is the Pydantic data model; TaskMod operates on it.

To define one: subclass, declare CLI-tunable parameters as positional-or-keyword __init__ arguments with type annotations, and implement :meth:apply. Non-CLI constructor state goes in keyword-only parameters (after *) — the CLI introspection skips those.

Quick reference (this is the convention agents miss most often)::

def __init__(
    self,
    difficulty: Literal["easy", "hard"] = "easy",  # ← CLI flag (--difficulty)
    *,
    entity_name: str = "robot",                    # ← non-CLI ctor state
):
    self.difficulty = difficulty
    self.entity_name = entity_name

Calling super().__init__() is optional — TaskMod keeps no instance state of its own; _prefix is a class-attribute default that :meth:with_prefix shadows on assignment. So your subclass __init__ can just store its parameters and return.

For per-field CLI --help text, wrap the annotation in typing.Annotated[T, "help text"]. The string metadata is forwarded to argparse; the inner type is what drives validation::

difficulty: Annotated[Literal["easy", "hard"], "Task difficulty preset."] = "easy"

Implement :meth:apply to return the modified config; in-place mutation is fine because :meth:TaskRegistry.build calls cls() fresh on every build (and Pydantic deep-copies the config's BaseModel defaults per instance), so mutations do not leak across builds.

Modifiers attach to a task by passing instances to @TASK_REGISTRY.register(modifiers=(...)) (a bare subclass is accepted and instantiated with no args). Modifiers are stored on the registry, not on the config class. A config registered without modifiers= inherits them from its nearest registered base class; modifiers=() opts out (see :meth:TaskRegistry.register).

Per-instance scoping: chain :meth:with_prefix on a constructed instance so its CLI flags rename from <field> to <prefix>_<field>. The same subclass can be registered multiple times with distinct prefixes.

Example

::

from typing import Annotated, Literal

class RobotMod(TaskMod):
    def __init__(
        self,
        robot: Annotated[
            Literal["allegro", "shadow", "xhand1"],
            "Hand model to load into the scene.",
        ] = "xhand1",
        *,
        entity_name: str = "robot",  # keyword-only → not CLI-exposed
    ):
        self.robot = robot
        self.entity_name = entity_name

    def apply(self, config):
        setattr(config.scene_options, self.entity_name, ROBOT_REGISTRY.get(self.robot)())
        return config

@TASK_REGISTRY.register(modifiers=(
    RobotMod(robot="shadow", entity_name="robot_left").with_prefix("left"),
    RobotMod(entity_name="robot_right").with_prefix("right"),
))
class TwoArmTask(EdenRLConfig):
    ...

# CLI: --left_robot shadow --right_robot xhand1
TASK_REGISTRY.build("two_arm_task", left_robot="allegro", right_robot="shadow")

Attributes

prefix property

prefix: str

Per-instance CLI/kwarg namespace; empty string means unprefixed.

Methods:

with_prefix

with_prefix(prefix: str) -> 'TaskMod'

Set this instance's CLI/kwarg prefix and return self for chaining.

Chain at registration time::

RobotMod(entity_name="robot_left").with_prefix("left")
Source code in eden/tasks/registry.py
def with_prefix(self, prefix: str) -> "TaskMod":
    """Set this instance's CLI/kwarg prefix and return self for chaining.

    Chain at registration time::

        RobotMod(entity_name="robot_left").with_prefix("left")
    """
    self._prefix = prefix
    return self

flag_for

flag_for(field_name: str) -> str

Return the CLI/build-kwarg name for a field on this instance.

With a non-empty :attr:prefix, returns f"{prefix}_{field_name}"; otherwise returns field_name unchanged.

Source code in eden/tasks/registry.py
def flag_for(self, field_name: str) -> str:
    """Return the CLI/build-kwarg name for a field on this instance.

    With a non-empty :attr:`prefix`, returns ``f"{prefix}_{field_name}"``;
    otherwise returns ``field_name`` unchanged.
    """
    return f"{self._prefix}_{field_name}" if self._prefix else field_name

apply

apply(config: Any) -> Any
Source code in eden/tasks/registry.py
def apply(self, config: Any) -> Any:
    raise NotImplementedError(f"{type(self).__name__} must implement apply().")

Command-line parsing

The helpers the task commands (eden train, eden inference, ...) use to turn --task, TaskMod flags and config overrides into a config instance.

CLI helpers for building task configs from argparse.

This module wires :class:~eden.tasks.registry.TaskMod parameters into a standard argparse.ArgumentParser so that user-facing scripts can accept flat flags (--difficulty hard, --robot g1) that are translated into typed mod kwargs and applied via :meth:TaskRegistry.build.

Functions:

get_task_argparser

get_task_argparser(description: str, default_task_name: str = '', argv: list[str] | None = None) -> ArgumentParser

Build an ArgumentParser with standard flags plus the selected task's mod flags.

The selected task is determined by pre-parsing --task from argv (defaults to sys.argv[1:]). Modifier flags are added only for the selected task, so --help shows only the relevant options. Pass argv explicitly when invoking this from a wrapper that already has its own arg slice (e.g. the eden train / eden inference subcommands forward extra_args) so the peek and the final parse agree on input.

Source code in eden/tasks/parser.py
def get_task_argparser(
    description: str,
    default_task_name: str = "",
    argv: list[str] | None = None,
) -> argparse.ArgumentParser:
    """Build an ``ArgumentParser`` with standard flags plus the selected task's mod flags.

    The selected task is determined by pre-parsing ``--task`` from ``argv``
    (defaults to ``sys.argv[1:]``). Modifier flags are added only for the
    selected task, so ``--help`` shows only the relevant options. Pass
    ``argv`` explicitly when invoking this from a wrapper that already has
    its own arg slice (e.g. the ``eden train`` / ``eden inference``
    subcommands forward ``extra_args``) so the peek and the final parse
    agree on input.
    """
    task_name = _peek_task_name(sys.argv[1:] if argv is None else argv, default_task_name)

    parser = argparse.ArgumentParser(description=description, formatter_class=ColorHelpFormatter)
    parser.add_argument(
        "--run_name",
        type=str,
        default="",
        help="Run name, put in the log directory name (<timestamp>-<run_name>-<id>) and so in the wandb run name.",
    )
    parser.add_argument(
        "--task",
        type=str,
        default=default_task_name,
        help="Task name. May expose additional mod flags depending on the task.",
    )
    parser.add_argument(
        "--config",
        type=str,
        default=None,
        help="Path to a config file (.json/.yaml/.yml). A saved config (one Eden wrote, carrying "
        "its class metadata) replaces the task's config; anything else layers onto it as a "
        "partial override.",
    )
    parser.add_argument(
        "--override",
        action="append",
        default=None,
        metavar="YAML",
        help="Inline config override as a YAML mapping, e.g. --override 'env_options: {num_envs: 64}'. "
        "Repeatable; later ones win. Applied on top of --config.",
    )
    parser.add_argument("--checkpoint", type=str, default=None, help="Path to a checkpoint file.")
    parser.add_argument("--cpu", action="store_true", help="Use CPU backend.")

    if task_name and task_name in TASK_REGISTRY:
        for mod in TASK_REGISTRY.get_modifiers(task_name):
            for field_name, annotation, _default, help_text in cli_fields(type(mod)):
                _add_mod_field_to_parser(parser, mod, field_name, annotation, help_text)

    return parser

get_task_config

get_task_config(task_name: str, *, run_name: str = '', modifier_kwargs: dict[str, Any] | None = None, config_path: str | None = None, config_overrides: Sequence[dict[str, Any]] | None = None) -> Any

Build a task config programmatically.

:meth:TaskRegistry.build, then — in this order, later winning — an optional config_path file and any config_overrides dicts, then a sanitized run_name written to runner_options if present.

PARAMETER DESCRIPTION
task_name

Registered task to build.

TYPE: str

run_name

Run name to write onto runner_options; ignored when empty.

TYPE: str DEFAULT: ''

modifier_kwargs

TaskMod kwargs forwarded to :meth:TaskRegistry.build.

TYPE: dict[str, Any] | None DEFAULT: None

config_path

A .json/.yaml/.yml config file. A saved config replaces the task build; a partial one layers onto it. See :func:_apply_config_file.

TYPE: str | None DEFAULT: None

config_overrides

Override dicts applied on top of everything else, in order — the --override payload.

TYPE: Sequence[dict[str, Any]] | None DEFAULT: None

RETURNS DESCRIPTION
Any

The task's config instance.

Source code in eden/tasks/parser.py
def get_task_config(
    task_name: str,
    *,
    run_name: str = "",
    modifier_kwargs: dict[str, Any] | None = None,
    config_path: str | None = None,
    config_overrides: Sequence[dict[str, Any]] | None = None,
) -> Any:
    """Build a task config programmatically.

    :meth:`TaskRegistry.build`, then — in this order, later winning — an optional
    ``config_path`` file and any ``config_overrides`` dicts, then a sanitized ``run_name``
    written to ``runner_options`` if present.

    Parameters
    ----------
    task_name: str
        Registered task to build.
    run_name: str
        Run name to write onto ``runner_options``; ignored when empty.
    modifier_kwargs: dict[str, Any] | None
        TaskMod kwargs forwarded to :meth:`TaskRegistry.build`.
    config_path: str | None
        A ``.json``/``.yaml``/``.yml`` config file. A saved config replaces the task build; a
        partial one layers onto it. See :func:`_apply_config_file`.
    config_overrides: Sequence[dict[str, Any]] | None
        Override dicts applied on top of everything else, in order — the ``--override`` payload.

    Returns
    -------
    Any
        The task's config instance.
    """
    modifier_kwargs = modifier_kwargs or {}
    config = TASK_REGISTRY.build(task_name, **modifier_kwargs)
    if config_path:
        config = _apply_config_file(config, config_path, task_name=task_name, modifier_kwargs=modifier_kwargs)
    for overrides in config_overrides or ():
        config = config.with_overrides_from_dict(overrides)
    _apply_run_name(config, run_name)
    return config

get_task_config_from_args

get_task_config_from_args(args: Namespace, run_name: str = '') -> Any

Build a task config from an argparse Namespace produced by :func:get_task_argparser.

Source code in eden/tasks/parser.py
def get_task_config_from_args(args: argparse.Namespace, run_name: str = "") -> Any:
    """Build a task config from an argparse Namespace produced by :func:`get_task_argparser`."""
    config = get_task_config(
        args.task,
        run_name=run_name,
        modifier_kwargs=_collect_modifier_kwargs(args, args.task),
        config_path=args.config,
        config_overrides=overrides_from_args(args),
    )

    runner_options = getattr(config, "runner_options", None)
    if runner_options is not None:
        if eden_dist.is_distributed():
            # Each rank sees one isolated GPU as cuda:0 via CUDA_VISIBLE_DEVICES;
            # "auto" resolves to that through gs.device, keeping device selection
            # uniform with the non-DDP path.
            runner_options.device = "auto"
        if args.cpu:
            runner_options.device = "cpu"
    return config

overrides_from_args

overrides_from_args(args: Namespace) -> list[dict[str, Any]]

Parse the repeated --override arguments on args, in the order given.

Source code in eden/tasks/parser.py
def overrides_from_args(args: argparse.Namespace) -> list[dict[str, Any]]:
    """Parse the repeated ``--override`` arguments on ``args``, in the order given."""
    return [overrides_from_str(text) for text in getattr(args, "override", None) or ()]

read_config_arg

read_config_arg(path: str) -> dict

Read the --config argument — :func:read_config_file plus the CLI's own redirect.

The two flags each take what the other rejects, so each names the other when handed it: --override <a path> already points at --config, and this is the mirror. The redirect fires on a positive identification — the value is something --override itself would accept — not on a guess about the string, so a genuine path with the wrong extension (or unreadable contents) still gets the plain message.

PARAMETER DESCRIPTION
path

The --config value.

TYPE: str

RETURNS DESCRIPTION
dict

The file's contents, as :func:read_config_file returns them.

RAISES DESCRIPTION
ValueError

Naming --override when path is an inline mapping; otherwise unchanged from :func:read_config_file.

Source code in eden/tasks/parser.py
def read_config_arg(path: str) -> dict:
    """Read the ``--config`` argument — :func:`read_config_file` plus the CLI's own redirect.

    The two flags each take what the other rejects, so each names the other when handed it:
    ``--override <a path>`` already points at ``--config``, and this is the mirror. The
    redirect fires on a *positive* identification — the value is something ``--override``
    itself would accept — not on a guess about the string, so a genuine path with the wrong
    extension (or unreadable contents) still gets the plain message.

    Parameters
    ----------
    path: str
        The ``--config`` value.

    Returns
    -------
    dict
        The file's contents, as :func:`read_config_file` returns them.

    Raises
    ------
    ValueError
        Naming ``--override`` when ``path`` is an inline mapping; otherwise unchanged from
        :func:`read_config_file`.
    """
    try:
        return read_config_file(path)
    except ValueError as e:
        if not _is_inline_mapping(path):
            raise  # a real path, just not one we can read — its own message is the right one
        raise ValueError(
            f"--config takes a .json/.yaml/.yml path, but {path!r} is an inline mapping. Pass it to --override instead."
        ) from e

explicit_task_mods

explicit_task_mods(task_name: str, modifier_kwargs: dict[str, Any]) -> list[str]

Return the -- flags for task mods set away from their default.

The comparison baseline is getattr(mod, field_name) — the mod instance's value, which is exactly what :func:_add_mod_field_to_parser uses as the argparse default (not the __init__ signature default, which can differ when a task registers a mod with an overridden instance value). So a flag left unset — or passed at its default — reads as not explicitly set.

Used to fail fast wherever mods collide with a config that is authoritative on its own (a saved --config, a checkpoint's sibling config.json) and would otherwise be silently ignored.

PARAMETER DESCRIPTION
task_name

The registered task whose mods to compare against.

TYPE: str

modifier_kwargs

Flag-keyed mod values, as produced by :func:_collect_modifier_kwargs.

TYPE: dict[str, Any]

RETURNS DESCRIPTION
list[str]

The dash-prefixed flag names that differ from their default.

Source code in eden/tasks/parser.py
def explicit_task_mods(task_name: str, modifier_kwargs: dict[str, Any]) -> list[str]:
    """Return the ``--`` flags for task mods set away from their default.

    The comparison baseline is ``getattr(mod, field_name)`` — the mod *instance's* value,
    which is exactly what :func:`_add_mod_field_to_parser` uses as the argparse default (not
    the ``__init__`` signature default, which can differ when a task registers a mod with an
    overridden instance value). So a flag left unset — or passed at its default — reads as
    *not* explicitly set.

    Used to fail fast wherever mods collide with a config that is authoritative on its own
    (a saved ``--config``, a checkpoint's sibling ``config.json``) and would otherwise be
    silently ignored.

    Parameters
    ----------
    task_name: str
        The registered task whose mods to compare against.
    modifier_kwargs: dict[str, Any]
        Flag-keyed mod values, as produced by :func:`_collect_modifier_kwargs`.

    Returns
    -------
    list[str]
        The dash-prefixed flag names that differ from their default.
    """
    if not task_name or task_name not in TASK_REGISTRY:
        return []
    changed = []
    for mod in TASK_REGISTRY.get_modifiers(task_name):
        for field_name, _annotation, _default, _help in cli_fields(type(mod)):
            flag = mod.flag_for(field_name)
            if flag in modifier_kwargs and modifier_kwargs[flag] != getattr(mod, field_name):
                changed.append(f"--{flag}")
    return changed

See also