name: add-functor description: Use when adding a new observation, event, reward, action, dataset, or randomization functor to an EmbodiChain environment
Add Functor
Scaffold a new functor following EmbodiChain's Functor/FunctorCfg pattern.
When to Use
- User asks to add an observation term, reward function, event handler, action term, dataset functor, or randomizer
- User says "add a reward", "new observation", "create a randomizer", "add event functor"
- Any new function needs to be registered in a manager config
Determine Functor Type
| Functor Type | Config Class | Module File | Manager | Signature |
|---|---|---|---|---|
| Observation | ObservationCfg (extends FunctorCfg) |
managers/observations.py |
ObservationManager |
(env, obs, entity_cfg, ...) -> Tensor |
| Reward | RewardCfg (extends FunctorCfg) |
managers/rewards.py |
RewardManager |
(env, obs, action, info, ...) -> Tensor |
| Event | EventCfg (extends FunctorCfg) |
managers/events.py |
EventManager |
(env, env_ids, ...) -> None |
| Action | ActionTermCfg (extends FunctorCfg) |
managers/actions.py |
ActionManager |
Varies |
| Dataset | DatasetFunctorCfg (extends FunctorCfg) |
managers/datasets.py |
DatasetManager |
(env, ...) -> dict |
| Randomization | EventCfg (randomizations ARE events) |
managers/randomization/<type>.py |
EventManager |
(env, env_ids, entity_cfg, ...) -> None |
Two Functor Styles
Function-style (Preferred for Simple Functors)
A plain function with the right signature. Registered via FunctorCfg(func=my_function, params={...}).
def my_reward(
env: EmbodiedEnv,
obs: dict,
action: EnvAction,
info: dict,
my_param: float = 1.0, # params become keyword args
) -> torch.Tensor:
"""Short one-line summary.
Longer description if needed.
Args:
env: The environment instance.
obs: The observation dictionary.
action: The action taken.
info: The info dictionary.
my_param: Description of this parameter.
Returns:
Reward tensor of shape (num_envs,).
"""
# implementation
return result
Class-style (Required When Functor Has State)
A class inheriting Functor, with __init__(cfg, env) and __call__(env, ...). Registered via FunctorCfg(func=MyClass, params={...}).
class my_randomizer(Functor):
"""One-line summary."""
def __init__(self, cfg: FunctorCfg, env: EmbodiedEnv):
super().__init__(cfg, env)
# Extract params and initialize state
self.entity_cfg: SceneEntityCfg = cfg.params["entity_cfg"]
def __call__(self, env: EmbodiedEnv, env_ids: torch.Tensor, **kwargs):
"""Apply the randomization.
Args:
env: The environment instance.
env_ids: Target environment IDs.
"""
# implementation
Steps
1. Identify Functor Type and Style
Ask the user:
- Which manager? (observation / reward / event / action / dataset / randomization)
- Function or class style? (function for stateless, class for stateful)
- What does it do? (brief description for naming + docstring)
2. Choose the Right Module File
Place the functor in the existing module for its type:
| Type | File |
|---|---|
| Observation | embodichain/lab/gym/envs/managers/observations.py |
| Reward | embodichain/lab/gym/envs/managers/rewards.py |
| Event | embodichain/lab/gym/envs/managers/events.py |
| Action | embodichain/lab/gym/envs/managers/actions.py |
| Dataset | embodichain/lab/gym/envs/managers/datasets.py |
| Physics randomization | embodichain/lab/gym/envs/managers/randomization/physics.py |
| Visual randomization | embodichain/lab/gym/envs/managers/randomization/visual.py |
| Spatial randomization | embodichain/lab/gym/envs/managers/randomization/spatial.py |
| Geometry randomization | embodichain/lab/gym/envs/managers/randomization/geometry.py |
3. Write the Functor
Follow the template for function-style or class-style (see above).
Key rules:
- First argument is always
env: EmbodiedEnv(useTYPE_CHECKINGguard for the import) - Use
from __future__ import annotationsat the top - Use
SceneEntityCfgfor entity references, not raw strings - For observation functors: add
shapekey toFunctorCfg.extradict - For randomization functors: second arg is
env_ids: torch.Tensor | list[int] - For reward functors: return shape must be
(num_envs,)
4. Update __all__
Add the new functor to the module's __all__ list. If no __all__ exists, create one.
5. Write a Test
Place at tests/gym/envs/managers/test_<functor_type>.py (append to existing file if present).
For functors that don't need a live simulation, use mock objects (MockEnv, MockSim, etc.) following the pattern in tests/gym/envs/managers/test_reward_functors.py.
6. Run black
black embodichain/lab/gym/envs/managers/<module>.py
black tests/gym/envs/managers/test_<functor_type>.py
Common Mistakes
| Mistake | Fix |
|---|---|
| Wrong first argument signature | Observation: (env, obs, ...), Reward: (env, obs, action, info, ...), Event/Randomization: (env, env_ids, ...) |
Importing EmbodiedEnv at module level |
Use TYPE_CHECKING guard to avoid circular imports |
Forgetting SceneEntityCfg for entity refs |
Always use SceneEntityCfg(uid="...") not bare strings |
| Returning wrong tensor shape | Rewards must return (num_envs,), observations must match declared shape |
Missing from __future__ import annotations |
Required in every file |
Class-style functor not calling super().__init__ |
Always call super().__init__(cfg, env) |
| Adding randomizer as standalone | Randomizations ARE events — they go in randomization/ but use EventCfg |
Quick Reference
| Step | Action |
|---|---|
| 1 | Identify manager type + function vs class style |
| 2 | Write functor in the correct module file |
| 3 | Update __all__ in that module |
| 4 | Write test with mocks (no sim needed for most) |
| 5 | Run black on changed files |