Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,10 @@ def build_checks_for_placer_params(graph_spec: ArenaEnvGraphSpec) -> ObjectPlace
enabled_checks=set(enabled_checks) if enabled_checks is not None else None,
required_checks=set(required_checks) if required_checks is not None else None,
solver_params=RelationSolverParams(verbose=False, save_position_history=False),
debug_visualize=placement_validators is not None and placement_validators.debug_visualize,
debug_visualize_output_path=(
placement_validators.debug_visualize_output_path if placement_validators is not None else None
),
)


Expand Down
16 changes: 16 additions & 0 deletions isaaclab_arena/environment_spec/arena_env_graph_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,22 @@ class PlacementValidatorSpec(BaseModel):
),
)

debug_visualize: bool = Field(
default=False,
description=(
"Stream every candidate layout the checks evaluate to a spawned Rerun viewer window. Debug "
"aid, off by default; needs a reachable display. The viewer is its own process, so this "
"never starts Isaac Sim, and it closes with the run."
),
)
debug_visualize_output_path: str | None = Field(
default=None,
description=(
"Path to record the debug visualization to as a Rerun .rrd file, for headless runs. Enables "
"the visualization on its own; combine with debug_visualize to both record and watch live."
),
)

@model_validator(mode="after")
def _validate_required_subset(self) -> PlacementValidatorSpec:
if self.enabled_checks is not None and self.required_checks is not None:
Expand Down
49 changes: 37 additions & 12 deletions isaaclab_arena/relations/object_placer.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from isaaclab_arena.relations.placement_result import PlacementResult
from isaaclab_arena.relations.placement_validation import PlacementValidationResults
from isaaclab_arena.relations.placement_validators import build_validators
from isaaclab_arena.relations.placement_visualizer import get_or_create_placement_visualizer
from isaaclab_arena.relations.relation_solver import RelationSolver
from isaaclab_arena.relations.relations import (
FaceTo,
Expand Down Expand Up @@ -78,6 +79,8 @@ class ObjectPlacer:
def __init__(self, params: ObjectPlacerParams | None = None):
self.params = params or ObjectPlacerParams()
self._solver = RelationSolver(params=self.params.solver_params)
# Created before the validators are built, since a check looks the view up when it is constructed.
self._visualizer = get_or_create_placement_visualizer(self.params)
self._validators: list[PlacementValidator] = build_validators(self.params)

def place(
Expand Down Expand Up @@ -595,17 +598,23 @@ def _validate_candidates(
# required_checks=None means "every enabled check is required"; an empty set means no checks.
required = self.params.required_checks
num_candidates = len(positions)
# Per-check count of layouts evaluated by that check
num_layouts_evaluated_by_check: dict[str, int] = {}
# Per check, which layouts of this batch (each refill) it actually ran on
evaluated_layout_indices_by_check: dict[str, list[int]] = {}
layout_pass_verdicts_by_check: dict[str, list[bool]] = {}

# Layouts are drawn before the checks run so a check's own layer lands on top of its layout.
layout_indices_across_batch: list[int] | None = None
if self._visualizer is not None:
layout_indices_across_batch = self._visualizer.log_layout_batch(positions, orientations, bboxes)

self._run_inexpensive_checks(
positions,
orientations,
bboxes,
collision_objects,
layout_pass_verdicts_by_check,
num_layouts_evaluated_by_check,
evaluated_layout_indices_by_check,
layout_indices_across_batch,
)
self._run_expensive_checks(
positions,
Expand All @@ -614,11 +623,19 @@ def _validate_candidates(
collision_objects,
required,
layout_pass_verdicts_by_check,
num_layouts_evaluated_by_check,
evaluated_layout_indices_by_check,
layout_indices_across_batch,
)
if self._visualizer is not None and layout_indices_across_batch is not None:
self._visualizer.log_layout_batch_verdicts(
layout_indices_across_batch,
layout_pass_verdicts_by_check,
evaluated_layout_indices_by_check,
self.params.required_checks,
)
if layout_pass_verdicts_by_check:
summary = ", ".join(
f"{check}={sum(verdicts)}/{num_layouts_evaluated_by_check[check]}"
f"{check}={sum(verdicts)}/{len(evaluated_layout_indices_by_check[check])}"
for check, verdicts in layout_pass_verdicts_by_check.items()
)
print(f"[placement] Validated {num_candidates} candidate layout(s); passed per check: {summary}")
Expand All @@ -639,16 +656,19 @@ def _run_inexpensive_checks(
bboxes: list[dict[PlaceableAsset, AxisAlignedBoundingBox]],
collision_objects: list[CollisionObject],
layout_pass_verdicts_by_check: dict[str, list[bool]],
num_layouts_evaluated_by_check: dict[str, int],
evaluated_layout_indices_by_check: dict[str, list[int]],
layout_indices_across_batch: list[int] | None,
) -> None:
"""Run every inexpensive validator on all candidates, recording verdicts and evaluated counts."""
"""Run every inexpensive validator on all candidates, recording verdicts and evaluated layouts."""
num_candidates = len(positions)
for validator in self._validators:
if not validator.run_after_inexpensive_checks:
if self._visualizer is not None:
self._visualizer.set_active_layout_indices_across_batch(layout_indices_across_batch or [])
layout_pass_verdicts_by_check[validator.check] = validator.validate_batch(
positions, orientations, bboxes, collision_objects
)
num_layouts_evaluated_by_check[validator.check] = num_candidates
evaluated_layout_indices_by_check[validator.check] = list(range(num_candidates))

def _run_expensive_checks(
self,
Expand All @@ -658,7 +678,8 @@ def _run_expensive_checks(
collision_objects: list[CollisionObject],
required: set[str] | None,
layout_pass_verdicts_by_check: dict[str, list[bool]],
num_layouts_evaluated_by_check: dict[str, int],
evaluated_layout_indices_by_check: dict[str, list[int]],
layout_indices_across_batch: list[int] | None,
) -> None:
"""Run each expensive validator only on candidates that passed the required inexpensive checks."""
num_candidates = len(positions)
Expand All @@ -669,6 +690,10 @@ def _run_expensive_checks(
for i in range(num_candidates)
if self._passes_required_checks(layout_pass_verdicts_by_check, required, i)
]
if self._visualizer is not None and layout_indices_across_batch is not None:
self._visualizer.set_active_layout_indices_across_batch(
[layout_indices_across_batch[i] for i in passed_layout_indices]
)
# only passed layouts are validated
verdicts_over_passed_layout = validator.validate_batch(
[positions[i] for i in passed_layout_indices],
Expand All @@ -677,10 +702,10 @@ def _run_expensive_checks(
collision_objects,
)
verdicts = [False] * num_candidates
for sub_idx, cand_idx in enumerate(passed_layout_indices):
verdicts[cand_idx] = verdicts_over_passed_layout[sub_idx]
for layout_index_within_batch, verdict in zip(passed_layout_indices, verdicts_over_passed_layout):
verdicts[layout_index_within_batch] = verdict
layout_pass_verdicts_by_check[validator.check] = verdicts
num_layouts_evaluated_by_check[validator.check] = len(passed_layout_indices)
evaluated_layout_indices_by_check[validator.check] = passed_layout_indices

@staticmethod
def _passes_required_checks(
Expand Down
6 changes: 6 additions & 0 deletions isaaclab_arena/relations/object_placer_params.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,3 +84,9 @@ class ObjectPlacerParams:

reachability_config: ReachabilityConfig = field(default_factory=ReachabilityConfig)
"""Tuning for the optional ``ik_reachable`` build-time check. See ReachabilityConfig for more details."""

debug_visualize: bool = False
"""If True, stream every validated candidate layout to a spawned Rerun viewer window. Off by default."""

debug_visualize_output_path: str | None = None
"""Path to record the debug visualization to as a Rerun ``.rrd`` file, for headless runs."""
Loading
Loading