Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -307,6 +307,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
39 changes: 26 additions & 13 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,7 +79,8 @@ class ObjectPlacer:
def __init__(self, params: ObjectPlacerParams | None = None):
self.params = params or ObjectPlacerParams()
self._solver = RelationSolver(params=self.params.solver_params)
self._validators: list[PlacementValidator] = build_validators(self.params)
self._visualizer = get_or_create_placement_visualizer(self.params)
self._validators: list[PlacementValidator] = build_validators(self.params, self._visualizer)

def place(
self,
Expand Down Expand Up @@ -595,17 +597,20 @@ 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]] = {}

if self._visualizer is not None:
self._visualizer.start_new_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,
)
self._run_expensive_checks(
positions,
Expand All @@ -614,11 +619,17 @@ def _validate_candidates(
collision_objects,
required,
layout_pass_verdicts_by_check,
num_layouts_evaluated_by_check,
evaluated_layout_indices_by_check,
)
if self._visualizer is not None:
self._visualizer.log_batch_verdicts(
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 +650,16 @@ 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]],
) -> 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:
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 +669,7 @@ 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]],
) -> None:
"""Run each expensive validator only on candidates that passed the required inexpensive checks."""
num_candidates = len(positions)
Expand All @@ -669,6 +680,8 @@ 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:
self._visualizer.set_active_layouts(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 +690,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."""
16 changes: 11 additions & 5 deletions isaaclab_arena/relations/placement_validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
from isaaclab_arena.relations.collision_object import CollisionObject
from isaaclab_arena.relations.object_placer_params import ObjectPlacerParams
from isaaclab_arena.relations.placement_asset import PlaceableAsset
from isaaclab_arena.relations.placement_visualizer import PlacementRerunVisualizer
from isaaclab_arena.relations.warp_mesh_manager import WarpMeshAndSphereCache
from isaaclab_arena.utils.bounding_box import AxisAlignedBoundingBox

Expand All @@ -48,8 +49,9 @@ class PlacementValidator(ABC):
set this flag, so an expensive check (e.g. IK reachability) never runs on a layout rejected on cheaper
geometry."""

def __init__(self, params: ObjectPlacerParams) -> None:
def __init__(self, params: ObjectPlacerParams, visualizer: PlacementRerunVisualizer | None = None) -> None:
self._params = params
self._visualizer = visualizer

@classmethod
def is_available(cls, params: ObjectPlacerParams) -> bool:
Expand Down Expand Up @@ -83,7 +85,9 @@ def get_build_time_checks() -> tuple[str, ...]:
return tuple(PlacementValidatorRegistry().get_all_keys())


def build_validators(params: ObjectPlacerParams) -> list[PlacementValidator]:
def build_validators(
params: ObjectPlacerParams, visualizer: PlacementRerunVisualizer | None = None
) -> list[PlacementValidator]:
"""Construct the enabled build-time validators in registration order.

A registered check whose is_available() returns False is delisted; a check named in
Expand All @@ -92,6 +96,8 @@ def build_validators(params: ObjectPlacerParams) -> list[PlacementValidator]:

Args:
params: Placement params injected into each registered validator.
visualizer: The caller's debug view, injected into each validator so a check can draw its
own visualization layer on it; None when the run has no view.
"""
registry = PlacementValidatorRegistry()
registered_checks = get_build_time_checks()
Expand All @@ -104,7 +110,7 @@ def build_validators(params: ObjectPlacerParams) -> list[PlacementValidator]:
for check in registered_checks:
validator_cls = registry.get_validator_by_name(check)
if validator_cls.is_available(params):
validators.append(validator_cls(params))
validators.append(validator_cls(params, visualizer))
return validators


Expand Down Expand Up @@ -355,8 +361,8 @@ class NoOverlapValidator(PlacementValidator):

check = PlacementCheck.NO_OVERLAP

def __init__(self, params: ObjectPlacerParams) -> None:
super().__init__(params)
def __init__(self, params: ObjectPlacerParams, visualizer: PlacementRerunVisualizer | None = None) -> None:
super().__init__(params, visualizer)
self._cpu_mesh_manager: WarpMeshAndSphereCache | None = None

def validate_batch(
Expand Down
Loading
Loading