Skip to content

Variations base 2.5/3.0 - In-memory recording#789

Open
alexmillane wants to merge 20 commits into
mainfrom
alex/feature/variations_recording
Open

Variations base 2.5/3.0 - In-memory recording#789
alexmillane wants to merge 20 commits into
mainfrom
alex/feature/variations_recording

Conversation

@alexmillane

@alexmillane alexmillane commented Jun 12, 2026

Copy link
Copy Markdown
Collaborator

Summary

Recording variation samples (in memory).

Detailed description

  • Recording which values were sampled in variations is critical for downstream analysis
  • This MR adds listeners to the samplers that record samples taken.
  • The record is in memory and attached to the env
  • Note: Implementing this required extending out ArenaEnvBuilder to emit constructor args to gym. I wish that was required, but I can't see another way of passing a run-time object from the compiler to the env.

Not Done

  • We still are not writing the variations to disk where another process can pick it up (e.g. sensitivity analysis).
  • This will be added in a follow up MR. (it's non-trivial so I'm breaking this up).

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>
@alexmillane alexmillane force-pushed the alex/feature/variations_recording branch from 5cb9285 to 64dce30 Compare June 15, 2026 09:38

@alexmillane alexmillane left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Self review again.

Comment on lines +361 to +372
"""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.
"""

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

"""

Comment on lines +72 to +78
# Print the variations record
print(env.unwrapped.variations_recorder.summary())
print("--------------------------------")
print(env.unwrapped.variations_recorder.details())


# %%

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove


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.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove "and notify listeners"

Comment on lines +33 to +36
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``.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove extended docstring.

Comment on lines +44 to +46

Listeners are invoked synchronously inside ``sample``, in registration order, with
the raw sample value (no copy / detach).

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove extended docstring.

Comment on lines +91 to +95
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)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we use remove_sample_listener anywhere except for the tests? I doubt it. Please remove remove functionality, and in the tests.

Comment on lines 98 to 103
"""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.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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."""
#

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Complete comment: "Add the title for this variation"

@alexmillane alexmillane changed the title DRAFT: variation recorder Variations base 2.5/3.0 - In-memory recording Jun 16, 2026
@alexmillane alexmillane marked this pull request as ready for review June 16, 2026 19:13
@greptile-apps

greptile-apps Bot commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This 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 VariationRecorder aggregates per-variation VariationRecord objects that accumulate every sample taken during both build-time and run-time phases. ArenaEnvBuilder is extended to construct the recorder, attach it before any sampling occurs, and forward it to gym.make via a new env_kwargs dict; IsaacLabArenaManagerBasedRLEnv receives and stores the recorder as self.variations_recorder.

  • Listener pipeline: SamplerBase gains add_listener/_notify; ContinuousSampler and ChoiceSampler split sample() into a template sample() + abstract _sample(), notifying after each draw. VariationBase holds variation-owned listeners and re-registers them on any new sampler built by apply_cfg, so subscriptions survive cfg swaps.
  • API change: compose_manager_cfg, build_registered, and make_registered* all return/accept an additional env_kwargs dict, and every caller (tests, scripts, eval runner) has been updated to unpack the 3-tuple and forward kwargs.
  • In-scope limitation: disk persistence is deferred; the recorder is purely in-memory and will accumulate samples for the entire env lifetime.

Confidence Score: 4/5

The 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

Filename Overview
isaaclab_arena/variations/variations_recorder.py New VariationRecorder and VariationRecord classes; missing getitem/contains on VariationRecorder (breaks test assertions) and cfg stored by reference rather than by snapshot
isaaclab_arena/tests/test_variations_recorder.py New test suite; multiple assertions will fail at runtime because VariationRecorder lacks getitem and contains, and records is a dict compared to []
isaaclab_arena/environments/arena_env_builder.py compose_manager_cfg, build_registered, and make_registered return/accept env_kwargs; when an external env_cfg is supplied without env_kwargs the recorder is silently absent (assert fires in IsaacLabArenaManagerBasedRLEnv)
isaaclab_arena/environments/isaaclab_arena_manager_based_env.py New init receives and stores variations_recorder; hard assert prevents direct instantiation without one, which can produce unhelpful errors for callers that bypass ArenaEnvBuilder
isaaclab_arena/variations/variation_base.py Adds _sample_listeners list and add_sample_listener; apply_cfg correctly re-binds listeners onto the new sampler after a cfg/sampler swap
isaaclab_arena/variations/sampler_base.py Adds init with _listeners list and add_listener/_notify helpers to SamplerBase; clean and correct
isaaclab_arena/variations/continuous_sampler.py sample() becomes a concrete template method that calls _sample() and _notify(); _sample() is the new abstract hook for subclasses
isaaclab_arena/variations/uniform_sampler.py Adds super().init() call and renames sample() to _sample() to fit the new ContinuousSampler template method pattern
isaaclab_arena/variations/choice_sampler.py sample() split into sample()/_sample() with _notify() call; listener wiring works through inherited SamplerBase.init
isaaclab_arena/evaluation/eval_runner.py Mechanical update to unpack 3-tuple from build_registered and forward env_kwargs to make_registered
isaaclab_arena/scripts/imitation_learning/record_demos.py Updated to thread env_kwargs through create_environment_config and create_environment so gym.make receives the variations_recorder

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
Loading
%%{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
Loading

Reviews (2): Last reviewed commit: "Restore compile_env_notebook.py" | Re-trigger Greptile

Comment on lines 377 to +381
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 = {}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant