Environments¶
An environment turns a task config into a running, vectorized simulation: it creates the entities,
builds the Genesis scene, owns the managers and runs the step loop. This page covers the environment
classes, building one from a config, what step() and reset() do and in which order, inspecting a
built environment, and writing a new environment class. Read it when you build an environment by hand,
write a term whose timing matters, or debug a reset.
The environment classes¶
Each environment class reads exactly one config class (its config leaf). The pairing is fixed, and
from_config refuses a config of another leaf:
Environment (eden.envs) |
Config leaf | Adds |
|---|---|---|
base.EnvBase |
EdenConfig |
the scene, entities, cameras, sensors, and the action, metric, event, observation and recorder managers |
base.RLEnvBase |
EdenRLConfig |
the reward, termination, command and curriculum managers, the RL step with auto-reset, reward_buf / reset_buf |
base.MimicRLEnvBase |
EdenMimicConfig |
the motion manager, which streams a reference motion |
splat.SplatEnvBase |
EdenSplatConfig |
a Gaussian-splat background (gsplat), on a non-RL environment |
splat.SplatRLEnvBase |
EdenSplatRLConfig |
the same splat background, on an RL environment |
nyx_splat.NyxSplatEnvBase |
EdenNyxSplatConfig |
a path-traced splat background (Nyx), on a non-RL environment |
classDiagram
EnvBase <|-- RLEnvBase
RLEnvBase <|-- MimicRLEnvBase
EnvBase <|-- SplatEnvBase
RLEnvBase <|-- SplatRLEnvBase
EnvBase <|-- NyxSplatEnvBase
The config classes are described in Task Configuration. API reference: Environments.
Building an environment¶
import eden as en # import eden before genesis and torch
import genesis as gs
from eden.envs.base import RLEnvBase
from eden.tasks import TASK_REGISTRY
en.init(backend=gs.gpu) # the default backend is gs.cpu
config = TASK_REGISTRY.build("reacher")
env = RLEnvBase.from_config(config) # constructs only: the scene is NOT built yet
env.build() # builds the Genesis scene and the managers
obs, extras = env.reset() # call reset() once before the first step()
Or let a training wrapper do all of it:
from eden.envs.wrappers.rsl_rl_env import RslRlVecEnvWrapper
env = RslRlVecEnvWrapper.from_config(config) # seeds, constructs, builds and resets
What from_config() does depends on the class
| Call | Constructs | build() |
reset() |
Seeds the RNGs |
|---|---|---|---|---|
EnvBase / RLEnvBase / MimicRLEnvBase.from_config(cfg) |
yes | no, call it yourself | no | no: an env has no runner_options to read a seed from |
RslRlVecEnvWrapper / RlGamesVecEnvWrapper.from_config(cfg) |
yes | yes | yes, in the wrapper's __init__ |
yes, from runner_options.seed, before it constructs the env |
A raw environment is unbuilt: its managers, entity handles and buffers do not exist until build(),
and build() does not reset. The wrapper path requires cfg.runner_options; a config without one
raises ValueError pointing back at RLEnvBase.from_config. A wrapper owns the environment its
from_config built: close() destroys the scene, and so does a wrapper that fails to construct. An
environment you build yourself and pass to a wrapper's __init__ stays yours to destroy, with
env.destroy().
from_config() takes only show_viewer and eval_mode as keywords; a wrapper's also takes
apply_seed. eval_mode=True sets env.training = False: curricula freeze and command debug drawing
turns on. en.init() also sets up the run's log directory and the console log theme; see
Training runs.
from_config() does not copy the config. The environment holds the config's own option objects, so an
edit to env.env_options after construction is also an edit to config.env_options.
Seeding is the wrapper's job. Put the seed on the config (cfg.runner_options.seed = ...) instead of
seeding around a wrapper's from_config(), which would overwrite it. If you build a raw environment and
wrap it yourself, call eden.utils.random.apply_run_seed(cfg.runner_options.seed) before from_config.
env.seed(seed) and env.reset(seed=...) seed the RNGs with exactly the value given, with no per-rank
offset. Seeds must be non-negative. Per-rank offsets under multi-GPU training are in
Training runs.
Picking the env class: env_class_for_config()¶
The environment class and the config leaf must match. An environment's from_config() forwards only the
option groups its own config leaf declares, so RLEnvBase.from_config(mimic_cfg) would drop
motion_options and train with no motion reference at all. from_config() therefore raises TypeError
on a mismatch, naming the class that should build the config. Task subclasses on either side are fine:
the check compares leaves, so a ReacherEnv(RLEnvBase) built from a ReacherConfig(EdenRLConfig) is one
pairing.
When the config class is not known statically (a CLI --task, a registry lookup, a helper that takes any
config), dispatch on the config's type:
from eden.envs.base import env_class_for_config
env = env_class_for_config(cfg).from_config(cfg, show_viewer=True)
env.build()
env.reset()
env_class_for_config (eden/envs/base.py) is the single source of the mapping. It checks the most
specific leaf first, because the configs form an inheritance chain:
| Config | Env class |
|---|---|
EdenNyxSplatConfig |
NyxSplatEnvBase |
EdenSplatRLConfig |
SplatRLEnvBase |
EdenSplatConfig |
SplatEnvBase |
EdenMimicConfig |
MimicRLEnvBase |
EdenRLConfig |
RLEnvBase |
EdenConfig |
EnvBase |
A config of none of these types raises TypeError. The splat classes are imported only when a splat
config asks for them, so a splat config without the optional renderer dependencies raises ImportError
(install the batch-splat or nyx extra), rather than falling back to a plain environment. eden
inference, the training wrappers and the smoke harness (eden.utils.smoke.build_and_step) all dispatch
through it, and new code should too.
Before it constructs anything, from_config() also calls cfg.check_bindings(), which names any field a
TaskMod should have filled (TaskMods).
What construction and build() do¶
Construction (__init__) creates the Genesis scene from env_options, then one Eden entity per
scene entry (a composite rig unfolds into its bodies and welds), the welds of attachments_dict, the
cameras and the sensors. A parent and the entities welded onto it are created consecutively, as Genesis
requires; a weld to an entity that does not exist raises ValueError here.
build() runs three steps:
pre_build_setup(): every entity's, camera's and sensor's pre-build hook, then the hooks registered withregister_pre_build_hook().scene.build(), withenv_options.num_envsenvironments (andenv_spacing/center_envs_at_origin, whichbuild()also accepts as arguments).post_build_setup(): every entity's and camera's post-build hook, then the managers are constructed in manager order and checked against it, the STARTUP events fire, and the hooks registered withregister_post_build_hook()run. The scene state at this point is saved as the state every reset restores.
Afterwards build() checks the built scene. A rigid body lighter than 1 g that is not carried by an
immobile base gets a warning and its mass set to 0.01 kg (a Mochi body raises instead, since its mass
cannot be patched). A scene with equality constraints (closed kinematic loops) and an effective substep
sim_dt / sim_substeps above 0.004 s gets a warning.
register_pre_build_hook() must be called before build(). A post-build hook may be registered later,
from a manager's construction inside step 3, because the queue is drained at the end of that step;
MotionManager registers one that way. Registering one after the drain raises RuntimeError, since it
could never run.
register_substep_callback(callback, before=False) registers a zero-argument callback that runs around
every scene.step() of a control step, that is decimation times per step: after the physics step, or
just before it with before=True (after the action manager has written its controls). It is for a term
that has to act between physics steps. Keep callbacks cheap and free of GPU syncs.
Stepping and resetting¶
obs, extras = env.reset() # all envs; env.reset(envs_idx) for some
obs, reward, terminated, timeouts, extras = env.step(action)
reset(envs_idx=None, *, seed=None)resets the given environments (all of them whenNone) and returns(obs, extras), whereextras["log"]holds the managers' reset logging.step(action)advances one control step and returns(obs, reward, terminated, timeouts, extras).obsmaps each observation group to its tensor;reward,terminatedandtimeoutshave shape(num_envs,).extras["log"]holds the logging of the envs that reset this step, andextras["final_observations"]the final observations, when enabled.actionmay beNone: the simulation still advances, with no new controls.- On an
EnvBase(no RL layer),step()returns(obs, None, None, None, extras).
The Step Loop¶
RLEnvBase.step() runs these phases in this order. The table is the contract, not a summary:
tests/envs/test_base.py runs the real step against a recording harness and fails if the code and this
table disagree.
| # | phase | call | runs when |
|---|---|---|---|
| 1 | Map the policy action onto the actuators | action_manager.compute |
action |
| 2 | Open the recorded step | recorder_manager._on_step_started |
always |
| 3 | Advance command references before physics | command_manager.pre_physics_advance |
command_manager |
| 4 | ×decimation: apply_actions() + scene.step() |
_scene_step_with_sensor_gating |
always |
| 5 | Refresh command caches on the post-physics state | command_manager.post_physics_refresh |
command_manager |
| 6 | Shared computations for the graded layer | metric_manager.compute |
metric_manager |
| 7 | Tick stateful observation terms (once per step) | observation_manager.post_physics_refresh |
always |
| 8 | Terminations | termination_manager.compute |
always |
| 9 | Rewards | reward_manager.compute |
always |
| 10 | Tag the ending episodes with their success flag | recorder_manager.set_episode_success |
metric_manager, recording, episode_ends |
| 11 | Close the recorded step (terminal envs still un-reset) | recorder_manager._on_step_finished |
always |
| 12 | Snapshot done envs' final obs (opt-in) | _record_final_observations |
record_final_observations |
| 13 | Reset done envs: scene reset + RESET events | _reset_idx |
always |
| 14 | post_reset() on every term, for the envs phase 13 reset |
_dispatch_post_reset |
always |
| 15 | Record new-episode data | recorder_manager._on_reset_finished |
always |
| 16 | Adapt difficulty (frozen at eval) | curriculum_manager.compute |
curriculum_manager |
| 17 | Resample / publish commands | command_manager.compute |
command_manager |
| 18 | INTERVAL events | event_manager.compute |
interval_events |
| 19 | Observations | observation_manager.compute |
always |
| 20 | Command debug draw (eval only) | command_manager.draw_vis |
command_manager, eval_mode |
A phase runs only when all of its conditions hold; always means every step of every RL environment.
The test switches each condition off in turn to check that the phase disappears with it and nothing else
does:
action:step()was called with an action. Theapply_actions()inside phase 4 is gated the same way; with no action the simulation still advances.command_manager,metric_manager,curriculum_manager: the task declares at least one term for that manager. A task with no command terms has no phases 3, 5, 17 or 20.recording: a recording is active (recorder_manager.is_recording).episode_ends: at least one environment's episode ends on this step.record_final_observations: opted in withEnvOptions.record_final_observations.interval_events: at least one event term uses INTERVAL mode.eval_mode: the environment is not training.
Phase 13 always runs, but it resets only the environments whose episode ended. episode_length_buf and
common_step_counter advance right after phase 4.
Grade the step, then prepare the next one¶
Phases 6–9 grade the step that just happened, phases 16–20 prepare the next one, and phases 10–15 close the books in between: the recorder closes the finished step, then the done environments are reset and their terms rebuilt.
- Phase 5 feeds the grading: it refreshes command-owned state on the post-physics robot, above everything that reads it.
- Phase 7 is the one call per step a stateful observation term can count on. It comes after the
command caches (5) and the metrics (6) that such a term may integrate, and before anything grades or
resets.
observation_manager.compute()at phase 19 is a read: it also runs at phase 12, at build, inreset()and fromget_observations(), so a counter advanced inside it would be wrong. - Commands update at phase 17, after the reward at phase 9, on purpose. The policy chose this step's action from the observations computed at phase 19 of the previous step. Grading at phase 9 against the command those observations carried keeps observation, action and reward aligned on one command.
- Phase 14 is where a term rebuilds per-step caches for the environments phase 13 reset. Without it, phase 19 would read the finished episode's values for those environments. It runs once the whole reset is done, including a subclass's extra work (the motion environment's buffer refill).
Which per-step hook a term's state belongs in follows from this split, and getting it wrong fails silently; the rules are in Terms, and why phase 19 must not advance state is in Terms.
Auto-reset runs before observation
Done environments are reset after the reward and the termination but before the observation,
so the observations step() returns for them already show the fresh post-reset state, not the
terminal one. This matters to an algorithm that uses the "next observation" of the done step; enable
final observations if it needs the terminal one.
The step of the other environment classes¶
MimicRLEnvBase.step()first advances the motion reference (motion_manager.compute), then runs the twenty phases above.EnvBase.step()is a subsequence of them, with no commands, no terminations or rewards, and no auto-reset: phases 1, 2, 4, 6, 7, 11, 18 and 19. It returns no reward.
Final observations¶
Because of the auto-reset, the observation step() returns for a done environment is the first one of
its next episode, not the terminal post-physics state. On-policy PPO in RSL-RL does not need the terminal
state: it masks natural terminations out of its value targets, and bootstraps time-outs from the value
it computed on the previous step. The wrapper reports extras["time_outs"] as truncated and not
terminated, so an environment that failed on the step it timed out counts as terminal.
A learner that needs the real terminal state opts in with EnvOptions(record_final_observations=True).
Phase 12 then computes the observations of the post-physics state before the reset, and step()
returns them in extras next to the reset log:
extras = {
"log": ..., # the managers' reset logging
"final_observations": { # one entry per observation group, laid out like obs
"policy": ..., # (num_envs, *obs_shape); a nested dict for a group that does not concatenate
"critic": ...,
},
}
- Rows of environments whose episode did not end are zero. Rows of done environments hold the observation of the pre-reset state.
- For a group with history, the row has the history's shape, with the fresh post-physics frame in the
most recent slot. The snapshot is computed with
update_history=False, so it does not advance the history buffer; the once-per-step advance still happens at phase 19. - The tensors are reused on every step. Copy them if you keep them, for example in a replay buffer.
- The cost is one more observation pass and one
torch.whereper group on every step, with no GPU sync.
The consumers it exists for are off-policy learners that bootstrap a timed-out transition from its real
next state (the FlashSAC learner of flashdexretarget refuses to run without it, from
check_bindings()), estimators whose target is the true next observation (homie turns it on for
HIMPPO), and evaluation recording that needs the terminal frame. It is off by default.
To change what counts as the terminal observation, override RLEnvBase._compute_final_observations(),
for example to skip a group whose terms are unsafe to evaluate on the post-physics state.
Solver errors¶
A physics solver that leaves an environment's state corrupt (a NaN contact, force or acceleration, or a diverged Mochi solve) does not stop the run:
- phases 6 and 9 drop what the metric and reward terms read from those environments, so no NaN reaches the learner;
- phase 8 truncates them, logged as
Episode_Termination/solver_error; - phase 13 resets them, which clears the flag and zeroes the control force the solver reports for them.
env.get_solver_error_envs_mask() returns the flagged environments. Genesis raises a flag that is still
set at the start of its next scene.step(), so phase 4 lets only the first scene.step() of a control
step check the flags: an environment flagged during any of the decimation physics steps is reset before
the check that would raise for the whole scene. An environment that nothing resets (a plain EnvBase)
raises at the next control step.
A misconfiguration of the scene is never reset away. An environment flagged with a contact capacity
overflow, an unsupported collision pair or, under requires_grad, a NaN gradient is truncated and its
values dropped like any other, but it keeps the flag across the reset. The flag is handed back to the
solver right before the first scene.step() of the next control step, which then raises with a message
naming the option to change (on CUDA, the step after that, since its check does not wait for the
device).
Replaying the graded phase from a CUDA graph¶
Phases 6–9 read the post-physics state and write only the managers' own buffers, in hundreds of small
kernels. When a step is limited by the host, launching them is the cost, and
EnvOptions(cuda_graph_options=CudaGraphOptions()) replays them from one CUDA graph instead. The module
docstring of eden/envs/graded_graph.py gives the scale: about 3 ms of host time for the eager phase on
beyondmimic at 4096 envs, a replay that launches in about 5 µs, and about 70 µs of checks before each
replay.
It is off by default (cuda_graph_options=None) and needs an RLEnvBase on a CUDA device; any other
environment refuses it. It is in EnvOptions.training_only_fields, which eden inference, eden export
and eden deploy reset when they replay a run's own config. CudaGraphOptions (eden.options.envs)
holds its settings:
| Field | Default | Meaning |
|---|---|---|
warmup_steps |
2 |
Eager steps before the first capture, so terms can build their lazy caches. |
validation_steps |
3 |
Replays after each capture that are also run eagerly and compared bit for bit (at least 1). |
recapture_window |
100 |
The span, in steps, over which re-captures are counted. |
max_recaptures_per_window |
2 |
Re-captures allowed within the window; one more raises, naming what changed. |
A term the graph cannot reproduce fails a validated replay, and the error names the buffers that differ. Before every replay the graph checks what it baked in, and re-captures (and re-validates) when one of these changed:
- a graded term's configured field, such as a termination threshold a curriculum moves;
- an environment constant a term may read:
dt,max_episode_length,training; - a tensor the graph reads, rebound to a new one (by a term, by
load_state_dict, or by replacing the container that holds it). The check walks every object reachable from the environment, except the environment itself, the standard library's objects, and those ofgenesis,quadrantsandtorch.
Reward weights are mirrored on the device and copied before every replay, so a weight curriculum needs no re-capture. A curriculum that moves a graded value on every step must keep it in a tensor written in place, or it re-captures too often and raises.
A term is replayable when it follows the term contract: state in buffers written in place, and no value read back to the host. These are not:
- A host read (
.item(),nonzero(), a boolean-mask gather) fails the capture, and the error names the term's source line. Contact queries switch to their padded, fixed-shape form inside the graded phase of graph mode, eager runs included, for this reason (see Entities). env.common_step_counterchanges every step, so reading it during the capture raisesBakedHostReadErrornaming the line: a replay would keep the branch the capture took.- Python state a term advances outside its configured fields fails validation.
- Any other Python value read from outside the term's own fields, such as another manager's term attribute or a module global, is not watched: the graph replays the value it read at capture. Keep such a value in a tensor written in place.
Events and resets¶
Event terms fire in one of three modes (eden.constants.EventMode):
| Mode | When it fires |
|---|---|
STARTUP |
once, in build(), after every manager is constructed |
RESET |
on every reset of an environment, right after the scene state of the reset environments is restored and before the managers' reset(): in env.reset() and at phase 13 |
INTERVAL |
at phase 18 of each step, with the control dt, when the term's interval has elapsed |
A reset therefore runs in this order: the scene state of the reset environments is restored, the RESET
events fire, every manager's reset() runs in manager order (publishing the finished
episode's statistics and clearing accumulators), episode_length_buf is zeroed for those environments,
and then every term's post_reset() runs, in the same order. env.reset() ends by computing the
observations. How priority orders the terms of one mode, and the fields of each mode, are in
Events.
Manager order¶
One declared order, _manager_order(), drives three walks: the construction of the managers in
build(), every manager's reset(), and every term's post_reset(). It is checked at build against
the managers the environment owns.
| env | _manager_order() |
|---|---|
EnvBase |
action → metric → event → observation → recorder |
RLEnvBase |
action → command → metric → termination → reward → curriculum → event → observation → recorder |
MimicRLEnvBase |
the RL order, bracketed by the motion manager (below) |
The order meets four constraints:
action_managerbeforeevent_manager. Reset-mode domain-randomization terms write the action modifiers' per-env buffers inreset(), so that the write lands afteraction_manager.reset()restores the baselines. With events first, every such draw would be overwritten within the same reset. This is the one constraint the check tests by position.- The metric manager and the curricula may sit anywhere in the loop. They normalize the finished
episode by
episode_length_buf, which is zeroed only after the loop. - Every build-time handle lookup points backwards. A term that resolves another manager's term in its
own
build()(an action-rate reward finding the action term, a velocity-tracking reward finding its command, a curriculum finding the reward or termination term it adjusts) needs that manager to exist already. Construction follows this order, so a bad name raises at build, not mid-rollout. - The ended episode's motion is read through
motion_manager.ended_episode_validity(). In a motion environment, the reset environments' reference window is refilled for the new episode before the manager loop, so readingmotion_manager.datafrom a manager'sreset()raises. A term that grades the ended episode registers withrecord_ended_episode_validityinbuild(); a term that wants the new episode's reference reads it inpost_reset().
Everything else is a choice of producer before consumer. Commands are the reference every graded term reads, so they come first; metric terms, the shared computations, feed termination and reward; the curriculum reads the graded outcome last. Observation and recorder terms consume every other layer and produce nothing, so they go last, with events just ahead of them.
Two consequences matter to term authors:
- Every manager before
event_managerpublishes the finished episode under the domain-randomization draw it actually ran with. The new draw lands atevent_manager.reset(). curriculum_manager.reset()runs afterreward_manager.reset()has cleared its accumulators. A curriculum that adapts on the finished episode's reward must read something published, such as another term'sget_state()from its owncompute()at phase 16, not the reward manager's internal sums. An evaluation environment never computes a curriculum, so it logs noCurriculum/*keys.
The motion manager brackets the loop. Its reset() must run before the scene reset, because
reference-state initialization (RSI) events read the freshly sampled clip, and its buffer is refilled
around the scene reset and the RESET events, before every other manager. No position in a flat list
expresses that, so MimicRLEnvBase constructs it before the others, calls motion_manager.reset() before
the base reset and motion_manager.post_reset() before the others' post_reset(), and declares it in
_BRACKETED_MANAGERS so the check knows it is handled.
Adding a manager to your own environment¶
- Add a
"<name>_manager": lambda: ...entry to your_manager_factories()override, merged intosuper()._manager_factories(). - Add
"<name>_manager"to your_manager_order()override, in the producer-before-consumer position it belongs in. The order is restated in full per class rather than composed, because the RL managers sit between the base ones. - If the manager may legitimately be absent (no terms declared), add it to
_OPTIONAL_MANAGERSas well: it is then left asNone, which every consumer'sis not Nonecheck expects. The metric, command and curriculum managers are optional; the others are always constructed.
Miss a step and the build raises RuntimeError naming the manager: a factory no position in the order
calls, an ordered name with no factory, and a *_manager attribute the order does not mention are all
refused. Managers are discovered by that _manager suffix, so do not give any other attribute that name.
Override _manager_order(), not _reset_idx(): an imperative reset override bypasses the declared order,
and the check cannot see it.
Inspecting a built environment¶
env.num_envs # parallel environments
env.device # torch device
env.sim_dt # physics time step
env.dt # control step: sim_dt * decimation
env.training # False when built with eval_mode=True
env.episode_length_buf # (num_envs,) steps into the current episode
env.max_episode_length # steps per episode (RLEnvBase and subclasses)
env.max_episode_length_s # seconds per episode (RLEnvBase and subclasses)
env.common_step_counter # control steps since construction (RLEnvBase and subclasses)
print(env.summary()) # every manager and its terms
Entities, cameras and sensors¶
robot = env.entities["robot"] # by the name given in scene_options
robot = env.get_entity("robot") # same, but a miss lists the entities that exist
arms = env.get_entities(["robot_.*", "box.*"]) # regex patterns, matched from the start of the name
camera = env.get_camera("wrist") # also env.cameras["wrist"]
sensor = env.sensors["imu"]
An entity's class follows its options: Terrain for TerrainOptions, ParticleEntity for MPM, SPH and
PBD materials, FEMEntity for FEM materials, the Mochi entity classes for Mochi materials, and
RigidEntity otherwise. Test it with isinstance. Poses, DOF indexing and contacts are in
Entities.
env.scene is the Genesis scene and env.rigid_solver its rigid solver. env.mochi_solver is the Mochi
solver, and raises RuntimeError when no entity uses a Mochi material.
Managers¶
env.action_manager, env.observation_manager, env.event_manager, env.recorder_manager
env.reward_manager, env.termination_manager # RLEnvBase and subclasses
env.metric_manager, env.command_manager, env.curriculum_manager # None when the task declares no terms
env.motion_manager # MimicRLEnvBase
term = env.reward_manager.get_term("action_rate")
The optional managers read None both before the build and after it when the task declares none of
their terms, so test them with is not None.
Reading what the controller commanded¶
Three buffers describe an action step, and logging the wrong one is a common mistake:
term = env.action_manager.get_term("joint_pos_controller")
term.action # the raw policy output this step, (num_envs, action_dim)
term.prev_action # the raw policy output last step
term.processed_action # the command the term issued, which deployment also sends
env.action_manager.processed_actions # {term_name: processed_action} for every term
Read processed_action when you need the controller target, for a rollout export, a sim-to-real
comparison or a tracking-error plot. Recomputing raw * scale + offset yourself drops any configured
control-step action modifier. A joint controller's resolved multiplier and bias are
term.effective_scale and term.effective_offset, the built values rather than the scale / offset
config fields. processed_action differs per term in units and width, so only its leading num_envs
dimension is guaranteed. An ActionDelay modifier acts later, once per physics step inside
apply_actions(), and no deployment applies it. Controllers and modifiers are covered in
Actions, and what deployment sends in Deployment.
Recovering the config¶
env.config reconstructs the config that describes the environment. For an environment built with
from_config, it is a round trip: an instance of the source config's own class (ReacherConfig,
not EdenRLConfig) that carries every field the environment never receives (runner_options,
language_instructions, task-specific fields), while the option groups the environment owns are read
off the live environment.
config = TASK_REGISTRY.build("reacher")
env = RLEnvBase.from_config(config)
type(env.config) # ReacherConfig
env.config.runner_options # kept, though no environment parameter takes it
env.config.save_as_json("config.json") # reloads into a ReacherConfig
The returned root is a fresh, unlocked object, but its option groups are the live ones, so editing
them edits the environment; use env.config.clone() for an independent copy. An environment constructed
directly (RLEnvBase(env_options=..., ...)) has no source config, so env.config synthesizes a plain
EdenConfig / EdenRLConfig / EdenMimicConfig, and the fields the environment does not own take their
class defaults.
The list of option groups is the same in both directions: from_config forwards every field the
environment's config leaf declares except runner_options and language_instructions, and env.config
reads that same list back. Neither side writes the list out, so they cannot drift apart. A loaded config
keeps its concrete class too (Saving and loading configs),
which is what lets eden train --config <a saved config.json> rebuild the right environment without
being told the task.
Writing a new environment class¶
A task environment: subclass, and do not re-parameterize¶
To customize an environment for one task, subclass the environment class its config leaf uses, and stop there:
from eden.envs.base import RLEnvBase
class MyTaskEnv(RLEnvBase): # the config leaf stays EdenRLConfig
...
env = MyTaskEnv.from_config(MyTaskConfig())
env.config.grasp_threshold # a task-specific field, recovered from the source config
Task-specific config fields need no environment parameter, because env.config carries them over from
the config the environment was built from.
Leave from_config alone in a subclass that only customizes step(). An override that genuinely needs to
exist must declare show_viewer and eval_mode by name: a **kwargs there would swallow a misspelled
keyword that the base would have rejected. tests/envs/test_base.py holds every from_config under
eden/ and examples/ to this.
A new config leaf: parameterize on it¶
An environment class declares its config leaf by parameterizing on it:
class RLEnvBase(EnvBase[EdenRLConfig]), class MimicRLEnvBase(RLEnvBase, EnvBase[EdenMimicConfig]).
That binding is the list from_config forwards from and env.config reads back, so it is a promise that
the environment's __init__ takes every field the leaf declares. The promise is checked when the class is
created:
class MyTaskConfig(EdenRLConfig):
grasp_threshold: float = 0.03
class MyTaskEnv(RLEnvBase, EnvBase[MyTaskConfig]): # TypeError, at class creation
pass
The TypeError names ['grasp_threshold'] as declared by the config and missing from
MyTaskEnv.__init__. Unchecked, both directions would fail without naming the config: from_config
would pass grasp_threshold=... into __init__, whose **kwargs hands it on to the Genesis scene
(Scene.__init__() got an unexpected keyword argument 'grasp_threshold'), and env.config would raise a
bare AttributeError.
Re-parameterize only to add a genuinely new leaf with option groups of its own, as the splat configs do.
Then declare those groups as constructor parameters, and add the config-to-environment pair to
env_class_for_config. The two directions are declared separately (each environment declares its own
leaf, and env_class_for_config dispatches), so a new leaf must update both;
tests/envs/test_base.py fails if only one of them lands.
See also¶
- Task Configuration: the config classes and option groups an environment reads
- Terms: which per-step hook a term's state belongs in
- Events: event modes, priorities and domain randomization
- Training runs: the training wrappers, seeding and logs
- RL libraries: Gymnasium, SB3, skrl and rl_games wrappers