yam-common 0.1.0__tar.gz
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.
- yam_common-0.1.0/PKG-INFO +22 -0
- yam_common-0.1.0/README.md +7 -0
- yam_common-0.1.0/pyproject.toml +29 -0
- yam_common-0.1.0/setup.cfg +4 -0
- yam_common-0.1.0/yam_common/__init__.py +13 -0
- yam_common-0.1.0/yam_common/motor_chain_robot.py +404 -0
- yam_common-0.1.0/yam_common/mujoco_kdl.py +206 -0
- yam_common-0.1.0/yam_common/utils.py +669 -0
- yam_common-0.1.0/yam_common.egg-info/PKG-INFO +22 -0
- yam_common-0.1.0/yam_common.egg-info/SOURCES.txt +11 -0
- yam_common-0.1.0/yam_common.egg-info/dependency_links.txt +1 -0
- yam_common-0.1.0/yam_common.egg-info/requires.txt +3 -0
- yam_common-0.1.0/yam_common.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: yam-common
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Shared utilities for YAM arm LeRobot plugins
|
|
5
|
+
Author: Praveen Selvaraj
|
|
6
|
+
License: MIT
|
|
7
|
+
Classifier: Programming Language :: Python :: 3
|
|
8
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
9
|
+
Classifier: Operating System :: OS Independent
|
|
10
|
+
Requires-Python: >=3.10
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
Requires-Dist: numpy
|
|
13
|
+
Requires-Dist: mujoco
|
|
14
|
+
Requires-Dist: lerobot>=0.4
|
|
15
|
+
|
|
16
|
+
# yam-common
|
|
17
|
+
|
|
18
|
+
Shared utilities for the YAM arm LeRobot plugins.
|
|
19
|
+
|
|
20
|
+
Includes the motor chain controller, MuJoCo gravity compensation, and gripper
|
|
21
|
+
utilities used by both the follower robot and leader teleoperator packages.
|
|
22
|
+
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "yam-common"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Shared utilities for YAM arm LeRobot plugins"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
authors = [
|
|
11
|
+
{ name = "Praveen Selvaraj" },
|
|
12
|
+
]
|
|
13
|
+
license = { text = "MIT" }
|
|
14
|
+
requires-python = ">=3.10"
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Programming Language :: Python :: 3",
|
|
17
|
+
"License :: OSI Approved :: MIT License",
|
|
18
|
+
"Operating System :: OS Independent",
|
|
19
|
+
]
|
|
20
|
+
dependencies = [
|
|
21
|
+
"numpy",
|
|
22
|
+
"mujoco",
|
|
23
|
+
"lerobot>=0.4",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
[tool.setuptools.packages.find]
|
|
27
|
+
where = ["."]
|
|
28
|
+
include = ["yam_common*"]
|
|
29
|
+
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from .mujoco_kdl import MuJoCoKDL, get_yam_mujoco_kdl
|
|
2
|
+
from .motor_chain_robot import MotorChainRobot
|
|
3
|
+
from .utils import GripperForceLimiter, GripperType, JointMapper
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
"MuJoCoKDL",
|
|
7
|
+
"get_yam_mujoco_kdl",
|
|
8
|
+
"MotorChainRobot",
|
|
9
|
+
"GripperForceLimiter",
|
|
10
|
+
"GripperType",
|
|
11
|
+
"JointMapper",
|
|
12
|
+
]
|
|
13
|
+
|
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
"""i2rt-authentic MotorChainRobot port for YAM."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import copy
|
|
6
|
+
import logging
|
|
7
|
+
import os
|
|
8
|
+
import threading
|
|
9
|
+
import time
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from typing import Any, Dict, List, Optional, Union
|
|
12
|
+
|
|
13
|
+
import numpy as np
|
|
14
|
+
|
|
15
|
+
from lerobot.motors.dm.dm_driver import MotorChain, MotorInfo
|
|
16
|
+
from yam_common.mujoco_kdl import MuJoCoKDL
|
|
17
|
+
from yam_common.utils import GripperForceLimiter, GripperType, JointMapper
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class JointStates:
|
|
24
|
+
names: List[str]
|
|
25
|
+
pos: np.ndarray
|
|
26
|
+
vel: np.ndarray
|
|
27
|
+
eff: np.ndarray
|
|
28
|
+
temp_mos: np.ndarray
|
|
29
|
+
temp_rotor: np.ndarray
|
|
30
|
+
|
|
31
|
+
def asdict(self) -> Dict[str, Any]:
|
|
32
|
+
return {
|
|
33
|
+
"names": self.names,
|
|
34
|
+
"pos": self.pos.flatten().tolist(),
|
|
35
|
+
"vel": self.vel.flatten().tolist(),
|
|
36
|
+
"eff": self.eff.flatten().tolist(),
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass
|
|
41
|
+
class JointCommands:
|
|
42
|
+
torques: np.ndarray
|
|
43
|
+
pos: np.ndarray
|
|
44
|
+
vel: np.ndarray
|
|
45
|
+
kp: np.ndarray
|
|
46
|
+
kd: np.ndarray
|
|
47
|
+
indices: Optional[List[int]] = None
|
|
48
|
+
|
|
49
|
+
@classmethod
|
|
50
|
+
def init_all_zero(cls, n_joints: int) -> "JointCommands":
|
|
51
|
+
return cls(
|
|
52
|
+
torques=np.zeros(n_joints),
|
|
53
|
+
pos=np.zeros(n_joints),
|
|
54
|
+
vel=np.zeros(n_joints),
|
|
55
|
+
kp=np.zeros(n_joints),
|
|
56
|
+
kd=np.zeros(n_joints),
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class MotorChainRobot:
|
|
61
|
+
"""i2rt-authentic motor-chain robot controller."""
|
|
62
|
+
|
|
63
|
+
def __init__(
|
|
64
|
+
self,
|
|
65
|
+
motor_chain: MotorChain,
|
|
66
|
+
xml_path: Optional[str] = None,
|
|
67
|
+
use_gravity_comp: bool = True,
|
|
68
|
+
gravity: Optional[np.ndarray] = None,
|
|
69
|
+
gravity_comp_factor: float = 1.0,
|
|
70
|
+
gripper_index: Optional[int] = None,
|
|
71
|
+
kp: Union[float, List[float]] = 10.0,
|
|
72
|
+
kd: Union[float, List[float]] = 1.0,
|
|
73
|
+
joint_limits: Optional[np.ndarray] = None,
|
|
74
|
+
gripper_limits: Optional[np.ndarray] = None,
|
|
75
|
+
limit_gripper_force: float = -1,
|
|
76
|
+
clip_motor_torque: float = np.inf,
|
|
77
|
+
gripper_type: GripperType = GripperType.CRANK_4310,
|
|
78
|
+
temp_record_flag: bool = False,
|
|
79
|
+
enable_gripper_calibration: bool = False,
|
|
80
|
+
zero_gravity_mode: bool = True,
|
|
81
|
+
test_torque: float = 0.5,
|
|
82
|
+
test_duration: float = 2.0,
|
|
83
|
+
position_threshold: float = 0.01,
|
|
84
|
+
check_interval: float = 0.05,
|
|
85
|
+
) -> None:
|
|
86
|
+
self.temp_record_flag = temp_record_flag
|
|
87
|
+
if gripper_index is not None:
|
|
88
|
+
assert gripper_index == len(motor_chain) - 1, (
|
|
89
|
+
"Gripper index should be the last one, but got {gripper_index}"
|
|
90
|
+
)
|
|
91
|
+
if gripper_limits is None and enable_gripper_calibration:
|
|
92
|
+
from lerobot.robots.yam_follower.utils import detect_gripper_limits
|
|
93
|
+
|
|
94
|
+
logger.info("Auto-detecting gripper limits...")
|
|
95
|
+
detected_limits = detect_gripper_limits(
|
|
96
|
+
motor_chain=motor_chain,
|
|
97
|
+
gripper_index=gripper_index,
|
|
98
|
+
test_torque=test_torque,
|
|
99
|
+
max_duration=test_duration,
|
|
100
|
+
position_threshold=position_threshold,
|
|
101
|
+
check_interval=check_interval,
|
|
102
|
+
)
|
|
103
|
+
gripper_limits = np.array(detected_limits)
|
|
104
|
+
logger.info(f"Gripper limits auto-detected: {gripper_limits}")
|
|
105
|
+
elif gripper_limits is None:
|
|
106
|
+
raise ValueError(
|
|
107
|
+
f"{self}: Gripper limits are required if gripper index is provided and auto-calibration is disabled."
|
|
108
|
+
)
|
|
109
|
+
else:
|
|
110
|
+
logger.info(f"Using provided gripper limits: {gripper_limits}")
|
|
111
|
+
|
|
112
|
+
self._last_gripper_command_qpos = 1
|
|
113
|
+
assert clip_motor_torque >= 0.0
|
|
114
|
+
self._clip_motor_torque = clip_motor_torque
|
|
115
|
+
self.motor_chain = motor_chain
|
|
116
|
+
self.use_gravity_comp = use_gravity_comp
|
|
117
|
+
self.gravity_comp_factor = gravity_comp_factor
|
|
118
|
+
|
|
119
|
+
self._gripper_index = gripper_index
|
|
120
|
+
self.remapper = JointMapper({}, len(motor_chain))
|
|
121
|
+
self._gripper_limits = gripper_limits
|
|
122
|
+
|
|
123
|
+
if self._gripper_index is not None:
|
|
124
|
+
self._gripper_force_limiter = GripperForceLimiter(
|
|
125
|
+
max_force=limit_gripper_force, gripper_type=gripper_type, kp=kp[gripper_index]
|
|
126
|
+
)
|
|
127
|
+
self._limit_gripper_force = limit_gripper_force
|
|
128
|
+
self.remapper = JointMapper(
|
|
129
|
+
index_range_map={gripper_index: gripper_limits},
|
|
130
|
+
total_dofs=len(motor_chain),
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
self._kp = (
|
|
134
|
+
np.array([kp] * len(motor_chain)) if isinstance(kp, float) else np.array(kp)
|
|
135
|
+
)
|
|
136
|
+
self._kd = (
|
|
137
|
+
np.array([kd] * len(motor_chain)) if isinstance(kd, float) else np.array(kd)
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
self._joint_limits: Optional[np.ndarray] = None
|
|
141
|
+
if xml_path is not None:
|
|
142
|
+
self.xml_path = os.path.expanduser(xml_path)
|
|
143
|
+
self.kdl = MuJoCoKDL(self.xml_path)
|
|
144
|
+
if gravity is not None:
|
|
145
|
+
self.kdl.set_gravity(gravity)
|
|
146
|
+
self._joint_limits = self.kdl.joint_limits
|
|
147
|
+
else:
|
|
148
|
+
assert use_gravity_comp is False, "Gravity compensation requires a valid XML path."
|
|
149
|
+
|
|
150
|
+
if joint_limits is not None:
|
|
151
|
+
joint_limits = np.array(joint_limits)
|
|
152
|
+
assert np.all(joint_limits[:, 0] < joint_limits[:, 1]), (
|
|
153
|
+
"Lower joint limits must be smaller than upper limits"
|
|
154
|
+
)
|
|
155
|
+
self._joint_limits = joint_limits
|
|
156
|
+
|
|
157
|
+
self._command_lock = threading.Lock()
|
|
158
|
+
self._state_lock = threading.Lock()
|
|
159
|
+
self._joint_state: Optional[JointStates] = None
|
|
160
|
+
while self._joint_state is None:
|
|
161
|
+
time.sleep(0.05)
|
|
162
|
+
self._joint_state = self._motor_state_to_joint_state(self.motor_chain.read_states())
|
|
163
|
+
self._commands = JointCommands.init_all_zero(len(motor_chain))
|
|
164
|
+
self._check_current_qpos_in_joint_limits()
|
|
165
|
+
|
|
166
|
+
self._stop_event = threading.Event()
|
|
167
|
+
self._server_thread = threading.Thread(target=self.start_server, name="robot_server")
|
|
168
|
+
self._server_thread.start()
|
|
169
|
+
|
|
170
|
+
if not zero_gravity_mode:
|
|
171
|
+
self.command_joint_pos(self._joint_state.pos)
|
|
172
|
+
|
|
173
|
+
def __repr__(self) -> str:
|
|
174
|
+
return f"MotorChainRobot(motor_chain={self.motor_chain})"
|
|
175
|
+
|
|
176
|
+
def _check_current_qpos_in_joint_limits(self, buffer_rad: float = 0.1) -> None:
|
|
177
|
+
if self._joint_state is None or self._joint_limits is None:
|
|
178
|
+
raise RuntimeError(f"{self}: Joint limits:{self._joint_limits} or joint state:{self._joint_state} are not set.")
|
|
179
|
+
|
|
180
|
+
current_pos = self._joint_state.pos
|
|
181
|
+
if self._gripper_index is not None:
|
|
182
|
+
arm_pos = current_pos[: self._gripper_index]
|
|
183
|
+
arm_limits = self._joint_limits
|
|
184
|
+
else:
|
|
185
|
+
arm_pos = current_pos
|
|
186
|
+
arm_limits = self._joint_limits
|
|
187
|
+
|
|
188
|
+
lower_limits = arm_limits[:, 0] - buffer_rad
|
|
189
|
+
upper_limits = arm_limits[:, 1] + buffer_rad
|
|
190
|
+
lower_violations = arm_pos < lower_limits
|
|
191
|
+
upper_violations = arm_pos > upper_limits
|
|
192
|
+
|
|
193
|
+
if np.any(lower_violations) or np.any(upper_violations):
|
|
194
|
+
violation_details = []
|
|
195
|
+
for i, (pos, lower, upper) in enumerate(zip(arm_pos, lower_limits, upper_limits)):
|
|
196
|
+
if pos < lower:
|
|
197
|
+
violation_details.append(f"Joint {i}: {pos:.4f} < {lower:.4f} (lower limit)")
|
|
198
|
+
elif pos > upper:
|
|
199
|
+
violation_details.append(f"Joint {i}: {pos:.4f} > {upper:.4f} (upper limit)")
|
|
200
|
+
violation_msg = "; ".join(violation_details)
|
|
201
|
+
self.motor_chain.running = False
|
|
202
|
+
raise RuntimeError(
|
|
203
|
+
f"{self}: Joint limit violation detected: {violation_msg}, the root reason should be zero position "
|
|
204
|
+
"offset. possible solution: 1. move the arm to zero position and power cycle the robot. "
|
|
205
|
+
"2. Recalibrate the motor zero position."
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
def start_server(self) -> None:
|
|
209
|
+
last_time = time.time()
|
|
210
|
+
iteration_count = 0
|
|
211
|
+
try:
|
|
212
|
+
self.update()
|
|
213
|
+
logging.info("initializing, ....")
|
|
214
|
+
while not self._stop_event.is_set():
|
|
215
|
+
current_time = time.time()
|
|
216
|
+
elapsed_time = current_time - last_time
|
|
217
|
+
self.update()
|
|
218
|
+
if not self.motor_chain.running:
|
|
219
|
+
raise RuntimeError(
|
|
220
|
+
f"{self}: motor_chain_robot's motor chain is not running, exiting the robot server"
|
|
221
|
+
)
|
|
222
|
+
time.sleep(0.004)
|
|
223
|
+
iteration_count += 1
|
|
224
|
+
if elapsed_time >= 10.0:
|
|
225
|
+
control_frequency = iteration_count / elapsed_time
|
|
226
|
+
logging.info(f"{self}: Grav Comp Control Frequency: {control_frequency:.2f} Hz")
|
|
227
|
+
if control_frequency < 100:
|
|
228
|
+
logging.warning(
|
|
229
|
+
f"{self}: Gravity compensation control loop is slow, current frequency: {control_frequency:.2f} Hz"
|
|
230
|
+
)
|
|
231
|
+
last_time = current_time
|
|
232
|
+
iteration_count = 0
|
|
233
|
+
except Exception as exc:
|
|
234
|
+
logging.error(f"{self}: robot server error, entering zero-torque mode: {exc}")
|
|
235
|
+
try:
|
|
236
|
+
self.zero_torque_mode()
|
|
237
|
+
except Exception:
|
|
238
|
+
pass
|
|
239
|
+
self._stop_event.set()
|
|
240
|
+
raise
|
|
241
|
+
|
|
242
|
+
def update(self) -> None:
|
|
243
|
+
with self._command_lock:
|
|
244
|
+
joint_commands = copy.deepcopy(self._commands)
|
|
245
|
+
with self._state_lock:
|
|
246
|
+
g = self._compute_gravity_compensation(self._joint_state)
|
|
247
|
+
motor_torques = joint_commands.torques + g * self.gravity_comp_factor
|
|
248
|
+
motor_torques = np.clip(motor_torques, -self._clip_motor_torque, self._clip_motor_torque)
|
|
249
|
+
|
|
250
|
+
if self._gripper_index is not None:
|
|
251
|
+
if self._limit_gripper_force > 0 and self._joint_state is not None:
|
|
252
|
+
gripper_state = {
|
|
253
|
+
"target_qpos": joint_commands.pos[self._gripper_index],
|
|
254
|
+
"current_qpos": self.remapper.to_robot_joint_pos_space(self._joint_state.pos)[
|
|
255
|
+
self._gripper_index
|
|
256
|
+
],
|
|
257
|
+
"current_qvel": self._joint_state.vel[self._gripper_index],
|
|
258
|
+
"current_eff": self._joint_state.eff[self._gripper_index],
|
|
259
|
+
"current_normalized_qpos": self._joint_state.pos[self._gripper_index],
|
|
260
|
+
"target_normalized_qpos": self.remapper.to_command_joint_pos_space(joint_commands.pos)[
|
|
261
|
+
self._gripper_index
|
|
262
|
+
],
|
|
263
|
+
"last_command_qpos": self._last_gripper_command_qpos,
|
|
264
|
+
}
|
|
265
|
+
joint_commands.pos[self._gripper_index] = self._gripper_force_limiter.update(gripper_state)
|
|
266
|
+
|
|
267
|
+
joint_commands.pos[self._gripper_index] = np.clip(
|
|
268
|
+
joint_commands.pos[self._gripper_index],
|
|
269
|
+
min(self._gripper_limits),
|
|
270
|
+
max(self._gripper_limits),
|
|
271
|
+
)
|
|
272
|
+
self._last_gripper_command_qpos = joint_commands.pos[self._gripper_index]
|
|
273
|
+
|
|
274
|
+
if not self.motor_chain.start_thread_flag:
|
|
275
|
+
self.motor_chain.set_commands(
|
|
276
|
+
motor_torques,
|
|
277
|
+
pos=joint_commands.pos,
|
|
278
|
+
vel=joint_commands.vel,
|
|
279
|
+
kp=joint_commands.kp,
|
|
280
|
+
kd=joint_commands.kd,
|
|
281
|
+
)
|
|
282
|
+
self.motor_chain.start_thread()
|
|
283
|
+
self.motor_chain.start_thread_flag = True
|
|
284
|
+
|
|
285
|
+
motor_state = self.motor_chain.set_commands(
|
|
286
|
+
motor_torques,
|
|
287
|
+
pos=joint_commands.pos,
|
|
288
|
+
vel=joint_commands.vel,
|
|
289
|
+
kp=joint_commands.kp,
|
|
290
|
+
kd=joint_commands.kd,
|
|
291
|
+
)
|
|
292
|
+
self._joint_state = self._motor_state_to_joint_state(motor_state)
|
|
293
|
+
self._check_current_qpos_in_joint_limits()
|
|
294
|
+
|
|
295
|
+
def _motor_state_to_joint_state(self, motor_state: List[MotorInfo]) -> JointStates:
|
|
296
|
+
names = [str(i) for i in range(len(motor_state))]
|
|
297
|
+
pos = np.array([motor.pos for motor in motor_state])
|
|
298
|
+
pos = self.remapper.to_command_joint_pos_space(pos)
|
|
299
|
+
vel = np.array([motor.vel for motor in motor_state])
|
|
300
|
+
vel = self.remapper.to_command_joint_vel_space(vel)
|
|
301
|
+
eff = np.array([motor.eff for motor in motor_state])
|
|
302
|
+
temp_mos = np.array([motor.temp_mos for motor in motor_state])
|
|
303
|
+
temp_rotor = np.array([motor.temp_rotor for motor in motor_state])
|
|
304
|
+
return JointStates(
|
|
305
|
+
names=names,
|
|
306
|
+
pos=pos,
|
|
307
|
+
vel=vel,
|
|
308
|
+
eff=eff,
|
|
309
|
+
temp_mos=temp_mos,
|
|
310
|
+
temp_rotor=temp_rotor,
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
def _compute_gravity_compensation(self, joint_state: Optional[JointStates]) -> np.ndarray:
|
|
314
|
+
if joint_state is None or not self.use_gravity_comp:
|
|
315
|
+
return np.zeros(len(self.motor_chain))
|
|
316
|
+
q = joint_state.pos[: self._gripper_index] if self._gripper_index is not None else joint_state.pos
|
|
317
|
+
t = self.kdl.compute_inverse_dynamics(q, np.zeros(q.shape), np.zeros(q.shape))
|
|
318
|
+
if np.max(np.abs(t)) > 20.0:
|
|
319
|
+
print([f"{s:.2f}" for s in t])
|
|
320
|
+
raise RuntimeError(f"{self}: too large torques")
|
|
321
|
+
if self._gripper_index is None:
|
|
322
|
+
return t
|
|
323
|
+
return np.append(t, 0.0)
|
|
324
|
+
|
|
325
|
+
def num_dofs(self) -> int:
|
|
326
|
+
return len(self.motor_chain)
|
|
327
|
+
|
|
328
|
+
def get_joint_pos(self) -> np.ndarray:
|
|
329
|
+
with self._state_lock:
|
|
330
|
+
return self._joint_state.pos
|
|
331
|
+
|
|
332
|
+
def _clip_robot_joint_pos_command(self, pos: np.ndarray) -> np.ndarray:
|
|
333
|
+
if self._joint_limits is not None:
|
|
334
|
+
if self._gripper_index is not None:
|
|
335
|
+
pos[: self._gripper_index] = np.clip(
|
|
336
|
+
pos[: self._gripper_index],
|
|
337
|
+
self._joint_limits[:, 0],
|
|
338
|
+
self._joint_limits[:, 1],
|
|
339
|
+
)
|
|
340
|
+
else:
|
|
341
|
+
pos = np.clip(pos, self._joint_limits[:, 0], self._joint_limits[:, 1])
|
|
342
|
+
return pos
|
|
343
|
+
|
|
344
|
+
def command_joint_pos(self, joint_pos: np.ndarray) -> None:
|
|
345
|
+
pos = self._clip_robot_joint_pos_command(joint_pos)
|
|
346
|
+
with self._command_lock:
|
|
347
|
+
self._commands = JointCommands.init_all_zero(len(self.motor_chain))
|
|
348
|
+
self._commands.pos = self.remapper.to_robot_joint_pos_space(pos)
|
|
349
|
+
self._commands.kp = self._kp
|
|
350
|
+
self._commands.kd = self._kd
|
|
351
|
+
|
|
352
|
+
def command_joint_state(self, joint_state: Dict[str, np.ndarray]) -> None:
|
|
353
|
+
pos = self._clip_robot_joint_pos_command(joint_state["pos"])
|
|
354
|
+
vel = joint_state["vel"]
|
|
355
|
+
self._commands = JointCommands.init_all_zero(len(self.motor_chain))
|
|
356
|
+
kp = joint_state.get("kp", self._kp)
|
|
357
|
+
kd = joint_state.get("kd", self._kd)
|
|
358
|
+
with self._command_lock:
|
|
359
|
+
self._commands.pos = self.remapper.to_robot_joint_pos_space(pos)
|
|
360
|
+
self._commands.vel = self.remapper.to_robot_joint_vel_space(vel)
|
|
361
|
+
self._commands.kp = kp
|
|
362
|
+
self._commands.kd = kd
|
|
363
|
+
|
|
364
|
+
def zero_torque_mode(self) -> None:
|
|
365
|
+
logging.info(f"Entering zero_torque_mode for {self}")
|
|
366
|
+
with self._command_lock:
|
|
367
|
+
self._commands = JointCommands.init_all_zero(len(self.motor_chain))
|
|
368
|
+
self._kp = np.zeros(len(self.motor_chain))
|
|
369
|
+
self._kd = np.zeros(len(self.motor_chain))
|
|
370
|
+
|
|
371
|
+
def get_observations(self) -> Dict[str, np.ndarray]:
|
|
372
|
+
with self._state_lock:
|
|
373
|
+
if self._gripper_index is None:
|
|
374
|
+
result = {
|
|
375
|
+
"joint_pos": self._joint_state.pos,
|
|
376
|
+
"joint_vel": self._joint_state.vel,
|
|
377
|
+
"joint_eff": self._joint_state.eff,
|
|
378
|
+
}
|
|
379
|
+
else:
|
|
380
|
+
result = {
|
|
381
|
+
"joint_pos": self._joint_state.pos[: self._gripper_index],
|
|
382
|
+
"gripper_pos": np.array([self._joint_state.pos[self._gripper_index]]),
|
|
383
|
+
"joint_vel": self._joint_state.vel,
|
|
384
|
+
"joint_eff": self._joint_state.eff,
|
|
385
|
+
}
|
|
386
|
+
if self.temp_record_flag:
|
|
387
|
+
result["temp_mos"] = self._joint_state.temp_mos
|
|
388
|
+
result["temp_rotor"] = self._joint_state.temp_rotor
|
|
389
|
+
return result
|
|
390
|
+
|
|
391
|
+
def __exit__(self, exc_type: Any, exc_value: Any, traceback: Any) -> None:
|
|
392
|
+
self.close()
|
|
393
|
+
|
|
394
|
+
def close(self) -> None:
|
|
395
|
+
self._stop_event.set()
|
|
396
|
+
self._server_thread.join()
|
|
397
|
+
self.motor_chain.close()
|
|
398
|
+
print("Robot closed with all torques set to zero.")
|
|
399
|
+
|
|
400
|
+
def update_kp_kd(self, kp: np.ndarray, kd: np.ndarray) -> None:
|
|
401
|
+
assert kp.shape == self._kp.shape == kd.shape
|
|
402
|
+
self._kp = kp
|
|
403
|
+
self._kd = kd
|
|
404
|
+
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
"""
|
|
2
|
+
MuJoCo-based gravity compensation for YAM robot.
|
|
3
|
+
|
|
4
|
+
Faithfully ported from i2rt/utils/mujoco_utils.py.
|
|
5
|
+
|
|
6
|
+
This module uses MuJoCo's inverse dynamics to compute the torques needed
|
|
7
|
+
to compensate for gravity at any given joint configuration.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import os
|
|
11
|
+
from typing import Optional
|
|
12
|
+
|
|
13
|
+
import numpy as np
|
|
14
|
+
|
|
15
|
+
try:
|
|
16
|
+
import mujoco
|
|
17
|
+
except ImportError:
|
|
18
|
+
mujoco = None
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class MuJoCoKDL:
|
|
22
|
+
"""
|
|
23
|
+
A class for computing inverse dynamics (gravity compensation) using MuJoCo.
|
|
24
|
+
|
|
25
|
+
Source: i2rt/utils/mujoco_utils.py:MuJoCoKDL
|
|
26
|
+
|
|
27
|
+
This class loads a MuJoCo XML model and uses mujoco.mj_inverse() to compute
|
|
28
|
+
the torques needed to maintain a given configuration against gravity.
|
|
29
|
+
|
|
30
|
+
For gravity compensation specifically:
|
|
31
|
+
- Set qpos to current joint positions
|
|
32
|
+
- Set qvel to zero (no velocity)
|
|
33
|
+
- Set qacc to zero (no acceleration)
|
|
34
|
+
- Call mj_inverse() to get the torques needed to maintain this configuration
|
|
35
|
+
|
|
36
|
+
Usage:
|
|
37
|
+
kdl = MuJoCoKDL("/path/to/yam.xml")
|
|
38
|
+
gravity_torques = kdl.compute_gravity_compensation(current_joint_positions)
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
def __init__(self, xml_path: str, gravity: Optional[np.ndarray] = None):
|
|
42
|
+
"""
|
|
43
|
+
Initialize MuJoCoKDL with a robot model.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
xml_path: Path to MuJoCo XML model file
|
|
47
|
+
gravity: Optional gravity vector (default: [0, 0, -9.81])
|
|
48
|
+
"""
|
|
49
|
+
if mujoco is None:
|
|
50
|
+
raise ImportError(
|
|
51
|
+
"mujoco is required for MuJoCoKDL gravity compensation. "
|
|
52
|
+
"Install with: pip install mujoco"
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
# Load model - expand user path as in i2rt
|
|
56
|
+
self.xml_path = os.path.expanduser(xml_path)
|
|
57
|
+
self.model = mujoco.MjModel.from_xml_path(self.xml_path)
|
|
58
|
+
self.data = mujoco.MjData(self.model)
|
|
59
|
+
|
|
60
|
+
# Set gravity (default: Earth gravity pointing down)
|
|
61
|
+
if gravity is None:
|
|
62
|
+
gravity = np.array([0.0, 0.0, -9.81])
|
|
63
|
+
self.set_gravity(gravity)
|
|
64
|
+
|
|
65
|
+
# Disable all collisions - we only care about dynamics, not contacts
|
|
66
|
+
# Source: i2rt/utils/mujoco_utils.py lines 16-17
|
|
67
|
+
self.model.geom_contype[:] = 0
|
|
68
|
+
self.model.geom_conaffinity[:] = 0
|
|
69
|
+
|
|
70
|
+
# Disable all joint limits - we handle limits separately
|
|
71
|
+
# Source: i2rt/utils/mujoco_utils.py line 19
|
|
72
|
+
self.model.jnt_limited[:] = 0
|
|
73
|
+
|
|
74
|
+
@property
|
|
75
|
+
def num_joints(self) -> int:
|
|
76
|
+
"""Number of joints in the model."""
|
|
77
|
+
return self.model.nq
|
|
78
|
+
|
|
79
|
+
@property
|
|
80
|
+
def joint_limits(self) -> np.ndarray:
|
|
81
|
+
"""
|
|
82
|
+
Get joint limits from model.
|
|
83
|
+
|
|
84
|
+
Returns:
|
|
85
|
+
Array of shape (num_joints, 2) with [min, max] for each joint
|
|
86
|
+
"""
|
|
87
|
+
return self.model.jnt_range.copy()
|
|
88
|
+
|
|
89
|
+
def set_gravity(self, gravity: np.ndarray) -> None:
|
|
90
|
+
"""
|
|
91
|
+
Set the gravity vector for the robot.
|
|
92
|
+
|
|
93
|
+
Source: i2rt/utils/mujoco_utils.py:MuJoCoKDL.set_gravity()
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
gravity: Gravity vector as a 3D NumPy array (e.g., [0, 0, -9.81])
|
|
97
|
+
"""
|
|
98
|
+
assert gravity.shape == (3,), f"Gravity must be 3D vector, got shape {gravity.shape}"
|
|
99
|
+
self.model.opt.gravity = gravity
|
|
100
|
+
|
|
101
|
+
def compute_inverse_dynamics(
|
|
102
|
+
self,
|
|
103
|
+
q: np.ndarray,
|
|
104
|
+
qdot: np.ndarray,
|
|
105
|
+
qdotdot: np.ndarray,
|
|
106
|
+
) -> np.ndarray:
|
|
107
|
+
"""
|
|
108
|
+
Compute inverse dynamics to get required joint torques.
|
|
109
|
+
|
|
110
|
+
Source: i2rt/utils/mujoco_utils.py:MuJoCoKDL.compute_inverse_dynamics()
|
|
111
|
+
|
|
112
|
+
This computes the torques needed to achieve the given acceleration
|
|
113
|
+
from the given position and velocity, accounting for gravity,
|
|
114
|
+
Coriolis forces, etc.
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
q: Joint positions (radians)
|
|
118
|
+
qdot: Joint velocities (rad/s)
|
|
119
|
+
qdotdot: Joint accelerations (rad/s^2)
|
|
120
|
+
|
|
121
|
+
Returns:
|
|
122
|
+
Joint torques (Nm) needed to achieve the given motion
|
|
123
|
+
"""
|
|
124
|
+
assert len(q) == len(qdot) == len(qdotdot), (
|
|
125
|
+
f"Input dimensions must match: q={len(q)}, qdot={len(qdot)}, qdotdot={len(qdotdot)}"
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
length = len(q)
|
|
129
|
+
|
|
130
|
+
# Set state
|
|
131
|
+
self.data.qpos[:length] = q
|
|
132
|
+
self.data.qvel[:length] = qdot
|
|
133
|
+
self.data.qacc[:length] = qdotdot
|
|
134
|
+
|
|
135
|
+
# Compute inverse dynamics
|
|
136
|
+
mujoco.mj_inverse(self.model, self.data)
|
|
137
|
+
|
|
138
|
+
# Return the computed torques
|
|
139
|
+
return self.data.qfrc_inverse[:length].copy()
|
|
140
|
+
|
|
141
|
+
def compute_gravity_compensation(self, q: np.ndarray) -> np.ndarray:
|
|
142
|
+
"""
|
|
143
|
+
Compute gravity compensation torques for a given joint configuration.
|
|
144
|
+
|
|
145
|
+
This is a convenience method that calls compute_inverse_dynamics with
|
|
146
|
+
zero velocity and zero acceleration, which gives the torques needed
|
|
147
|
+
to hold the arm stationary against gravity.
|
|
148
|
+
|
|
149
|
+
Args:
|
|
150
|
+
q: Joint positions (radians)
|
|
151
|
+
|
|
152
|
+
Returns:
|
|
153
|
+
Gravity compensation torques (Nm)
|
|
154
|
+
"""
|
|
155
|
+
zeros = np.zeros_like(q)
|
|
156
|
+
return self.compute_inverse_dynamics(q, zeros, zeros)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def get_yam_mujoco_kdl(gripper_type: str = "crank_4310") -> MuJoCoKDL:
|
|
160
|
+
"""
|
|
161
|
+
Get MuJoCoKDL instance for YAM robot with appropriate XML model.
|
|
162
|
+
|
|
163
|
+
Args:
|
|
164
|
+
gripper_type: Type of gripper ("crank_4310", "linear_3507", "linear_4310",
|
|
165
|
+
"yam_teaching_handle", "no_gripper")
|
|
166
|
+
|
|
167
|
+
Returns:
|
|
168
|
+
MuJoCoKDL instance configured for YAM robot
|
|
169
|
+
"""
|
|
170
|
+
# Map gripper type to XML file
|
|
171
|
+
# Source: i2rt/robots/utils.py:GripperType.get_xml_path()
|
|
172
|
+
xml_paths = {
|
|
173
|
+
"crank_4310": "yam.xml",
|
|
174
|
+
"linear_3507": "yam_lw_gripper.xml",
|
|
175
|
+
"linear_4310": "yam_4310_linear.xml",
|
|
176
|
+
"yam_teaching_handle": "yam_teaching_handle.xml",
|
|
177
|
+
"no_gripper": "yam_no_gripper.xml",
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if gripper_type not in xml_paths:
|
|
181
|
+
raise ValueError(f"Unknown gripper type: {gripper_type}. Valid types: {list(xml_paths.keys())}")
|
|
182
|
+
|
|
183
|
+
# Try to find the XML file in several locations
|
|
184
|
+
xml_filename = xml_paths[gripper_type]
|
|
185
|
+
|
|
186
|
+
# Search paths (in order of preference):
|
|
187
|
+
# 1. i2rt installation (if available)
|
|
188
|
+
# 2. Local lerobot models directory
|
|
189
|
+
search_paths = [
|
|
190
|
+
# i2rt installation
|
|
191
|
+
os.path.expanduser("~/Desktop/code/i2rt/i2rt/robot_models/yam"),
|
|
192
|
+
# Relative to this file
|
|
193
|
+
os.path.join(os.path.dirname(__file__), "robot_models"),
|
|
194
|
+
# Package data
|
|
195
|
+
os.path.join(os.path.dirname(__file__), "..", "..", "data", "robot_models", "yam"),
|
|
196
|
+
]
|
|
197
|
+
|
|
198
|
+
for base_path in search_paths:
|
|
199
|
+
xml_path = os.path.join(base_path, xml_filename)
|
|
200
|
+
if os.path.exists(xml_path):
|
|
201
|
+
return MuJoCoKDL(xml_path)
|
|
202
|
+
|
|
203
|
+
raise FileNotFoundError(
|
|
204
|
+
f"Could not find MuJoCo XML file '{xml_filename}' for YAM robot. "
|
|
205
|
+
f"Searched paths: {search_paths}"
|
|
206
|
+
)
|