Skip to content

Add standalone physics settle check over pooled layouts#769

Open
xyao-nv wants to merge 19 commits into
mainfrom
xyao/dev/add_physics_validation
Open

Add standalone physics settle check over pooled layouts#769
xyao-nv wants to merge 19 commits into
mainfrom
xyao/dev/add_physics_validation

Conversation

@xyao-nv

@xyao-nv xyao-nv commented Jun 9, 2026

Copy link
Copy Markdown
Collaborator

Summary

When a solver returns a pooled layouts, there are 2 types of validations. One is analytical, fast, does not require SimApp, e.g. check no overlapping & on relation (existing). The other involves stepping physics, slow but more closely reflects how end user consumes, e.g. physics stability.
In an ideal world, placement_enev shall only use valid layout everytime thru env.reset()

Detailed description

Why

Agentic Env Gen requires a post-validation step to rule out envs with unstable physics, and re-trigger generation if nothing valid. It has to be automatic.

How

python isaaclab_arena/scripts/run_placement_pool_validation.py (--viz kit --render) --num_envs 10 pick_and_place_maple_table
The standalone script checks if physics settled by advancing physics only, over the pool of layouts returned by the solver, and report results.

What

  1. Refactored existing validation:
  • added a PlacementValidationChecklist data class to contain each individual validation check results;
  • included a required/optional validation failures to let user decide which optimized layouts shall be selected as the best-ranked.
  1. Added physics_settle.py to execute physics stepping without rendering, and check linear_vel, and ang_vel if below thresholds.
  2. Added placement_pool_validation.py orchestras how many episodes over how many envs needed for the fixed pool of layouts
  3. Added user-interface run_placement_pool_validation.py to execute the check

Next

Add integration with Pool filling and lookup table.
Ideal pipeline:
Each time pool is filled (after solver, before placement event) -> [Optional] run physics check for all env's pool unvalidated layouts & log it down -> Select (valid) one to load in placement events

@isaaclab-review-bot isaaclab-review-bot Bot left a comment

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.

Code Review: Add physics settle validation check to PooledObjectSolver

Good feature direction — gating layouts on physical settling is the right post-validation strategy for agentic env gen. The PlacementValidationChecklist refactor cleanly generalizes validation, and the failure cache with layout-signature invalidation is well-thought-out. A few issues need attention before merging:


🔴 Critical: CLI argument --enable_physics_settle_check is always True

isaaclab_arena/cli/isaaclab_arena_cli.py

arena_group.add_argument(
    "--enable_physics_settle_check",
    action="store_true",
    default=True,
    ...
)

action="store_true" combined with default=True means the flag is always True — there's no way for the user to disable it. store_true only sets the value to True when the flag is present; since the default is already True, the flag is a no-op.

Fix: Use BooleanOptionalAction (Python 3.9+) to get both --enable_physics_settle_check and --no-enable_physics_settle_check:

arena_group.add_argument(
    "--enable_physics_settle_check",
    action=argparse.BooleanOptionalAction,
    default=True,
    help=...,
)

🔴 Critical: Wrong TYPE_CHECKING import in placement_events.py

isaaclab_arena/relations/placement_events.py:19

if TYPE_CHECKING:
    from isaaclab_arena.assets.object_base import ObjectBase
    from isaaclab_arena.relations.placement_validation import PlacementResult

PlacementResult lives in isaaclab_arena.relations.placement_result, not placement_validation (which only defines PlacementValidationChecklist). This won't crash at runtime (guarded by TYPE_CHECKING), but type checkers and IDE navigation will break on the result: PlacementResult annotation in write_layout_to_sim.

Fix:

from isaaclab_arena.relations.placement_result import PlacementResult

🟡 Warning: PhysicsSettleFailureCache._load can crash on corrupted JSON

isaaclab_arena/relations/physics_settle_failure_cache.py:99-108

def _load(self) -> set[int]:
    path = self._path()
    if path is None or not os.path.exists(path):
        return set()
    with open(path) as cache_file:
        data = json.load(cache_file)
    ...

A corrupted or truncated cache file (e.g. from a crash mid-write, disk full, or manual editing) will raise json.JSONDecodeError and crash pool construction with a confusing traceback. Since this is an optimization cache (not critical state), the safe behavior is to log a warning and return an empty set.

Suggested fix:

def _load(self) -> set[int]:
    path = self._path()
    if path is None or not os.path.exists(path):
        return set()
    try:
        with open(path) as cache_file:
            data = json.load(cache_file)
    except (json.JSONDecodeError, OSError):
        return set()
    ...

🟡 Warning: verify_in_sim_and_reselect may append duplicate entries to newly_failed

isaaclab_arena/relations/pooled_object_placer.py:600-620

In the retry loop, when an env fails to settle on attempt 0, the layout is appended to newly_failed. On attempt 1, a replacement is drawn and written, but if the same original layout object (already marked with physics_settled: False via add_checklist_item) is somehow still referenced, the "physics_settled" in checklist.checklist_items guard will use the cached False verdict and append it again. More practically: the current[env_id] is updated to the replacement after the first failure, but the original layout is already in newly_failed — so the replacement (if it also fails) gets appended too, which is correct. However, the if "physics_settled" in checklist.checklist_items branch re-reads the old verdict without re-stepping, which means a replacement layout that was previously cached as failed will short-circuit correctly. This logic is sound but fragile — a brief comment clarifying that current[env_id] is always the replacement at this point would help future readers.


🟡 Warning: _last_applied is never cleared between resets

isaaclab_arena/relations/pooled_object_placer.py:563-565

note_applied accumulates entries in _last_applied keyed by env_id. On subsequent resets, solve_and_place_objects calls note_applied again which overwrites the entry — so it's fine for the settle pass. But if some envs are not reset (partial reset), _last_applied still holds stale entries from prior resets. When verify_in_sim_and_reselect is called, it processes all entries in _last_applied, potentially re-stepping and re-checking envs that weren't just reset.

Suggestion: Clear _last_applied at the start of verify_in_sim_and_reselect (after copying current = dict(self._last_applied)) or only record applied layouts for the current reset batch.


🔵 Improvement: EnvLayoutPool.available is O(n) on every call

isaaclab_arena/relations/pooled_object_placer.py:37-40

@property
def available(self) -> int:
    return sum(
        1 for layout in self.layouts[self.cursor :] if not layout.validation_checklist.physics_settled_failed
    )

This scans all remaining layouts on every access. It's called from _available_per_env() which is called frequently during solve loops. For typical pool sizes (~100 layouts) this is fine, but if pools grow large or the check is called in tight loops, consider maintaining a counter decremented when a layout is marked as a settle failure.


🧪 Test Coverage

The PR includes solid test coverage:

  • test_physics_settle.py: End-to-end sim tests for the settle primitives (at-rest, colliding, parallel per-env) — good.
  • test_physics_settle_failure_cache.py: Persistence round-trip, invalidation (seed mismatch, signature mismatch), and signature determinism/sensitivity — thorough.
  • Existing tests updated to use the new PlacementValidationChecklist API — no regressions.

Gap: No unit test for verify_in_sim_and_reselect with the retry loop (e.g. mock that demonstrates re-selection when first layout fails and next one settles). The sim tests exercise the primitives, but the orchestration logic is only tested end-to-end via the sim. Consider a focused mock test for the retry/re-selection flow.


CI Status

Pre-commit is currently failing — likely a lint/formatting issue that needs a fix-up commit before merge.


Verdict: Minor fixes needed. The critical CLI bug and the wrong import should be fixed before merge. The cache robustness and _last_applied clearing are recommended improvements.


Update (e6d8178): Significant refactor addressing several issues from the initial review and restructuring the physics settle check architecture:

✅ Issues Addressed

  1. CLI default fixed--enable_physics_settle_check now defaults to False with action="store_true", which is the correct pattern (flag absent → disabled, flag present → enabled). This resolves the "always True" bug from the initial review.

  2. PhysicsSettleFailureCache removed entirely — The on-disk persistence cache (physics_settle_failure_cache.py) and its test file have been deleted. The approach was replaced by a simpler design: failed layouts are marked in-memory only via the PlacementValidationChecklist. This eliminates the corrupted-JSON crash risk and the layout-signature complexity flagged in the initial review.

  3. _last_applied is now a public propertynote_applied() was removed; callers write directly to pool.last_applied[env_id] = result. The stale-partial-reset concern remains (the dict is never cleared), but the logic is now in placement_events.py where the caller has full control.

  4. verify_in_sim_and_reselect moved out of PooledObjectPlacer — It's now a free function in placement_events.py, receiving the pool, env, and a new PhysicsSettleParams dataclass. This is a cleaner separation of concerns: the pool owns layout storage/sampling; the event module owns sim-stepping logic.

  5. newly_failed tracking removed — Since the cache is gone, there's no need to accumulate and persist failures. The logic now simply marks the checklist and moves on, removing the duplicate-append fragility.

🆕 New Architecture

  • PhysicsSettleParams (new dataclass): Holds num_steps, lin_vel_thresh, ang_vel_thresh, max_retries. Decoupled from ObjectPlacerParams which previously held these fields.
  • PlacementCheck enum (new StrEnum): Replaces raw string keys ("no_overlap", "on_relation", "physics_settled") with typed constants. Good for IDE support and typo prevention.
  • run_placement_physics_settle_check returns VecEnvObs | None — After the settle pass steps physics and potentially swaps layouts, the reset-time observation is stale. The function now recomputes and returns a fresh observation (including RTX sensor re-renders). The caller in policy_runner.py adopts it. This is a correctness fix for camera-based policies.
  • draw_replacement is now public — renamed from _draw_replacement since it's called from outside the class.

🟡 Remaining Concerns

  1. _last_applied stale-entry issue persists — The dict is still never cleared between resets. In partial-reset scenarios, verify_in_sim_and_reselect processes all entries including envs not just reset. The caller (policy_runner.py) invokes it once after env.reset(), so for full resets this is fine, but partial resets could re-verify stale layouts unnecessarily.

  2. PlacementResult import — The TYPE_CHECKING import in placement_events.py still imports PlacementResult from placement_validation (line in the diff shows from isaaclab_arena.relations.placement_validation import PlacementResult). This was flagged in the initial review and appears unchanged. (Needs verification — the diff shows the import block was restructured but the target module may still be wrong.)

  3. objects_settled logic flip — The condition was changed from not (lin > thresh or ang > thresh) to lin <= thresh and ang <= thresh. These are logically equivalent (De Morgan's law), so this is a readability improvement, not a bug fix. 👍

  4. New test test_settle_check_reselects_unstable_layout — Excellent addition! This directly addresses the test coverage gap identified in the initial review. It builds a real sim env, seeds an unstable layout (cracker box dropped from height), and verifies the settle pass re-selects a stable resting layout. This is the orchestration-level test that was missing.

🧪 Test Coverage Update

  • test_physics_settle_failure_cache.py removed (no longer needed since cache is deleted)
  • test_settle_check_reselects_unstable_layout added — covers the full retry/re-selection flow with live physics
  • test_placement_events.py updated — mock pools now expose last_applied: dict = {} instead of note_applied() method

Updated Verdict: Good progress. The refactor simplifies the architecture significantly (removing the cache, separating concerns, fixing the CLI default). The observation-refresh after settle is a meaningful correctness improvement. The TYPE_CHECKING import issue from the initial review should still be verified/fixed. Overall, closer to merge-ready.


Update (9078efe): Minor cleanup commit — no new issues.

Changes

  1. policy_runner.py — Added from __future__ import annotations and removed string-quoted type annotations ("PolicyBase"PolicyBase). This is a standard Python 3.10+ forward-reference pattern backported via the future import. Clean.

  2. test_relation_solver_interface.py — Introduced _fallback_layout() test helper that constructs PlacementResult with the new PlacementValidationChecklist API (using PlacementCheck.NO_OVERLAP: False to indicate failure). Replaced inline PlacementResult(success=False, ...) calls throughout the test file. This aligns the tests with the API changes from the previous commit — good housekeeping.

Assessment

Both changes are mechanical/cleanup in nature. No new logic, no new issues. Tests properly updated to match the new validation checklist interface. ✅

@cvolkcvolk

cvolkcvolk commented Jun 9, 2026

Copy link
Copy Markdown
Collaborator

Thanks for putting this together! I read through it and wanted to share some thoughts, mostly around keeping the placement/solver layer sim-agnostic.

Main design point to keep the solver/placement code sim-free

We deliberately kept the solver and placement code (ObjectPlacer, PooledObjectPlacer, ...) free of any SimulationApp dependency. Both to keep its dependency surface lean and because that path is fast, whereas anything touching sim is comparatively expensive. I'd love to preserve that boundary here.

Conceptually PooledObjectPlacer, a pure pool/solver object, now owns verify_in_sim_and_reselect(env), which takes a live env and steps physics. The lazy imports hide that from the dependency graph, but the class has effectively grown sim-coupled.

Idea: move the settle/reselection loop into placement_events.py (which already imports isaaclab and owns the sim writes), and have it reach the pool only through a small public API. That keeps PooledObjectPlacer as sim-agnostic as it is today.

On the cross-run failure cache

PhysicsSettleFailureCache + compute_layout_signature are a lot of machinery for skipping a known-bad layout within a run...
Would physics_settled_failed be enough to cover our current blocking use cases? Consider keeping just the in-run skip and dropping the disk cache + signature for now. It might be cleaner to reintroduce it later behind an explicit, simpler check (once cross-run skipping turns out to matter for the env-gen loop)

Comment thread isaaclab_arena/cli/isaaclab_arena_cli.py
Comment thread isaaclab_arena/evaluation/policy_runner.py Outdated
placement_seed: int | None = None,
resolve_on_reset: bool | None = None,
random_yaw_init: bool = False,
physics_settle_cache_key: str | None = None,

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.

We may want to add more validation checks at the end, which would likely introduce more arguments to the function. I’m wondering whether we can design a clean and extensible interface so that users can easily understand the options, or at least define a well-structured argument list.

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.

let's scope out a design tgt and imple it in future MRs

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.

Sounds good — scoping a design target first makes sense here. Happy to review the follow-up when it lands. 👍

Comment thread isaaclab_arena/utils/physics_settle.py Outdated
Comment thread isaaclab_arena/relations/physics_settle_failure_cache.py Outdated
Comment thread isaaclab_arena/relations/physics_settle_failure_cache.py Outdated
Comment thread isaaclab_arena/relations/placement_events.py Outdated
Comment thread isaaclab_arena/relations/placement_validation.py
@xyao-nv xyao-nv marked this pull request as ready for review June 9, 2026 23:47
@xyao-nv xyao-nv changed the title Add physics settle validation check to PooledObjectSolver Add physics settle validation check over pooled layouts into Placement Event Jun 9, 2026
@xyao-nv

xyao-nv commented Jun 9, 2026

Copy link
Copy Markdown
Collaborator Author

@isaaclab-review-bot

@greptile-apps

greptile-apps Bot commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds a standalone physics-settle validation sweep over pooled placement layouts. It refactors the existing boolean success / is_valid fields into a structured PlacementValidationChecklist that tracks per-check results (geometric and runtime), adds physics_settle.py primitives that step physics without triggering the metric recorder, and orchestrates a full-pool sweep in placement_pool_validation.py that stamps each layout's checklist with a PHYSICS_SETTLED verdict.

  • PlacementValidationChecklist replaces the flat success: bool on PlacementResult and PlacementCandidate, enabling ranked sorting by (required-failures, optional-failures, loss) and distinguishing geometric vs. physics validity.
  • validate_pool_layouts steps physics per episode in parallel across all envs, then reads back velocities with objects_settled_per_episode and stamps each layout's checklist in-place; the mutated checklists remain in the pool so downstream placement events can consult settled/unsettled status.
  • run_placement_pool_validation.py is a two-phase argparse standalone script that builds any Arena env, resets it, and reports per-layout settle verdicts.

Confidence Score: 5/5

Safe to merge; the new physics-settle pipeline is well-structured, self-contained, and covered by end-to-end sim tests.

The changes are additive and well-tested: the existing placement path is refactored cleanly (boolean flags replaced by structured checklists), and the new validate_pool_layouts sweep is guarded by comprehensive subprocess-sim tests. The two checklist issues (KeyError on bare dict subscript and the implicit-to-explicit required-item transition) are latent API risks rather than bugs reachable from current call sites. No regressions in existing tests, no broken import paths, and the previously-flagged PlacementResult import is now correctly pointing at placement_result.

placement_validation.py — the pass_validation_checklist subscript and the _required() implicit-to-explicit transition are the two spots worth a second look before the checklist API is exposed more broadly.

Important Files Changed

Filename Overview
isaaclab_arena/relations/placement_validation.py New PlacementCheck enum and PlacementValidationChecklist dataclass; pass_validation_checklist uses bare dict subscript that raises KeyError when required_items diverges from checklist_items, and the implicit-to-explicit required-item transition via add_checklist_item(required=True) silently downgrades previously-required items.
isaaclab_arena/relations/placement_pool_validation.py New orchestration module: writes each pooled layout to sim, steps physics, grades settle verdict. Steps physics for ALL envs in every episode even when only a subset has a layout for that episode.
isaaclab_arena/utils/physics_settle.py New physics-settle primitives (step_physics, objects_settled_per_episode); uses wp.to_torch() on asset velocity data consistent with Isaac Lab's warp backend.
isaaclab_arena/relations/placement_result.py Replaced success: bool field with validation_checklist: PlacementValidationChecklist; success becomes a computed property. Clean refactor.
isaaclab_arena/relations/placement_events.py Added get_placement_pool, write_layout_to_sim, and helpers; PlacementResult is now correctly imported from placement_result, resolving the previously-flagged import bug.
isaaclab_arena/relations/object_placer.py Refactored to use PlacementValidationChecklist throughout; ranking now sorts by (required_failures, optional_failures, loss).
isaaclab_arena/relations/physics_settle_params.py New dataclass for settle-check params (num_steps, lin_vel_thresh, ang_vel_thresh). Clean.
isaaclab_arena/scripts/run_placement_pool_validation.py Standalone validation script with two-phase argparse; runs validate_pool_layouts and logs results.
isaaclab_arena/relations/pooled_object_placer.py Added objects property and layouts_per_env() introspection method for the offline validator; minimal change.
isaaclab_arena/environments/arena_env_builder.py Uses constant PLACEMENT_RESET_EVENT_NAME instead of inline string literal.
isaaclab_arena/tests/test_physics_settle.py Comprehensive end-to-end tests: settled-at-rest, colliding, parallel per-env, and pool sweep scenarios.

Sequence Diagram

sequenceDiagram
    participant Script as run_placement_pool_validation
    participant VPL as validate_pool_layouts
    participant WL as _write_layouts_per_episode
    participant PS as step_physics
    participant GB as _grade_settled_batch
    participant OSE as objects_settled_per_episode
    participant Pool as PooledObjectPlacer
    participant Sim as Isaac Lab Sim

    Script->>VPL: validate_pool_layouts(env, settle_params)
    VPL->>Pool: layouts_per_env()
    Pool-->>VPL: list[list[PlacementResult]]
    loop for each episode_index
        VPL->>WL: write layouts for this episode
        WL->>Sim: write_root_pose_to_sim + write_root_velocity_to_sim (per env)
        WL-->>VPL: batch [(env_id, layout)]
        VPL->>PS: "step_physics(env, num_steps * decimation)"
        PS->>Sim: sim.step() x num_steps + scene.update()
        VPL->>GB: _grade_settled_batch(env, batch, ...)
        GB->>OSE: objects_settled_per_episode(env, env_ids, ...)
        OSE->>Sim: read root_lin_vel_w / root_ang_vel_w via wp.to_torch
        OSE-->>GB: list[bool]
        GB->>Pool: layout.validation_checklist.add_checklist_item(PHYSICS_SETTLED, settled)
        GB-->>VPL: [(env_id, checklist)]
    end
    VPL-->>Script: [(env_id, episode_index, checklist)]
    Script->>Script: log_validation_results(results)
Loading

Reviews (7): Last reviewed commit: "self revoew" | Re-trigger Greptile

@isaaclab-review-bot

Copy link
Copy Markdown
Contributor

👋 @xyao-nv — I see 5 new commits since my last review (5d94431e6d8178). Here's a quick incremental pass:

Summary of Changes

Major refactor that lifts the physics-settle logic out of PooledObjectPlacer and into a standalone function in placement_events.py, drops the failure cache entirely, introduces a dedicated PhysicsSettleParams dataclass, and adopts a PlacementCheck enum for checklist keys.

What Looks Good ✅

  1. Separation of concernsPooledObjectPlacer is now purely a geometric pool/draw engine; the physics-based verification lives in placement_events.verify_in_sim_and_reselect. Much cleaner ownership boundaries.
  2. PhysicsSettleParams dataclass — well-documented, sensible defaults, and cleanly threaded from the CLI all the way down.
  3. PlacementCheck enum — eliminates stringly-typed checklist keys; compile-time typo protection.
  4. Observation refresh after settlerun_placement_physics_settle_check now returns the updated VecEnvObs so the caller doesn't start a rollout on stale obs. Good catch.
  5. Simplified velocity checklin_velocity <= thresh and ang_velocity <= thresh is clearer than the double-negation it replaced.
  6. Removing the failure cache — significantly reduces complexity; the bounded retry budget in max_retries handles the same concern more simply.

Minor Observations / Questions ❓

  1. Default enable_physics_settle_check=False in rollout_policy — previously it was True. Was this intentional (opt-in only via CLI flag)? Just want to confirm the default-off is desired so existing callers aren't surprised.
  2. last_applied is now a plain dict exposed as a property (placement_pool.last_applied[env_id] = result). Since external code mutates it directly, consider whether a setter method (or at least documenting the contract) would be safer long-term — though for now the two call-sites are well-understood.
  3. Typo in commit message: "udpate" → "update" (commits 7 & 10). Non-blocking, just cosmetic for history.
  4. Comment in physics_settle.py: "metric recoder" → "metric recorder".

Overall this is a clean refactor. The interface is simpler, responsibilities are better isolated, and the new test (_test_settle_check_reselects_unstable_layout) exercises the live-physics path end-to-end. LGTM on this iteration. 🚀

Comment thread isaaclab_arena/relations/placement_events.py Outdated
Comment on lines +43 to +52
def pass_validation_checklist(self, checklist_items: list[str] | None = None) -> bool:
"""Check whether the gating checks pass. Defaults to the required checks only.
Args:
checklist_items: checks that must pass. If None, only required checks gate (optional checks are ignored).
Returns:
True if all the considered checks pass, False otherwise
"""
if checklist_items is None:
checklist_items = self._required()
return all(self.checklist_items[item] for item in checklist_items)

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 Parameter name shadows instance attribute, risking a silent KeyError

The parameter checklist_items in pass_validation_checklist has the same name as the instance attribute self.checklist_items. Inside the function, the local checklist_items is used as the set of keys to evaluate, while self.checklist_items is the dict of results. The lookup self.checklist_items[item] is correct today, but any future edit that accidentally drops the self. prefix would silently attempt to subscript the list[str] parameter and either produce a wrong result or raise a TypeError. Renaming the parameter (e.g., items_to_check) removes the ambiguity entirely.

Suggested change
def pass_validation_checklist(self, checklist_items: list[str] | None = None) -> bool:
"""Check whether the gating checks pass. Defaults to the required checks only.
Args:
checklist_items: checks that must pass. If None, only required checks gate (optional checks are ignored).
Returns:
True if all the considered checks pass, False otherwise
"""
if checklist_items is None:
checklist_items = self._required()
return all(self.checklist_items[item] for item in checklist_items)
def pass_validation_checklist(self, items_to_check: list[str] | None = None) -> bool:
"""Check whether the gating checks pass. Defaults to the required checks only.
Args:
items_to_check: checks that must pass. If None, only required checks gate (optional checks are ignored).
Returns:
True if all the considered checks pass, False otherwise
"""
if items_to_check is None:
items_to_check = self._required()
return all(self.checklist_items[item] for item in items_to_check)

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 thread isaaclab_arena/relations/placement_events.py Outdated
Comment thread isaaclab_arena/utils/physics_settle.py Outdated
@xyao-nv xyao-nv changed the title Add physics settle validation check over pooled layouts into Placement Event Add standalone physics settle check over pooled layouts Jun 10, 2026

@isaaclab-review-bot isaaclab-review-bot Bot left a comment

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.

Review: Add standalone physics settle check over pooled layouts

Nice work structuring the physics settle validation as a separate, composable layer on top of the existing analytical checks. The PlacementValidationChecklist abstraction is clean, and separating the sim-side primitives (physics_settle.py) from the orchestration logic (placement_pool_validation.py) is a good design decision. The test coverage for the new settle primitives is thorough.

A few issues I'd like to flag:


🐛 Bug: Incorrect TYPE_CHECKING import in placement_events.py

if TYPE_CHECKING:
    from isaaclab_arena.assets.object_base import ObjectBase
    from isaaclab_arena.relations.placement_validation import PlacementResult  # ← wrong module

PlacementResult lives in isaaclab_arena.relations.placement_result, not placement_validation. This will cause type-checker failures (mypy/pyright) when analyzing this file. Should be:

from isaaclab_arena.relations.placement_result import PlacementResult

⚠️ enable_physics_settle_check is accepted but unused

The rollout_policy() function in policy_runner.py accepts enable_physics_settle_check: bool = False (line 68), and main() passes the CLI arg through (line 255), but the parameter is never referenced in the function body. The CLI arg and plumbing exist, but no settle check is actually invoked during rollout.

Is this intentional scaffolding for a follow-up PR? If so, consider adding a # TODO or raising NotImplementedError when the flag is True, so users don't assume it's functional.


📝 Misleading docstring on draw_replacement

The docstring states:

"Draw the next pooled layout to retry in one env, skipping layouts known to fail to settle."

But the implementation simply draws the next layout from the queue (sample_for_envs or _sample_reusable_without_replacement) without any filtering of previously-failed layouts. The description implies a skip-list mechanism that isn't present. Consider updating the docstring to match what it actually does (draws the next available layout), or noting that the skip behavior is planned for the follow-up cache PR.


🛡️ Potential KeyError in pass_validation_checklist

In PlacementValidationChecklist.pass_validation_checklist():

def pass_validation_checklist(self, checklist_items: list[str] | None = None) -> bool:
    if checklist_items is None:
        checklist_items = self._required()
    return all(self.checklist_items[item] for item in checklist_items)

If required_items contains a check name that hasn't yet been added to checklist_items (e.g., a check that's registered as required but the validation hasn't run yet), this raises a KeyError. Consider using .get(item, False) with a clear semantic ("missing checks are treated as failing"), or validating upfront that all required items are present and raising a more informative error.


💡 Minor: _grade_settled_batch passes gym-wrapped env to objects_settled_per_episode

In placement_pool_validation.py, _grade_settled_batch passes the gym-wrapped env to physics_settle.objects_settled_per_episode, which then calls env.unwrapped.scene internally. This works fine, but it's inconsistent with _write_layouts_per_episode which passes env.unwrapped directly to write_layout_to_sim. Consider making the unwrap point consistent across helpers.


Overall this is solid foundational work. The split between the offline validator script (run_placement_pool_validation.py) and the rollout-time check is well-motivated. Looking forward to seeing the runtime settle-and-reselect loop land in a follow-up!


Update (2cfa1b8): New commits address most previous concerns. Summary:

enable_physics_settle_check unused parameter — Resolved. The CLI arg and rollout_policy parameter have been removed entirely. Clean approach.

Misleading draw_replacement docstring — Resolved. The method (and last_applied property) have been removed entirely, simplifying the PooledObjectPlacer API.

Typo: "recoder" → "recorder" — Fixed in physics_settle.py.

Removed unused last_applied tracking — The last_applied dict and draw_replacement method have been completely removed from PooledObjectPlacer, along with the tracking in placement_events.py. This is a cleaner approach than trying to maintain state for a runtime settle-and-reselect feature that isn't implemented yet.

Tests updated — Mock pools in test_placement_events.py no longer expose last_applied, and test_physics_settle.py now reads from layouts_per_env() instead of last_applied.

Wrong TYPE_CHECKING import (PlacementResult from placement_validation instead of placement_result) — Still present at line 20 of placement_events.py. This bug was flagged in the initial review and remains unaddressed.

pass_validation_checklist potential KeyError — Not addressed in this diff (not blocking).


Verdict: Good cleanup. The new commit removes scaffolding for the runtime settle-and-reselect feature that wasn't being used, keeping only the offline pool validator path. One TYPE_CHECKING import bug remains from the initial review (wrong module for PlacementResult). Fix that and this is merge-ready.

Comment thread isaaclab_arena/evaluation/policy_runner.py Outdated
or ``None`` when ``placement_pool`` is omitted and the env has no pooled layouts.
"""
# No-ops when no layouts are stored in the pool
if placement_pool is None:

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.

Can we simplify the if condition here? We have two if placement_pool is None

xyao-nv added 13 commits June 11, 2026 22:58
…_cache)

Strip the persistent disk cache from this branch: PhysicsSettleFailureCache,
compute_layout_signature, the physics_settle_cache_key plumbing, and the
_enqueue wrappers that pre-marked cached failures. The in-sim settle check
and live re-selection (verify_in_sim_and_reselect) remain.
Full cache implementation is preserved on branch xyao/dev/physcis_failure_cache.

Signed-off-by: Xinjie Yao <xyao@nvidia.com>
@xyao-nv xyao-nv force-pushed the xyao/dev/add_physics_validation branch from 4ce8ad8 to 48c1d9d Compare June 12, 2026 06:03

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

Looks great!

I have lots of nits about naming, but nothing too substantial.

@property
def is_valid(self) -> bool:
"""True when every validation check passes."""
return self.validation_checklist.pass_validation_checklist()

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.

Does this method check that all checks pass? Suggestion to: validation_checklist.do_all_checks_pass()


Returns:
True if no overlaps exist and On relations hold, False otherwise.
PlacementValidationChecklist containing the validation results.

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.

To me a "checklist" is something to be done. whereas the return here is something that has been done.

I would call this PlacementValidationResults

Does that make sense?

sorted(
env_candidates,
key=lambda candidate: (
*candidate.validation_checklist.rank_required_and_optional_failures,

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.

suggestion to change the name of: rank_required_and_optional_failures

It doesn't really do any ranking. It produces a score, which is then used to do the ranking (via the sorted here).


anchor_objects_set = set(get_anchor_objects(objects))
base_rotations = {obj: get_rotation_xyzw(obj) for obj in objects if obj not in anchor_objects_set}
base_rotations = get_base_rotations(objects, anchor_objects_set)

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.

From looking at how base_rotations are used, the anchor objects are excluded anyway. Suggestion to simplify the function to get_rotation_per_object which gets all rotations for all objects.

objects: list[ObjectBase],
anchor_objects_set: set[ObjectBase],
) -> list[str]:
"""Return the names of non-anchor objects, used for the settle velocity check."""

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.

Suggestion to remove the "used for the settle velocity check", a docstring should specify what a function does, not how the caller uses it.

"""Required check names, defaulting to every check when none are declared."""
return self.required_items or set(self.checklist_items.keys())

def pass_validation_checklist(self, required_checks: list[PlacementCheck] | None = None) -> bool:

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.

In, general verb method names do something, and question method names return an answer. For example, here a more natural name would be does_validation_checklist_pass. The current name tells the user, "I'm going to make this checklist pass", whereas I believe that this method answers the question "do all the items in the checklist pass".

Comment on lines +58 to +64
def _grade_settled_batch(
env: ManagerBasedEnv,
layouts: list[tuple[int, PlacementResult]],
movable_object_names: list[str],
settle_params: PhysicsSettleParams,
) -> list[tuple[int, PlacementValidationChecklist]]:
"""Read back per-object velocities for a list of layouts and stamp ``PHYSICS_SETTLED`` per layout.

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.

compute_physics_settled_and_add_to_validation_checklist

for env_id, checklist in graded_layouts:
results.append((env_id, episode_index, checklist))

results.sort(key=lambda item: (item[0], item[1]))

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.

The docstring on this function doesn't mention that the results are sorted. Is this important or can it be removed?

Comment on lines +96 to +101
results = validate_pool_layouts(env, settle_params=settle_params, render=args_cli.render)
assert results is not None, (
"The selected environment has no pooled placement, so there are no candidates to validate. "
"Pooled placement is created when objects declare placement relations (e.g. On)."
)
log_validation_results(results)

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.

Cool!

return results


def log_validation_results(results: list[tuple[int, int, PlacementValidationChecklist]]) -> None:

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.

log -> print

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.

4 participants