Skip to content

Commit 8c1c404

Browse files
authored
Merge pull request #156 from utn-mi/juelg/register-gym-envs
Register envs in gym
2 parents 53c0834 + d8dfe4f commit 8c1c404

File tree

13 files changed

+310
-37
lines changed

13 files changed

+310
-37
lines changed

CMakeLists.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ FetchContent_MakeAvailable(libfranka rl pybind11)
9696
if (${INCLUDE_UTN_MODELS})
9797
if (DEFINED GITLAB_MODELS_TOKEN)
9898
include(download_scenes)
99-
set(ref 77e8782a3488cbf01361d2322f02e75c0d820644)
99+
set(ref b8234983a5e35e222c955f9145ac4cd129827a8e)
100100
FetchContent_Declare(
101101
scenes
102102
URL "https://gitos.rrze.fau.de/api/v4/projects/1100/repository/archive?path=scenes&sha=${ref}"

pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ dependencies = ["websockets>=11.0",
2323
"python-dotenv==1.0.1",
2424
"opencv-python~=4.10.0.84",
2525
# NOTE: when changing the mujoco version, also change it in requirements_dev.txt
26-
"mujoco==3.1.5"
26+
"mujoco==3.2.6"
2727
]
2828
readme = "README.md"
2929
maintainers = [
@@ -98,6 +98,7 @@ ignore = [
9898
"T201", # print() used
9999
"PLR5501", # elif to reduce indentation
100100
"PT018", # assertion should be broken down into multiple parts
101+
"NPY002",
101102
]
102103

103104
[tool.pylint.format]

python/examples/fr3.py

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -79,13 +79,8 @@ def main():
7979

8080
# add camera to have a rendering gui
8181
cameras = {
82-
"default_free": SimCameraConfig(
83-
identifier="", type=int(CameraType.default_free)
84-
),
85-
"wrist": SimCameraConfig(
86-
identifier="eye-in-hand_0", type=int(CameraType.fixed)
87-
),
88-
# TODO: odd behavior when not both cameras are used: only last image is shown
82+
"default_free": SimCameraConfig(identifier="", type=int(CameraType.default_free)),
83+
"wrist": SimCameraConfig(identifier="eye-in-hand_0", type=int(CameraType.fixed)),
8984
}
9085
cam_cfg = SimCameraSetConfig(cameras=cameras, resolution_width=1280, resolution_height=720, frame_rate=20)
9186
camera_set = SimCameraSet(simulation, cam_cfg)

python/examples/grasp_demo.py

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import logging
2+
3+
import gymnasium as gym
4+
import mujoco
5+
import numpy as np
6+
from rcsss._core.common import Pose
7+
from rcsss._core.sim import FR3State
8+
from rcsss.envs.base import GripperWrapper
9+
10+
logger = logging.getLogger(__name__)
11+
logger.setLevel(logging.INFO)
12+
13+
14+
class PickUpDemo:
15+
def __init__(self, env: gym.Env):
16+
self.env = env
17+
self.home_pose = self.env.unwrapped.robot.get_cartesian_position()
18+
19+
def _action(self, pose: Pose, gripper: float) -> np.ndarray:
20+
return {"xyzrpy": pose.xyzrpy(), "gripper": gripper}
21+
22+
def get_object_pose(self, geom_name) -> Pose:
23+
model = self.env.get_wrapper_attr("sim").model
24+
data = self.env.get_wrapper_attr("sim").data
25+
26+
geom_id = mujoco.mj_name2id(model, mujoco.mjtObj.mjOBJ_GEOM, geom_name)
27+
return Pose(translation=data.geom_xpos[geom_id], rotation=data.geom_xmat[geom_id].reshape(3, 3))
28+
29+
def generate_waypoints(self, start_pose: Pose, end_pose: Pose, num_waypoints: int) -> list[Pose]:
30+
waypoints = []
31+
for i in range(num_waypoints + 1):
32+
t = i / (num_waypoints + 1)
33+
waypoints.append(start_pose.interpolate(end_pose, t))
34+
return waypoints
35+
36+
def step(self, action: np.ndarray) -> dict:
37+
re = self.env.step(action)
38+
s: FR3State = self.env.unwrapped.robot.get_state()
39+
return re[0]
40+
41+
def plan_linear_motion(self, geom_name: str, delta_up: float, num_waypoints: int = 20) -> list[Pose]:
42+
end_eff_pose = self.env.unwrapped.robot.get_cartesian_position()
43+
44+
goal_pose = self.get_object_pose(geom_name=geom_name)
45+
# goal pose is above the object and gripper coordinate must flip z-axis (end effector base rotation is [1, 0, 0, 0])
46+
# be careful we define identity quaternion as as [0, 0, 0, 1]
47+
# this does not work if the object is flipped
48+
goal_pose *= Pose(translation=[0, 0, delta_up], quaternion=[1, 0, 0, 0])
49+
50+
waypoints = self.generate_waypoints(end_eff_pose, goal_pose, num_waypoints=num_waypoints)
51+
return waypoints
52+
53+
def execute_motion(self, waypoints: list[Pose], gripper: float = GripperWrapper.BINARY_GRIPPER_OPEN) -> dict:
54+
for i in range(1, len(waypoints)):
55+
# calculate delta action
56+
delta_action = waypoints[i] * waypoints[i - 1].inverse()
57+
obs = self.step(self._action(delta_action, gripper))
58+
return obs
59+
60+
def approach(self, geom_name: str):
61+
waypoints = self.plan_linear_motion(geom_name=geom_name, delta_up=0.2, num_waypoints=5)
62+
self.execute_motion(waypoints=waypoints, gripper=GripperWrapper.BINARY_GRIPPER_OPEN)
63+
64+
def grasp(self, geom_name: str):
65+
66+
waypoints = self.plan_linear_motion(geom_name=geom_name, delta_up=0, num_waypoints=15)
67+
self.execute_motion(waypoints=waypoints, gripper=GripperWrapper.BINARY_GRIPPER_OPEN)
68+
69+
self.step(self._action(Pose(), GripperWrapper.BINARY_GRIPPER_CLOSED))
70+
71+
waypoints = self.plan_linear_motion(geom_name=geom_name, delta_up=0.2, num_waypoints=20)
72+
self.execute_motion(waypoints=waypoints, gripper=GripperWrapper.BINARY_GRIPPER_CLOSED)
73+
74+
def move_home(self):
75+
end_eff_pose = self.env.unwrapped.robot.get_cartesian_position()
76+
waypoints = self.generate_waypoints(end_eff_pose, self.home_pose, num_waypoints=10)
77+
self.execute_motion(waypoints=waypoints, gripper=GripperWrapper.BINARY_GRIPPER_CLOSED)
78+
79+
def pickup(self, geom_name: str):
80+
self.approach(geom_name)
81+
self.grasp(geom_name)
82+
self.move_home()
83+
84+
85+
def main():
86+
env = gym.make("rcs/SimplePickUpSim-v0", render_mode="human", delta_actions=True)
87+
env.reset()
88+
controller = PickUpDemo(env)
89+
controller.pickup("yellow_box_geom")
90+
91+
92+
if __name__ == "__main__":
93+
main()

python/rcsss/__init__.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,25 @@
33
import pathlib
44
import site
55

6+
from gymnasium import register
67
from rcsss import camera, control, envs, sim
78
from rcsss._core import __version__, common, hw
9+
from rcsss.envs.factories import FR3Real, FR3SimplePickUpSim
810

11+
# available mujoco scenes
912
scenes = {
1013
path.stem: path / "scene.mjb" for path in (pathlib.Path(site.getsitepackages()[0]) / "rcsss" / "scenes").glob("*")
1114
}
1215

16+
# make submodules available
1317
__all__ = ["__doc__", "__version__", "common", "hw", "sim", "camera", "scenes", "control", "envs"]
18+
19+
# register gymnasium environments
20+
register(
21+
id="rcs/SimplePickUpSim-v0",
22+
entry_point=FR3SimplePickUpSim(),
23+
)
24+
register(
25+
id="rcs/FR3Real-v0",
26+
entry_point=FR3Real(),
27+
)

python/rcsss/envs/base.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -523,6 +523,10 @@ def close(self):
523523

524524
class GripperWrapper(ActObsInfoWrapper):
525525
# TODO: sticky gripper, like in aloha
526+
527+
BINARY_GRIPPER_CLOSED = 0
528+
BINARY_GRIPPER_OPEN = 1
529+
526530
def __init__(self, env, gripper: common.Gripper, binary: bool = True):
527531
super().__init__(env)
528532
self.unwrapped: FR3Env
@@ -543,7 +547,9 @@ def reset(self, **kwargs) -> tuple[dict[str, Any], dict[str, Any]]:
543547
def observation(self, observation: dict[str, Any], info: dict[str, Any]) -> tuple[dict[str, Any], dict[str, Any]]:
544548
observation = copy.deepcopy(observation)
545549
if self.binary:
546-
observation[self.gripper_key] = self._last_gripper_cmd if self._last_gripper_cmd is not None else 1
550+
observation[self.gripper_key] = (
551+
self._last_gripper_cmd if self._last_gripper_cmd is not None else self.BINARY_GRIPPER_OPEN
552+
)
547553
else:
548554
observation[self.gripper_key] = self._gripper.get_normalized_width()
549555

@@ -565,7 +571,7 @@ def action(self, action: dict[str, Any]) -> dict[str, Any]:
565571

566572
if self._last_gripper_cmd is None or self._last_gripper_cmd != gripper_action:
567573
if self.binary:
568-
self._gripper.grasp() if gripper_action == 0 else self._gripper.open()
574+
self._gripper.grasp() if gripper_action == self.BINARY_GRIPPER_CLOSED else self._gripper.open()
569575
else:
570576
self._gripper.set_normalized_width(gripper_action)
571577
self._last_gripper_cmd = gripper_action

python/rcsss/envs/factories.py

Lines changed: 96 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
import logging
22
from os import PathLike
3+
from typing import Type
34

45
import gymnasium as gym
6+
import numpy as np
57
import rcsss
8+
from gymnasium.envs.registration import EnvCreator
69
from rcsss import sim
710
from rcsss._core.hw import FR3Config, IKController
811
from rcsss._core.sim import CameraType
@@ -21,7 +24,13 @@
2124
RelativeTo,
2225
)
2326
from rcsss.envs.hw import FR3HW
24-
from rcsss.envs.sim import CollisionGuard, FR3Sim
27+
from rcsss.envs.sim import (
28+
CollisionGuard,
29+
FR3Sim,
30+
FR3SimplePickUpSimSuccessWrapper,
31+
RandomCubePos,
32+
SimWrapper,
33+
)
2534

2635
logger = logging.getLogger(__name__)
2736
logger.setLevel(logging.INFO)
@@ -43,7 +52,9 @@ def default_fr3_hw_gripper_cfg():
4352
return gripper_cfg
4453

4554

46-
def default_realsense(name2id: dict[str, str]):
55+
def default_realsense(name2id: dict[str, str] | None) -> RealSenseCameraSet | None:
56+
if name2id is None:
57+
return None
4758
cameras = {name: BaseCameraConfig(identifier=id) for name, id in name2id.items()}
4859
cam_cfg = RealSenseSetConfig(
4960
cameras=cameras,
@@ -132,6 +143,7 @@ def fr3_hw_env(
132143
control_mode=control_mode,
133144
tcp_offset=rcsss.common.Pose(rcsss.common.FrankaHandTCPOffset()),
134145
sim_gui=True,
146+
truncate_on_collision=False,
135147
)
136148
if max_relative_movement is not None:
137149
env = RelativeActionSpace(env, max_mov=max_relative_movement, relative_to=relative_to)
@@ -173,6 +185,7 @@ def fr3_sim_env(
173185
relative_to: RelativeTo = RelativeTo.LAST_STEP,
174186
urdf_path: str | PathLike | None = None,
175187
mjcf: str | PathLike = "fr3_empty_world",
188+
sim_wrapper: Type[SimWrapper] | None = None,
176189
) -> gym.Env[ObsArmsGrCam, LimitedJointsRelDictType]:
177190
"""
178191
Creates a simulation environment for the FR3 robot.
@@ -190,6 +203,8 @@ def fr3_sim_env(
190203
urdf_path (str | PathLike | None): Path to the URDF file. If None, the URDF file is automatically deduced
191204
which requires a UTN compatible lab scene to be present.
192205
mjcf (str | PathLike): Path to the Mujoco scene XML file. Defaults to "fr3_empty_world".
206+
sim_wrapper (Type[SimWrapper] | None): Wrapper to be applied before the simulation wrapper. This is useful
207+
for reset management e.g. resetting objects in the scene with correct observations. Defaults to None.
193208
194209
Returns:
195210
gym.Env: The configured simulation environment for the FR3 robot.
@@ -204,15 +219,15 @@ def fr3_sim_env(
204219
robot = rcsss.sim.FR3(simulation, "0", ik)
205220
robot.set_parameters(robot_cfg)
206221
env: gym.Env = FR3Env(robot, control_mode)
207-
env = FR3Sim(env, simulation)
222+
env = FR3Sim(env, simulation, sim_wrapper)
208223

209224
if camera_set_cfg is not None:
210225
camera_set = SimCameraSet(simulation, camera_set_cfg)
211226
env = CameraSetWrapper(env, camera_set, include_depth=True)
212227

213228
if gripper_cfg is not None:
214229
gripper = sim.FrankaHand(simulation, "0", gripper_cfg)
215-
env = GripperWrapper(env, gripper, binary=False)
230+
env = GripperWrapper(env, gripper, binary=True)
216231

217232
if collision_guard:
218233
env = CollisionGuard.env_from_xml_paths(
@@ -224,8 +239,85 @@ def fr3_sim_env(
224239
control_mode=control_mode,
225240
tcp_offset=rcsss.common.Pose(rcsss.common.FrankaHandTCPOffset()),
226241
sim_gui=True,
242+
truncate_on_collision=True,
227243
)
228244
if max_relative_movement is not None:
229245
env = RelativeActionSpace(env, max_mov=max_relative_movement, relative_to=relative_to)
230246

231247
return env
248+
249+
250+
class FR3Real(EnvCreator):
251+
def __call__( # type: ignore
252+
self,
253+
robot_ip: str,
254+
control_mode: str = "xyzrpy",
255+
delta_actions: bool = True,
256+
camera_config: dict[str, str] | None = None,
257+
gripper: bool = True,
258+
) -> gym.Env:
259+
camera_set = default_realsense(camera_config)
260+
return fr3_hw_env(
261+
ip=robot_ip,
262+
camera_set=camera_set,
263+
control_mode=(
264+
ControlMode.CARTESIAN_TRPY
265+
if control_mode == "xyzrpy"
266+
else ControlMode.JOINTS if control_mode == "joints" else ControlMode.CARTESIAN_TQuart
267+
),
268+
robot_cfg=default_fr3_hw_robot_cfg(),
269+
collision_guard=None,
270+
gripper_cfg=default_fr3_hw_gripper_cfg() if gripper else None,
271+
max_relative_movement=(0.2, np.deg2rad(45)) if delta_actions else None,
272+
relative_to=RelativeTo.LAST_STEP,
273+
)
274+
275+
276+
class FR3SimplePickUpSim(EnvCreator):
277+
def __call__( # type: ignore
278+
self,
279+
render_mode: str = "human",
280+
control_mode: str = "xyzrpy",
281+
resolution: tuple[int, int] | None = None,
282+
frame_rate: int = 10,
283+
delta_actions: bool = True,
284+
) -> gym.Env:
285+
if resolution is None:
286+
resolution = (256, 256)
287+
288+
cameras = {
289+
"wrist": SimCameraConfig(identifier="eye-in-hand_0", type=int(CameraType.fixed)),
290+
"bird_eye": SimCameraConfig(identifier="bird-eye-cam", type=int(CameraType.fixed)),
291+
"side": SimCameraConfig(identifier="side_view", type=int(CameraType.fixed)),
292+
"right_side": SimCameraConfig(identifier="right_side", type=int(CameraType.fixed)),
293+
"left_side": SimCameraConfig(identifier="left_side", type=int(CameraType.fixed)),
294+
"front": SimCameraConfig(identifier="front", type=int(CameraType.fixed)),
295+
}
296+
297+
camera_cfg = SimCameraSetConfig(
298+
cameras=cameras,
299+
resolution_width=resolution[0],
300+
resolution_height=resolution[1],
301+
frame_rate=frame_rate,
302+
physical_units=True,
303+
)
304+
env_rel = fr3_sim_env(
305+
control_mode=(
306+
ControlMode.CARTESIAN_TRPY
307+
if control_mode == "xyzrpy"
308+
else ControlMode.JOINTS if control_mode == "joints" else ControlMode.CARTESIAN_TQuart
309+
),
310+
robot_cfg=default_fr3_sim_robot_cfg(),
311+
collision_guard=False,
312+
gripper_cfg=default_fr3_sim_gripper_cfg(),
313+
camera_set_cfg=camera_cfg,
314+
max_relative_movement=(0.2, np.deg2rad(45)) if delta_actions else None,
315+
relative_to=RelativeTo.LAST_STEP,
316+
mjcf="fr3_simple_pick_up",
317+
sim_wrapper=RandomCubePos,
318+
)
319+
env_rel = FR3SimplePickUpSimSuccessWrapper(env_rel)
320+
if render_mode == "human":
321+
env_rel.get_wrapper_attr("sim").open_gui()
322+
323+
return env_rel

0 commit comments

Comments
 (0)