Skip to content

Commit af76ed6

Browse files
authored
Merge pull request #53 from KraHsu/fix/scene-entity-cfg-resolve-order
Fix instantiate_class_term: resolve SceneEntityCfg before class init
2 parents e488203 + a8d5d34 commit af76ed6

2 files changed

Lines changed: 33 additions & 4 deletions

File tree

‎src/genelab/managers/_base.py‎

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,17 +11,21 @@
1111

1212

1313
def instantiate_class_term(term_cfg: ManagerTermBaseCfg, env: "ManagerBasedRlEnv") -> None:
14-
"""Resolve ``term_cfg`` against ``env``: class funcs get instantiated, any
15-
:class:`SceneEntityCfg` values inside ``params`` get their ``*_ids`` populated.
14+
"""Resolve ``term_cfg`` against ``env``: any :class:`SceneEntityCfg` values inside
15+
``params`` get their ``*_ids`` populated, then class funcs get instantiated.
1616
1717
Single entrypoint called by every manager's ``__init__`` so per-term setup that
1818
needs the live env (index resolution, stateful func construction) happens in
1919
exactly one place. Idempotent re-runs are safe — :meth:`SceneEntityCfg.resolve`
2020
skips fields whose ids are already filled, and the class-instantiation branch
2121
only fires when ``func`` is still a class.
22+
23+
**Order matters**: stateful class terms (e.g. ``feet_swing_height``) cache
24+
``asset_cfg.link_ids`` in their ``__init__``. If resolve ran after the class
25+
instantiation, those reads would see ``None``. So resolve goes first.
2226
"""
23-
if inspect.isclass(term_cfg.func):
24-
term_cfg.func = term_cfg.func(cfg=term_cfg, env=env)
2527
for value in term_cfg.params.values():
2628
if isinstance(value, SceneEntityCfg):
2729
value.resolve(env)
30+
if inspect.isclass(term_cfg.func):
31+
term_cfg.func = term_cfg.func(cfg=term_cfg, env=env)

‎tests/test_scene_entity_cfg.py‎

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,3 +93,28 @@ def test_instantiate_class_term_ignores_non_scene_entity_params() -> None:
9393
instantiate_class_term(term_cfg, env) # type: ignore[arg-type]
9494
# Sanity: params untouched.
9595
assert term_cfg.params == {"threshold": 0.5, "names": ("a",), "ranges": (-1.0, 1.0)}
96+
97+
98+
def test_instantiate_class_term_resolves_asset_cfg_before_class_init() -> None:
99+
"""Regression: stateful class terms like ``feet_swing_height`` cache
100+
``asset_cfg.link_ids`` in their ``__init__``. The resolve pass MUST run before
101+
the class is instantiated — otherwise the init reads ``None`` and crashes
102+
(caught in production by an 8-GPU G1 training run that hit
103+
``ValueError: SceneEntityCfg(name='robot') has no link_ids``).
104+
"""
105+
captured: dict[str, tuple[int, ...] | None] = {}
106+
107+
class _RecordingTerm:
108+
def __init__(self, cfg, env) -> None: # type: ignore[no-untyped-def]
109+
# Mirrors what feet_swing_height.__init__ does — read the cfg at init time.
110+
captured["link_ids"] = cfg.params["asset_cfg"].link_ids
111+
112+
def __call__(self, env) -> None: # noqa: ARG002 - placeholder
113+
return None
114+
115+
env = _FakeEnv(link_names=["base", "left_foot", "right_foot"], joint_names=[])
116+
asset_cfg = SceneEntityCfg(name="robot", link_names=("left_foot", "right_foot"))
117+
term_cfg = ManagerTermBaseCfg(func=_RecordingTerm, params={"asset_cfg": asset_cfg})
118+
instantiate_class_term(term_cfg, env) # type: ignore[arg-type]
119+
# The recording term's init saw fully resolved link_ids.
120+
assert captured["link_ids"] == (1, 2)

0 commit comments

Comments
 (0)