Skip to content

Commit fc6957f

Browse files
committed
remove post init diff
1 parent 74dd0ea commit fc6957f

9 files changed

Lines changed: 51 additions & 181 deletions

File tree

source/isaaclab/docs/CHANGELOG.rst

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,19 +14,16 @@ Added
1414
a ``FutureWarning`` because this path is temporary until neutral USD materials
1515
replace OmniPBR in content.
1616
* Added ``test_newton_model_utils`` tests for the Newton shape color pass.
17-
* Added :meth:`~isaaclab.utils.configclass.post_init_diff` method to all
18-
``@configclass`` instances, returning a dict of scalar fields changed by
19-
``__post_init__``. Helps users discover silent config overrides.
2017
* Added debug-level logging of the ``__post_init__`` call chain in
2118
``@configclass`` so that ``ISAACLAB_LOG_LEVEL=DEBUG`` reveals which classes
2219
modify fields during initialization.
23-
* Added automatic dump of the fully-resolved env config to
24-
``<log_dir>/params/resolved_env.yaml`` from
25-
:class:`~isaaclab.envs.ManagerBasedEnv`,
20+
* Added :func:`~isaaclab.utils.io.dump_resolved_cfg` standalone utility for
21+
writing ``<log_dir>/params/resolved_env.yaml``. All env base classes
22+
(:class:`~isaaclab.envs.ManagerBasedEnv`,
2623
:class:`~isaaclab.envs.DirectRLEnv`, and
27-
:class:`~isaaclab.envs.DirectMARLEnv` at the end of ``__init__``. This
28-
gives users a single source of truth for the values the environment actually
29-
uses.
24+
:class:`~isaaclab.envs.DirectMARLEnv`) now call this at the end of
25+
``__init__``, giving users a single source of truth for the values the
26+
environment actually uses.
3027

3128
Changed
3229
^^^^^^^

source/isaaclab/isaaclab/envs/direct_marl_env.py

Lines changed: 2 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
from isaaclab.sim import SimulationContext
2929
from isaaclab.sim.utils.stage import use_stage
3030
from isaaclab.utils.configclass import resolve_cfg_presets
31-
from isaaclab.utils.io import dump_yaml
31+
from isaaclab.utils.io import dump_resolved_cfg
3232
from isaaclab.utils.noise import NoiseModel
3333
from isaaclab.utils.seed import configure_seed
3434
from isaaclab.utils.timer import Timer
@@ -250,9 +250,7 @@ def _init_sim(self, render_mode: str | None = None, **kwargs):
250250
self.event_manager.apply(mode="startup")
251251
self.has_rtx_sensors = self.sim.get_setting("/isaaclab/render/rtx_sensors")
252252

253-
# dump the fully-resolved env config so users have a single source of
254-
# truth for what values the environment is actually using
255-
self._dump_resolved_cfg()
253+
dump_resolved_cfg(self.cfg, getattr(self.cfg, "log_dir", None), logger)
256254

257255
# print the environment information
258256
print("[INFO]: Completed setting up the environment...")
@@ -610,24 +608,6 @@ def set_debug_vis(self, debug_vis: bool) -> bool:
610608
Helper functions.
611609
"""
612610

613-
def _dump_resolved_cfg(self):
614-
"""Dump the fully-resolved env config to ``<log_dir>/params/resolved_env.yaml``.
615-
616-
This runs after all ``__post_init__`` hooks, preset resolution, and
617-
any training-script mutations, giving users a single source of truth
618-
for the values the environment actually uses.
619-
"""
620-
import os
621-
622-
if self.cfg.log_dir is None:
623-
return
624-
resolved_path = os.path.join(self.cfg.log_dir, "params", "resolved_env.yaml")
625-
try:
626-
dump_yaml(resolved_path, self.cfg)
627-
logger.info("Resolved env config written to %s", resolved_path)
628-
except Exception:
629-
logger.warning("Failed to dump resolved env config to %s", resolved_path, exc_info=True)
630-
631611
def _configure_env_spaces(self):
632612
"""Configure the spaces for the environment."""
633613
self.agents = self.cfg.possible_agents

source/isaaclab/isaaclab/envs/direct_rl_env.py

Lines changed: 2 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
from isaaclab.sim import SimulationContext
2525
from isaaclab.sim.utils.stage import use_stage
2626
from isaaclab.utils.configclass import resolve_cfg_presets
27-
from isaaclab.utils.io import dump_yaml
27+
from isaaclab.utils.io import dump_resolved_cfg
2828
from isaaclab.utils.noise import NoiseModel
2929
from isaaclab.utils.seed import configure_seed
3030
from isaaclab.utils.timer import Timer
@@ -271,9 +271,7 @@ def _init_sim(self, render_mode: str | None = None, **kwargs):
271271
if self.cfg.num_rerenders_on_reset == 0:
272272
self.cfg.num_rerenders_on_reset = 1
273273

274-
# dump the fully-resolved env config so users have a single source of
275-
# truth for what values the environment is actually using
276-
self._dump_resolved_cfg()
274+
dump_resolved_cfg(self.cfg, getattr(self.cfg, "log_dir", None), logger)
277275

278276
# print the environment information
279277
print("[INFO]: Completed setting up the environment...")
@@ -578,24 +576,6 @@ def set_debug_vis(self, debug_vis: bool) -> bool:
578576
Helper functions.
579577
"""
580578

581-
def _dump_resolved_cfg(self):
582-
"""Dump the fully-resolved env config to ``<log_dir>/params/resolved_env.yaml``.
583-
584-
This runs after all ``__post_init__`` hooks, preset resolution, and
585-
any training-script mutations, giving users a single source of truth
586-
for the values the environment actually uses.
587-
"""
588-
import os
589-
590-
if self.cfg.log_dir is None:
591-
return
592-
resolved_path = os.path.join(self.cfg.log_dir, "params", "resolved_env.yaml")
593-
try:
594-
dump_yaml(resolved_path, self.cfg)
595-
logger.info("Resolved env config written to %s", resolved_path)
596-
except Exception:
597-
logger.warning("Failed to dump resolved env config to %s", resolved_path, exc_info=True)
598-
599579
def _configure_gym_env_spaces(self):
600580
"""Configure the action and observation spaces for the Gym environment."""
601581
# show deprecation message and overwrite configuration

source/isaaclab/isaaclab/envs/manager_based_env.py

Lines changed: 2 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
from isaaclab.sim.utils.stage import use_stage
2020
from isaaclab.ui.widgets import ManagerLiveVisualizer
2121
from isaaclab.utils.configclass import resolve_cfg_presets
22-
from isaaclab.utils.io import dump_yaml
22+
from isaaclab.utils.io import dump_resolved_cfg
2323
from isaaclab.utils.seed import configure_seed
2424
from isaaclab.utils.timer import Timer
2525

@@ -242,9 +242,7 @@ def _init_sim(self):
242242
if self.cfg.num_rerenders_on_reset == 0:
243243
self.cfg.num_rerenders_on_reset = 1
244244

245-
# dump the fully-resolved env config so users have a single source of
246-
# truth for what values the environment is actually using
247-
self._dump_resolved_cfg()
245+
dump_resolved_cfg(self.cfg, getattr(self.cfg, "log_dir", None), logger)
248246

249247
def __del__(self):
250248
"""Cleanup for the environment."""
@@ -588,24 +586,6 @@ def close(self):
588586
Helper functions.
589587
"""
590588

591-
def _dump_resolved_cfg(self):
592-
"""Dump the fully-resolved env config to ``<log_dir>/params/resolved_env.yaml``.
593-
594-
This runs after all ``__post_init__`` hooks, preset resolution, and
595-
any training-script mutations, giving users a single source of truth
596-
for the values the environment actually uses.
597-
"""
598-
import os
599-
600-
if self.cfg.log_dir is None:
601-
return
602-
resolved_path = os.path.join(self.cfg.log_dir, "params", "resolved_env.yaml")
603-
try:
604-
dump_yaml(resolved_path, self.cfg)
605-
logger.info("Resolved env config written to %s", resolved_path)
606-
except Exception:
607-
logger.warning("Failed to dump resolved env config to %s", resolved_path, exc_info=True)
608-
609589
def _reset_idx(self, env_ids: Sequence[int]):
610590
"""Reset environments based on specified indices.
611591

source/isaaclab/isaaclab/utils/configclass.py

Lines changed: 5 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
_CALLABLE_STR_RE = re.compile(r"^[A-Za-z_][A-Za-z0-9_\\.]*:[A-Za-z_][A-Za-z0-9_]*$")
2323
_CALLABLE_STR_WITH_DIR_RE = re.compile(r"^\{DIR\}(?:\.[A-Za-z_][A-Za-z0-9_]*)*:[A-Za-z_][A-Za-z0-9_]*$")
2424

25-
_CONFIGCLASS_METHODS = ["to_dict", "from_dict", "replace", "copy", "validate", "post_init_diff"]
25+
_CONFIGCLASS_METHODS = ["to_dict", "from_dict", "replace", "copy", "validate"]
2626
"""List of class methods added at runtime to dataclass."""
2727

2828
"""
@@ -113,7 +113,6 @@ class EnvCfg:
113113
setattr(cls, "replace", _replace_class_with_kwargs)
114114
setattr(cls, "copy", _copy_class)
115115
setattr(cls, "validate", _validate)
116-
setattr(cls, "post_init_diff", _post_init_diff)
117116
# wrap around dataclass
118117
cls = dataclass(cls, **kwargs)
119118
# return wrapped class
@@ -498,30 +497,20 @@ def _custom_post_init(obj):
498497

499498

500499
def _combined_function(f1: Callable, f2: Callable) -> Callable:
501-
"""Combine a user ``__post_init__`` with the configclass deep-copy hook.
502-
503-
Before *f1* (the user hook) runs, a shallow snapshot of every scalar/tuple
504-
field is taken. After both hooks finish, the snapshot is stored on the
505-
object so that :meth:`post_init_diff` can report which fields were silently
506-
changed by ``__post_init__``.
500+
"""Combine two functions into one by calling them sequentially.
507501
508502
Args:
509-
f1: The user-defined ``__post_init__``.
510-
f2: The configclass ``_custom_post_init`` (deep-copy + ResolvableString wrapping).
503+
f1: The first function to call (user-defined ``__post_init__``).
504+
f2: The second function to call (configclass ``_custom_post_init``).
511505
512506
Returns:
513507
The combined function.
514508
"""
515509

516510
def _combined(*args, **kwargs):
517-
obj = args[0]
518-
# Snapshot scalar / tuple field values before the user hook runs.
519-
before = _snapshot_fields(obj)
520-
_logger.debug("Running __post_init__ for %s", type(obj).__name__)
511+
_logger.debug("Running __post_init__ for %s", type(args[0]).__name__)
521512
f1(*args, **kwargs)
522513
f2(*args, **kwargs)
523-
after = _snapshot_fields(obj)
524-
obj.__post_init_field_diff__ = _compute_field_diff(before, after)
525514

526515
return _combined
527516

@@ -607,53 +596,6 @@ def _wrap():
607596
return _wrap
608597

609598

610-
"""
611-
Post-init diff helpers.
612-
"""
613-
614-
615-
def _snapshot_fields(obj: object, prefix: str = "") -> dict[str, Any]:
616-
"""Capture a flat ``{dotted.path: value}`` snapshot of scalar and tuple fields.
617-
618-
Nested configclass objects are walked recursively so that changes at any
619-
depth are captured.
620-
"""
621-
snap: dict[str, Any] = {}
622-
for key in list(getattr(obj, "__dataclass_fields__", {})):
623-
value = getattr(obj, key, MISSING)
624-
if value is MISSING:
625-
continue
626-
full_key = f"{prefix}{key}"
627-
if hasattr(value, "__dataclass_fields__"):
628-
snap.update(_snapshot_fields(value, prefix=f"{full_key}."))
629-
elif isinstance(value, (int, float, bool, str, type(None), tuple)):
630-
snap[full_key] = value
631-
return snap
632-
633-
634-
def _compute_field_diff(before: dict[str, Any], after: dict[str, Any]) -> dict[str, tuple[Any, Any]]:
635-
"""Return ``{field: (old_value, new_value)}`` for fields that changed."""
636-
diff: dict[str, tuple[Any, Any]] = {}
637-
for key in before:
638-
if key in after and before[key] != after[key]:
639-
diff[key] = (before[key], after[key])
640-
return diff
641-
642-
643-
def _post_init_diff(obj: object) -> dict[str, tuple[Any, Any]]:
644-
"""Return fields changed by ``__post_init__`` as ``{field: (before, after)}``.
645-
646-
Only scalar and tuple fields are tracked (mutable containers like lists and
647-
dicts are excluded because their identity changes during deep-copy).
648-
649-
Returns:
650-
A dictionary mapping dotted field paths to ``(old_value, new_value)``
651-
tuples. Empty when no ``__post_init__`` was defined or no scalar
652-
fields were modified.
653-
"""
654-
return getattr(obj, "__post_init_field_diff__", {})
655-
656-
657599
def resolve_cfg_presets(cfg: object) -> object:
658600
"""Recursively replace preset-wrapper fields with their *default* preset.
659601

source/isaaclab/isaaclab/utils/io/yaml.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
"""Utilities for file I/O with yaml."""
77

8+
import logging
89
import os
910

1011
import yaml
@@ -54,3 +55,30 @@ def dump_yaml(filename: str, data: dict | object, sort_keys: bool = False):
5455
# save data
5556
with open(filename, "w") as f:
5657
yaml.dump(data, f, default_flow_style=False, sort_keys=sort_keys)
58+
59+
60+
def dump_resolved_cfg(cfg: object, log_dir: str | None, logger: logging.Logger | None = None):
61+
"""Dump a fully-resolved config to ``<log_dir>/params/resolved_env.yaml``.
62+
63+
This is intended to be called at the end of environment ``__init__``
64+
after all ``__post_init__`` hooks, preset resolution, and training-script
65+
mutations have been applied, giving users a single source of truth for the
66+
values the environment actually uses.
67+
68+
Args:
69+
cfg: The config object to serialize (typically ``self.cfg``).
70+
log_dir: The logging directory. When ``None``, the dump is silently
71+
skipped.
72+
logger: Optional logger for status messages. When ``None``, messages
73+
are suppressed.
74+
"""
75+
if log_dir is None:
76+
return
77+
resolved_path = os.path.join(log_dir, "params", "resolved_env.yaml")
78+
try:
79+
dump_yaml(resolved_path, cfg)
80+
if logger is not None:
81+
logger.info("Resolved env config written to %s", resolved_path)
82+
except Exception:
83+
if logger is not None:
84+
logger.warning("Failed to dump resolved env config to %s", resolved_path, exc_info=True)

source/isaaclab_experimental/docs/CHANGELOG.rst

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@ Added
1111
``<log_dir>/params/resolved_env.yaml`` from
1212
:class:`~isaaclab_experimental.envs.ManagerBasedEnvWarp` and
1313
:class:`~isaaclab_experimental.envs.DirectRLEnvWarp` at the end of
14-
``__init__``, matching the stable env base classes.
14+
``__init__`` via the shared :func:`~isaaclab.utils.io.dump_resolved_cfg`
15+
utility, matching the stable env base classes.
1516

1617

1718
0.0.2 (2026-03-16)

source/isaaclab_experimental/isaaclab_experimental/envs/direct_rl_env_warp.py

Lines changed: 2 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
from isaaclab.managers import EventManager
3434
from isaaclab.sim import SimulationContext
3535
from isaaclab.sim.utils import use_stage
36-
from isaaclab.utils.io import dump_yaml
36+
from isaaclab.utils.io import dump_resolved_cfg
3737
from isaaclab.utils.noise import NoiseModel
3838
from isaaclab.utils.seed import configure_seed
3939
from isaaclab.utils.timer import Timer
@@ -271,9 +271,7 @@ def __init__(self, cfg: DirectRLEnvCfg, render_mode: str | None = None, **kwargs
271271
# video matches the simulation
272272
self.metadata["render_fps"] = 1 / self.step_dt
273273

274-
# dump the fully-resolved env config so users have a single source of
275-
# truth for what values the environment is actually using
276-
self._dump_resolved_cfg()
274+
dump_resolved_cfg(self.cfg, getattr(self.cfg, "log_dir", None), logger)
277275

278276
# print the environment information
279277
print("[INFO]: Completed setting up the environment...")
@@ -685,22 +683,6 @@ def set_debug_vis(self, debug_vis: bool) -> bool:
685683
Helper functions.
686684
"""
687685

688-
def _dump_resolved_cfg(self):
689-
"""Dump the fully-resolved env config to ``<log_dir>/params/resolved_env.yaml``.
690-
691-
This runs after all ``__post_init__`` hooks, preset resolution, and
692-
any training-script mutations, giving users a single source of truth
693-
for the values the environment actually uses.
694-
"""
695-
if self.cfg.log_dir is None:
696-
return
697-
resolved_path = os.path.join(self.cfg.log_dir, "params", "resolved_env.yaml")
698-
try:
699-
dump_yaml(resolved_path, self.cfg)
700-
logger.info("Resolved env config written to %s", resolved_path)
701-
except Exception:
702-
logger.warning("Failed to dump resolved env config to %s", resolved_path, exc_info=True)
703-
704686
def _configure_gym_env_spaces(self):
705687
"""Configure the action and observation spaces for the Gym environment."""
706688
# show deprecation message and overwrite configuration

0 commit comments

Comments
 (0)