Source code for kimodo.exports.mujoco

# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
"""Convert kimodo motion (y-up, z-forward) to MuJoCo qpos (z-up, x-forward) for G1 skeleton."""

import os
import xml.etree.ElementTree as ET
from typing import Optional

import numpy as np
import torch
from scipy.spatial.transform import Rotation

from kimodo.assets import skeleton_asset_path
from kimodo.geometry import (
    axis_angle_to_matrix,
    matrix_to_axis_angle,
    matrix_to_quaternion,
    quaternion_to_matrix,
)
from kimodo.skeleton import G1Skeleton34, SkeletonBase, global_rots_to_local_rots
from kimodo.tools import ensure_batched, to_numpy, to_torch

# Cache so that the same (skeleton, xml_path) returns the same converter instance.
_converter_cache: dict[tuple[int, str], "MujocoQposConverter"] = {}


[docs] class MujocoQposConverter: """Fast batch converter from our dictionary format to mujoco qpos with precomputed transforms. In mujoco, the coordination is z up and x forward, right handed. Features (30 joints): - root (pelvis, 7 = translation + rotation) + 29 dof joints (29) In kimodo, the coordinate system is y up and z forward, right handed. Features (34 joints): - root (pelvis) + (34 - 1) joints; among these joints, 4 are end-effector joints added by kimodo. Cached by (input_skeleton id, xml_path); repeated calls with the same args return the same instance. """ def __new__( cls, input_skeleton: SkeletonBase, xml_path: str = str(skeleton_asset_path("g1skel34", "xml", "g1.xml")), ): key = (id(input_skeleton), xml_path) if key not in _converter_cache: inst = object.__new__(cls) _converter_cache[key] = inst return _converter_cache[key]
[docs] def __init__( self, input_skeleton: SkeletonBase, xml_path: str = str(skeleton_asset_path("g1skel34", "xml", "g1.xml")), ): """Initialize converter with precomputed transforms. Args: xml_path: Path to the mujoco XML file containing joint definitions """ if getattr(self, "_initialized", False): return self.xml_path = xml_path self.skeleton = input_skeleton self._prepare_transforms() self._subtree_joints = {} self._initialized = True
def _prepare_transforms(self): """Precompute all necessary transforms for efficient batch processing.""" # Define coordinate transformations between mujoco and kimodo space # 1) R_zup_to_yup: rotation around x-axis by -90 degrees # 2) x_forward_to_y_forward: rotation around z-axis by -90 degrees # Combined transformation matrix: mujoco_to_kimodo = R_zup_to_yup * x_forward_to_y_forward self.mujoco_to_kimodo_matrix = torch.tensor( [[0.0, 1.0, 0.0], [0.0, 0.0, 1.0], [1.0, 0.0, 0.0]], dtype=torch.float32 ) self.kimodo_to_mujoco_matrix = self.mujoco_to_kimodo_matrix.T # Inverse transformation: kimodo_to_mujoco # Parse XML once and extract joint information tree = ET.parse(self.xml_path) root = tree.getroot() xml_classes = [x for x in tree.findall(".//default") if "class" in x.attrib] joint_axes = dict() class_ranges: dict[str, tuple[float, float]] = {} for xml_class in xml_classes: j = xml_class.findall("joint") if j: joint_axes[xml_class.get("class")] = j[0].get("axis") range_str = j[0].get("range") if range_str: range_vals = [float(x) for x in range_str.split()] if len(range_vals) == 2: class_ranges[xml_class.get("class")] = ( range_vals[0], range_vals[1], ) mujoco_hinge_joints = root.find("worldbody").findall(".//joint") # skip the base joint self._mujoco_joint_axis_values_kimodo_space = torch.zeros( (len(mujoco_hinge_joints), 3), dtype=torch.float32 ) # mujoco order but kimodo space self._mujoco_joint_axis_values_mujoco_space = torch.zeros( (len(mujoco_hinge_joints), 3), dtype=torch.float32 ) # mujoco order but mujoco space # for the below indices, mujoco_indices_to_kimodo_indices does not include mujoco root (30 - 1 = 29 elements), # while kimodo_indices_to_mujoco_indices inclues the kimodo root (32 elements). self._mujoco_indices_to_kimodo_indices = torch.zeros((len(mujoco_hinge_joints),), dtype=torch.int32) self._kimodo_indices_to_mujoco_indices = ( torch.ones((self.skeleton.nbjoints,), dtype=torch.int32) * -1 ) # -1 means not in the csv skeleton self._nb_joints_mujoco = len(mujoco_hinge_joints) + 1 self._nb_joints_kimodo = self.skeleton.nbjoints self._mujoco_joint_including_root_parent_list = torch.full( (len(mujoco_hinge_joints) + 1,), -1, dtype=torch.int32 ) self._mujoco_joint_including_root_list = ["pelvis_skel"] for joint_id_in_csv, joint in enumerate(mujoco_hinge_joints): joint_name_in_skeleton = joint.get("name").replace("_joint", "_skel") joint_parent_name_in_skeleton = self.skeleton.bone_parents[joint_name_in_skeleton] self._mujoco_joint_including_root_list.append(joint_name_in_skeleton) self._mujoco_joint_including_root_parent_list[joint_id_in_csv + 1] = ( self._mujoco_joint_including_root_list.index(joint_parent_name_in_skeleton) ) joint_idx_in_kimodo_skeleton = self.skeleton.bone_order_names.index(joint_name_in_skeleton) axis_values = [float(x) for x in (joint.get("axis") or joint_axes[joint.get("class")]).split(" ")] # the mapped axis in kimodo skeleton space is calculated as bones_axis = mujoco_to_kimodo.apply(axis_values) # [1, 0, 0] -> [0, 0, 1]; [0, 1, 0] -> [1, 0, 0]; [0, 0, 1] -> [0, 1, 0] mujoco_joint_axis_mapping_kimodo_space = [ torch.tensor([0, 0, 1]), torch.tensor([1, 0, 0]), torch.tensor([0, 1, 0]), ][np.argmax(axis_values)] self._mujoco_joint_axis_values_kimodo_space[joint_id_in_csv] = mujoco_joint_axis_mapping_kimodo_space self._mujoco_joint_axis_values_mujoco_space[joint_id_in_csv] = torch.tensor(axis_values) self._mujoco_indices_to_kimodo_indices[joint_id_in_csv] = joint_idx_in_kimodo_skeleton self._kimodo_indices_to_mujoco_indices[joint_idx_in_kimodo_skeleton] = ( joint_id_in_csv + 1 ) # +1 for the root self._kimodo_indices_to_mujoco_indices[0] = 0 # the root joint mapping # Joint limits (min, max) in radians for each mujoco hinge, for clamping self._joint_limits_min = torch.full((len(mujoco_hinge_joints),), float("-inf"), dtype=torch.float32) self._joint_limits_max = torch.full((len(mujoco_hinge_joints),), float("inf"), dtype=torch.float32) for joint_id_in_csv, joint in enumerate(mujoco_hinge_joints): range_vals = None if joint.get("range"): range_vals = [float(x) for x in joint.get("range").split()] elif joint.get("class") and joint.get("class") in class_ranges: lo, hi = class_ranges[joint.get("class")] range_vals = [lo, hi] if range_vals is not None and len(range_vals) == 2: self._joint_limits_min[joint_id_in_csv] = range_vals[0] self._joint_limits_max[joint_id_in_csv] = range_vals[1] # load the offset matrices from the xml R_zup_to_yup = Rotation.from_euler("x", -90, degrees=True) x_forward_to_y_forward = Rotation.from_euler("z", -90, degrees=True) mujoco_to_kimodo = R_zup_to_yup * x_forward_to_y_forward self._rot_offsets_q2t = torch.zeros(len(self._kimodo_indices_to_mujoco_indices), 3, 3, dtype=torch.float32) self._rot_offsets_q2t[...] = torch.eye(3)[None] self._rot_offsets_f2q = torch.zeros(len(self._kimodo_indices_to_mujoco_indices), 3, 3, dtype=torch.float32) self._rot_offsets_f2q[...] = torch.eye(3)[None] parent_map = {child: parent for parent in root.iter() for child in parent} for i, joint in enumerate(mujoco_hinge_joints): body = parent_map[joint] if "quat" in body.attrib: rot = Rotation.from_quat( [float(x) for x in body.get("quat").strip().split(" ")], scalar_first=True, ) idx = self._mujoco_indices_to_kimodo_indices[i] self._rot_offsets_q2t[idx] = torch.from_numpy(rot.as_matrix()) rot = mujoco_to_kimodo * rot * mujoco_to_kimodo.inv() self._rot_offsets_f2q[idx] = torch.from_numpy(rot.as_matrix().T) # Hinge axis in f2q space so extraction uses the same frame as joint_rot_f2q. # Then extract(offset) gives the angle s.t. axis_angle(angle * axis_f2q) = offset, and # reconstruction R_local = offset.T @ axis_angle(angle * axis_f2q) = I when input is identity. axis_kimodo = self._mujoco_joint_axis_values_kimodo_space self._mujoco_joint_axis_values_f2q_space = torch.zeros_like(axis_kimodo) for i in range(len(mujoco_hinge_joints)): j = self._mujoco_indices_to_kimodo_indices[i].item() axis_f2q = torch.mv(self._rot_offsets_f2q[j], axis_kimodo[i]) n = axis_f2q.norm() if n > 1e-8: axis_f2q = axis_f2q / n self._mujoco_joint_axis_values_f2q_space[i] = axis_f2q # Rest-pose DOFs: angle we extract when R_local = I (t-pose). MuJoCo limits are # relative to joint zero (rest pose), so we must clamp in MuJoCo space: convert # joint_dofs to mujoco_angle = joint_dofs - rest_dofs, clamp, then back. rest_rot_f2q = self._rot_offsets_f2q[self._mujoco_indices_to_kimodo_indices] rest_rot_f2q = rest_rot_f2q.unsqueeze(0).unsqueeze(0) self._rest_dofs = self._local_rots_f2q_to_joint_dofs(rest_rot_f2q).squeeze(0).squeeze(0) # Axis-angle rest DOFs: angle s.t. axis_angle(angle * axis_f2q) = offset. Used in # project_to_real_robot_rotations so extract+reconstruct round-trip and t-pose is preserved. rest_rot_f2q_flat = self._rot_offsets_f2q[self._mujoco_indices_to_kimodo_indices] full_aa = matrix_to_axis_angle(rest_rot_f2q_flat) self._rest_dofs_axis_angle = (full_aa * self._mujoco_joint_axis_values_f2q_space).sum(dim=-1)
[docs] def dict_to_qpos( self, output: dict, device: Optional[str] = None, root_quat_w_first: bool = True, numpy: bool = True, mujoco_rest_zero: bool = False, ): """Convert kimodo output dict to mujoco qpos format. Args: output: dict with keys "local_rot_mats" and "root_positions". device: device to use for the output. root_quat_w_first: If True, quaternion in qpos is (w,x,y,z). numpy: If True, convert the output to numpy array. mujoco_rest_zero: If True, joint angles are written so that kimodo rest (t-pose) maps to q=0 in MuJoCo. If False, write raw joint_dofs. Returns: qpos: (B, T, 7+J) mujoco qpos format. """ local_rot_mats = to_torch(output["local_rot_mats"], device) root_positions = to_torch(output["root_positions"], device) qpos = self.to_qpos( local_rot_mats, root_positions, root_quat_w_first=root_quat_w_first, mujoco_rest_zero=mujoco_rest_zero, ) if numpy: qpos = to_numpy(qpos) return qpos
[docs] def qpos_to_motion_dict( self, qpos: torch.Tensor | np.ndarray, source_fps: float, *, root_quat_w_first: bool = True, mujoco_rest_zero: bool = False, ): """Inverse of :meth:`to_qpos` / :meth:`dict_to_qpos` for MuJoCo CSV ``(T, 36)`` rows. Args: qpos: Shape ``(T, 36)`` or ``(1, T, 36)`` (root xyz, root quat wxyz, 29 joint angles). source_fps: Source frame rate (Hz) of the qpos data. root_quat_w_first: Must match how the CSV was written (default ``True``). mujoco_rest_zero: Must match :meth:`dict_to_qpos` / :meth:`to_qpos`. Returns: Kimodo motion dict (see :func:`kimodo.exports.motion_io.complete_motion_dict`). """ from kimodo.exports.motion_io import complete_motion_dict qpos = to_torch(qpos, None) if qpos.dim() == 2: qpos = qpos.unsqueeze(0) device = qpos.device dtype = qpos.dtype batch_size, num_frames, ncols = qpos.shape if ncols != 36: raise ValueError(f"Expected qpos last dim 36; got {ncols}") kimodo_to_mujoco_matrix = self.kimodo_to_mujoco_matrix.to(device=device, dtype=dtype) mujoco_to_kimodo_matrix = kimodo_to_mujoco_matrix.T root_mujoco = qpos[..., :3] root_positions = torch.matmul(mujoco_to_kimodo_matrix[None, None, ...], root_mujoco[..., None]).squeeze(-1) quat = qpos[..., 3:7] if root_quat_w_first: root_rot_mujoco = quaternion_to_matrix(quat) else: quat_wxyz = quat[..., [3, 0, 1, 2]] root_rot_mujoco = quaternion_to_matrix(quat_wxyz) O0 = self._rot_offsets_f2q[0].to(device=device, dtype=dtype) # root_rot_mujoco is (..., 3, 3) after optional batch unsqueeze (e.g. (1, T, 3, 3)). # Use ``...il`` so ``k`` sums with ``kl``; ``...ik`` incorrectly keeps ``k`` in the output. R_f2q_root = torch.einsum( "ij,...jk,kl->...il", mujoco_to_kimodo_matrix, root_rot_mujoco, kimodo_to_mujoco_matrix, ) R_kimodo_root = torch.einsum("ij,...jk->...ik", O0.T, R_f2q_root) joint_dofs = qpos[..., 7:] if mujoco_rest_zero: rest_dofs = self._rest_dofs.to(device=device, dtype=dtype) angles = joint_dofs + rest_dofs[None, None, :] use_relative = True else: angles = joint_dofs use_relative = False nb_joints = self.skeleton.nbjoints template = torch.eye(3, device=device, dtype=dtype).expand(batch_size, num_frames, nb_joints, 3, 3).contiguous() template[:, :, 0] = R_kimodo_root local_rot_mats = self._joint_dofs_to_local_rot_mats( angles, template, device, dtype, use_relative=use_relative, ) if batch_size != 1: raise ValueError(f"Only a single clip is supported; got batch_size={batch_size}") return complete_motion_dict(local_rot_mats[0], root_positions[0], self.skeleton, source_fps)
[docs] def save_csv(self, qpos: torch.Tensor | np.ndarray, csv_path): # comment this qpos = to_numpy(qpos) shape = qpos.shape if len(shape) == 2: # only one motion: save it np.savetxt(csv_path, qpos, delimiter=",") if len(shape) == 3: # batch of motions if shape[0] == 1: # if only one motion, just save it np.savetxt(csv_path, qpos[0], delimiter=",") else: csv_path_base, ext = os.path.splitext(csv_path) for i in range(shape[0]): self.save_csv(qpos[i], csv_path_base + "_" + str(i).zfill(2) + ext)
def _local_rots_to_joint_dofs( self, local_rot_mats: torch.Tensor, axis_vals: torch.Tensor, ) -> torch.Tensor: """Extract per-joint single-DoF angles (radians) via Euler projection (for to_qpos/f2q).""" x_joint_dof = torch.atan2(local_rot_mats[..., 2, 1], local_rot_mats[..., 2, 2]) y_joint_dof = torch.atan2(local_rot_mats[..., 0, 2], local_rot_mats[..., 0, 0]) z_joint_dof = torch.atan2(local_rot_mats[..., 1, 0], local_rot_mats[..., 1, 1]) xyz_joint_dofs = torch.stack([x_joint_dof, y_joint_dof, z_joint_dof], dim=-1) axis_vals = axis_vals.to(device=local_rot_mats.device, dtype=local_rot_mats.dtype) joint_dofs = (xyz_joint_dofs * axis_vals[None, None, :, :]).sum(dim=-1) return joint_dofs def _local_rots_to_joint_dofs_axis_angle( self, local_rot_mats: torch.Tensor, axis_vals: torch.Tensor, ) -> torch.Tensor: """Extract per-joint single-DoF angles (radians) via axis-angle; round-trips with axis_angle_to_matrix. Args: local_rot_mats: (..., num_hinges, 3, 3) in same frame as axis_vals. axis_vals: (num_hinges, 3) unit axis per hinge. Returns: joint_dofs: (..., num_hinges) signed angle = dot(axis_angle(R), axis). """ axis_vals = axis_vals.to(device=local_rot_mats.device, dtype=local_rot_mats.dtype) full_aa = matrix_to_axis_angle(local_rot_mats) joint_dofs = (full_aa * axis_vals).sum(dim=-1) return joint_dofs def _local_rots_f2q_to_joint_dofs(self, local_rot_mats_f2q: torch.Tensor) -> torch.Tensor: """Extract per-joint single-DoF angles from local rotations in f2q space (for to_qpos).""" axis_vals = self._mujoco_joint_axis_values_f2q_space return self._local_rots_to_joint_dofs(local_rot_mats_f2q, axis_vals) def _clamp_to_limits(self, joint_dofs: torch.Tensor) -> torch.Tensor: """Clamp joint angles to XML limits (radians). Angles are in kimodo convention (0 = rest). """ device = joint_dofs.device lo = self._joint_limits_min.to(device=device, dtype=joint_dofs.dtype) hi = self._joint_limits_max.to(device=device, dtype=joint_dofs.dtype) return torch.clamp(joint_dofs, lo[None, None, :], hi[None, None, :]) def _clamp_joint_dofs(self, joint_dofs: torch.Tensor, rest_dofs: torch.Tensor) -> torch.Tensor: """Clamp joint angles to MuJoCo limits (radians), with rest_dofs conversion.""" device = joint_dofs.device rest_dofs = rest_dofs.to(device=device, dtype=joint_dofs.dtype) mujoco_dofs = joint_dofs - rest_dofs[None, None, :] lo = self._joint_limits_min.to(device=device, dtype=joint_dofs.dtype) hi = self._joint_limits_max.to(device=device, dtype=joint_dofs.dtype) mujoco_dofs = torch.clamp(mujoco_dofs, lo[None, None, :], hi[None, None, :]) return mujoco_dofs + rest_dofs[None, None, :] def _joint_dofs_to_local_rot_mats( self, joint_dofs: torch.Tensor, original_local_rot_mats: torch.Tensor, device: torch.device, dtype: torch.dtype, use_relative: bool = False, ) -> torch.Tensor: """Reconstruct full local rotation matrices from 1-DoF angles.""" out = original_local_rot_mats.clone() axis_kimodo = self._mujoco_joint_axis_values_kimodo_space.to(device=device, dtype=dtype) for i in range(joint_dofs.shape[-1]): j = self._mujoco_indices_to_kimodo_indices[i].item() angle = joint_dofs[..., i] axis = axis_kimodo[i] if use_relative: axis_angle = angle[..., None] * axis[None, None, :] R_local = axis_angle_to_matrix(axis_angle) else: rot_offsets_f2q = self._rot_offsets_f2q.to(device=device, dtype=dtype) axis_in_f2q = torch.mv(rot_offsets_f2q[j], axis) axis_angle = angle[..., None] * axis_in_f2q[None, None, :] R_f2q = axis_angle_to_matrix(axis_angle) R_local = torch.einsum("ij,btjk->btik", rot_offsets_f2q[j].T, R_f2q) out[:, :, j, :, :] = R_local return out
[docs] @ensure_batched(local_rot_mats=5, root_positions=3, lengths=1) def project_to_real_robot_rotations( self, local_rot_mats: torch.Tensor, root_positions: torch.Tensor, clamp_to_limits: bool = True, mujoco_rest_zero: bool = False, ) -> dict: """Project full 3D local rotations to G1 real robot DoF and back to 3D for viz. Joint angles are extracted along each hinge axis, optionally clamped to XML limits, then reconstructed to 3D rotations. When mujoco_rest_zero=False (default), raw angles are used (baked-with-quat). When True, angles are relative to rest (0 = T-pose in MuJoCo). """ device = local_rot_mats.device dtype = local_rot_mats.dtype # Transform to f2q frame and extract 1-DoF angles (axis-angle projection). local_rot_f2q = torch.matmul(self._rot_offsets_f2q.to(device=device, dtype=dtype), local_rot_mats) hinge_rots = local_rot_f2q[:, :, self._mujoco_indices_to_kimodo_indices, :, :] axis_f2q = self._mujoco_joint_axis_values_f2q_space.to(device=device, dtype=dtype) joint_dofs = self._local_rots_to_joint_dofs_axis_angle(hinge_rots, axis_f2q) # Optionally express angles relative to rest (MuJoCo q=0 at T-pose). if mujoco_rest_zero: rest_dofs = self._rest_dofs_axis_angle.to(device=device, dtype=dtype) angles = joint_dofs - rest_dofs[None, None, :] use_relative = True else: angles = joint_dofs use_relative = False if clamp_to_limits: if mujoco_rest_zero: angles = self._clamp_to_limits(angles) else: rest_dofs_aa = self._rest_dofs_axis_angle.to(device=device, dtype=dtype) angles = self._clamp_joint_dofs(angles, rest_dofs_aa) # Reconstruct 3D local rotations from 1-DoF angles and run FK. local_rot_mats_proj = self._joint_dofs_to_local_rot_mats( angles, local_rot_mats, device, dtype, use_relative=use_relative ) global_rot_mats, posed_joints, _ = self.skeleton.fk(local_rot_mats_proj, root_positions) return { "local_rot_mats": local_rot_mats_proj, "global_rot_mats": global_rot_mats, "posed_joints": posed_joints, "root_positions": root_positions, }
[docs] @ensure_batched(local_rot_mats=5, root_positions=3, lengths=1) def to_qpos( self, local_rot_mats: torch.Tensor, root_positions: torch.Tensor, root_quat_w_first: bool = True, mujoco_rest_zero: bool = False, ) -> torch.Tensor: """Fast batch conversion from kimodo features to mujoco qpos format. Args: local_rot_mats: (B, T, J, 3, 3) local rotation matrices (kimodo convention). root_positions: (B, T, 3) root positions. root_quat_w_first: If True, quaternion in qpos is (w,x,y,z). mujoco_rest_zero: If True, joint angles are written so that kimodo rest (t-pose) maps to q=0 in MuJoCo. If False, write raw joint_dofs. Returns: torch.Tensor of shape [batch, numFrames, 36] containing mujoco qpos data: - root_trans (3) + root_quat (4) + joint_dofs (29) = 36 columns """ batch_size, num_frames, nb_joints = local_rot_mats.shape[:3] device, dtype = local_rot_mats.device, local_rot_mats.dtype local_rot_mats = torch.matmul(self._rot_offsets_f2q.to(device), local_rot_mats) batch_size, num_frames = root_positions.shape[0], root_positions.shape[1] # Move precomputed matrices to the same device/dtype kimodo_to_mujoco_matrix = self.kimodo_to_mujoco_matrix.to(device=device, dtype=dtype) # Initialize output tensor: [batch, numFrames, 36] qpos = torch.zeros((batch_size, num_frames, 36), dtype=dtype, device=device) # Convert root translation: apply coordinate transformation root_positions_mujoco = torch.matmul(kimodo_to_mujoco_matrix[None, None, ...], root_positions[..., None]) qpos[:, :, :3] = root_positions_mujoco.view(batch_size, num_frames, 3) # Convert root rotation: apply coordinate transformation to rotation matrix root_rot = local_rot_mats[:, :, 0, :] # [batch, numFrames, 3, 3] # Apply coordinate transformation: R_mujoco = kimodo_to_mujoco * R_kimodo * kimodo_to_mujoco^T mujoco_to_kimodo_matrix = kimodo_to_mujoco_matrix.T root_rot_mujoco = torch.matmul( torch.matmul(kimodo_to_mujoco_matrix[None, None, ...], root_rot), mujoco_to_kimodo_matrix[None, None, ...], ) root_rot_quat = matrix_to_quaternion(root_rot_mujoco) # [w, x, y, z] if root_quat_w_first: qpos[:, :, 3:7] = root_rot_quat[:, :, [0, 1, 2, 3]] # [w, x, y, z] else: qpos[:, :, 3:7] = root_rot_quat[:, :, [1, 2, 3, 0]] # [w, x, y, z] -> [x, y, z, w] # Joint DOFs: raw angles or relative to rest (rest = q=0 in MuJoCo). joint_rot_f2q = local_rot_mats[:, :, self._mujoco_indices_to_kimodo_indices, :, :] joint_dofs = self._local_rots_f2q_to_joint_dofs(joint_rot_f2q) if mujoco_rest_zero: rest_dofs = self._rest_dofs.to(device=device, dtype=dtype) qpos[:, :, 7:] = joint_dofs - rest_dofs[None, None, :] else: qpos[:, :, 7:] = joint_dofs return qpos
[docs] def apply_g1_real_robot_projection( skeleton: G1Skeleton34, joints_pos: torch.Tensor, joints_rot: torch.Tensor, clamp_to_limits: bool = True, ) -> tuple[torch.Tensor, torch.Tensor]: """Project G1 motion to real robot DoF (1-DoF per joint) with optional axis limits. Extracts a single angle per hinge along its axis (1-DoF), optionally clamps to joint limits from the MuJoCo XML (when clamp_to_limits=True), then reconstructs 3D rotations and runs FK. T-pose (identity local rotations) is preserved. Args: skeleton: G1 skeleton instance. joints_pos: (T, J, 3) or (B, T, J, 3) joint positions in global space. joints_rot: (T, J, 3, 3) or (B, T, J, 3, 3) global rotation matrices. clamp_to_limits: If True, clamp joint angles to XML axis limits (default True). Returns: (posed_joints, global_rot_mats) as tensors, same shape as inputs (batch preserved). """ local_rot_mats = global_rots_to_local_rots(joints_rot, skeleton) root_positions = joints_pos[..., skeleton.root_idx, :] # Converter expects batch dim (B, T, ...); add and remove if single sequence. single_sequence = local_rot_mats.dim() == 4 if single_sequence: local_rot_mats = local_rot_mats.unsqueeze(0) root_positions = root_positions.unsqueeze(0) converter = MujocoQposConverter(skeleton) projected = converter.project_to_real_robot_rotations( local_rot_mats, root_positions, clamp_to_limits=clamp_to_limits ) out_pos = projected["posed_joints"] out_rot = projected["global_rot_mats"] if single_sequence: out_pos = out_pos.squeeze(0) out_rot = out_rot.squeeze(0) return out_pos, out_rot