Skip to content

variations in eval runner#779

Open
alexmillane wants to merge 73 commits into
mainfrom
alex/feature/variations_in_eval_runner
Open

variations in eval runner#779
alexmillane wants to merge 73 commits into
mainfrom
alex/feature/variations_in_eval_runner

Conversation

@alexmillane

Copy link
Copy Markdown
Collaborator

Summary

Connects the eval_runner.py to the variations.

Detailed description

  • Expands our Job/json to add a field for variations.
  • Passes those variations into the ArenaEnvBuilder for building.
  • Added a small example droid_pnp_variations_config.json that demonstrates how to use this feature and added it to the (placeholder) docs.

Introduce the variation framework: a VariationBase with build-time and
run-time flavors, a SamplerBase abstraction with uniform and categorical
samplers, and two concrete variations -- HDR dome-light image selection
(build-time) and camera extrinsic decalibration (run-time).

Variations attach to any Asset (scene objects or embodiments) and are
collected by ArenaEnvBuilder, which applies build-time variations before
scene composition and folds run-time variations into the event manager
cfg. All variations default to disabled so existing envs are unchanged.

The variations package exposes its API lazily (PEP 562 __getattr__):
camera_decalibration pulls in torch and isaaclab.sensors, and importing
that pair before the SimulationApp launches corrupts USD's Python
bindings. Lazy exports keep importing the package -- or its lightweight
base/sampler submodules -- safe at module-load time (e.g. pytest
collection).

Signed-off-by: alex <amillane@nvidia.com>
- Rename camera_decalibration.py -> camera_decalibration_variation.py
- Drop package-level re-exports; import concrete classes from submodules
- Remove sampler/variation listener plumbing (deferred to a follow-up MR)
- Hoist a compulsory sampler_cfg field onto VariationBaseCfg and rename the
  per-variation sampler field to sampler_cfg
- Replace UniformSampler.event_shape with an abstract SamplerBase.shape_per_sample
- Drop the camera variation's unused mode field (all variations fire on reset)
- Move attribute docs onto each member and trim docstrings to one line

Signed-off-by: alex <amillane@nvidia.com>
Agents drove the container with bare `docker exec` (root), while users
attach via run_docker.sh as their host user (su $(id -un)). Root-run
commands left root-owned files in shared caches (/tmp/Assets, ~/.config)
that the host user could not read, breaking their later interactive runs.

Update AGENTS.md and the dev-container/run-tests skills to route every
in-container command through `su $(id -un) -c`, mirroring run_docker.sh.

Signed-off-by: alex <amillane@nvidia.com>

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

Comment on lines +61 to +64
Mirrors policy_runner's ``--list-variations`` but, because each eval job defines its own
environment via ``arena_env_args``, the catalogue is printed per job. Each job's own
``variations`` overrides are applied so the catalogue reflects what the run would use.
Must be called inside a ``SimulationAppContext``.

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 long docstring

registered set. The ``sampler_cfg.low`` / ``sampler_cfg.high`` vectors widen the camera
extrinsics jitter range to ±10 mm per axis.

Configuring variations in an eval jobs config

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.

Move this section below the one below, to the end of the file.

with open(args_cli.eval_jobs_config, encoding="utf-8") as f:
eval_jobs_config = json.load(f)

# Print the variations catalogue for each job's environment and exit, without running anything.

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.

shorten to "Print the variations catalogue for each job's environment and exit."

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.

Delete this file. This was deleted on main and is being accidentally readded here.

Comment on lines +213 to +218
"""Drive the eval-config path (Job.from_dict -> load_env) and check the override reached a manager.

A runtime variation enabled via the job's ``variations`` block must surface as an
``EventTermCfg`` in the composed ``env_cfg.events`` manager. We use the droid wrist-camera
extrinsics variation, which produces the event term ``wrist_camera_extrinsics_variation``.
"""

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.

Shorten to: "Enable a wrist camera extrinsics variation and check that it shows up as an event term in the cfg."

"num_steps": NUM_STEPS,
"policy_type": "zero_action",
"policy_config_dict": {},
"variations": {"droid_abs_joint_pos": {f"camera_extrinsics_{camera_name}": {"enabled": True}}},

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.

Put a comment before this line "Enabling wrist camera extrinsics variation."

Comment on lines +76 to +88
def test_job_from_dict_with_variations():
"""Job.from_dict populates job.variations from a nested variations block."""
job_dict = {
"name": "job_with_variations",
"arena_env_args": {"environment": "env1"},
"policy_type": "zero_action",
"policy_config_dict": {},
"variations": {"light": {"hdr_image": {"enabled": True}}},
}

job = Job.from_dict(job_dict)

assert job.variations == ["light.hdr_image.enabled=true"]

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.

Combine this test with the test test_job_convert_variations_dict_to_hydra_overrides above.

Comment on lines +91 to +102
def test_job_from_dict_without_variations_defaults_empty():
"""A job without a variations field defaults to no overrides."""
job_dict = {
"name": "job_without_variations",
"arena_env_args": {"environment": "env1"},
"policy_type": "zero_action",
"policy_config_dict": {},
}

job = Job.from_dict(job_dict)

assert job.variations == []

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.

Combine this test with test_job_from_dict_with_variations above.

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.

Delete this file. Added by accident.

@alexmillane alexmillane marked this pull request as ready for review June 12, 2026 10:21
@greptile-apps

greptile-apps Bot commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR wires the variations feature into eval_runner.py end-to-end: a new variations field is added to the JSON job config, converted from a nested dict to Hydra override strings, and passed through load_env into ArenaEnvBuilder. A --list-variations path is also plumbed in.

  • Job.from_dict / convert_variations_dict_to_hydra_overrides: a new classmethod walks the nested variations dict using an iterative DFS and flattens each leaf into a dotted.path=value Hydra override string using json.dumps for type-aware serialization (booleans, lists, numbers, strings).
  • eval_runner.py: load_env gains a variations parameter forwarded as hydra_overrides; a new list_variations helper prints the variations catalogue per job when --list-variations is passed, exiting before the main run loop.
  • Tests and example config: unit tests cover the flattening logic and round-trip through Job.from_dict; a new integration test verifies the override lands in env_cfg.events; a new example config and updated RST doc demonstrate the feature.

Confidence Score: 4/5

Safe to merge after addressing the null-variations crash in job_manager.py.

The one concrete defect is in Job.from_dict: a JSON config with "variations": null silently passes None into convert_variations_dict_to_hydra_overrides, which throws an opaque AssertionError about a non-empty path rather than indicating the actual problem. Everything else — the DFS flattener, load_env wiring, list_variations, and tests — looks correct and well-covered.

isaaclab_arena/evaluation/job_manager.py — the from_dict call to convert_variations_dict_to_hydra_overrides needs or {} to safely handle an explicit JSON null in the variations field.

Important Files Changed

Filename Overview
isaaclab_arena/evaluation/job_manager.py Adds variations field to Job and convert_variations_dict_to_hydra_overrides classmethod; data.get("variations", {}) passes None to the converter when the JSON key is present with an explicit null value, causing a misleading AssertionError; asserts used for runtime input validation are stripped under -O.
isaaclab_arena/evaluation/eval_runner.py Adds variations parameter to load_env and a new list_variations helper; both correctly forward the already-converted list of Hydra override strings; integration with --list-variations and the main run loop looks correct.
isaaclab_arena/tests/test_job_manager.py New unit tests cover flattening logic (including booleans, lists, empty dict) and round-trip through Job.from_dict; all assertions look correct.
isaaclab_arena/tests/test_eval_runner.py Adds subprocess test for variations end-to-end and an integration test that verifies the enabled variation appears in env_cfg.events; test structure is consistent with existing patterns.
isaaclab_arena_environments/eval_jobs_configs/droid_pnp_variations_config.json New example config enabling three variations (HDR image, intensity, wrist-camera extrinsics); matches the documented schema exactly.
docs/pages/concepts/variations/variations.rst New section documents the variations JSON field with an inline example config and two shell commands; content accurately reflects the implementation.

Sequence Diagram

sequenceDiagram
    participant User
    participant eval_runner as eval_runner.py
    participant JobManager
    participant Job
    participant ArenaEnvBuilder

    User->>eval_runner: --eval_jobs_config config.json
    eval_runner->>eval_runner: json.load(config)
    eval_runner->>JobManager: JobManager(eval_jobs_config["jobs"])
    JobManager->>Job: Job.from_dict(job_dict)
    Job->>Job: "convert_variations_dict_to_hydra_overrides({"light": {"hdr_image": {"enabled": true}}})"
    Job-->>JobManager: "job.variations = ["light.hdr_image.enabled=true"]"
    JobManager-->>eval_runner: job_manager

    alt --list-variations flag
        eval_runner->>ArenaEnvBuilder: "get_arena_builder_from_cli(hydra_overrides=job.variations)"
        ArenaEnvBuilder-->>eval_runner: catalogue string
        eval_runner->>User: print catalogue and return
    else normal run
        loop for each job rebuild
            eval_runner->>eval_runner: "load_env(job.arena_env_args, job.name, variations=job.variations)"
            eval_runner->>ArenaEnvBuilder: "get_arena_builder_from_cli(hydra_overrides=job.variations)"
            ArenaEnvBuilder->>ArenaEnvBuilder: apply Hydra overrides to env cfg
            ArenaEnvBuilder-->>eval_runner: env
            eval_runner->>eval_runner: rollout_policy(env, policy)
        end
    end
Loading

Reviews (2): Last reviewed commit: "Revert the submodule." | Re-trigger Greptile

policy_config_dict=data["policy_config_dict"],
status=status,
language_instruction=data.get("language_instruction"),
variations=cls.convert_variations_dict_to_hydra_overrides(data.get("variations", {})),

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 "variations": null in JSON crashes with a misleading error

data.get("variations", {}) returns the default {} only when the key is absent. If the JSON contains "variations": null (explicit JSON null), it returns None. Passing None into convert_variations_dict_to_hydra_overrides pushes ("", None) onto the stack; when that item is popped, isinstance(None, dict) is False and the code falls through to assert prefix != "", which fires with AssertionError: "Variation override path must be non-empty" — a message that gives no hint that null is the actual problem. Replace with data.get("variations") or {} to treat both absent and null as "no overrides".

Comment on lines +145 to +149
if isinstance(node, dict):
for key, value in node.items():
assert key != "", "Variation override keys must be non-empty"
child_prefix = f"{prefix}.{key}" if prefix else str(key)
stack.append((child_prefix, value))

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.

P2 The stack-based DFS pops in LIFO order, so sibling keys are emitted in reverse insertion order. While Hydra override semantics are generally order-independent, reversing the author's dict order is surprising. Switching to reversed(list(node.items())) before appending preserves input order with no algorithmic cost.

Suggested change
if isinstance(node, dict):
for key, value in node.items():
assert key != "", "Variation override keys must be non-empty"
child_prefix = f"{prefix}.{key}" if prefix else str(key)
stack.append((child_prefix, value))
if isinstance(node, dict):
for key, value in reversed(list(node.items())):
assert key != "", "Variation override keys must be non-empty"
child_prefix = f"{prefix}.{key}" if prefix else str(key)
stack.append((child_prefix, value))

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Comment on lines +147 to +151
assert key != "", "Variation override keys must be non-empty"
child_prefix = f"{prefix}.{key}" if prefix else str(key)
stack.append((child_prefix, value))
continue
assert prefix != "", "Variation override path must be non-empty"

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.

P2 assert for runtime input validation

Python strips assert statements when the interpreter runs with the -O (optimize) flag. Both assert key != "" and assert prefix != "" are meant to guard against bad user input, but they become no-ops under optimization. Prefer raising ValueError with a descriptive message so the check is always enforced regardless of how the process is launched.

@alexmillane alexmillane changed the title DRAFT: variations in eval runner variations in eval runner Jun 12, 2026
"num_rebuilds": 5,
"policy_type": "zero_action",
"policy_config_dict": {},
"variations": {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Really cool!

Comment thread submodules/IsaacLab

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Is this submodule bump intentional?

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.

Not at all. Good catch! That would have reintroduced the texture bug no doubt.

@cvolkcvolk cvolkcvolk left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Nice! lgtm. Keep an eye on the submodule bump.

policy_config_dict=data["policy_config_dict"],
status=status,
language_instruction=data.get("language_instruction"),
variations=cls.convert_variations_dict_to_hydra_overrides(data.get("variations", {})),

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 data.get("variations", {}) only returns the {} default when the key is absent. If the JSON contains "variations": null (explicit JSON null), .get returns None, and convert_variations_dict_to_hydra_overrides(None) pushes ("", None) onto the stack. When that item is popped, isinstance(None, dict) is False and the code hits assert prefix != "" — raising AssertionError: "Variation override path must be non-empty", with no hint that null is the root cause. Using or {} treats both absent and explicit-null as "no overrides".

Suggested change
variations=cls.convert_variations_dict_to_hydra_overrides(data.get("variations", {})),
variations=cls.convert_variations_dict_to_hydra_overrides(data.get("variations") or {}),

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.

2 participants