tinysim 0.0.1__py3-none-any.whl → 0.0.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- tinysim/frogger/tk.py +6 -0
- tinysim/topdown_driving/widget.py +3 -4
- tinysim-0.0.3.dist-info/METADATA +80 -0
- {tinysim-0.0.1.dist-info → tinysim-0.0.3.dist-info}/RECORD +21 -9
- {tinysim-0.0.1.dist-info → tinysim-0.0.3.dist-info}/top_level.txt +1 -0
- tinysim_mujoco/__init__.py +0 -0
- tinysim_mujoco/unitree_a1/__init__.py +354 -0
- tinysim_mujoco/unitree_a1/unitree_a1/CHANGELOG.md +6 -0
- tinysim_mujoco/unitree_a1/unitree_a1/LICENSE +29 -0
- tinysim_mujoco/unitree_a1/unitree_a1/README.md +41 -0
- tinysim_mujoco/unitree_a1/unitree_a1/a1.xml +206 -0
- tinysim_mujoco/unitree_a1/unitree_a1/assets/calf.obj +141854 -0
- tinysim_mujoco/unitree_a1/unitree_a1/assets/hip.obj +53148 -0
- tinysim_mujoco/unitree_a1/unitree_a1/assets/thigh.obj +86609 -0
- tinysim_mujoco/unitree_a1/unitree_a1/assets/thigh_mirror.obj +89990 -0
- tinysim_mujoco/unitree_a1/unitree_a1/assets/trunk.obj +170318 -0
- tinysim_mujoco/unitree_a1/unitree_a1/scene.xml +23 -0
- tinysim_warp/cart_pole/__init__.py +3 -3
- tinysim_warp/quadruped/__init__.py +3 -3
- tinysim_warp/simple_quadruped/__init__.py +3 -3
- tinysim-0.0.1.dist-info/METADATA +0 -56
- {tinysim-0.0.1.dist-info → tinysim-0.0.3.dist-info}/WHEEL +0 -0
tinysim/frogger/tk.py
CHANGED
|
@@ -21,6 +21,8 @@ class FroggerTkFrontend(_tk_base.TkBaseFrontend):
|
|
|
21
21
|
self.sim_env = sim_env
|
|
22
22
|
self._viewport_size = viewport_size
|
|
23
23
|
|
|
24
|
+
self.keys = set()
|
|
25
|
+
|
|
24
26
|
async def step(self, action, dt=0.01):
|
|
25
27
|
state = self.sim_env.step(action, dt=dt)
|
|
26
28
|
if self._root:
|
|
@@ -42,6 +44,10 @@ class FroggerTkFrontend(_tk_base.TkBaseFrontend):
|
|
|
42
44
|
canvas.pack(fill="both", expand=True)
|
|
43
45
|
self._root = root
|
|
44
46
|
self._canvas = canvas
|
|
47
|
+
|
|
48
|
+
root.bind("<KeyPress>", lambda e: self.keys.add(e.keysym))
|
|
49
|
+
root.bind("<KeyRelease>", lambda e: self.keys.discard(e.keysym))
|
|
50
|
+
|
|
45
51
|
self.bring_to_front(root)
|
|
46
52
|
self._draw_state(self.sim_env)
|
|
47
53
|
self._pump()
|
|
@@ -14,7 +14,7 @@ class TopDownDrivingWidget(anywidget.AnyWidget):
|
|
|
14
14
|
|
|
15
15
|
sim_state = traitlets.Dict(default_value={}).tag(sync=True)
|
|
16
16
|
wall_positions = traitlets.List(default_value=LOCAL_WALLS).tag(sync=True)
|
|
17
|
-
|
|
17
|
+
|
|
18
18
|
_viewport_size = traitlets.Tuple(
|
|
19
19
|
traitlets.Int(), traitlets.Int(), default_value=(800, 600)
|
|
20
20
|
).tag(sync=True)
|
|
@@ -38,8 +38,7 @@ class TopDownDrivingWidget(anywidget.AnyWidget):
|
|
|
38
38
|
ui_poll(100)
|
|
39
39
|
except Exception:
|
|
40
40
|
pass
|
|
41
|
-
|
|
42
|
-
|
|
41
|
+
|
|
43
42
|
def copy_py_state(self, sim_state: dict):
|
|
44
43
|
self.sim_state = {
|
|
45
44
|
"x": sim_state["x"].tolist(),
|
|
@@ -57,4 +56,4 @@ class TopDownDrivingWidget(anywidget.AnyWidget):
|
|
|
57
56
|
async def reset(self) -> dict:
|
|
58
57
|
sim_state = self.sim_env.reset()
|
|
59
58
|
self.copy_py_state(sim_state)
|
|
60
|
-
return sim_state
|
|
59
|
+
return sim_state
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: tinysim
|
|
3
|
+
Version: 0.0.3
|
|
4
|
+
Summary: small modular simulation environments
|
|
5
|
+
Author-email: Matthew Taylor <matthew.taylor.andre@gmail.com>
|
|
6
|
+
Project-URL: Homepage, https://github.com/MatthewAndreTaylor/TinySim
|
|
7
|
+
Keywords: jupyter,simulation
|
|
8
|
+
Classifier: Framework :: Jupyter
|
|
9
|
+
Classifier: Framework :: Jupyter :: JupyterLab
|
|
10
|
+
Classifier: Framework :: Jupyter :: JupyterLab :: 4
|
|
11
|
+
Classifier: Framework :: Jupyter :: JupyterLab :: Extensions
|
|
12
|
+
Classifier: Programming Language :: Python
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Requires-Python: >=3.9
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
Requires-Dist: numpy
|
|
22
|
+
Requires-Dist: anywidget
|
|
23
|
+
Requires-Dist: jupyter-ui-poll
|
|
24
|
+
Provides-Extra: warp
|
|
25
|
+
Requires-Dist: warp-lang==1.8.1; extra == "warp"
|
|
26
|
+
Provides-Extra: mujoco
|
|
27
|
+
Requires-Dist: mujoco; extra == "mujoco"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
# TinySim
|
|
31
|
+
|
|
32
|
+
[](https://pypi.org/project/tinysim)
|
|
33
|
+
[](https://matthewandretaylor.github.io/TinySim/lab?path=example.ipynb)
|
|
34
|
+
|
|
35
|
+
The goal of this project is to create more minimal simulation environments for robotics and machine learning.
|
|
36
|
+
|
|
37
|
+
Existing solutions like [ROS 2](https://github.com/ros2) and [Gymnasium](https://gymnasium.farama.org/) come with many heavy dependencies.
|
|
38
|
+
|
|
39
|
+
Additionally they are usually difficult to get setup and may require domain specific knowledge to use.
|
|
40
|
+
Simulation environments should be able to run in any notebook environment and require fewer dependencies.
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
## Get Started 🚀
|
|
44
|
+
|
|
45
|
+
To use the baseline Python only simulation environments.
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
pip install tinysim
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
| [MountainCarEnv](https://github.com/MatthewAndreTaylor/TinySim/blob/main/tinysim/mountain_car) | [TopDownDrivingEnv](https://github.com/MatthewAndreTaylor/TinySim/blob/main/tinysim/topdown_driving) |
|
|
52
|
+
|:-------------------------:|:-------------------------:|
|
|
53
|
+
| <img width="312" alt="mountain_car" src="https://github.com/user-attachments/assets/adbde7f4-6b4e-4c06-8dd3-ea278d5576ad" /> | <img width="312" alt="topdown_driving" src="https://github.com/user-attachments/assets/79dcbf45-055e-4a4e-b6ff-38e938e99a4e" /> |
|
|
54
|
+
|
|
55
|
+
| [FroggerEnv](https://github.com/MatthewAndreTaylor/TinySim/blob/main/tinysim/frogger) | [FlappyEnv](https://github.com/MatthewAndreTaylor/TinySim/blob/main/tinysim/flappy) |
|
|
56
|
+
|:-------------------------:|:-------------------------:|
|
|
57
|
+
| <img width="312" alt="frogger" src="https://github.com/user-attachments/assets/bb9a8382-b9a0-4280-ae23-cbd02e2d78f6" /> | <img width="312" alt="flappy" src="https://github.com/user-attachments/assets/b5e2a502-d9fe-4ff1-9e4e-a47f7e50ebd9" /> |
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
To use the [warp](https://github.com/NVIDIA/warp) simulation environments.
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
pip install tinysim[warp]
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
| [CartPoleBaseEnv](https://github.com/MatthewAndreTaylor/TinySim/tree/main/tinysim_warp/cart_pole) | [SimpleRobotDogBaseEnv](https://github.com/MatthewAndreTaylor/TinySim/tree/main/tinysim_warp/simple_quadruped) |
|
|
67
|
+
|:-------------------------:|:-------------------------:|
|
|
68
|
+
| <img width="312" alt="warp_cart_pole" src="https://github.com/user-attachments/assets/f29f3749-d836-4afa-9f27-8578aca020dc" /> | <img width="312" alt="warp_simple_quadruped" src="https://github.com/user-attachments/assets/a966e881-358e-47ee-b0e1-b67eba903987" /> |
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
To use the [mujoco](https://github.com/google-deepmind/mujoco) simulation environments.
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
pip install tinysim[mujoco]
|
|
75
|
+
```
|
|
76
|
+
| [UnitreeA1WalkEnv](https://github.com/MatthewAndreTaylor/TinySim/tree/main/tinysim_mujoco/unitree_a1) |
|
|
77
|
+
|:-------------------------:|
|
|
78
|
+
| <img width="312" alt="mujoco_sim" src="https://github.com/user-attachments/assets/1109488e-0563-4694-8926-682edd0c8278" /> |
|
|
79
|
+
|
|
80
|
+
|
|
@@ -6,7 +6,7 @@ tinysim/flappy/tk.py,sha256=DUJqy0uPn-AvNE4tuA3a4Dh6xaj0twsUOP8leqGgBX4,2458
|
|
|
6
6
|
tinysim/flappy/widget.py,sha256=zyaoW-rWgAkmbCY7yrSYyAZDUQaAPnuQEzldSawkOnI,1597
|
|
7
7
|
tinysim/frogger/__init__.py,sha256=gA2jvG_WDZRpZY0Zg_aJmWrdsk2siIL49wjFTRfOSxo,5227
|
|
8
8
|
tinysim/frogger/sim.js,sha256=b1HfEtFZFbbsrZNtltuWO4h-QM4Fw3XjMRydwm5l2NU,3081
|
|
9
|
-
tinysim/frogger/tk.py,sha256=
|
|
9
|
+
tinysim/frogger/tk.py,sha256=gb2V8bVCVuVazRrfTbAWq46bK20d5ugHxau_rlNABXE,2992
|
|
10
10
|
tinysim/frogger/widget.py,sha256=Bvb6aUiVqJTHTdd6EzpCp0FqpRa0bl9RNcK12_weiPM,1810
|
|
11
11
|
tinysim/mountain_car/__init__.py,sha256=V3Qxj_ydWAqOJiQUqmIRegBe9GNjU82BfbG2XvV0OQg,1986
|
|
12
12
|
tinysim/mountain_car/sim.js,sha256=z7kxl7LbkRglfzXxmrBbwz2vMYdQV84QP3WhE0hoPVQ,4581
|
|
@@ -22,12 +22,24 @@ tinysim/topdown_driving/__init__.py,sha256=HiRpxQNeu4IIOPFraGHLTO2F0VNISAk6bUiK1
|
|
|
22
22
|
tinysim/topdown_driving/sim.js,sha256=5jmyQk4zp4KFhaG-uTsW_Cq1gHoIkR9d2Cv_ezDKarM,3076
|
|
23
23
|
tinysim/topdown_driving/tk.py,sha256=8UB95ZjIJiUQeXC5w3pXkjukOJXfisLKrfIn0bVFzjU,4751
|
|
24
24
|
tinysim/topdown_driving/track_0.json,sha256=3zPfPX72s5zxs4znoO8rXBqGNciai10Jbl2rFCaACow,15338
|
|
25
|
-
tinysim/topdown_driving/widget.py,sha256=
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
25
|
+
tinysim/topdown_driving/widget.py,sha256=Eh7tvtmMbgmzFEvQZ10D6npfF2s1k7QD5AHLYX5Jaqw,1718
|
|
26
|
+
tinysim_mujoco/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
27
|
+
tinysim_mujoco/unitree_a1/__init__.py,sha256=lxN_94WK_9JdZd3pjZfS_BpvtCY7lmPqQ8KLvaKoLmE,12684
|
|
28
|
+
tinysim_mujoco/unitree_a1/unitree_a1/CHANGELOG.md,sha256=FIdhX4_Q21xamWMS3-2677HhlETlCnfaAzGemqs4Lpo,143
|
|
29
|
+
tinysim_mujoco/unitree_a1/unitree_a1/LICENSE,sha256=K3lKzx4lD1VFrlpH2B5WhepUiM4HfRCH7Yvl-B6bpcA,1559
|
|
30
|
+
tinysim_mujoco/unitree_a1/unitree_a1/README.md,sha256=tHCjy_A1jcGD7bkFy9Qw9z249Kb6HTDQQBlCk8buqlM,1606
|
|
31
|
+
tinysim_mujoco/unitree_a1/unitree_a1/a1.xml,sha256=gJidzvTdw5WwD0X_qrJWjsLwCYmHNqK19fExTtkws4s,9674
|
|
32
|
+
tinysim_mujoco/unitree_a1/unitree_a1/scene.xml,sha256=kvMcNRf1mXFg8CQBp9jrM8B8GCm_TcnQgW68k_iVKis,887
|
|
33
|
+
tinysim_mujoco/unitree_a1/unitree_a1/assets/calf.obj,sha256=duTEMtWXoV4C0q_TFBCxQ_QhodZ7yG1_OALcbzkfCh4,5327177
|
|
34
|
+
tinysim_mujoco/unitree_a1/unitree_a1/assets/hip.obj,sha256=2xx78ZCRU3bnj31Q6iqztiXRtu3QLsLtDQLXweGVqrs,1974009
|
|
35
|
+
tinysim_mujoco/unitree_a1/unitree_a1/assets/thigh.obj,sha256=kUajiusjGyU_X8EyFormznSFUCvGtvrT2K8IhyTGILk,3245342
|
|
36
|
+
tinysim_mujoco/unitree_a1/unitree_a1/assets/thigh_mirror.obj,sha256=A8ynmhTjB9AKowPWTcNk9OYsoWE41Y-XifThe-PdL1g,3354472
|
|
37
|
+
tinysim_mujoco/unitree_a1/unitree_a1/assets/trunk.obj,sha256=wp6C0K3bTH4PArWQV_zMU-m28rFJ1BQMwmRHEqObiHg,5410512
|
|
38
|
+
tinysim_warp/cart_pole/__init__.py,sha256=ltL0cgySOSbeqBvks-UqV1OpT-K-kxo3GylNdvfCB1w,8344
|
|
39
|
+
tinysim_warp/quadruped/__init__.py,sha256=aIOlF9vO9GIYn1y4dth4q-fpUdvB9KZq2rf25YcOcqc,6393
|
|
40
|
+
tinysim_warp/simple_quadruped/__init__.py,sha256=sXL7EOtlB4qm_l4NsYTOFKvpt438voq7OaPgzciPOU0,5329
|
|
29
41
|
tinysim_warp/simple_quadruped/simple_quadruped.urdf,sha256=UfyGROUD5Qlf_t5ISipE2L7uQPBl-hwFnmRsjNL9yH4,8311
|
|
30
|
-
tinysim-0.0.
|
|
31
|
-
tinysim-0.0.
|
|
32
|
-
tinysim-0.0.
|
|
33
|
-
tinysim-0.0.
|
|
42
|
+
tinysim-0.0.3.dist-info/METADATA,sha256=C8c2FG_mLRBcUv3j8Rqd3Z14zHif-BI8G4LUjfvQw2A,3941
|
|
43
|
+
tinysim-0.0.3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
44
|
+
tinysim-0.0.3.dist-info/top_level.txt,sha256=v-9NmTLfZukVjPwJwEdtCU7yY4TdLsBf1H8s25tR3V4,36
|
|
45
|
+
tinysim-0.0.3.dist-info/RECORD,,
|
|
File without changes
|
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
import pathlib
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
|
|
5
|
+
try:
|
|
6
|
+
import mujoco
|
|
7
|
+
import glfw
|
|
8
|
+
except ImportError:
|
|
9
|
+
raise ImportError(
|
|
10
|
+
"Mujoco is not properly installed. Install using `pip install tinysim[mujoco]`"
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Viewer:
|
|
15
|
+
def __init__(self, model, data, width=1200, height=900):
|
|
16
|
+
self.model = model
|
|
17
|
+
self.data = data
|
|
18
|
+
self.width = width
|
|
19
|
+
self.height = height
|
|
20
|
+
|
|
21
|
+
self.window = None
|
|
22
|
+
self.scene = None
|
|
23
|
+
self.context = None
|
|
24
|
+
if not glfw.init():
|
|
25
|
+
raise RuntimeError("Failed to initialize GLFW")
|
|
26
|
+
|
|
27
|
+
self._create_window()
|
|
28
|
+
|
|
29
|
+
def _create_window(self):
|
|
30
|
+
glfw.window_hint(glfw.VISIBLE, glfw.TRUE)
|
|
31
|
+
self.window = glfw.create_window(self.width, self.height, "Viewer", None, None)
|
|
32
|
+
if not self.window:
|
|
33
|
+
raise RuntimeError("Failed to create GLFW window")
|
|
34
|
+
|
|
35
|
+
glfw.make_context_current(self.window)
|
|
36
|
+
glfw.swap_interval(1)
|
|
37
|
+
glfw.set_window_close_callback(self.window, self._on_close)
|
|
38
|
+
|
|
39
|
+
self.cam = mujoco.MjvCamera()
|
|
40
|
+
self.opt = mujoco.MjvOption()
|
|
41
|
+
self.scene = mujoco.MjvScene(self.model, maxgeom=500)
|
|
42
|
+
self.context = mujoco.MjrContext(
|
|
43
|
+
self.model, mujoco.mjtFontScale.mjFONTSCALE_150
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
self.cam.azimuth = 90.0
|
|
47
|
+
self.cam.elevation = -25.0
|
|
48
|
+
self.cam.distance = 4.0
|
|
49
|
+
self.cam.lookat[:] = np.array([0.0, 0.0, 0.0])
|
|
50
|
+
self.cam.type = mujoco.mjtCamera.mjCAMERA_FREE
|
|
51
|
+
|
|
52
|
+
def render(self):
|
|
53
|
+
if self.window is None:
|
|
54
|
+
return
|
|
55
|
+
|
|
56
|
+
glfw.make_context_current(self.window)
|
|
57
|
+
width, height = glfw.get_framebuffer_size(self.window)
|
|
58
|
+
viewport = mujoco.MjrRect(0, 0, width, height)
|
|
59
|
+
mujoco.mjv_updateScene(
|
|
60
|
+
self.model,
|
|
61
|
+
self.data,
|
|
62
|
+
self.opt,
|
|
63
|
+
None,
|
|
64
|
+
self.cam,
|
|
65
|
+
mujoco.mjtCatBit.mjCAT_ALL,
|
|
66
|
+
self.scene,
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
mujoco.mjr_render(viewport, self.scene, self.context)
|
|
70
|
+
glfw.swap_buffers(self.window)
|
|
71
|
+
glfw.poll_events()
|
|
72
|
+
|
|
73
|
+
def _on_close(self, window):
|
|
74
|
+
self.close()
|
|
75
|
+
|
|
76
|
+
def close(self):
|
|
77
|
+
if self.window is not None:
|
|
78
|
+
glfw.destroy_window(self.window)
|
|
79
|
+
self.window = None
|
|
80
|
+
self.scene = None
|
|
81
|
+
self.context = None
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class UnitreeA1BaseEnv:
|
|
85
|
+
|
|
86
|
+
def __init__(self, headless=False):
|
|
87
|
+
model_path = str(pathlib.Path(__file__).parent / "unitree_a1/scene.xml")
|
|
88
|
+
self.model = mujoco.MjModel.from_xml_path(model_path)
|
|
89
|
+
self.data = mujoco.MjData(self.model)
|
|
90
|
+
self.action_space_size = self.model.nu
|
|
91
|
+
|
|
92
|
+
self.default_joint_position = np.array(
|
|
93
|
+
[-0.05, 0.8, -1.5, 0.05, 0.8, -1.5, -0.03, 0.9, -1.4, 0.03, 0.9, -1.4],
|
|
94
|
+
dtype=np.float32,
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
if not headless:
|
|
98
|
+
self.viewer = Viewer(self.model, self.data)
|
|
99
|
+
else:
|
|
100
|
+
self.viewer = None
|
|
101
|
+
|
|
102
|
+
def step(self, actions, n_frames=20):
|
|
103
|
+
if self.viewer is not None:
|
|
104
|
+
self.render()
|
|
105
|
+
|
|
106
|
+
if len(actions) != self.action_space_size:
|
|
107
|
+
raise ValueError(
|
|
108
|
+
f"Expected {self.action_space_size} actions, but got {len(actions)}"
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
self.data.ctrl[:] = actions
|
|
112
|
+
mujoco.mj_step(self.model, self.data, nstep=n_frames)
|
|
113
|
+
mujoco.mj_rnePostConstraint(self.model, self.data)
|
|
114
|
+
|
|
115
|
+
def render(self):
|
|
116
|
+
if self.viewer is None:
|
|
117
|
+
return
|
|
118
|
+
self.viewer.render()
|
|
119
|
+
|
|
120
|
+
def set_state(self, qpos, qvel):
|
|
121
|
+
self.data.qpos[:] = qpos
|
|
122
|
+
self.data.qvel[:] = qvel
|
|
123
|
+
mujoco.mj_forward(self.model, self.data)
|
|
124
|
+
|
|
125
|
+
def close(self):
|
|
126
|
+
self.viewer.close()
|
|
127
|
+
|
|
128
|
+
def reset(self):
|
|
129
|
+
mujoco.mj_resetData(self.model, self.data)
|
|
130
|
+
self.data.qpos[0:3] = [0.0, 0.0, 0.3]
|
|
131
|
+
self.data.qpos[7:19] = self.default_joint_position.copy()
|
|
132
|
+
self.data.qvel[:] = 0
|
|
133
|
+
self.data.ctrl[:] = 0
|
|
134
|
+
mujoco.mj_forward(self.model, self.data)
|
|
135
|
+
|
|
136
|
+
def get_position_data(self):
|
|
137
|
+
return self.data.qpos[0:3].copy()
|
|
138
|
+
|
|
139
|
+
def get_joint_data(self):
|
|
140
|
+
return self.data.qpos[7:19].copy()
|
|
141
|
+
|
|
142
|
+
def get_joint_effort(self):
|
|
143
|
+
return self.data.ctrl[:12].copy()
|
|
144
|
+
|
|
145
|
+
def get_velocity_data(self):
|
|
146
|
+
return self.data.qvel.copy()
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
class UnitreeA1WalkEnv:
|
|
150
|
+
def __init__(self, reward_weights, cost_weights, headless=False):
|
|
151
|
+
self.env = UnitreeA1BaseEnv(headless=headless)
|
|
152
|
+
self.frame_skip = 20
|
|
153
|
+
self.obs_dim = 45
|
|
154
|
+
self._previous_observation = np.zeros(self.obs_dim, dtype=np.float32)
|
|
155
|
+
self.joint_limits_low = np.array(
|
|
156
|
+
[-0.1, 0.25, -2.0, -0.1, 0.25, -2.0, -0.03, 0.25, -1.6, 0.03, 0.25, -1.6],
|
|
157
|
+
dtype=np.float32,
|
|
158
|
+
)
|
|
159
|
+
self.joint_limits_high = np.array(
|
|
160
|
+
[0.1, 1.2, -0.5, 0.1, 1.2, -0.5, 0.03, 1.2, -1.1, 0.03, 1.2, -1.1],
|
|
161
|
+
dtype=np.float32,
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
self._feet_air_time = np.zeros(4)
|
|
165
|
+
self._last_contacts = np.zeros(4)
|
|
166
|
+
self.step_counter = 0
|
|
167
|
+
self.tracking_sigma = 0.25
|
|
168
|
+
self.maximum_episode_steps = 1024
|
|
169
|
+
self.prev_action = np.zeros_like(self.env.default_joint_position)
|
|
170
|
+
self.target_lin_vel = np.array([0.5, 0.0])
|
|
171
|
+
self.target_ang_vel = 0.0
|
|
172
|
+
self._healthy_z_range = (0.3, 0.335)
|
|
173
|
+
self._healthy_rotation = np.array(
|
|
174
|
+
[np.deg2rad(6), np.deg2rad(6), np.deg2rad(10)]
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
self._cfrc_ext_feet_indices = [4, 7, 10, 13]
|
|
178
|
+
self.reward_weights = reward_weights
|
|
179
|
+
self.cost_weights = cost_weights
|
|
180
|
+
# TODO: requires keys
|
|
181
|
+
|
|
182
|
+
def reset(self, **kwargs):
|
|
183
|
+
self.env.reset()
|
|
184
|
+
self.step_counter = 0
|
|
185
|
+
self._feet_air_time = np.zeros(4)
|
|
186
|
+
self._last_contacts = np.zeros(4)
|
|
187
|
+
self.prev_action = self.env.default_joint_position.copy()
|
|
188
|
+
self.target_lin_vel = np.array([0.5, 0.0])
|
|
189
|
+
current_obs = self._get_obs()
|
|
190
|
+
self._previous_observation = np.zeros_like(current_obs)
|
|
191
|
+
full_obs = np.concatenate([current_obs, self._previous_observation])
|
|
192
|
+
self._previous_observation = current_obs.copy()
|
|
193
|
+
return full_obs, {}
|
|
194
|
+
|
|
195
|
+
def step(self, action):
|
|
196
|
+
self.step_counter += 1
|
|
197
|
+
a = 0.7
|
|
198
|
+
action_filtered = a * self.prev_action + (1 - a) * action
|
|
199
|
+
if self.step_counter < 1:
|
|
200
|
+
action_filtered = self.env.default_joint_position.copy()
|
|
201
|
+
self.env.step(action_filtered, self.frame_skip)
|
|
202
|
+
|
|
203
|
+
current_obs = self._get_obs()
|
|
204
|
+
obs = np.concatenate([current_obs, self._previous_observation])
|
|
205
|
+
reward = self._compute_reward(action)
|
|
206
|
+
terminated = not self.is_healthy
|
|
207
|
+
truncated = self.step_counter >= self.maximum_episode_steps
|
|
208
|
+
self.prev_action = action_filtered
|
|
209
|
+
self._previous_observation = current_obs.copy()
|
|
210
|
+
return obs, reward, terminated, truncated, {}
|
|
211
|
+
|
|
212
|
+
def _get_obs(self):
|
|
213
|
+
base_ang_vel = self.euler_from_quaternion(*self.env.data.qpos[3:7])
|
|
214
|
+
joint_positions = self.env.get_joint_data()
|
|
215
|
+
joint_velocities = self.env.get_velocity_data()[6:]
|
|
216
|
+
curr_contact = self.feet_contact_forces > 1.0
|
|
217
|
+
return np.concatenate(
|
|
218
|
+
(
|
|
219
|
+
base_ang_vel,
|
|
220
|
+
joint_positions,
|
|
221
|
+
joint_velocities,
|
|
222
|
+
curr_contact,
|
|
223
|
+
self.target_lin_vel,
|
|
224
|
+
self.prev_action,
|
|
225
|
+
),
|
|
226
|
+
dtype=np.float32,
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
def _compute_reward(self, action):
|
|
230
|
+
vel = self.env.get_velocity_data()
|
|
231
|
+
lin_vel = np.array(vel[:3])
|
|
232
|
+
ang_vel = np.array(vel[3:6])
|
|
233
|
+
|
|
234
|
+
tracking_lin_vel_reward, tracking_ang_vel_reward, lin_vel_z_penalty = (
|
|
235
|
+
self._tracking_velocity_penalty(lin_vel, ang_vel)
|
|
236
|
+
)
|
|
237
|
+
action_diff_penalty = np.sum(np.abs(action - self.prev_action))
|
|
238
|
+
action_sym = self.action_sym()
|
|
239
|
+
|
|
240
|
+
Positive_rewards = (
|
|
241
|
+
tracking_lin_vel_reward * self.reward_weights["linear_vel_tracking"]
|
|
242
|
+
+ tracking_ang_vel_reward * self.reward_weights["angular_vel_tracking"]
|
|
243
|
+
+ self.is_healthy * self.reward_weights["healthy"]
|
|
244
|
+
+ self.feet_air_time_reward * self.reward_weights["feet_airtime"]
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
Negative_rewards = (
|
|
248
|
+
self.torque_cost * self.cost_weights["torque"]
|
|
249
|
+
+ action_diff_penalty * self.cost_weights["action_rate"]
|
|
250
|
+
+ lin_vel_z_penalty * self.cost_weights["vertical_vel"]
|
|
251
|
+
+ self.xy_angular_velocity_cost * self.cost_weights["xy_angular_vel"]
|
|
252
|
+
+ action_sym * self.cost_weights["action_sym"]
|
|
253
|
+
+ self.acceleration_cost * self.cost_weights["joint_acceleration"]
|
|
254
|
+
+ self.orientation_cost * self.cost_weights["orientation"]
|
|
255
|
+
+ self.default_joint_position_cost
|
|
256
|
+
* self.cost_weights["default_joint_position"]
|
|
257
|
+
)
|
|
258
|
+
reward = Positive_rewards - Negative_rewards
|
|
259
|
+
return reward
|
|
260
|
+
|
|
261
|
+
def _tracking_velocity_penalty(self, lin_vel, ang_vel):
|
|
262
|
+
lin_vel_error = np.sum(np.abs(self.target_lin_vel[0] - lin_vel[0]))
|
|
263
|
+
tracking_lin_vel_reward = np.exp(-lin_vel_error / self.tracking_sigma)
|
|
264
|
+
ang_vel_error = np.sum(np.square(self.target_ang_vel - ang_vel[2]))
|
|
265
|
+
tracking_ang_vel_reward = np.exp(-ang_vel_error / self.tracking_sigma)
|
|
266
|
+
lin_vel_z_penalty = np.square((lin_vel[2]))
|
|
267
|
+
return tracking_lin_vel_reward, tracking_ang_vel_reward, lin_vel_z_penalty
|
|
268
|
+
|
|
269
|
+
@property
|
|
270
|
+
def feet_air_time_reward(self):
|
|
271
|
+
curr_contact = self.feet_contact_forces > 1.0
|
|
272
|
+
contact_filter = np.logical_or(curr_contact, self._last_contacts)
|
|
273
|
+
self._last_contacts = curr_contact
|
|
274
|
+
first_contact = (self._feet_air_time > 0.0) * contact_filter
|
|
275
|
+
self._feet_air_time += self.env.model.opt.timestep * self.frame_skip
|
|
276
|
+
air_time_reward = np.sum((self._feet_air_time) * first_contact)
|
|
277
|
+
air_time_reward *= np.linalg.norm(self.target_lin_vel) > 0.1
|
|
278
|
+
self._feet_air_time *= ~contact_filter
|
|
279
|
+
return air_time_reward
|
|
280
|
+
|
|
281
|
+
@property
|
|
282
|
+
def torque_cost(self):
|
|
283
|
+
return np.sum(np.abs(self.env.get_joint_effort()))
|
|
284
|
+
|
|
285
|
+
@property
|
|
286
|
+
def xy_angular_velocity_cost(self):
|
|
287
|
+
return np.sum(np.square(self.env.data.qvel[5]))
|
|
288
|
+
|
|
289
|
+
@property
|
|
290
|
+
def is_healthy(self):
|
|
291
|
+
_, _, z = self.env.get_position_data()
|
|
292
|
+
min_z, max_z = self._healthy_z_range
|
|
293
|
+
if not (min_z <= z <= max_z):
|
|
294
|
+
return False
|
|
295
|
+
|
|
296
|
+
r = self.euler_from_quaternion(*self.env.data.qpos[3:7])
|
|
297
|
+
return np.all(np.abs(r) <= self._healthy_rotation)
|
|
298
|
+
|
|
299
|
+
@property
|
|
300
|
+
def feet_contact_forces(self):
|
|
301
|
+
contact_forces = self.env.data.cfrc_ext[self._cfrc_ext_feet_indices].copy()
|
|
302
|
+
return np.linalg.norm(contact_forces, axis=1)
|
|
303
|
+
|
|
304
|
+
@property
|
|
305
|
+
def orientation_cost(self):
|
|
306
|
+
roll, pitch, _ = self.euler_from_quaternion(*self.env.data.qpos[3:7])
|
|
307
|
+
return np.square(roll) + np.square(pitch)
|
|
308
|
+
|
|
309
|
+
@property
|
|
310
|
+
def default_joint_position_cost(self):
|
|
311
|
+
joint_pos = self.env.get_joint_data()
|
|
312
|
+
soft_joint_limits_low = self.joint_limits_low * 0.9
|
|
313
|
+
soft_joint_limits_high = self.joint_limits_high * 0.9
|
|
314
|
+
|
|
315
|
+
lower_violation = np.maximum(soft_joint_limits_low - joint_pos, 0)
|
|
316
|
+
upper_violation = np.maximum(joint_pos - soft_joint_limits_high, 0)
|
|
317
|
+
return np.sum(np.square(lower_violation + upper_violation))
|
|
318
|
+
|
|
319
|
+
def action_sym(self):
|
|
320
|
+
in_phase_pairs_thigh = [(1, 7), (4, 10)]
|
|
321
|
+
out_of_phase_pairs_thigh = [(1, 4), (7, 10)]
|
|
322
|
+
out_of_phase_pairs_calf = [(2, 5), (8, 11)]
|
|
323
|
+
in_phase_pairs_calf = [(2, 8), (5, 11)]
|
|
324
|
+
|
|
325
|
+
jointpositions = self.env.get_joint_data()
|
|
326
|
+
loss_in_thigh = sum(
|
|
327
|
+
(jointpositions[i] - jointpositions[j]) ** 2
|
|
328
|
+
for i, j in in_phase_pairs_thigh
|
|
329
|
+
)
|
|
330
|
+
loss_out_thigh = sum(
|
|
331
|
+
(jointpositions[i] + jointpositions[j] - 1.5) ** 2
|
|
332
|
+
for i, j in out_of_phase_pairs_thigh
|
|
333
|
+
)
|
|
334
|
+
loss_in_calf = sum(
|
|
335
|
+
(jointpositions[i] - jointpositions[j]) ** 2 for i, j in in_phase_pairs_calf
|
|
336
|
+
)
|
|
337
|
+
loss_out_calf = sum(
|
|
338
|
+
(jointpositions[i] + jointpositions[j] + 3.6) ** 2
|
|
339
|
+
for i, j in out_of_phase_pairs_calf
|
|
340
|
+
)
|
|
341
|
+
loss_in = loss_in_thigh + 0.1 * loss_in_calf
|
|
342
|
+
loss_out = loss_out_thigh + 0.1 * loss_out_calf
|
|
343
|
+
return loss_in + loss_out
|
|
344
|
+
|
|
345
|
+
@property
|
|
346
|
+
def acceleration_cost(self):
|
|
347
|
+
return np.sum(np.square(self.env.data.qacc[6:]))
|
|
348
|
+
|
|
349
|
+
@staticmethod
|
|
350
|
+
def euler_from_quaternion(w, x, y, z):
|
|
351
|
+
roll = np.arctan2(2 * (w * x + y * z), 1 - 2 * (x * x + y * y))
|
|
352
|
+
pitch = np.arcsin(np.clip(2 * (w * y - z * x), -1.0, 1.0))
|
|
353
|
+
yaw = np.arctan2(2 * (w * z + x * y), 1 - 2 * (y * y + z * z))
|
|
354
|
+
return roll, pitch, yaw
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
BSD 3-Clause License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2016-2022 HangZhou YuShu TECHNOLOGY CO.,LTD. ("Unitree Robotics")
|
|
4
|
+
All rights reserved.
|
|
5
|
+
|
|
6
|
+
Redistribution and use in source and binary forms, with or without
|
|
7
|
+
modification, are permitted provided that the following conditions are met:
|
|
8
|
+
|
|
9
|
+
* Redistributions of source code must retain the above copyright notice, this
|
|
10
|
+
list of conditions and the following disclaimer.
|
|
11
|
+
|
|
12
|
+
* Redistributions in binary form must reproduce the above copyright notice,
|
|
13
|
+
this list of conditions and the following disclaimer in the documentation
|
|
14
|
+
and/or other materials provided with the distribution.
|
|
15
|
+
|
|
16
|
+
* Neither the name of the copyright holder nor the names of its
|
|
17
|
+
contributors may be used to endorse or promote products derived from
|
|
18
|
+
this software without specific prior written permission.
|
|
19
|
+
|
|
20
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
21
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
22
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
23
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
24
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
25
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
26
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
27
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
28
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
29
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# Unitree A1 Description (MJCF)
|
|
2
|
+
|
|
3
|
+
> [!IMPORTANT]
|
|
4
|
+
> Requires MuJoCo 2.2.2 or later.
|
|
5
|
+
|
|
6
|
+
## Changelog
|
|
7
|
+
|
|
8
|
+
See [CHANGELOG.md](./CHANGELOG.md) for a full history of changes.
|
|
9
|
+
|
|
10
|
+
## Overview
|
|
11
|
+
|
|
12
|
+
This package contains a simplified robot description (MJCF) of the [A1 Quadruped
|
|
13
|
+
Robot](https://www.unitree.com/a1/) developed by [Unitree
|
|
14
|
+
Robotics](https://www.unitree.com/). It is derived from the [publicly available
|
|
15
|
+
URDF
|
|
16
|
+
description](https://github.com/unitreerobotics/unitree_mujoco/tree/main/data/a1/urdf).
|
|
17
|
+
|
|
18
|
+
<p float="left">
|
|
19
|
+
<img src="a1.png" width="400">
|
|
20
|
+
</p>
|
|
21
|
+
|
|
22
|
+
## URDF → MJCF derivation steps
|
|
23
|
+
|
|
24
|
+
1. Converted the DAE [mesh
|
|
25
|
+
files](https://github.com/unitreerobotics/unitree_mujoco/tree/main/data/a1/meshes)
|
|
26
|
+
to OBJ format using [Blender](https://www.blender.org/).
|
|
27
|
+
2. Processed `.obj` files with [`obj2mjcf`](https://github.com/kevinzakka/obj2mjcf).
|
|
28
|
+
3. Added `<mujoco> <compiler discardvisual="false"/> </mujoco>` to the URDF's
|
|
29
|
+
`<robot>` clause in order to preserve visual geometries.
|
|
30
|
+
4. Loaded the URDF into MuJoCo and saved a corresponding MJCF.
|
|
31
|
+
5. Added a `<freejoint/>` to the base, and a tracking light.
|
|
32
|
+
6. Manually edited the MJCF to extract common properties into the `<default>` section.
|
|
33
|
+
7. Manually designed collision geometries.
|
|
34
|
+
8. Shifted joint reference values and ranges so that 0 corresponds to standing pose.
|
|
35
|
+
9. Softened the contacts of the feet to approximate the effect of rubber and
|
|
36
|
+
increased `impratio` to reduce slippage.
|
|
37
|
+
10. Added `scene.xml` which includes the robot, with a textured groundplane, skybox, and haze.
|
|
38
|
+
|
|
39
|
+
## License
|
|
40
|
+
|
|
41
|
+
This model is released under a [BSD-3-Clause License](LICENSE).
|