Skip to content

Commit c3e39fd

Browse files
Add pressable toaster example task (#74)
## Summary Adds a _press_button_environment_ and a respektive task that uses the _Pressable_ affordance. --------- Co-authored-by: Vikram Ramasamy <158473438+viiik-inside@users.noreply.github.com> Co-authored-by: viiik-inside <vramasamy@nvidia.com>
1 parent a3c352f commit c3e39fd

7 files changed

Lines changed: 183 additions & 11 deletions

File tree

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ jobs:
2626
test:
2727
name: Run pre commit checks and tests
2828
runs-on: [self-hosted, zurich]
29-
timeout-minutes: 30
29+
timeout-minutes: 45
3030

3131
container:
3232
image: nvcr.io/nvstaging/isaac-amr/isaac_arena:latest

isaac_arena/affordances/pressable.py

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -24,25 +24,23 @@
2424
class Pressable(AffordanceBase):
2525
"""Interface for pressable objects."""
2626

27-
def __init__(self, pressable_joint_name: str, pressable_pressed_threshold: float = 0.5, **kwargs):
27+
def __init__(self, pressable_joint_name: str, pressedness_threshold: float = 0.5, **kwargs):
2828
super().__init__(**kwargs)
2929
self.pressable_joint_name = pressable_joint_name
30-
self.pressable_pressed_threshold = pressable_pressed_threshold
30+
self.pressedness_threshold = pressedness_threshold
3131

3232
def is_pressed(
33-
self, env: ManagerBasedEnv, asset_cfg: SceneEntityCfg | None = None, threshold: float | None = None
33+
self, env: ManagerBasedEnv, asset_cfg: SceneEntityCfg | None = None, pressedness_threshold: float | None = None
3434
) -> torch.Tensor:
3535
"""Returns a boolean tensor of whether the object is pressed."""
3636
if asset_cfg is None:
3737
asset_cfg = SceneEntityCfg(self.name)
3838
# We allow for overriding the object-level threshold by passing an argument to this
3939
# function explicitly. Otherwise we use the object-level threshold.
40-
if threshold is not None:
41-
pressable_pressed_threshold = threshold
42-
else:
43-
pressable_pressed_threshold = self.pressable_pressed_threshold
40+
if pressedness_threshold is None:
41+
pressedness_threshold = self.pressedness_threshold
4442
asset_cfg = self._add_joint_name_to_scene_entity_cfg(asset_cfg)
45-
return get_normalized_joint_position(env, asset_cfg) > pressable_pressed_threshold
43+
return get_normalized_joint_position(env, asset_cfg) > pressedness_threshold
4644

4745
def press(
4846
self,

isaac_arena/assets/object_library.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -193,14 +193,14 @@ class Toaster(LibraryObject, Pressable):
193193

194194
# Openable affordance parameters
195195
pressable_joint_name = "button_cancel_joint"
196-
pressable_pressed_threshold = 0.5
196+
pressedness_threshold = 0.5
197197

198198
def __init__(self, prim_path: str | None = None, initial_pose: Pose | None = None):
199199
super().__init__(
200200
prim_path=prim_path,
201201
initial_pose=initial_pose,
202202
pressable_joint_name=self.pressable_joint_name,
203-
pressable_pressed_threshold=self.pressable_pressed_threshold,
203+
pressedness_threshold=self.pressedness_threshold,
204204
)
205205

206206

isaac_arena/examples/example_environments/cli.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
from isaac_arena.examples.example_environments.lightwheel_kitchen_pot_pick_and_place import (
2525
LightwheelKitchenPotPickAndPlaceEnvironment,
2626
)
27+
from isaac_arena.examples.example_environments.press_button_environment import PressButtonEnvironment
2728

2829
# NOTE(alexmillane, 2025.09.04): There is an issue with type annotation in this file.
2930
# We cannot annotate types which require the simulation app to be started in order to
@@ -39,6 +40,7 @@
3940
GalileoPickAndPlaceEnvironment.name: GalileoPickAndPlaceEnvironment,
4041
LightwheelKitchenPotPickAndPlaceEnvironment.name: LightwheelKitchenPotPickAndPlaceEnvironment,
4142
GalileoG1LocomanipPickAndPlaceEnvironment.name: GalileoG1LocomanipPickAndPlaceEnvironment,
43+
PressButtonEnvironment.name: PressButtonEnvironment,
4244
}
4345

4446

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
# Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
import argparse
16+
17+
from isaac_arena.examples.example_environments.example_environment_base import ExampleEnvironmentBase
18+
19+
# NOTE(alexmillane, 2025.09.04): There is an issue with type annotation in this file.
20+
# We cannot annotate types which require the simulation app to be started in order to
21+
# import, because this file is used to retrieve CLI arguments, so it must be imported
22+
# before the simulation app is started.
23+
# TODO(alexmillane, 2025.09.04): Fix this.
24+
25+
26+
class PressButtonEnvironment(ExampleEnvironmentBase):
27+
28+
name: str = "press_button"
29+
30+
def get_env(self, args_cli: argparse.Namespace): # -> IsaacArenaEnvironment:
31+
from isaac_arena.environments.isaac_arena_environment import IsaacArenaEnvironment
32+
from isaac_arena.scene.scene import Scene
33+
from isaac_arena.tasks.press_button_task import PressButtonTask
34+
from isaac_arena.utils.pose import Pose
35+
36+
embodiment = self.asset_registry.get_asset_by_name(args_cli.embodiment)()
37+
38+
background = self.asset_registry.get_asset_by_name("packing_table")()
39+
press_object = self.asset_registry.get_asset_by_name("toaster")()
40+
41+
assets = [background, press_object]
42+
43+
if args_cli.teleop_device is not None:
44+
teleop_device = self.device_registry.get_device_by_name(args_cli.teleop_device)()
45+
else:
46+
teleop_device = None
47+
48+
# Put the toaster on the packing table.
49+
press_object_pose = Pose(position_xyz=(0.7, 0.4, 0.19), rotation_wxyz=(0.7071, 0.0, 0.0, -0.7071))
50+
press_object.set_initial_pose(press_object_pose)
51+
52+
# Compose the scene
53+
scene = Scene(assets=assets)
54+
55+
isaac_arena_environment = IsaacArenaEnvironment(
56+
name=self.name,
57+
embodiment=embodiment,
58+
scene=scene,
59+
task=PressButtonTask(press_object, reset_pressedness=0.8),
60+
teleop_device=teleop_device,
61+
)
62+
return isaac_arena_environment
63+
64+
@staticmethod
65+
def add_cli_args(parser: argparse.ArgumentParser) -> None:
66+
parser.add_argument("--object", type=str, default=None)
67+
# NOTE(alexmillane, 2025.09.04): We need a teleop device argument in order
68+
# to be used in the record_demos.py script.
69+
parser.add_argument("--teleop_device", type=str, default=None)
70+
parser.add_argument("--embodiment", type=str, default="franka")
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
# Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
from dataclasses import MISSING
16+
17+
import isaaclab.envs.mdp as mdp_isaac_lab
18+
from isaaclab.managers import EventTermCfg, TerminationTermCfg
19+
from isaaclab.utils import configclass
20+
21+
from isaac_arena.affordances.pressable import Pressable
22+
from isaac_arena.metrics.metric_base import MetricBase
23+
from isaac_arena.metrics.success_rate import SuccessRateMetric
24+
from isaac_arena.tasks.task_base import TaskBase
25+
26+
27+
class PressButtonTask(TaskBase):
28+
def __init__(
29+
self,
30+
pressable_object: Pressable,
31+
pressedness_threshold: float | None = None,
32+
reset_pressedness: float | None = None,
33+
):
34+
super().__init__()
35+
assert isinstance(pressable_object, Pressable), "Pressable object must be an instance of Pressable"
36+
self.pressable_object = pressable_object
37+
self.pressedness_threshold = pressedness_threshold
38+
self.reset_pressedness = reset_pressedness
39+
40+
def get_scene_cfg(self):
41+
pass
42+
43+
def get_termination_cfg(self):
44+
params = {}
45+
if self.pressedness_threshold is not None:
46+
params["threshold"] = self.pressedness_threshold
47+
success = TerminationTermCfg(
48+
func=self.pressable_object.is_pressed,
49+
params=params,
50+
)
51+
return TerminationsCfg(success=success)
52+
53+
def get_events_cfg(self):
54+
return PressEventCfg(self.pressable_object, reset_pressedness=self.reset_pressedness)
55+
56+
def get_prompt(self):
57+
raise NotImplementedError("Function not implemented yet.")
58+
59+
def get_mimic_env_cfg(self, embodiment_name: str):
60+
raise NotImplementedError("Function not implemented yet.")
61+
62+
def get_metrics(self) -> list[MetricBase]:
63+
return [
64+
SuccessRateMetric(),
65+
]
66+
67+
68+
@configclass
69+
class TerminationsCfg:
70+
"""Termination terms for the MDP."""
71+
72+
time_out: TerminationTermCfg = TerminationTermCfg(func=mdp_isaac_lab.time_out, time_out=False)
73+
74+
# Dependent on the openable object, so this is passed in from the task at
75+
# construction time.
76+
success: TerminationTermCfg = MISSING
77+
78+
79+
@configclass
80+
class PressEventCfg:
81+
"""Configuration for Open Door."""
82+
83+
reset_button_state: EventTermCfg = MISSING
84+
85+
def __init__(self, pressable_object: Pressable, reset_pressedness: float | None):
86+
assert isinstance(pressable_object, Pressable), "Object pose must be an instance of Pressable"
87+
params = {}
88+
if reset_pressedness is not None:
89+
params["unpressed_percentage"] = reset_pressedness
90+
self.reset_button_state = EventTermCfg(
91+
func=pressable_object.unpress,
92+
mode="reset",
93+
params=params,
94+
)

isaac_arena/tests/test_policy_runner.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,14 @@ def run_policy_runner(
5757
run_subprocess(args)
5858

5959

60+
def test_zero_action_policy_press_button():
61+
run_policy_runner(
62+
policy_type="zero_action",
63+
example_environment="press_button",
64+
num_steps=NUM_STEPS,
65+
)
66+
67+
6068
def test_zero_action_policy_kitchen_pick_and_place():
6169
# TODO(alexmillane, 2025.07.29): Get an exhaustive list of all scenes and embodiments
6270
# from a registry when we have one.

0 commit comments

Comments
 (0)