dim-go2-sim 0.1.0__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.
- dim_go2_sim/__init__.py +30 -0
- dim_go2_sim/camera.py +175 -0
- dim_go2_sim/config.py +82 -0
- dim_go2_sim/contacts.py +103 -0
- dim_go2_sim/data/go2/LICENSE +27 -0
- dim_go2_sim/data/go2/README.md +57 -0
- dim_go2_sim/data/go2/assets/base_0.obj +36346 -0
- dim_go2_sim/data/go2/assets/base_1.obj +23131 -0
- dim_go2_sim/data/go2/assets/base_2.obj +8367 -0
- dim_go2_sim/data/go2/assets/base_3.obj +10932 -0
- dim_go2_sim/data/go2/assets/base_4.obj +199205 -0
- dim_go2_sim/data/go2/assets/calf_0.obj +24148 -0
- dim_go2_sim/data/go2/assets/calf_1.obj +9243 -0
- dim_go2_sim/data/go2/assets/calf_mirror_0.obj +24145 -0
- dim_go2_sim/data/go2/assets/calf_mirror_1.obj +9252 -0
- dim_go2_sim/data/go2/assets/foot.obj +30786 -0
- dim_go2_sim/data/go2/assets/hip_0.obj +76960 -0
- dim_go2_sim/data/go2/assets/hip_1.obj +73961 -0
- dim_go2_sim/data/go2/assets/thigh_0.obj +75706 -0
- dim_go2_sim/data/go2/assets/thigh_1.obj +40483 -0
- dim_go2_sim/data/go2/assets/thigh_mirror_0.obj +75572 -0
- dim_go2_sim/data/go2/assets/thigh_mirror_1.obj +40450 -0
- dim_go2_sim/data/go2/go2.xml +219 -0
- dim_go2_sim/data/mid360/NOTICE.md +21 -0
- dim_go2_sim/data/mid360/fourier.npz +0 -0
- dim_go2_sim/data/policy/LICENSE +201 -0
- dim_go2_sim/data/policy/NOTICE +12 -0
- dim_go2_sim/data/policy/policy.onnx +0 -0
- dim_go2_sim/lidar.py +243 -0
- dim_go2_sim/mid360.py +136 -0
- dim_go2_sim/module.py +303 -0
- dim_go2_sim/policy.py +102 -0
- dim_go2_sim/world.py +254 -0
- dim_go2_sim-0.1.0.dist-info/METADATA +105 -0
- dim_go2_sim-0.1.0.dist-info/RECORD +39 -0
- dim_go2_sim-0.1.0.dist-info/WHEEL +4 -0
- dim_go2_sim-0.1.0.dist-info/licenses/LICENSE +17 -0
- dim_go2_sim-0.1.0.dist-info/licenses/src/dim_go2_sim/data/go2/LICENSE +27 -0
- dim_go2_sim-0.1.0.dist-info/licenses/src/dim_go2_sim/data/policy/LICENSE +201 -0
dim_go2_sim/__init__.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Copyright 2026 Dimensional Inc.
|
|
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
|
+
"""A simulated Unitree Go2 for dimOS: cmd_vel in; Mid-360, camera, IMU, odometry, contacts out."""
|
|
16
|
+
|
|
17
|
+
from dim_go2_sim.config import CameraConfig, LidarConfig, PhysicsConfig, SceneConfig
|
|
18
|
+
from dim_go2_sim.contacts import Contact, Contacts
|
|
19
|
+
from dim_go2_sim.module import Go2Sim, Go2SimConfig
|
|
20
|
+
|
|
21
|
+
__all__ = [
|
|
22
|
+
"CameraConfig",
|
|
23
|
+
"Contact",
|
|
24
|
+
"Contacts",
|
|
25
|
+
"Go2Sim",
|
|
26
|
+
"Go2SimConfig",
|
|
27
|
+
"LidarConfig",
|
|
28
|
+
"PhysicsConfig",
|
|
29
|
+
"SceneConfig",
|
|
30
|
+
]
|
dim_go2_sim/camera.py
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
# Copyright 2026 Dimensional Inc.
|
|
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
|
+
"""Camera frames drawn from a scene's textured glTF with pygfx (after PimSim's mesh renderer).
|
|
16
|
+
|
|
17
|
+
Cooked scenes' MJCFs are collision-only, so MuJoCo would draw grey boxes. Only the static scene
|
|
18
|
+
is drawn, not the robot: a camera must not see its own body. Rendering runs in a fresh child
|
|
19
|
+
process, since GPU device state cannot cross dimOS's forkserver.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import json
|
|
25
|
+
import math
|
|
26
|
+
import pickle
|
|
27
|
+
import subprocess
|
|
28
|
+
import sys
|
|
29
|
+
from typing import Any
|
|
30
|
+
|
|
31
|
+
import numpy as np
|
|
32
|
+
from numpy.typing import NDArray
|
|
33
|
+
|
|
34
|
+
from dim_go2_sim.config import CameraConfig
|
|
35
|
+
|
|
36
|
+
# glTF is Y-up; the world is Z-up.
|
|
37
|
+
Y_UP_TO_Z_UP = np.array([[1, 0, 0, 0], [0, 0, -1, 0], [0, 1, 0, 0], [0, 0, 0, 1]], dtype=np.float64)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def focal_length(config: CameraConfig) -> float:
|
|
41
|
+
return config.height / (2 * math.tan(math.radians(config.fovy_deg) / 2))
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class _Renderer:
|
|
45
|
+
def __init__(self, visual: str, y_up: bool, config: CameraConfig) -> None:
|
|
46
|
+
import pygfx as gfx # type: ignore[import-untyped]
|
|
47
|
+
from rendercanvas.offscreen import OffscreenRenderCanvas # type: ignore[import-untyped]
|
|
48
|
+
|
|
49
|
+
root = gfx.Group()
|
|
50
|
+
root.add(gfx.load_gltf(visual, quiet=True, remote_ok=False).scene)
|
|
51
|
+
if y_up:
|
|
52
|
+
root.local.matrix = Y_UP_TO_Z_UP
|
|
53
|
+
self._scene = gfx.Scene()
|
|
54
|
+
background = gfx.Background(None, gfx.BackgroundMaterial(config.background))
|
|
55
|
+
self._scene.add(background, _merged(gfx, root))
|
|
56
|
+
self._scene.add(gfx.AmbientLight("#ffffff", config.ambient))
|
|
57
|
+
sun = gfx.DirectionalLight("#fff4e6", config.sun)
|
|
58
|
+
sun.local.position = (4.0, -6.0, 10.0)
|
|
59
|
+
self._scene.add(sun)
|
|
60
|
+
canvas = OffscreenRenderCanvas(size=(config.width, config.height), pixel_ratio=1.0)
|
|
61
|
+
self._renderer = gfx.WgpuRenderer(canvas, pixel_ratio=1.0)
|
|
62
|
+
# pygfx's fov spans the mean of the view's height and width; match the vertical fovy.
|
|
63
|
+
aspect = config.width / config.height
|
|
64
|
+
fov = math.degrees(2 * math.atan(config.height * (1 + aspect) / (4 * focal_length(config))))
|
|
65
|
+
self._camera = gfx.PerspectiveCamera(
|
|
66
|
+
fov=fov, aspect=aspect, depth_range=(config.near, config.far)
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
def capture(
|
|
70
|
+
self, position: NDArray[np.float64], forward: NDArray[np.float64], up: NDArray[np.float64]
|
|
71
|
+
) -> NDArray[np.uint8]:
|
|
72
|
+
self._camera.world.position = tuple(position)
|
|
73
|
+
self._camera.world.reference_up = tuple(up)
|
|
74
|
+
self._camera.look_at(tuple(position + forward))
|
|
75
|
+
self._renderer.render(self._scene, self._camera)
|
|
76
|
+
self._renderer.flush()
|
|
77
|
+
return np.ascontiguousarray(np.asarray(self._renderer.snapshot())[:, :, :3])
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _merged(gfx: Any, root: Any) -> Any:
|
|
81
|
+
"""One mesh per material, in world coordinates: a store is ~47k nodes, one draw call each."""
|
|
82
|
+
meshes: list[Any] = []
|
|
83
|
+
root.traverse(lambda node: meshes.append(node) if isinstance(node, gfx.Mesh) else None)
|
|
84
|
+
by_material: dict[int, list[Any]] = {}
|
|
85
|
+
for mesh in meshes:
|
|
86
|
+
by_material.setdefault(id(mesh.material), []).append(mesh)
|
|
87
|
+
group = gfx.Group()
|
|
88
|
+
for same in by_material.values():
|
|
89
|
+
positions, normals, texcoords, indices, offset = [], [], [], [], 0
|
|
90
|
+
for mesh in same:
|
|
91
|
+
geometry, matrix = mesh.geometry, mesh.world.matrix
|
|
92
|
+
local = geometry.positions.data.reshape(-1, 3).astype(np.float64)
|
|
93
|
+
positions.append((local @ matrix[:3, :3].T + matrix[:3, 3]).astype(np.float32))
|
|
94
|
+
normal = getattr(geometry, "normals", None)
|
|
95
|
+
if normal is not None:
|
|
96
|
+
rotated = normal.data.reshape(-1, 3) @ np.linalg.inv(matrix[:3, :3])
|
|
97
|
+
rotated /= np.maximum(np.linalg.norm(rotated, axis=1, keepdims=True), 1e-12)
|
|
98
|
+
normals.append(rotated.astype(np.float32))
|
|
99
|
+
uv = getattr(geometry, "texcoords", None)
|
|
100
|
+
if uv is not None:
|
|
101
|
+
texcoords.append(uv.data.reshape(-1, 2).astype(np.float32))
|
|
102
|
+
index = getattr(geometry, "indices", None)
|
|
103
|
+
index = np.arange(len(local)) if index is None else index.data.reshape(-1)
|
|
104
|
+
indices.append(index.astype(np.uint32) + offset)
|
|
105
|
+
offset += len(local)
|
|
106
|
+
attributes: dict[str, NDArray[Any]] = {
|
|
107
|
+
"positions": np.concatenate(positions),
|
|
108
|
+
"indices": np.concatenate(indices).reshape(-1, 3),
|
|
109
|
+
}
|
|
110
|
+
# A partial attribute can't be merged; drop it rather than misalign vertices.
|
|
111
|
+
if len(normals) == len(same):
|
|
112
|
+
attributes["normals"] = np.concatenate(normals)
|
|
113
|
+
if len(texcoords) == len(same):
|
|
114
|
+
attributes["texcoords"] = np.concatenate(texcoords)
|
|
115
|
+
group.add(gfx.Mesh(gfx.Geometry(**attributes), same[0].material))
|
|
116
|
+
return group
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _serve(visual: str, y_up: bool, config: CameraConfig) -> None:
|
|
120
|
+
renderer = _Renderer(visual, y_up, config)
|
|
121
|
+
out = sys.stdout.buffer
|
|
122
|
+
pickle.dump(None, out)
|
|
123
|
+
out.flush()
|
|
124
|
+
while True:
|
|
125
|
+
try:
|
|
126
|
+
request = pickle.load(sys.stdin.buffer)
|
|
127
|
+
except EOFError: # the parent exited without saying goodbye
|
|
128
|
+
break
|
|
129
|
+
if request is None:
|
|
130
|
+
break
|
|
131
|
+
pickle.dump(renderer.capture(*request), out, protocol=pickle.HIGHEST_PROTOCOL)
|
|
132
|
+
out.flush()
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
class SceneCamera:
|
|
136
|
+
"""Renders *visual* in a child process. Loading a large scene there takes a while."""
|
|
137
|
+
|
|
138
|
+
def __init__(self, visual: str, y_up: bool, config: CameraConfig) -> None:
|
|
139
|
+
self._process = subprocess.Popen(
|
|
140
|
+
[sys.executable, "-m", __name__, visual, json.dumps(y_up), config.model_dump_json()],
|
|
141
|
+
stdin=subprocess.PIPE,
|
|
142
|
+
stdout=subprocess.PIPE,
|
|
143
|
+
)
|
|
144
|
+
assert self._process.stdout is not None
|
|
145
|
+
try:
|
|
146
|
+
pickle.load(self._process.stdout)
|
|
147
|
+
except EOFError:
|
|
148
|
+
raise RuntimeError(
|
|
149
|
+
f"the camera renderer exited loading {visual}; see its stderr"
|
|
150
|
+
) from None
|
|
151
|
+
|
|
152
|
+
def capture(
|
|
153
|
+
self, position: NDArray[np.float64], forward: NDArray[np.float64], up: NDArray[np.float64]
|
|
154
|
+
) -> NDArray[np.uint8]:
|
|
155
|
+
assert self._process.stdin is not None and self._process.stdout is not None
|
|
156
|
+
pickle.dump((position, forward, up), self._process.stdin)
|
|
157
|
+
self._process.stdin.flush()
|
|
158
|
+
rgb: NDArray[np.uint8] = pickle.load(self._process.stdout)
|
|
159
|
+
return rgb
|
|
160
|
+
|
|
161
|
+
def close(self) -> None:
|
|
162
|
+
try:
|
|
163
|
+
assert self._process.stdin is not None
|
|
164
|
+
pickle.dump(None, self._process.stdin)
|
|
165
|
+
self._process.stdin.close()
|
|
166
|
+
except OSError:
|
|
167
|
+
pass
|
|
168
|
+
try:
|
|
169
|
+
self._process.wait(timeout=5)
|
|
170
|
+
except subprocess.TimeoutExpired:
|
|
171
|
+
self._process.kill()
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
if __name__ == "__main__":
|
|
175
|
+
_serve(sys.argv[1], json.loads(sys.argv[2]), CameraConfig.model_validate_json(sys.argv[3]))
|
dim_go2_sim/config.py
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# Copyright 2026 Dimensional Inc.
|
|
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
|
+
"""Everything about a Go2Sim run: the scene, the physics, and each sensor (no dimOS imports:
|
|
16
|
+
the camera's child process loads this)."""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
|
|
22
|
+
from pydantic import BaseModel
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class SceneConfig(BaseModel):
|
|
26
|
+
# A cooked dimOS scene package by name (resolved with dimOS's catalog), or explicit files.
|
|
27
|
+
package: str | None = None
|
|
28
|
+
mjcf: Path | None = None # collision geometry; a robot-free MJCF
|
|
29
|
+
visual: Path | None = None # textured glTF/GLB the camera draws; None draws nothing
|
|
30
|
+
visual_y_up: bool = True # glTF's convention; the world is Z-up
|
|
31
|
+
start: tuple[float, float, float] = (0.0, 0.0, 0.0) # x, y, yaw of the robot
|
|
32
|
+
floor_z: float = 0.0 # height the feet start on
|
|
33
|
+
# Scene geometry whose top is below this counts as floor for contacts; the rest is clutter.
|
|
34
|
+
floor_top_z: float = 0.05
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class PhysicsConfig(BaseModel):
|
|
38
|
+
timestep: float = 0.0025 # seconds per MuJoCo step; the policy runs every 0.02 s
|
|
39
|
+
# The physics were fitted to real Go2 runs under the bundled policy (go2-sim, dimOS #4441).
|
|
40
|
+
fitted: bool = True
|
|
41
|
+
actuator_tau: float = 0.0151 # seconds of first-order lag between a torque and the joint
|
|
42
|
+
policy: Path | None = None # an rl_sar-layout ONNX walking policy; None is the bundled one
|
|
43
|
+
# Trim the policy's command closed-loop on the true body velocity, as a Go2's sport mode
|
|
44
|
+
# does: the policy alone ignores slow commands and drifts when told to stand.
|
|
45
|
+
track_velocity: bool = True
|
|
46
|
+
command_timeout: float = 0.5 # seconds a cmd_vel holds before the robot stands still
|
|
47
|
+
max_velocity: tuple[float, float, float] = (1.0, 0.5, 1.5) # vx, vy m/s and wz rad/s
|
|
48
|
+
realtime: bool = True # pace to the wall clock; False runs as fast as it can
|
|
49
|
+
cache_dir: Path | None = None # compiled worlds; None is the user cache directory
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class LidarConfig(BaseModel):
|
|
53
|
+
enabled: bool = True
|
|
54
|
+
rate_hz: float = 10.0
|
|
55
|
+
# The Mid-360 on a Go2's head, from the body origin, pitched nose-down.
|
|
56
|
+
xyz: tuple[float, float, float] = (0.29515, -0.00003, 0.16297)
|
|
57
|
+
pitch_deg: float = 60.0
|
|
58
|
+
point_rate_hz: int = 200_000
|
|
59
|
+
downsample: int = 1
|
|
60
|
+
min_range: float = 0.16
|
|
61
|
+
max_range: float = 40.0
|
|
62
|
+
noise: bool = True
|
|
63
|
+
dropout: bool = True
|
|
64
|
+
self_occlusion: bool = True # the robot's own body blocks rays
|
|
65
|
+
motion_bins_hz: float = 200.0 # how often a scan's origin follows the moving body
|
|
66
|
+
seed: int = 0
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class CameraConfig(BaseModel):
|
|
70
|
+
enabled: bool = True
|
|
71
|
+
rate_hz: float = 10.0
|
|
72
|
+
width: int = 640
|
|
73
|
+
height: int = 480
|
|
74
|
+
fovy_deg: float = 60.0
|
|
75
|
+
# The Go2's nose camera, from the body origin, looking straight ahead.
|
|
76
|
+
xyz: tuple[float, float, float] = (0.32715, -0.00003, 0.04297)
|
|
77
|
+
pitch_deg: float = 0.0
|
|
78
|
+
near: float = 0.05
|
|
79
|
+
far: float = 100.0
|
|
80
|
+
ambient: float = 1.8
|
|
81
|
+
sun: float = 2.5
|
|
82
|
+
background: str = "#1b2230"
|
dim_go2_sim/contacts.py
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# Copyright 2026 Dimensional Inc.
|
|
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
|
+
"""Which scene geometry the robot touches, by robot part and kind of geometry. Ground truth."""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
from dataclasses import dataclass
|
|
20
|
+
import json
|
|
21
|
+
import time
|
|
22
|
+
from typing import Literal
|
|
23
|
+
|
|
24
|
+
from dimos.types.timestamped import Timestamped
|
|
25
|
+
from dimos_lcm.std_msgs import String as LCMString
|
|
26
|
+
import mujoco
|
|
27
|
+
import numpy as np
|
|
28
|
+
|
|
29
|
+
Part = Literal["trunk", "leg", "foot"]
|
|
30
|
+
Kind = Literal["floor", "clutter"]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass(frozen=True, order=True)
|
|
34
|
+
class Contact:
|
|
35
|
+
part: Part
|
|
36
|
+
kind: Kind
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class Contacts(Timestamped):
|
|
40
|
+
"""The set of contacts at one instant, published whenever it changes."""
|
|
41
|
+
|
|
42
|
+
msg_name = "dim_go2_sim.Contacts"
|
|
43
|
+
|
|
44
|
+
def __init__(self, contacts: list[Contact] | None = None, ts: float | None = None) -> None:
|
|
45
|
+
self.contacts = contacts or []
|
|
46
|
+
self.ts = time.time() if ts is None else ts
|
|
47
|
+
|
|
48
|
+
def lcm_encode(self) -> bytes:
|
|
49
|
+
payload = {"ts": self.ts, "contacts": [[c.part, c.kind] for c in self.contacts]}
|
|
50
|
+
return LCMString(data=json.dumps(payload)).lcm_encode() # type: ignore[no-any-return]
|
|
51
|
+
|
|
52
|
+
@classmethod
|
|
53
|
+
def lcm_decode(cls, data: bytes) -> Contacts:
|
|
54
|
+
payload = json.loads(LCMString.lcm_decode(data).data)
|
|
55
|
+
return cls([Contact(part, kind) for part, kind in payload["contacts"]], payload["ts"])
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class ContactSensor:
|
|
59
|
+
"""Classifies each static scene geom once as floor (its top below floor_top_z) or clutter."""
|
|
60
|
+
|
|
61
|
+
def __init__(
|
|
62
|
+
self,
|
|
63
|
+
model: mujoco.MjModel,
|
|
64
|
+
data: mujoco.MjData,
|
|
65
|
+
robot_bodies: set[int],
|
|
66
|
+
trunk: int,
|
|
67
|
+
feet: tuple[int, ...],
|
|
68
|
+
floor_top_z: float,
|
|
69
|
+
) -> None:
|
|
70
|
+
self._data = data
|
|
71
|
+
self._trunk = trunk
|
|
72
|
+
self._feet = set(feet)
|
|
73
|
+
self._robot = {g for g in range(model.ngeom) if int(model.geom_bodyid[g]) in robot_bodies}
|
|
74
|
+
self._body = model.geom_bodyid
|
|
75
|
+
self._kind: dict[int, Kind] = {}
|
|
76
|
+
for g in range(model.ngeom):
|
|
77
|
+
if g in self._robot or model.body_weldid[model.geom_bodyid[g]] != 0:
|
|
78
|
+
continue
|
|
79
|
+
if model.geom_type[g] == mujoco.mjtGeom.mjGEOM_PLANE:
|
|
80
|
+
self._kind[g] = "floor"
|
|
81
|
+
continue
|
|
82
|
+
up = data.geom_xmat[g].reshape(3, 3)[2]
|
|
83
|
+
center, half = model.geom_aabb[g, :3], model.geom_aabb[g, 3:]
|
|
84
|
+
top = data.geom_xpos[g, 2] + up @ center + np.abs(up) @ half
|
|
85
|
+
self._kind[g] = "floor" if top < floor_top_z else "clutter"
|
|
86
|
+
|
|
87
|
+
def contacts(self) -> list[Contact]:
|
|
88
|
+
"""Every (robot part, scene kind) pair in contact right now, sorted."""
|
|
89
|
+
found: set[Contact] = set()
|
|
90
|
+
for i in range(self._data.ncon):
|
|
91
|
+
c = self._data.contact[i]
|
|
92
|
+
for scene, robot in ((c.geom1, c.geom2), (c.geom2, c.geom1)):
|
|
93
|
+
kind = self._kind.get(int(scene))
|
|
94
|
+
if kind is not None and int(robot) in self._robot:
|
|
95
|
+
found.add(Contact(self._part(int(robot)), kind))
|
|
96
|
+
return sorted(found)
|
|
97
|
+
|
|
98
|
+
def _part(self, geom: int) -> Part:
|
|
99
|
+
if geom in self._feet:
|
|
100
|
+
return "foot"
|
|
101
|
+
if self._body[geom] == self._trunk:
|
|
102
|
+
return "trunk"
|
|
103
|
+
return "leg"
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
Copyright (c) 2016-2022 HangZhou YuShu TECHNOLOGY CO.,LTD. ("Unitree Robotics")
|
|
2
|
+
All rights reserved.
|
|
3
|
+
|
|
4
|
+
Redistribution and use in source and binary forms, with or without
|
|
5
|
+
modification, are permitted provided that the following conditions are met:
|
|
6
|
+
|
|
7
|
+
* Redistributions of source code must retain the above copyright notice, this
|
|
8
|
+
list of conditions and the following disclaimer.
|
|
9
|
+
|
|
10
|
+
* Redistributions in binary form must reproduce the above copyright notice,
|
|
11
|
+
this list of conditions and the following disclaimer in the documentation
|
|
12
|
+
and/or other materials provided with the distribution.
|
|
13
|
+
|
|
14
|
+
* Neither the name of the copyright holder nor the names of its
|
|
15
|
+
contributors may be used to endorse or promote products derived from
|
|
16
|
+
this software without specific prior written permission.
|
|
17
|
+
|
|
18
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
19
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
20
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
21
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
22
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
23
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
24
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
25
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
26
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
27
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# Unitree Go2 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 [Go2
|
|
13
|
+
Quadruped Robot](https://www.unitree.com/go2/) 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_ros/tree/master/robots/go2_description).
|
|
17
|
+
|
|
18
|
+
<p float="left">
|
|
19
|
+
<img src="go2.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_ros/tree/master/robots/go2_description/meshes) to OBJ format using [Blender](https://www.blender.org/).
|
|
26
|
+
- When exporting, ensure "up axis" is `+Z`, and "forward axis" is `+Y`.
|
|
27
|
+
2. Processed `.obj` files with [`obj2mjcf`](https://github.com/kevinzakka/obj2mjcf).
|
|
28
|
+
3. Added `<mujoco> <compiler discardvisual="false" strippath="false" fusestatic="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.
|
|
32
|
+
6. Manually edited the MJCF to extract common properties into the `<default>` section.
|
|
33
|
+
7. Softened the contacts of the feet to approximate the effect of rubber and
|
|
34
|
+
increased `impratio` to reduce slippage.
|
|
35
|
+
8. Added `scene.xml` which includes the robot, with a textured groundplane, skybox, and haze.
|
|
36
|
+
|
|
37
|
+
## MJX
|
|
38
|
+
|
|
39
|
+
A version of the Unitree Go2 model for use in MJX is available in `scene_mjx.xml` with the following changes:
|
|
40
|
+
|
|
41
|
+
1. The solver `iterations` and `ls_iterations` were modified for performance.
|
|
42
|
+
2. `frictionloss` was removed.
|
|
43
|
+
3. The friction cone was changed from `elliptic` to `pyramidal`.
|
|
44
|
+
4. All contacts other than the sphere geoms were changed to sphere geoms and placed at the joints positions to simulate collisions at critical locations.
|
|
45
|
+
- Note: Currently, MJX cannot deal with collision geoms other than spheres. In this case, we have to put many collision spheres in critical locations of the robot such as joints, torso, and feet as illustrated below.
|
|
46
|
+
|
|
47
|
+
<p float="left">
|
|
48
|
+
<img src="go2_mjx.png" width="800">
|
|
49
|
+
</p>
|
|
50
|
+
|
|
51
|
+
## License
|
|
52
|
+
|
|
53
|
+
This model is released under a [BSD-3-Clause License](LICENSE).
|
|
54
|
+
|
|
55
|
+
## Acknowledgments
|
|
56
|
+
|
|
57
|
+
The MJX model of Go2 was contributed by [lonelyfluency](https://github.com/lonelyfluency).
|