Skip to content

Commit 9392f87

Browse files
authored
Fix geometric pick-and-place success checks (#1132)
Provide a runtime lookup to provide a positional check `GeometricObjectOnDestinationTerm` that holds the cached aabb's of the objects and provide runtime position lookup. Success now requires all three conditions: - New `object_bounds_center_over_destination`: The center of the spawned object bounds is over the destination footprint and above its bottom. - New `contact_force_is_upward_support`: The filtered contact force on the object points upward and exceeds the configured threshold. - The object linear speed is below the configured threshold. The old max_separation workaround is removed because the destination footprint now provides the geometric placement check --------- Signed-off-by: Clemens Volk <cvolk@nvidia.com>
1 parent d44331e commit 9392f87

46 files changed

Lines changed: 973 additions & 214 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

isaaclab_arena/assets/object_reference.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
from isaaclab_arena.utils.pose import Pose
1919
from isaaclab_arena.utils.usd_helpers import (
2020
NoCollisionMeshError,
21-
compute_local_bounding_box_from_prim,
21+
compute_world_aligned_bounding_box_relative_to_prim_origin,
2222
extract_trimesh_from_prim,
2323
open_stage,
2424
)
@@ -81,10 +81,10 @@ def add_relation(self, relation: RelationBase) -> None:
8181
self.relations.append(relation)
8282

8383
def get_bounding_box(self) -> AxisAlignedBoundingBox:
84-
"""Get local bounding box of the referenced prim (relative to prim transform).
84+
"""Get world-axis-aligned bounds measured from the referenced prim's origin.
8585
86-
The bounding box is relative to the prim's transform origin, consistent with
87-
how Object.get_bounding_box() returns bbox relative to USD origin.
86+
The coordinates use the parent asset's USD axes, with the origin shifted to
87+
the referenced prim's world position.
8888
8989
The bounding box is computed lazily and cached for subsequent calls.
9090
"""
@@ -93,7 +93,7 @@ def get_bounding_box(self) -> AxisAlignedBoundingBox:
9393
prim_path_in_usd = self.isaaclab_prim_path_to_original_prim_path(
9494
self.prim_path, self.parent_asset, parent_stage
9595
)
96-
raw_bbox = compute_local_bounding_box_from_prim(parent_stage, prim_path_in_usd)
96+
raw_bbox = compute_world_aligned_bounding_box_relative_to_prim_origin(parent_stage, prim_path_in_usd)
9797
# Apply parent's scale (no centering - solver is origin-agnostic)
9898
self._bounding_box = raw_bbox.scaled(self._parent_scale)
9999
return self._bounding_box

isaaclab_arena/tasks/composite_task_base.py

Lines changed: 20 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -174,26 +174,26 @@ def _remove_configclass_transform(fields: list[tuple], exclude_fields: set[str])
174174
@staticmethod
175175
def _evaluate_subtask_successes(
176176
env,
177-
subtasks: list[TaskBase],
177+
subtask_success_cfgs: list[TerminationTermCfg],
178178
subtask_indices,
179179
) -> list[list[bool]]:
180180
"""Evaluate the success function of selected subtasks across all envs.
181181
182182
Args:
183183
env: The environment instance.
184-
subtasks: Full list of subtasks for this composite task.
184+
subtask_success_cfgs: Success configurations whose class-based functions have been
185+
constructed by the manager.
185186
subtask_indices: Iterable of subtask indices to evaluate. Indices not in this
186187
iterable are left as False in the returned matrix.
187188
188189
Returns:
189-
A (num_envs x len(subtasks)) list of bools, where entry [env_idx][subtask_idx]
190-
is True if that subtask's success function returned True this step.
190+
A (num_envs x len(subtask_success_cfgs)) list of bools, where entry
191+
[env_idx][subtask_idx] is True if that subtask's success function returned True this step.
191192
"""
192-
subtask_currently_succeeding = [[False for _ in subtasks] for _ in range(env.num_envs)]
193+
subtask_currently_succeeding = [[False for _ in subtask_success_cfgs] for _ in range(env.num_envs)]
193194
for subtask_idx in subtask_indices:
194-
subtask_success_func = subtasks[subtask_idx].get_termination_cfg().success.func
195-
subtask_success_params = subtasks[subtask_idx].get_termination_cfg().success.params
196-
results = subtask_success_func(env, **subtask_success_params)
195+
subtask_success_cfg = subtask_success_cfgs[subtask_idx]
196+
results = subtask_success_cfg.func(env, **subtask_success_cfg.params)
197197
for env_idx in range(env.num_envs):
198198
if results[env_idx]:
199199
subtask_currently_succeeding[env_idx][subtask_idx] = True
@@ -202,30 +202,33 @@ def _evaluate_subtask_successes(
202202
@staticmethod
203203
def composite_task_success_func(
204204
env,
205-
subtasks: list[TaskBase],
205+
subtask_success_cfgs: list[TerminationTermCfg],
206206
desired_subtask_success_state: list[bool | None] | None,
207207
) -> torch.Tensor:
208208
"""Composite task composite success function.
209209
210210
Args:
211211
env: The environment instance.
212-
subtasks: List of subtasks that compose this composite task.
212+
subtask_success_cfgs: Success configurations whose class-based functions have been
213+
constructed by the manager.
213214
desired_subtask_success_state: (Optional) Precise success state for each subtask during the final time step.
214215
Can be used to enforce a specific current state for each subtask at the end of the episode.
215216
216217
Returns:
217218
A bool tensor of shape (num_envs,) indicating composite success per env.
218219
"""
220+
num_subtasks = len(subtask_success_cfgs)
221+
219222
# Initialize each env's subtask success state to False if not already initialized
220223
if not hasattr(env, "_subtask_ever_succeeded"):
221-
env._subtask_ever_succeeded = [[False for _ in subtasks] for _ in range(env.num_envs)]
224+
env._subtask_ever_succeeded = [[False for _ in range(num_subtasks)] for _ in range(env.num_envs)]
222225

223226
# Evaluate every subtask's success function (composite tasks have no ordering constraint).
224227
subtask_currently_succeeding = CompositeTaskBase._evaluate_subtask_successes(
225-
env, subtasks, range(len(subtasks))
228+
env, subtask_success_cfgs, range(num_subtasks)
226229
)
227230
for env_idx in range(env.num_envs):
228-
for subtask_idx in range(len(subtasks)):
231+
for subtask_idx in range(num_subtasks):
229232
if subtask_currently_succeeding[env_idx][subtask_idx]:
230233
env._subtask_ever_succeeded[env_idx][subtask_idx] = True
231234

@@ -321,10 +324,13 @@ def get_events_cfg(self) -> Any:
321324

322325
def _make_composite_task_termination_cfg(self) -> Any:
323326
"Make composite success check termination term."
327+
subtask_success_cfgs = [subtask.get_termination_cfg().success for subtask in self.subtasks]
324328
success = TerminationTermCfg(
325329
func=self.composite_task_success_func,
326330
params={
327-
"subtasks": self.subtasks,
331+
# Child success configs must be direct term parameters so Isaac Lab can construct
332+
# class-backed child terms; ManagerBase does not traverse TaskBase objects.
333+
"subtask_success_cfgs": subtask_success_cfgs,
328334
"desired_subtask_success_state": self.desired_subtask_success_state,
329335
},
330336
)

isaaclab_arena/tasks/pick_and_place_task.py

Lines changed: 34 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -23,33 +23,37 @@
2323
from isaaclab_arena.metrics.success_rate import SuccessRateMetric
2424
from isaaclab_arena.progress_tracking.progress_objective import ProgressObjective
2525
from isaaclab_arena.tasks.common.mimic_default_params import MIMIC_DATAGEN_CONFIG_DEFAULTS
26+
from isaaclab_arena.tasks.predicates.object_on_destination_term import ObjectOnDestinationTerm
2627
from isaaclab_arena.tasks.predicates.object_settling import objects_settled
27-
from isaaclab_arena.tasks.predicates.spatial import object_is_above_height, object_on_destination, objects_in_proximity
28+
from isaaclab_arena.tasks.predicates.spatial import object_is_above_height, object_on_destination
2829
from isaaclab_arena.tasks.task_base import TaskBase
2930
from isaaclab_arena.tasks.task_transition import Relocate, TaskTransition
30-
from isaaclab_arena.tasks.terminations import SuccessMode, check_success
3131
from isaaclab_arena.utils.cameras import get_viewer_cfg_look_at_object
3232
from isaaclab_arena.utils.configclass import make_configclass
3333

3434

3535
@agent_ready
3636
@register_task
3737
class PickAndPlaceTask(TaskBase):
38-
"""Pick-and-place task. Success fires when the pick-up object contacts the destination
39-
with low velocity and, when ``max_separation`` is set, is within axis-aligned proximity
40-
of the destination. Failure (object_dropped) fires when the object falls below the
41-
background's ``object_min_z``.
38+
"""Pick an object up and place it on or in a destination.
39+
40+
Success requires the object's bounds center over the destination footprint, upward support
41+
force, and low linear speed. Failure occurs when the object falls below the background.
42+
43+
Args:
44+
pick_up_object: Rigid object or rigid object set to pick up.
45+
destination_location: Destination whose live pose and spawned geometry define placement.
46+
It must be included in the environment scene.
47+
background_scene: Background whose minimum object height defines the drop failure.
48+
destination_object: Destination asset used by the default Mimic configuration.
49+
episode_length_s: Maximum episode duration in seconds.
50+
task_description: Natural-language task instruction. A description is generated when omitted.
51+
force_threshold: Minimum filtered normal force exerted on the object by the destination, in newtons.
52+
velocity_threshold: Object linear-speed threshold in meters per second. Speed must be below it.
53+
mimic_env_cfg_factory: Optional factory for a custom Mimic environment configuration.
54+
support_cone_half_angle_deg: Maximum angle in degrees between the filtered contact force and
55+
world +Z. Smaller values require the support force to be more vertical.
4256
43-
The default Mimic cfg is ``PickPlaceMimicEnvCfg``. When a task needs a different cfg
44-
shape (different arm subtask sequences, different per-subtask numerical knobs,
45-
bespoke fields), pass ``mimic_env_cfg_factory`` to inject a custom ``MimicEnvCfg``::
46-
47-
def _factory(arm_mode):
48-
return MyCustomMimicEnvCfg(arm_mode=arm_mode, ...)
49-
50-
PickAndPlaceTask(..., mimic_env_cfg_factory=_factory)
51-
52-
The factory receives ``arm_mode`` from the env builder and returns a constructed cfg.
5357
"""
5458

5559
def __init__(
@@ -62,8 +66,8 @@ def __init__(
6266
task_description: str | None = None,
6367
force_threshold: float = 0.1,
6468
velocity_threshold: float = 0.1,
65-
max_separation: tuple[float, float, float] | None = None,
6669
mimic_env_cfg_factory: Callable[[ArmMode], MimicEnvCfg] | None = None,
70+
support_cone_half_angle_deg: float = 45.0,
6771
):
6872
super().__init__(episode_length_s=episode_length_s)
6973
self.pick_up_object = pick_up_object
@@ -73,10 +77,12 @@ def __init__(
7377
self.contact_sensor_name = f"contact_sensor_{pick_up_object.name}"
7478
self.scene_config = self.make_scene_cfg()
7579
self.force_threshold = force_threshold
80+
assert velocity_threshold >= 0.0, f"velocity_threshold must be non-negative, got {velocity_threshold}"
7681
self.velocity_threshold = velocity_threshold
77-
if max_separation is not None:
78-
assert len(max_separation) == 3, f"max_separation must be (x, y, z), got {max_separation!r}"
79-
self.max_separation = max_separation
82+
assert (
83+
0.0 <= support_cone_half_angle_deg < 90.0
84+
), f"support_cone_half_angle_deg must be in [0, 90), got {support_cone_half_angle_deg}"
85+
self.support_cone_half_angle_deg = support_cone_half_angle_deg
8086
self.mimic_env_cfg_factory = mimic_env_cfg_factory
8187
self.events_cfg = None
8288
self.termination_cfg = self.make_termination_cfg()
@@ -107,39 +113,15 @@ def get_termination_cfg(self):
107113
return self.termination_cfg
108114

109115
def make_termination_cfg(self):
110-
predicates = [
111-
TerminationTermCfg(
112-
func=object_on_destination,
113-
params={
114-
"object_cfg": SceneEntityCfg(self.pick_up_object.name),
115-
"contact_sensor_cfg": SceneEntityCfg(self.contact_sensor_name),
116-
"force_threshold": self.force_threshold,
117-
"velocity_threshold": self.velocity_threshold,
118-
},
119-
),
120-
]
121-
if self.max_separation is not None:
122-
# TODO(qianl): replace objects_in_proximity with object_centroid_in_proximity
123-
# for tighter container placement checks.
124-
# TODO (qianl): current implementation doesn't support ObjectReference as target_object_cfg.
125-
max_x_separation, max_y_separation, max_z_separation = self.max_separation
126-
predicates.append(
127-
TerminationTermCfg(
128-
func=objects_in_proximity,
129-
params={
130-
"object_cfg": SceneEntityCfg(self.pick_up_object.name),
131-
"target_object_cfg": SceneEntityCfg(self.destination_location.name),
132-
"max_x_separation": max_x_separation,
133-
"max_y_separation": max_y_separation,
134-
"max_z_separation": max_z_separation,
135-
},
136-
)
137-
)
138116
success = TerminationTermCfg(
139-
func=check_success,
117+
func=ObjectOnDestinationTerm,
140118
params={
141-
"mode": SuccessMode.ALL,
142-
"predicates": predicates,
119+
"object_cfg": SceneEntityCfg(self.pick_up_object.name),
120+
"destination_cfg": SceneEntityCfg(self.destination_location.name),
121+
"contact_sensor_cfg": SceneEntityCfg(self.contact_sensor_name),
122+
"force_threshold": self.force_threshold,
123+
"velocity_threshold": self.velocity_threshold,
124+
"support_cone_half_angle_deg": self.support_cone_half_angle_deg,
143125
},
144126
)
145127
object_dropped = TerminationTermCfg(
@@ -208,7 +190,7 @@ def get_viewer_cfg(self) -> ViewerCfg:
208190

209191
@classmethod
210192
def success_state_transition(cls, pick_up_object: str, destination_location: str, **_) -> TaskTransition:
211-
"""Success (``object_on_destination``): the picked object ends up a relation with the destination."""
193+
"""Relate the picked object to the destination after geometric placement succeeds."""
212194
# Note: with the current AABB-based object solver, placing an object ``on`` an open container
213195
# and letting it fall is equivalent to it being ``in`` the container, so a single ``on``
214196
# relation covers both surfaces and containers.

0 commit comments

Comments
 (0)