mirror of
https://github.com/huggingface/lerobot.git
synced 2026-07-25 02:36:11 +00:00
adds locomotion control
This commit is contained in:
@@ -174,6 +174,11 @@ def _to_mujoco(a):
|
|||||||
return a[MUJOCO_TO_ISAACLAB]
|
return a[MUJOCO_TO_ISAACLAB]
|
||||||
|
|
||||||
|
|
||||||
|
def mujoco_to_isaaclab(q: np.ndarray) -> np.ndarray:
|
||||||
|
"""Reorder 29-DOF vector from robot/MuJoCo order to SONIC Isaac Lab order."""
|
||||||
|
return np.asarray(q, dtype=np.float32)[MUJOCO_TO_ISAACLAB]
|
||||||
|
|
||||||
|
|
||||||
def _to_runtime(a):
|
def _to_runtime(a):
|
||||||
r = np.zeros(29, np.float32)
|
r = np.zeros(29, np.float32)
|
||||||
r[MUJOCO_TO_ISAACLAB] = a
|
r[MUJOCO_TO_ISAACLAB] = a
|
||||||
@@ -984,6 +989,33 @@ class PlannerController(StandingEncoderDecoder):
|
|||||||
self.playing = True
|
self.playing = True
|
||||||
self.delta_heading = 0.0
|
self.delta_heading = 0.0
|
||||||
|
|
||||||
|
def set_manual_g1_reference(
|
||||||
|
self,
|
||||||
|
q_isaac: np.ndarray,
|
||||||
|
body_quat: np.ndarray | None = None,
|
||||||
|
) -> None:
|
||||||
|
"""Feed encoder mode 0 from an external 29-DOF pose (Isaac Lab joint order).
|
||||||
|
|
||||||
|
Holds the pose static: all 10 encoder frames read the same reference (playing=False).
|
||||||
|
"""
|
||||||
|
q = np.asarray(q_isaac, dtype=np.float64).reshape(29)
|
||||||
|
bq = (
|
||||||
|
np.asarray(body_quat, dtype=np.float64).reshape(4)
|
||||||
|
if body_quat is not None
|
||||||
|
else np.array([1.0, 0.0, 0.0, 0.0], dtype=np.float64)
|
||||||
|
)
|
||||||
|
with self.motion_lock:
|
||||||
|
self.encode_mode = 0
|
||||||
|
self.playing = False
|
||||||
|
self.motion_timesteps = 1
|
||||||
|
self.ref_cursor = 0
|
||||||
|
self.motion_joint_positions[0] = q
|
||||||
|
self.motion_joint_velocities[0] = 0.0
|
||||||
|
self.motion_body_quats[0] = bq
|
||||||
|
self.motion_body_pos[0] = np.array([0.0, 0.0, DEFAULT_HEIGHT], dtype=np.float64)
|
||||||
|
self.init_ref_quat = bq.copy()
|
||||||
|
self.reinit_heading = True
|
||||||
|
|
||||||
def blend_new_motion(self, new_motion, gen_frame):
|
def blend_new_motion(self, new_motion, gen_frame):
|
||||||
"""Blend like C++ CurrentFrameAdvancement: 8-frame cross-fade, then copy tail."""
|
"""Blend like C++ CurrentFrameAdvancement: 8-frame cross-fade, then copy tail."""
|
||||||
with self.motion_lock:
|
with self.motion_lock:
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
import onnxruntime as ort
|
import onnxruntime as ort
|
||||||
from huggingface_hub import hf_hub_download
|
from huggingface_hub import hf_hub_download
|
||||||
|
|
||||||
@@ -36,9 +37,11 @@ from lerobot.robots.unitree_g1.controllers.sonic_pipeline import (
|
|||||||
clamp_mode_params,
|
clamp_mode_params,
|
||||||
compute_kp_kd,
|
compute_kp_kd,
|
||||||
lowstate_to_obs,
|
lowstate_to_obs,
|
||||||
|
mujoco_to_isaaclab,
|
||||||
process_joystick,
|
process_joystick,
|
||||||
should_replan_request,
|
should_replan_request,
|
||||||
)
|
)
|
||||||
|
from lerobot.robots.unitree_g1.g1_utils import G1_29_JointIndex
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -72,6 +75,7 @@ class SonicRuntime:
|
|||||||
self.step = 0
|
self.step = 0
|
||||||
self.replan_timer = 0.0
|
self.replan_timer = 0.0
|
||||||
self.last_ms = _snapshot_ms(self.ms)
|
self.last_ms = _snapshot_ms(self.ms)
|
||||||
|
self.manual_g1_reference = False
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def pipeline(self):
|
def pipeline(self):
|
||||||
@@ -82,13 +86,15 @@ class SonicRuntime:
|
|||||||
self.step += 1
|
self.step += 1
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
if use_joystick:
|
manual = self.manual_g1_reference
|
||||||
|
if use_joystick and not manual:
|
||||||
process_joystick(obs, self.ms, self.controller)
|
process_joystick(obs, self.ms, self.controller)
|
||||||
clamp_mode_params(self.ms)
|
if not manual:
|
||||||
|
clamp_mode_params(self.ms)
|
||||||
|
|
||||||
if self.step > 0:
|
if not manual and self.step > 0:
|
||||||
self.replan_timer += CONTROL_DT
|
self.replan_timer += CONTROL_DT
|
||||||
if should_replan_request(self.ms, self.last_ms, self.replan_timer, self.step):
|
if not manual and should_replan_request(self.ms, self.last_ms, self.replan_timer, self.step):
|
||||||
self.planner.request_replan(self.controller.ref_cursor, self.ms)
|
self.planner.request_replan(self.controller.ref_cursor, self.ms)
|
||||||
self.replan_timer = 0.0
|
self.replan_timer = 0.0
|
||||||
self.ms.needs_replan = False
|
self.ms.needs_replan = False
|
||||||
@@ -99,11 +105,12 @@ class SonicRuntime:
|
|||||||
debug = self.step % DEBUG_PRINT_EVERY == 0
|
debug = self.step % DEBUG_PRINT_EVERY == 0
|
||||||
action = self.controller.step(obs, update_encoder=do_enc, debug=debug)
|
action = self.controller.step(obs, update_encoder=do_enc, debug=debug)
|
||||||
|
|
||||||
result = self.planner.try_get_new_motion()
|
if not manual:
|
||||||
if result:
|
result = self.planner.try_get_new_motion()
|
||||||
self.controller.blend_new_motion(*result)
|
if result:
|
||||||
|
self.controller.blend_new_motion(*result)
|
||||||
|
self.controller.advance_cursor()
|
||||||
|
|
||||||
self.controller.advance_cursor()
|
|
||||||
self.step += 1
|
self.step += 1
|
||||||
return action
|
return action
|
||||||
|
|
||||||
@@ -111,6 +118,7 @@ class SonicRuntime:
|
|||||||
self.ms = MovementState()
|
self.ms = MovementState()
|
||||||
self.controller.reinit_heading = True
|
self.controller.reinit_heading = True
|
||||||
self.controller.playing = True
|
self.controller.playing = True
|
||||||
|
self.manual_g1_reference = False
|
||||||
self.step = 0
|
self.step = 0
|
||||||
self.replan_timer = 0.0
|
self.replan_timer = 0.0
|
||||||
self.last_ms = _snapshot_ms(self.ms)
|
self.last_ms = _snapshot_ms(self.ms)
|
||||||
@@ -142,10 +150,36 @@ class SonicWholeBodyController:
|
|||||||
if lowstate is None:
|
if lowstate is None:
|
||||||
return {}
|
return {}
|
||||||
obs = lowstate_to_obs(lowstate)
|
obs = lowstate_to_obs(lowstate)
|
||||||
return self._runtime.tick(obs, debug=False)
|
q_ref = _joint_reference_from_action(action)
|
||||||
|
if q_ref is not None:
|
||||||
|
self._runtime.manual_g1_reference = True
|
||||||
|
body_quat = np.array(
|
||||||
|
[
|
||||||
|
obs.get("imu.quat.w", 1.0),
|
||||||
|
obs.get("imu.quat.x", 0.0),
|
||||||
|
obs.get("imu.quat.y", 0.0),
|
||||||
|
obs.get("imu.quat.z", 0.0),
|
||||||
|
],
|
||||||
|
dtype=np.float64,
|
||||||
|
)
|
||||||
|
self.controller.set_manual_g1_reference(mujoco_to_isaaclab(q_ref), body_quat=body_quat)
|
||||||
|
return self._runtime.tick(obs, debug=False, use_joystick=not self._runtime.manual_g1_reference)
|
||||||
|
|
||||||
def reset(self):
|
def reset(self):
|
||||||
self._runtime.reset()
|
self._runtime.reset()
|
||||||
|
|
||||||
def shutdown(self):
|
def shutdown(self):
|
||||||
self._runtime.shutdown()
|
self._runtime.shutdown()
|
||||||
|
|
||||||
|
|
||||||
|
def _joint_reference_from_action(action: dict) -> np.ndarray | None:
|
||||||
|
"""Return a full 29-DOF reference if every joint .q key is present."""
|
||||||
|
if not action:
|
||||||
|
return None
|
||||||
|
q = np.zeros(29, dtype=np.float32)
|
||||||
|
for motor in G1_29_JointIndex:
|
||||||
|
key = f"{motor.name}.q"
|
||||||
|
if key not in action:
|
||||||
|
return None
|
||||||
|
q[motor.value] = float(action[key])
|
||||||
|
return q
|
||||||
|
|||||||
@@ -285,3 +285,338 @@ class G1_29_ArmIK: # noqa: N801
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"ERROR in convergence, plotting debug info.{e}")
|
logger.error(f"ERROR in convergence, plotting debug info.{e}")
|
||||||
return np.zeros(self.reduced_robot.model.nv)
|
return np.zeros(self.reduced_robot.model.nv)
|
||||||
|
|
||||||
|
|
||||||
|
_LEG_JOINT_NAMES_G1 = [
|
||||||
|
"left_hip_pitch_joint",
|
||||||
|
"left_hip_roll_joint",
|
||||||
|
"left_hip_yaw_joint",
|
||||||
|
"left_knee_joint",
|
||||||
|
"left_ankle_pitch_joint",
|
||||||
|
"left_ankle_roll_joint",
|
||||||
|
"right_hip_pitch_joint",
|
||||||
|
"right_hip_roll_joint",
|
||||||
|
"right_hip_yaw_joint",
|
||||||
|
"right_knee_joint",
|
||||||
|
"right_ankle_pitch_joint",
|
||||||
|
"right_ankle_roll_joint",
|
||||||
|
]
|
||||||
|
|
||||||
|
_LEFT_FOOT_FRAME = "left_ankle_roll_link"
|
||||||
|
_RIGHT_FOOT_FRAME = "right_ankle_roll_link"
|
||||||
|
|
||||||
|
|
||||||
|
def _homogeneous_matrix(rotation: np.ndarray, translation: np.ndarray) -> np.ndarray:
|
||||||
|
mat = np.eye(4, dtype=np.float64)
|
||||||
|
mat[:3, :3] = rotation
|
||||||
|
mat[:3, 3] = translation
|
||||||
|
return mat
|
||||||
|
|
||||||
|
|
||||||
|
class G1_29_LegIK: # noqa: N801
|
||||||
|
"""12-DOF leg IK (pelvis frame) targeting ankle roll link positions."""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
unit_test: bool = False,
|
||||||
|
max_iter: int = 50,
|
||||||
|
tol: float = 1e-6,
|
||||||
|
smoothing_weights: np.ndarray | None = None,
|
||||||
|
) -> None:
|
||||||
|
import casadi
|
||||||
|
import pinocchio as pin
|
||||||
|
from huggingface_hub import snapshot_download
|
||||||
|
from pinocchio import casadi as cpin
|
||||||
|
|
||||||
|
self._pin = pin
|
||||||
|
self.unit_test = unit_test
|
||||||
|
|
||||||
|
self.repo_path = snapshot_download("lerobot/unitree-g1-mujoco")
|
||||||
|
urdf_path = os.path.join(self.repo_path, "assets", "g1_body29_hand14.urdf")
|
||||||
|
mesh_dir = os.path.join(self.repo_path, "assets")
|
||||||
|
|
||||||
|
self.robot = self._pin.RobotWrapper.BuildFromURDF(urdf_path, mesh_dir)
|
||||||
|
|
||||||
|
joints_to_lock = [
|
||||||
|
"waist_yaw_joint",
|
||||||
|
"waist_roll_joint",
|
||||||
|
"waist_pitch_joint",
|
||||||
|
"left_shoulder_pitch_joint",
|
||||||
|
"left_shoulder_roll_joint",
|
||||||
|
"left_shoulder_yaw_joint",
|
||||||
|
"left_elbow_joint",
|
||||||
|
"left_wrist_roll_joint",
|
||||||
|
"left_wrist_pitch_joint",
|
||||||
|
"left_wrist_yaw_joint",
|
||||||
|
"right_shoulder_pitch_joint",
|
||||||
|
"right_shoulder_roll_joint",
|
||||||
|
"right_shoulder_yaw_joint",
|
||||||
|
"right_elbow_joint",
|
||||||
|
"right_wrist_roll_joint",
|
||||||
|
"right_wrist_pitch_joint",
|
||||||
|
"right_wrist_yaw_joint",
|
||||||
|
"left_hand_thumb_0_joint",
|
||||||
|
"left_hand_thumb_1_joint",
|
||||||
|
"left_hand_thumb_2_joint",
|
||||||
|
"left_hand_middle_0_joint",
|
||||||
|
"left_hand_middle_1_joint",
|
||||||
|
"left_hand_index_0_joint",
|
||||||
|
"left_hand_index_1_joint",
|
||||||
|
"right_hand_thumb_0_joint",
|
||||||
|
"right_hand_thumb_1_joint",
|
||||||
|
"right_hand_thumb_2_joint",
|
||||||
|
"right_hand_index_0_joint",
|
||||||
|
"right_hand_index_1_joint",
|
||||||
|
"right_hand_middle_0_joint",
|
||||||
|
"right_hand_middle_1_joint",
|
||||||
|
]
|
||||||
|
|
||||||
|
self.reduced_robot = self.robot.buildReducedRobot(
|
||||||
|
list_of_joints_to_lock=joints_to_lock,
|
||||||
|
reference_configuration=np.array([0.0] * self.robot.model.nq),
|
||||||
|
)
|
||||||
|
|
||||||
|
self._leg_joint_names_g1 = list(_LEG_JOINT_NAMES_G1)
|
||||||
|
self._leg_joint_names_pin = sorted(
|
||||||
|
self._leg_joint_names_g1,
|
||||||
|
key=lambda name: self.reduced_robot.model.idx_qs[self.reduced_robot.model.getJointId(name)],
|
||||||
|
)
|
||||||
|
self._leg_reorder_g1_to_pin = [
|
||||||
|
self._leg_joint_names_g1.index(name) for name in self._leg_joint_names_pin
|
||||||
|
]
|
||||||
|
self._leg_reorder_pin_to_g1 = np.argsort(self._leg_reorder_g1_to_pin)
|
||||||
|
|
||||||
|
self.left_foot_id = self.reduced_robot.model.getFrameId(_LEFT_FOOT_FRAME)
|
||||||
|
self.right_foot_id = self.reduced_robot.model.getFrameId(_RIGHT_FOOT_FRAME)
|
||||||
|
|
||||||
|
self.cmodel = cpin.Model(self.reduced_robot.model)
|
||||||
|
self.cdata = self.cmodel.createData()
|
||||||
|
|
||||||
|
self.cq = casadi.SX.sym("q", self.reduced_robot.model.nq, 1)
|
||||||
|
self.cTf_l = casadi.SX.sym("tf_l", 4, 4)
|
||||||
|
self.cTf_r = casadi.SX.sym("tf_r", 4, 4)
|
||||||
|
cpin.framesForwardKinematics(self.cmodel, self.cdata, self.cq)
|
||||||
|
|
||||||
|
self.translational_error = casadi.Function(
|
||||||
|
"leg_translational_error",
|
||||||
|
[self.cq, self.cTf_l, self.cTf_r],
|
||||||
|
[
|
||||||
|
casadi.vertcat(
|
||||||
|
self.cdata.oMf[self.left_foot_id].translation - self.cTf_l[:3, 3],
|
||||||
|
self.cdata.oMf[self.right_foot_id].translation - self.cTf_r[:3, 3],
|
||||||
|
)
|
||||||
|
],
|
||||||
|
)
|
||||||
|
self.rotational_error = casadi.Function(
|
||||||
|
"leg_rotational_error",
|
||||||
|
[self.cq, self.cTf_l, self.cTf_r],
|
||||||
|
[
|
||||||
|
casadi.vertcat(
|
||||||
|
cpin.log3(self.cdata.oMf[self.left_foot_id].rotation @ self.cTf_l[:3, :3].T),
|
||||||
|
cpin.log3(self.cdata.oMf[self.right_foot_id].rotation @ self.cTf_r[:3, :3].T),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
self.opti = casadi.Opti()
|
||||||
|
self.var_q = self.opti.variable(self.reduced_robot.model.nq)
|
||||||
|
self.var_q_last = self.opti.parameter(self.reduced_robot.model.nq)
|
||||||
|
self.param_tf_l = self.opti.parameter(4, 4)
|
||||||
|
self.param_tf_r = self.opti.parameter(4, 4)
|
||||||
|
self.translational_cost = casadi.sumsqr(
|
||||||
|
self.translational_error(self.var_q, self.param_tf_l, self.param_tf_r)
|
||||||
|
)
|
||||||
|
self.rotation_cost = casadi.sumsqr(self.rotational_error(self.var_q, self.param_tf_l, self.param_tf_r))
|
||||||
|
self.regularization_cost = casadi.sumsqr(self.var_q)
|
||||||
|
self.smooth_cost = casadi.sumsqr(self.var_q - self.var_q_last)
|
||||||
|
|
||||||
|
self.opti.subject_to(
|
||||||
|
self.opti.bounded(
|
||||||
|
self.reduced_robot.model.lowerPositionLimit,
|
||||||
|
self.var_q,
|
||||||
|
self.reduced_robot.model.upperPositionLimit,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self.opti.minimize(
|
||||||
|
50 * self.translational_cost
|
||||||
|
+ 0.5 * self.rotation_cost
|
||||||
|
+ 0.02 * self.regularization_cost
|
||||||
|
+ 0.1 * self.smooth_cost
|
||||||
|
)
|
||||||
|
|
||||||
|
opts = {
|
||||||
|
"ipopt": {"print_level": 0, "max_iter": max_iter, "tol": tol},
|
||||||
|
"print_time": False,
|
||||||
|
"calc_lam_p": False,
|
||||||
|
}
|
||||||
|
self.opti.solver("ipopt", opts)
|
||||||
|
|
||||||
|
self.init_data = np.zeros(self.reduced_robot.model.nq)
|
||||||
|
if smoothing_weights is None:
|
||||||
|
smoothing_weights = np.array([0.4, 0.3, 0.2, 0.1])
|
||||||
|
self.smooth_filter = WeightedMovingFilter(np.asarray(smoothing_weights, dtype=float), 12)
|
||||||
|
self._default_foot_rot_l: np.ndarray | None = None
|
||||||
|
self._default_foot_rot_r: np.ndarray | None = None
|
||||||
|
|
||||||
|
def _g1_leg_to_pin(self, q_g1: np.ndarray) -> np.ndarray:
|
||||||
|
q = np.asarray(q_g1, dtype=np.float64).reshape(12)
|
||||||
|
return q[self._leg_reorder_g1_to_pin]
|
||||||
|
|
||||||
|
def _pin_leg_to_g1(self, q_pin: np.ndarray) -> np.ndarray:
|
||||||
|
q = np.asarray(q_pin, dtype=np.float64).reshape(len(self._leg_joint_names_pin))
|
||||||
|
return q[self._leg_reorder_pin_to_g1]
|
||||||
|
|
||||||
|
def foot_poses(self, q_leg_g1: np.ndarray) -> tuple[np.ndarray, np.ndarray]:
|
||||||
|
"""Return 4x4 foot poses in the pelvis frame."""
|
||||||
|
q_pin = self._g1_leg_to_pin(q_leg_g1)
|
||||||
|
self._pin.forwardKinematics(self.reduced_robot.model, self.reduced_robot.data, q_pin)
|
||||||
|
self._pin.updateFramePlacements(self.reduced_robot.model, self.reduced_robot.data)
|
||||||
|
left = self.reduced_robot.data.oMf[self.left_foot_id].homogeneous
|
||||||
|
right = self.reduced_robot.data.oMf[self.right_foot_id].homogeneous
|
||||||
|
return left.copy(), right.copy()
|
||||||
|
|
||||||
|
def foot_positions(self, q_leg_g1: np.ndarray) -> tuple[np.ndarray, np.ndarray]:
|
||||||
|
left, right = self.foot_poses(q_leg_g1)
|
||||||
|
return left[:3, 3].copy(), right[:3, 3].copy()
|
||||||
|
|
||||||
|
def default_foot_state(
|
||||||
|
self, q_leg_g1: np.ndarray
|
||||||
|
) -> tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]:
|
||||||
|
"""Positions (3,) and rotations (3,3) for both feet at the given leg configuration."""
|
||||||
|
left, right = self.foot_poses(q_leg_g1)
|
||||||
|
return left[:3, 3], left[:3, :3], right[:3, 3], right[:3, :3]
|
||||||
|
|
||||||
|
def targets_from_xyz(
|
||||||
|
self,
|
||||||
|
left_xyz: np.ndarray,
|
||||||
|
right_xyz: np.ndarray,
|
||||||
|
left_rot: np.ndarray | None = None,
|
||||||
|
right_rot: np.ndarray | None = None,
|
||||||
|
) -> tuple[np.ndarray, np.ndarray]:
|
||||||
|
if left_rot is None:
|
||||||
|
if self._default_foot_rot_l is None:
|
||||||
|
raise RuntimeError("default foot orientation not set; call cache_default_orientation first")
|
||||||
|
left_rot = self._default_foot_rot_l
|
||||||
|
if right_rot is None:
|
||||||
|
if self._default_foot_rot_r is None:
|
||||||
|
raise RuntimeError("default foot orientation not set; call cache_default_orientation first")
|
||||||
|
right_rot = self._default_foot_rot_r
|
||||||
|
return (
|
||||||
|
_homogeneous_matrix(left_rot, np.asarray(left_xyz, dtype=np.float64)),
|
||||||
|
_homogeneous_matrix(right_rot, np.asarray(right_xyz, dtype=np.float64)),
|
||||||
|
)
|
||||||
|
|
||||||
|
def cache_default_orientation(self, q_leg_g1: np.ndarray) -> None:
|
||||||
|
_, rot_l, _, rot_r = self.default_foot_state(q_leg_g1)
|
||||||
|
self._default_foot_rot_l = rot_l
|
||||||
|
self._default_foot_rot_r = rot_r
|
||||||
|
|
||||||
|
def solve_ik(
|
||||||
|
self,
|
||||||
|
left_xyz: np.ndarray,
|
||||||
|
right_xyz: np.ndarray,
|
||||||
|
current_leg_q_g1: np.ndarray | None = None,
|
||||||
|
) -> np.ndarray:
|
||||||
|
"""Solve for 12 leg joint angles (G1 motor order) from foot positions in pelvis frame."""
|
||||||
|
if current_leg_q_g1 is not None:
|
||||||
|
self.init_data = self._g1_leg_to_pin(current_leg_q_g1)
|
||||||
|
|
||||||
|
left_tf, right_tf = self.targets_from_xyz(left_xyz, right_xyz)
|
||||||
|
self.opti.set_initial(self.var_q, self.init_data)
|
||||||
|
self.opti.set_value(self.param_tf_l, left_tf)
|
||||||
|
self.opti.set_value(self.param_tf_r, right_tf)
|
||||||
|
self.opti.set_value(self.var_q_last, self.init_data)
|
||||||
|
|
||||||
|
fallback = (
|
||||||
|
self._pin_leg_to_g1(self.init_data)
|
||||||
|
if current_leg_q_g1 is None
|
||||||
|
else np.asarray(current_leg_q_g1, dtype=np.float64)
|
||||||
|
)
|
||||||
|
converged = True
|
||||||
|
try:
|
||||||
|
self.opti.solve()
|
||||||
|
sol_q = self.opti.value(self.var_q)
|
||||||
|
except Exception as e:
|
||||||
|
converged = False
|
||||||
|
logger.error(f"Leg IK convergence error: {e}")
|
||||||
|
sol_q = self.opti.debug.value(self.var_q)
|
||||||
|
|
||||||
|
self.smooth_filter.add_data(sol_q)
|
||||||
|
sol_q = self.smooth_filter.filtered_data
|
||||||
|
self.init_data = sol_q
|
||||||
|
|
||||||
|
if not converged:
|
||||||
|
logger.warning("Leg IK did not converge; returning last solution")
|
||||||
|
return fallback
|
||||||
|
|
||||||
|
return self._pin_leg_to_g1(sol_q)
|
||||||
|
|
||||||
|
def solve_ik_dls(
|
||||||
|
self,
|
||||||
|
left_xyz: np.ndarray,
|
||||||
|
right_xyz: np.ndarray,
|
||||||
|
current_leg_q_g1: np.ndarray,
|
||||||
|
left_rot: np.ndarray | None = None,
|
||||||
|
right_rot: np.ndarray | None = None,
|
||||||
|
iters: int = 100,
|
||||||
|
damping: float = 1e-2,
|
||||||
|
max_step: float = 0.4,
|
||||||
|
pos_weight: float = 1.0,
|
||||||
|
rot_weight: float = 0.3,
|
||||||
|
tol: float = 1e-4,
|
||||||
|
) -> np.ndarray:
|
||||||
|
"""Fast damped-least-squares leg IK (sub-ms), warm-started from the current pose.
|
||||||
|
|
||||||
|
Iteratively Newton-steps ``q`` toward foot pose targets using the frame
|
||||||
|
Jacobian, instead of solving a full NLP. Ideal for interactive/real-time use
|
||||||
|
where the target moves in small increments each step.
|
||||||
|
"""
|
||||||
|
pin = self._pin
|
||||||
|
model = self.reduced_robot.model
|
||||||
|
data = self.reduced_robot.data
|
||||||
|
|
||||||
|
if left_rot is None:
|
||||||
|
left_rot = self._default_foot_rot_l
|
||||||
|
if right_rot is None:
|
||||||
|
right_rot = self._default_foot_rot_r
|
||||||
|
if left_rot is None or right_rot is None:
|
||||||
|
raise RuntimeError("default foot orientation not set; call cache_default_orientation first")
|
||||||
|
|
||||||
|
q = self._g1_leg_to_pin(np.asarray(current_leg_q_g1, dtype=np.float64))
|
||||||
|
lower = model.lowerPositionLimit
|
||||||
|
upper = model.upperPositionLimit
|
||||||
|
weights = np.tile(
|
||||||
|
np.array([pos_weight] * 3 + [rot_weight] * 3, dtype=np.float64), 2
|
||||||
|
) # 12-vector
|
||||||
|
|
||||||
|
targets = (
|
||||||
|
(self.left_foot_id, np.asarray(left_xyz, dtype=np.float64), np.asarray(left_rot)),
|
||||||
|
(self.right_foot_id, np.asarray(right_xyz, dtype=np.float64), np.asarray(right_rot)),
|
||||||
|
)
|
||||||
|
|
||||||
|
err = np.zeros(12)
|
||||||
|
jac = np.zeros((12, model.nv))
|
||||||
|
eye = np.eye(12)
|
||||||
|
for _ in range(iters):
|
||||||
|
pin.forwardKinematics(model, data, q)
|
||||||
|
pin.updateFramePlacements(model, data)
|
||||||
|
for k, (fid, pos, rot) in enumerate(targets):
|
||||||
|
target_se3 = pin.SE3(rot, pos)
|
||||||
|
# Pose error expressed in the foot's LOCAL frame: log( oMf^{-1} * target ).
|
||||||
|
local_err = pin.log6(data.oMf[fid].actInv(target_se3)).vector
|
||||||
|
err[6 * k : 6 * k + 6] = local_err
|
||||||
|
jac[6 * k : 6 * k + 6, :] = pin.computeFrameJacobian(model, data, q, fid, pin.LOCAL)
|
||||||
|
|
||||||
|
if np.linalg.norm(err) < tol:
|
||||||
|
break
|
||||||
|
we = weights * err
|
||||||
|
wj = weights[:, None] * jac
|
||||||
|
# dq = Jw^T (Jw Jw^T + λ² I)^{-1} e_w
|
||||||
|
dq = wj.T @ np.linalg.solve(wj @ wj.T + damping**2 * eye, we)
|
||||||
|
step = np.linalg.norm(dq)
|
||||||
|
if step > max_step:
|
||||||
|
dq *= max_step / step
|
||||||
|
q = pin.integrate(model, q, dq)
|
||||||
|
q = np.clip(q, lower, upper)
|
||||||
|
|
||||||
|
return self._pin_leg_to_g1(q)
|
||||||
|
|||||||
@@ -508,6 +508,10 @@ class UnitreeG1(Robot):
|
|||||||
for key in REMOTE_KEYS:
|
for key in REMOTE_KEYS:
|
||||||
if key in action:
|
if key in action:
|
||||||
self.controller_input[key] = action[key]
|
self.controller_input[key] = action[key]
|
||||||
|
for motor in G1_29_JointIndex:
|
||||||
|
key = f"{motor.name}.q"
|
||||||
|
if key in action:
|
||||||
|
self.controller_input[key] = action[key]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_calibrated(self) -> bool:
|
def is_calibrated(self) -> bool:
|
||||||
|
|||||||
@@ -104,6 +104,7 @@ from lerobot.teleoperators import ( # noqa: F401
|
|||||||
rebot_102_leader,
|
rebot_102_leader,
|
||||||
so_leader,
|
so_leader,
|
||||||
unitree_g1,
|
unitree_g1,
|
||||||
|
g1_sonic_slider as g1_sonic_slider_teleop,
|
||||||
)
|
)
|
||||||
from lerobot.utils.import_utils import register_third_party_plugins
|
from lerobot.utils.import_utils import register_third_party_plugins
|
||||||
from lerobot.utils.robot_utils import precise_sleep
|
from lerobot.utils.robot_utils import precise_sleep
|
||||||
|
|||||||
@@ -79,6 +79,10 @@ def make_teleoperator_from_config(config: TeleoperatorConfig) -> "Teleoperator":
|
|||||||
from .unitree_g1 import UnitreeG1Teleoperator
|
from .unitree_g1 import UnitreeG1Teleoperator
|
||||||
|
|
||||||
return UnitreeG1Teleoperator(config)
|
return UnitreeG1Teleoperator(config)
|
||||||
|
elif config.type == "g1_sonic_slider":
|
||||||
|
from .g1_sonic_slider import G1SonicSliderTeleop
|
||||||
|
|
||||||
|
return G1SonicSliderTeleop(config)
|
||||||
elif config.type == "bi_so_leader":
|
elif config.type == "bi_so_leader":
|
||||||
from .bi_so_leader import BiSOLeader
|
from .bi_so_leader import BiSOLeader
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user