Variations base 2.5/3.0 - In-memory recording#789
Conversation
Record every value drawn by an enabled variation's sampler so downstream sensitivity-analysis tooling has the input factors that produced each episode. This adds a sample-observer layer the recorder builds on: SamplerBase gains add_listener/remove_listener and a sample() template method that notifies listeners around the concrete _sample(); VariationBase gains add_sample_listener/remove_sample_listener, re-binding subscriptions onto the sampler rebuilt by apply_cfg so they survive cfg swaps. ArenaEnvBuilder constructs a VariationRecorder, attaches it after Hydra overrides but before any sampling, and exposes it on env.unwrapped. The recorder is stashed on the builder rather than the env cfg because the configclass __post_init__ deep-copies its attributes, which would orphan the listener closures. Signed-off-by: alex <amillane@nvidia.com>
5cb9285 to
64dce30
Compare
…riations_recording
…riations_recording
…riations_recording
alexmillane
left a comment
There was a problem hiding this comment.
Self review again.
| """Register the Gym env and parse the runtime cfg. | ||
|
|
||
| When ``env_cfg`` is omitted it is compiled/composed in this function from the IsaacLabArenaEnvironment | ||
| passed to the ArenaEnvBuilder at construction. | ||
|
|
||
| Args: | ||
| env_cfg: The optional environment cfg to use. | ||
| env_kwargs: The optional environment kwargs to use. | ||
|
|
||
| Returns: | ||
| A ``(name, cfg, env_kwargs)`` tuple. | ||
| """ |
There was a problem hiding this comment.
Can we change the docstring to say
"""
Build env cfg and register the env with gym. Stop short of env.make()
The default operation is to call with no arguments, in which case the env_cfg is built from the Arena description passed to the builder at construction.
"""
| render_mode: str | None = None, | ||
| ) -> ManagerBasedEnv: | ||
| env, _ = self.make_registered_and_return_cfg(env_cfg, render_mode=render_mode) | ||
| """Build and return the environment from the registered environment configuration. |
There was a problem hiding this comment.
Can we change the docstring to say
"""
Build env cfg, register the env with gym, and make the env.
The default operation is to call with no arguments, in which case the env_cfg is built from the Arena description passed to the builder at construction.
"""
| ) -> tuple[ManagerBasedEnv, IsaacLabArenaManagerBasedRLEnvCfg]: | ||
| name, cfg = self.build_registered(env_cfg) | ||
| env = gym.make(name, cfg=cfg, render_mode=render_mode) | ||
| """Build and return the environment from the registered environment configuration and return the configuration. |
There was a problem hiding this comment.
Can we change the docstring to say
"""
Build env cfg, register the env with gym, and make the env.
The default operation is to call with no arguments, in which case the env_cfg is built from the Arena description passed to the builder at construction.
"""
| # Print the variations record | ||
| print(env.unwrapped.variations_recorder.summary()) | ||
| print("--------------------------------") | ||
| print(env.unwrapped.variations_recorder.details()) | ||
|
|
||
|
|
||
| # %% |
|
|
||
| def sample(self, num_samples: int, choices: Sequence[T]) -> list[T]: | ||
| """Draw ``num_samples`` items from ``choices``. | ||
| """Draw ``num_samples`` items from ``choices`` and notify listeners. |
There was a problem hiding this comment.
remove "and notify listeners"
| Observers subscribe via ``add_listener`` to see every value drawn; prefer | ||
| ``VariationBase.add_sample_listener`` so subscriptions survive sampler swaps. Each sampler | ||
| family declares its own typed ``sample``; that method draws the value and forwards it to | ||
| listeners via ``_notify``. |
There was a problem hiding this comment.
Remove extended docstring.
|
|
||
| Listeners are invoked synchronously inside ``sample``, in registration order, with | ||
| the raw sample value (no copy / detach). |
There was a problem hiding this comment.
remove extended docstring.
| def remove_sample_listener(self, listener: Callable[[Any], None]) -> None: | ||
| """Unsubscribe a previously-registered ``listener``.""" | ||
| self._sample_listeners.remove(listener) | ||
| if self._sampler is not None: | ||
| self._sampler.remove_listener(listener) |
There was a problem hiding this comment.
Do we use remove_sample_listener anywhere except for the tests? I doubt it. Please remove remove functionality, and in the tests.
| """Install ``cfg`` as the variation's new source of truth. | ||
|
|
||
| Replaces :attr:`cfg` and rebuilds :attr:`sampler` from ``cfg.sampler_cfg``. | ||
| Subclasses with extra derived state should override and call | ||
| ``super().apply_cfg(cfg)`` first. | ||
| Replaces ``cfg`` and rebuilds ``sampler`` from ``cfg.sampler_cfg``, re-binding any | ||
| variation-owned sample listeners onto the new sampler. Subclasses with extra derived | ||
| state should override and call ``super().apply_cfg(cfg)`` first. | ||
|
|
There was a problem hiding this comment.
First line of docstring should be: `Apply new ``cfg```
|
|
||
| def _header_lines(self) -> list[str]: | ||
| """Return the shared preamble (identity, cfg, sample-call count) for renderers.""" | ||
| # |
There was a problem hiding this comment.
Complete comment: "Add the title for this variation"
Greptile SummaryThis PR introduces an in-memory variation recording system: samplers now fire observer callbacks on every draw, variations store those callbacks and re-bind them after cfg/sampler swaps, and a new
Confidence Score: 4/5The core listener/recorder wiring is correct and the broad API migration across 36 files is mechanically sound; the test suite for the new recorder contains assertions that will fail at runtime due to missing protocol methods on VariationRecorder. The new test file calls recorder["key"] (subscript) and "key" not in recorder (membership test) on VariationRecorder, but the class defines neither getitem nor contains; both raise TypeError. The same test also asserts recorder.records == [] against a dict, which always evaluates False. These issues were flagged in the previous review round and are still unaddressed, meaning the recorder test suite cannot pass as written. The production code path itself is sound — env construction, listener attachment, and sample capture all work correctly. isaaclab_arena/tests/test_variations_recorder.py and isaaclab_arena/variations/variations_recorder.py need attention: the recorder's missing getitem/contains methods break multiple test assertions. Important Files Changed
Sequence Diagram%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
participant Builder as ArenaEnvBuilder
participant Recorder as VariationRecorder
participant Variation as VariationBase
participant Sampler as SamplerBase
participant Record as VariationRecord
participant Env as IsaacLabArenaManagerBasedRLEnv
Builder->>Recorder: VariationRecorder()
Builder->>Recorder: attach(get_all_variations())
Recorder->>Variation: add_sample_listener(on_sample)
Variation->>Sampler: add_listener(on_sample)
Recorder->>Record: VariationRecord(key, variation.cfg)
Recorder-->>Builder: recorder ready
Builder->>Variation: apply() [build-time variations]
Variation->>Sampler: sample(num_samples)
Sampler->>Sampler: _sample() → result
Sampler->>Sampler: _notify(result)
Sampler->>Record: on_sample(result) → samples.append
Builder->>Builder: compose env_cfg
Builder-->>Builder: "return (env_cfg, {variations_recorder: recorder})"
Builder->>Env: "gym.make(name, cfg=env_cfg, variations_recorder=recorder)"
Env->>Env: "self.variations_recorder = recorder"
Env->>Env: super().__init__()
Note over Env,Sampler: During simulation resets (runtime variations)
Env->>Variation: event fires → func(env)
Variation->>Sampler: sample(num_samples)
Sampler->>Record: on_sample(result) → samples.append
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
participant Builder as ArenaEnvBuilder
participant Recorder as VariationRecorder
participant Variation as VariationBase
participant Sampler as SamplerBase
participant Record as VariationRecord
participant Env as IsaacLabArenaManagerBasedRLEnv
Builder->>Recorder: VariationRecorder()
Builder->>Recorder: attach(get_all_variations())
Recorder->>Variation: add_sample_listener(on_sample)
Variation->>Sampler: add_listener(on_sample)
Recorder->>Record: VariationRecord(key, variation.cfg)
Recorder-->>Builder: recorder ready
Builder->>Variation: apply() [build-time variations]
Variation->>Sampler: sample(num_samples)
Sampler->>Sampler: _sample() → result
Sampler->>Sampler: _notify(result)
Sampler->>Record: on_sample(result) → samples.append
Builder->>Builder: compose env_cfg
Builder-->>Builder: "return (env_cfg, {variations_recorder: recorder})"
Builder->>Env: "gym.make(name, cfg=env_cfg, variations_recorder=recorder)"
Env->>Env: "self.variations_recorder = recorder"
Env->>Env: super().__init__()
Note over Env,Sampler: During simulation resets (runtime variations)
Env->>Variation: event fires → func(env)
Variation->>Sampler: sample(num_samples)
Sampler->>Record: on_sample(result) → samples.append
Reviews (2): Last reviewed commit: "Restore compile_env_notebook.py" | Re-trigger Greptile |
| name = self.arena_env.name | ||
| cfg_entry = env_cfg if env_cfg is not None else self.compose_manager_cfg() | ||
| if env_cfg is None: | ||
| env_cfg, env_kwargs = self.compose_manager_cfg() | ||
| elif env_kwargs is None: | ||
| env_kwargs = {} |
There was a problem hiding this comment.
Silent
variations_recorder-less path in build_registered
When a caller passes an explicit env_cfg but omits env_kwargs (i.e. build_registered(my_cfg)), the branch at line 380 sets env_kwargs = {} — no variations_recorder. Any subsequent gym.make(**env_kwargs) will reach IsaacLabArenaManagerBasedRLEnv.__init__, which hard-asserts that variations_recorder is not None. The result is an unhelpful AssertionError rather than a clear API-usage error.
Consider either always constructing a fresh VariationRecorder() in that branch, or renaming the parameter to make it explicit that callers must supply one whenever env_cfg is provided externally.
Summary
Recording variation samples (in memory).
Detailed description
envenv.Not Done