Skip to content

Commit 42c213a

Browse files
authored
Add mimic and metrics support to sequential composite tasks (#337)
## Summary Add Mimic datagen and metrics support for sequential composite tasks. For Mimic: 1. Add feature to automatically combine Mimic subtask configs from atomic tasks into single config for composite task 2. Add example of a mimic config for a sequential task (`franka_put_and_close_door_task.py`) 3. Update annotate_demos.py script to call the `success_term` after each step. This is required to make sure the sequential task's success state machine is properly updated after each step. For Metrics: 1. Add support for `get_metrics` in sequential composite tasks. 2. Add sequential task specific success/subtask success metrics. 4. Update existing arena metrics recorder term naming to enable metric recorder terms to be renamed. This allows sequential composite tasks to rename metrics with a subtask suffix.
1 parent 7b801f7 commit 42c213a

10 files changed

Lines changed: 246 additions & 37 deletions

File tree

isaaclab_arena/affordances/turnable.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ def turn_to_level(
122122
"""
123123
assert (
124124
target_level >= -1 and target_level < self.num_levels
125-
), f"target_level must be between -1 and {self.num_levels-1}"
125+
), f"target_level must be between -1 and {self.num_levels - 1}"
126126

127127
if asset_cfg is None:
128128
asset_cfg = SceneEntityCfg(self.name)

isaaclab_arena/metrics/object_moved.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,10 @@
1717
class ObjectVelocityRecorder(RecorderTerm):
1818
"""Records the linear velocity of an object for each sim step of an episode."""
1919

20-
name = "object_linear_velocity"
21-
2220
def __init__(self, cfg: RecorderTermCfg, env: ManagerBasedEnv):
2321
super().__init__(cfg, env)
22+
# Get name from config instead of class attribute
23+
self.name = cfg.name
2424
self.object_name = cfg.object_name
2525

2626
def record_post_step(self):
@@ -33,6 +33,7 @@ def record_post_step(self):
3333
@configclass
3434
class ObjectVelocityRecorderCfg(RecorderTermCfg):
3535
class_type: type[RecorderTerm] = ObjectVelocityRecorder
36+
name: str = "object_linear_velocity"
3637
object_name: str = MISSING
3738

3839

@@ -44,7 +45,7 @@ class ObjectMovedRateMetric(MetricBase):
4445
"""
4546

4647
name = "object_moved_rate"
47-
recorder_term_name = ObjectVelocityRecorder.name
48+
recorder_term_name = "object_linear_velocity"
4849

4950
def __init__(self, object: Asset, object_velocity_threshold: float = 0.5):
5051
"""Initializes the object-moved rate metric.
@@ -59,7 +60,7 @@ def __init__(self, object: Asset, object_velocity_threshold: float = 0.5):
5960

6061
def get_recorder_term_cfg(self) -> RecorderTermCfg:
6162
"""Return the recorder term configuration for the object-moved rate metric."""
62-
return ObjectVelocityRecorderCfg(object_name=self.object.name)
63+
return ObjectVelocityRecorderCfg(name=self.recorder_term_name, object_name=self.object.name)
6364

6465
def compute_metric_from_recording(self, recorded_metric_data: list[np.ndarray]) -> float:
6566
"""Computes the object-moved rate from the recorded metric data.

isaaclab_arena/metrics/revolute_joint_moved_rate.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,9 @@
1818
class RevoluteJointStateRecorder(RecorderTerm):
1919
"""Records the openness of an object for each sim step of an episode."""
2020

21-
name = "revolute_joint_state"
22-
2321
def __init__(self, cfg: RecorderTermCfg, env: ManagerBasedEnv):
2422
super().__init__(cfg, env)
23+
self.name = cfg.name
2524
self.object = cfg.object
2625

2726
def record_post_step(self):
@@ -32,6 +31,7 @@ def record_post_step(self):
3231
@configclass
3332
class JointStateRecorderCfg(RecorderTermCfg):
3433
class_type: type[RecorderTerm] = RevoluteJointStateRecorder
34+
name: str = "revolute_joint_state"
3535
object: ObjectBase = MISSING
3636

3737

@@ -43,7 +43,7 @@ class RevoluteJointMovedRateMetric(MetricBase):
4343
"""
4444

4545
name = "revolute_joint_moved_rate"
46-
recorder_term_name = RevoluteJointStateRecorder.name
46+
recorder_term_name = "revolute_joint_state"
4747

4848
def __init__(self, object: Openable, reset_joint_percentage: float, joint_percentage_delta_threshold: float = 0.05):
4949
"""Initializes the door-moved rate metric.
@@ -62,7 +62,7 @@ def __init__(self, object: Openable, reset_joint_percentage: float, joint_percen
6262

6363
def get_recorder_term_cfg(self) -> RecorderTermCfg:
6464
"""Return the recorder term configuration for the revolute joint moved rate metric."""
65-
return JointStateRecorderCfg(object=self.object)
65+
return JointStateRecorderCfg(name=self.recorder_term_name, object=self.object)
6666

6767
def compute_metric_from_recording(self, recorded_metric_data: list[np.ndarray]) -> float:
6868
"""Computes the revolute joint moved rate from the recorded metric data.

isaaclab_arena/metrics/success_rate.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,10 @@
1515
class SuccessRecorder(RecorderTerm):
1616
"""Records whether an episode was successful just before the environment is reset."""
1717

18-
name = "success"
19-
2018
def __init__(self, cfg, env):
2119
super().__init__(cfg, env)
20+
# Get name from config instead of class attribute
21+
self.name = cfg.name
2222
# We track the first reset for each environment
2323
self.first_reset = True
2424

@@ -35,12 +35,14 @@ def record_pre_reset(self, env_ids):
3535
assert "success" in self._env.termination_manager.active_terms
3636
success_results = torch.zeros(len(env_ids), dtype=bool, device=self._env.device)
3737
success_results |= self._env.termination_manager.get_term("success")[env_ids]
38+
3839
return self.name, success_results
3940

4041

4142
@configclass
4243
class SuccessRecorderCfg(RecorderTermCfg):
4344
class_type: type[RecorderTerm] = SuccessRecorder
45+
name: str = "success"
4446

4547

4648
class SuccessRateMetric(MetricBase):
@@ -51,11 +53,11 @@ class SuccessRateMetric(MetricBase):
5153
"""
5254

5355
name = "success_rate"
54-
recorder_term_name = SuccessRecorder.name
56+
recorder_term_name = "success"
5557

5658
def get_recorder_term_cfg(self) -> RecorderTermCfg:
5759
"""Return the recorder term configuration for the success rate metric."""
58-
return SuccessRecorderCfg()
60+
return SuccessRecorderCfg(name=self.recorder_term_name)
5961

6062
def compute_metric_from_recording(self, recorded_metric_data: list[np.ndarray]) -> float:
6163
"""Gets the average success rate from a list of recorded success flags.

isaaclab_arena/scripts/imitation_learning/annotate_demos.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -320,6 +320,8 @@ def replay_episode(
320320
continue
321321
action_tensor = torch.Tensor(action).reshape([1, action.shape[0]])
322322
env.step(torch.Tensor(action_tensor))
323+
# Run the success term so that tasks with a success term dependent state machine can update their state.
324+
success_term.func(env, **success_term.params)
323325
if success_term is not None:
324326
if not bool(success_term.func(env, **success_term.params)[0]):
325327
return False

isaaclab_arena/tasks/common/open_close_door_mimic.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ def __post_init__(self):
4646
object_ref=self.openable_object_name,
4747
# This key corresponds to the binary indicator in "datagen_info" that signals
4848
# when this subtask is finished (e.g., on a 0 to 1 edge).
49-
subtask_term_signal="grasp_1",
49+
subtask_term_signal="move_to_door",
5050
# Specifies time offsets for data generation when splitting a trajectory into
5151
# subtask segments. Random offsets are added to the termination boundary.
5252
subtask_term_offset_range=(10, 20),

isaaclab_arena/tasks/pick_and_place_task.py

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
import isaaclab.envs.mdp as mdp_isaac_lab
1010
from isaaclab.envs.common import ViewerCfg
1111
from isaaclab.envs.mimic_env_cfg import MimicEnvCfg, SubTaskConfig
12-
from isaaclab.managers import SceneEntityCfg, TerminationTermCfg
12+
from isaaclab.managers import EventTermCfg, SceneEntityCfg, TerminationTermCfg
1313
from isaaclab.sensors.contact_sensor.contact_sensor_cfg import ContactSensorCfg
1414
from isaaclab.utils import configclass
1515

@@ -21,6 +21,7 @@
2121
from isaaclab_arena.tasks.task_base import TaskBase
2222
from isaaclab_arena.tasks.terminations import object_on_destination
2323
from isaaclab_arena.utils.cameras import get_viewer_cfg_look_at_object
24+
from isaaclab_arena.utils.pose import PoseRange
2425

2526

2627
class PickAndPlaceTask(TaskBase):
@@ -30,19 +31,24 @@ def __init__(
3031
pick_up_object: Asset,
3132
destination_location: Asset,
3233
background_scene: Asset,
34+
destination_object: Asset | None = None,
3335
episode_length_s: float | None = None,
3436
task_description: str | None = None,
37+
reset_pose_range: PoseRange = PoseRange(),
3538
):
3639
super().__init__(episode_length_s=episode_length_s)
3740
self.pick_up_object = pick_up_object
41+
self.destination_object = destination_object
3842
self.background_scene = background_scene
3943
self.destination_location = destination_location
4044
self.scene_config = SceneCfg(
4145
pick_up_object_contact_sensor=self.pick_up_object.get_contact_sensor_cfg(
4246
contact_against_prim_paths=[self.destination_location.get_prim_path()],
4347
),
4448
)
45-
self.events_cfg = None
49+
self.events_cfg = PickPlaceEventsCfg(
50+
pick_up_object=self.pick_up_object, reset_pose_range=reset_pose_range.to_dict()
51+
)
4652
self.termination_cfg = self.make_termination_cfg()
4753
self.task_description = (
4854
f"Pick up the {pick_up_object.name}, and place it into the {destination_location.name}"
@@ -85,7 +91,7 @@ def get_mimic_env_cfg(self, arm_mode: ArmMode):
8591
return PickPlaceMimicEnvCfg(
8692
arm_mode=arm_mode,
8793
pick_up_object_name=self.pick_up_object.name,
88-
destination_location_name=self.destination_location.name,
94+
destination_location_name=self.destination_object.name,
8995
)
9096

9197
def get_metrics(self) -> list[MetricBase]:
@@ -116,6 +122,28 @@ class TerminationsCfg:
116122
object_dropped: TerminationTermCfg = MISSING
117123

118124

125+
@configclass
126+
class PickPlaceEventsCfg:
127+
"""Configuration for Pick Up Object."""
128+
129+
reset_pick_up_object_pose: EventTermCfg = MISSING
130+
131+
def __init__(self, pick_up_object: Asset, reset_pose_range: dict[str, tuple[float, float]]):
132+
self.reset_pick_up_object_pose = EventTermCfg(
133+
func=mdp_isaac_lab.reset_root_state_uniform,
134+
mode="reset",
135+
params={
136+
"pose_range": {
137+
"x": reset_pose_range["x"],
138+
"y": reset_pose_range["y"],
139+
"z": reset_pose_range["z"],
140+
},
141+
"velocity_range": {},
142+
"asset_cfg": SceneEntityCfg(pick_up_object.name),
143+
},
144+
)
145+
146+
119147
@configclass
120148
class PickPlaceMimicEnvCfg(MimicEnvCfg):
121149
"""
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
# Copyright (c) 2025-2026, The Isaac Lab Arena Project Developers (https://github.com/isaac-sim/IsaacLab-Arena/blob/main/CONTRIBUTORS.md).
2+
# All rights reserved.
3+
#
4+
# SPDX-License-Identifier: Apache-2.0
5+
6+
import numpy as np
7+
8+
from isaaclab.envs.common import ViewerCfg
9+
from isaaclab.envs.mimic_env_cfg import MimicEnvCfg
10+
from isaaclab.utils import configclass
11+
12+
from isaaclab_arena.embodiments.common.arm_mode import ArmMode
13+
from isaaclab_arena.tasks.sequential_task_base import SequentialTaskBase
14+
from isaaclab_arena.tasks.task_base import TaskBase
15+
from isaaclab_arena.utils.cameras import get_viewer_cfg_look_at_object
16+
17+
18+
class FrankaPutAndCloseDoorTask(SequentialTaskBase):
19+
20+
def __init__(
21+
self,
22+
openable_object,
23+
subtasks: list[TaskBase],
24+
episode_length_s: float | None = None,
25+
):
26+
super().__init__(subtasks=subtasks, episode_length_s=episode_length_s)
27+
self.openable_object = openable_object
28+
29+
def get_viewer_cfg(self) -> ViewerCfg:
30+
return get_viewer_cfg_look_at_object(lookat_object=self.openable_object, offset=np.array([-1.3, -1.3, 1.3]))
31+
32+
def get_prompt(self) -> str:
33+
return None
34+
35+
def get_mimic_env_cfg(self, arm_mode: ArmMode):
36+
mimic_env_cfg = FrankaPutAndCloseDoorTaskMimicEnvCfg()
37+
mimic_env_cfg.subtask_configs = self.combine_mimic_subtask_configs(arm_mode)
38+
return mimic_env_cfg
39+
40+
41+
@configclass
42+
class FrankaPutAndCloseDoorTaskMimicEnvCfg(MimicEnvCfg):
43+
"""
44+
Isaac Lab Mimic environment config class for Franka put and close door task.
45+
"""
46+
47+
def __post_init__(self):
48+
# post init of parents
49+
super().__post_init__()
50+
51+
# Override the existing values
52+
self.datagen_config.name = "franka_put_and_close_door_task_D0"
53+
self.datagen_config.generation_guarantee = True
54+
self.datagen_config.generation_keep_failed = False
55+
self.datagen_config.generation_num_trials = 100
56+
self.datagen_config.generation_select_src_per_subtask = False
57+
self.datagen_config.generation_select_src_per_arm = False
58+
self.datagen_config.generation_relative = False
59+
self.datagen_config.generation_joint_pos = False
60+
self.datagen_config.generation_transform_first_robot_pose = False
61+
self.datagen_config.generation_interpolate_from_last_target_pose = True
62+
self.datagen_config.max_num_failures = 25
63+
self.datagen_config.seed = 1

0 commit comments

Comments
 (0)