Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion source/isaaclab/config/extension.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[package]

# Note: Semantic Versioning is used: https://semver.org/
version = "4.5.22"
version = "4.5.23"

# Description
title = "Isaac Lab framework for Robot Learning"
Expand Down
13 changes: 13 additions & 0 deletions source/isaaclab/docs/CHANGELOG.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,19 @@
Changelog
---------

4.5.23 (2026-03-16)
~~~~~~~~~~~~~~~~~~~

Added
^^^^^

* Added :attr:`~isaaclab.envs.mdp.actions.PinkInverseKinematicsActionCfg.expect_base_link_frame`
flag. When ``True``, incoming action poses are assumed to already be in the
robot base link frame and the internal world-to-base-link transform in
:meth:`~isaaclab.envs.mdp.actions.PinkInverseKinematicsAction.process_actions`
is skipped.


4.5.22 (2026-03-16)
~~~~~~~~~~~~~~~~~~~

Expand Down
11 changes: 11 additions & 0 deletions source/isaaclab/isaaclab/envs/mdp/actions/pink_actions_cfg.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,14 @@ class PinkInverseKinematicsActionCfg(ActionTermCfg):
This dictionary should map the task names (e.g., 'left_wrist', 'right_wrist') to the
corresponding link names in the URDF that will be controlled by the IK solver.
"""

expect_base_link_frame: bool = False
"""When ``True``, incoming action poses are assumed to already be in the
robot base link frame and the internal world-to-base-link transform is
skipped.

Set this to ``True`` when the teleop device applies the rebase transform
itself (e.g. via :attr:`~isaaclab_teleop.IsaacTeleopCfg.target_frame_prim_path`
or an explicit ``target_T_world`` argument to
:meth:`~isaaclab_teleop.IsaacTeleopDevice.advance`).
"""
Original file line number Diff line number Diff line change
Expand Up @@ -192,20 +192,27 @@ def process_actions(self, actions: torch.Tensor) -> None:
"""Process the input actions and set targets for each task.

Args:
actions: The input actions tensor.
actions: The input actions tensor. Poses are expected in the
simulation world frame by default. When
:attr:`~PinkInverseKinematicsActionCfg.expect_base_link_frame`
is ``True``, poses must already be in the robot base link
frame (the internal world-to-base-link transform is skipped).
"""
# Store raw actions
self._raw_actions[:] = actions

# Extract hand joint positions directly (no cloning needed)
self._target_hand_joint_positions = actions[:, -self.hand_joint_dim :]

# Get base link frame transformation
self.base_link_frame_in_world_rf = self._get_base_link_frame_transform()

# Process controlled frame poses (pass original actions, no clone needed)
controlled_frame_poses = self._extract_controlled_frame_poses(actions)
transformed_poses = self._transform_poses_to_base_link_frame(controlled_frame_poses)

if self.cfg.expect_base_link_frame:
positions, rotation_matrices = math_utils.unmake_pose(controlled_frame_poses)
transformed_poses = (positions, rotation_matrices)
else:
self.base_link_frame_in_world_rf = self._get_base_link_frame_transform()
transformed_poses = self._transform_poses_to_base_link_frame(controlled_frame_poses)

# Set targets for all tasks
self._set_task_targets(transformed_poses)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -429,6 +429,10 @@ def __post_init__(self):
# Set the URDF path for the IK controller. Path resolution (Nucleus → local) happens at runtime.
self.actions.upper_body_ik.controller.urdf_path = f"{ISAACLAB_NUCLEUS_DIR}/Controllers/LocomanipulationAssets/unitree_g1_kinematics_asset/g1_29dof_with_hand_only_kinematics.urdf" # noqa: E501

# IsaacTeleop rebases poses into the pelvis frame via target_frame_prim_path,
# so the IK action term can skip its internal world-to-base-link transform.
self.actions.upper_body_ik.expect_base_link_frame = True

self.xr = XrCfg(
anchor_pos=(0.0, 0.0, -0.95),
anchor_rot=(0.0, 0.0, 0.0, 1.0),
Expand All @@ -441,4 +445,5 @@ def __post_init__(self):
pipeline_builder=_build_g1_locomanipulation_pipeline,
sim_device=self.sim.device,
xr_cfg=self.xr,
target_frame_prim_path="/World/envs/env_0/Robot/pelvis",
)
2 changes: 1 addition & 1 deletion source/isaaclab_teleop/config/extension.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
# Semantic Versioning is used: https://semver.org/
version = "0.3.3"
version = "0.3.5"

# Description
title = "Isaac Lab Teleop"
Expand Down
26 changes: 26 additions & 0 deletions source/isaaclab_teleop/docs/CHANGELOG.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,32 @@
Changelog
---------

0.3.5 (2026-03-16)
~~~~~~~~~~~~~~~~~~~

Added
^^^^^

* Added :attr:`~isaaclab_teleop.IsaacTeleopCfg.target_frame_prim_path` for
config-driven frame rebasing. When set to a USD prim path, the device
automatically reads the prim's world transform each frame and uses its
inverse as the ``target_T_world`` rebase matrix, so all output poses are
expressed in the target frame (e.g. robot base link for IK).


0.3.4 (2026-03-16)
~~~~~~~~~~~~~~~~~~~

Added
^^^^^

* Added ``target_T_world`` parameter to
:meth:`~isaaclab_teleop.IsaacTeleopDevice.advance` for rebasing all output
poses into an arbitrary target coordinate frame (e.g. robot base link for
IK). Accepts :class:`numpy.ndarray`, :class:`torch.Tensor`, or
``wp.array``.


0.3.3 (2026-03-13)
~~~~~~~~~~~~~~~~~~~

Expand Down
21 changes: 21 additions & 0 deletions source/isaaclab_teleop/isaaclab_teleop/isaac_teleop_cfg.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,5 +108,26 @@ def build_pipeline():
If ``None``, the tuning UI will not be opened.
"""

target_frame_prim_path: str | None = None
"""Optional USD prim path whose world frame becomes the target coordinate
frame for all output poses.

When set, the device automatically reads this prim's world transform each
frame and uses its inverse as the ``target_T_world`` rebase matrix in
:meth:`~isaaclab_teleop.IsaacTeleopDevice.advance`. An explicit
``target_T_world`` argument to :meth:`~isaaclab_teleop.IsaacTeleopDevice.advance`
takes precedence over this config.

Typical usage: set to the robot base link prim path so that an IK
controller receives end-effector poses in the robot's base frame.

Example::

IsaacTeleopCfg(
target_frame_prim_path="/World/envs/env_0/Robot/base_link",
...
)
"""

app_name: str = "IsaacLabTeleop"
"""Application name for the IsaacTeleop session."""
124 changes: 123 additions & 1 deletion source/isaaclab_teleop/isaaclab_teleop/isaac_teleop_device.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import logging
from collections.abc import Callable

import numpy as np
import torch

from .command_handler import CommandHandler
Expand Down Expand Up @@ -42,6 +43,24 @@ class IsaacTeleopDevice:
The device uses IsaacTeleop's TensorReorderer to flatten pipeline outputs
into a single action tensor matching the environment's action space.

Frame rebasing:
By default, all output poses are expressed in the simulation world
frame. When an application needs poses in a different frame (e.g.
robot base link for IK), there are two options:

* **Config-driven** (recommended): set
:attr:`~IsaacTeleopCfg.target_frame_prim_path` to the USD prim
whose frame the output should be expressed in. The device reads
the prim's world transform each frame and applies the rebase
automatically.
* **Explicit**: pass a ``target_T_world`` matrix directly to
:meth:`advance`.

In both cases the device composes
``target_T_world @ world_T_anchor`` before feeding the matrix into
the retargeting pipeline, so all resulting poses are expressed in the
target frame.

Teleop commands:
The device supports callbacks for START, STOP, and RESET commands
that can be triggered via XR controller buttons or the message bus.
Expand All @@ -54,10 +73,25 @@ class IsaacTeleopDevice:
sim_device="cuda:0",
)

# Poses in world frame (default)
with IsaacTeleopDevice(cfg) as device:
while running:
action = device.advance()
env.step(action.repeat(num_envs, 1))

# Config-driven rebase into robot base frame
cfg.target_frame_prim_path = "/World/Robot/base_link"
with IsaacTeleopDevice(cfg) as device:
while running:
action = device.advance()
env.step(action.repeat(num_envs, 1))

# Explicit rebase into robot base frame
with IsaacTeleopDevice(cfg) as device:
while running:
robot_T_world = get_robot_base_transform()
action = device.advance(target_T_world=robot_T_world)
env.step(action.repeat(num_envs, 1))
"""

def __init__(self, cfg: IsaacTeleopCfg):
Expand Down Expand Up @@ -147,14 +181,34 @@ def add_callback(self, key: str, func: Callable) -> None:
"""
self._command_handler.add_callback(key, func)

def advance(self) -> torch.Tensor | None:
def advance(self, target_T_world: np.ndarray | torch.Tensor | None = None) -> torch.Tensor | None:
"""Process current device state and return control commands.

If the IsaacTeleop session has not been started yet (because the OpenXR
handles were not available at ``__enter__`` time), this method will
attempt to start it on each call. Once the user clicks "Start AR" and
the handles become available, the session is created transparently.

Args:
target_T_world: Optional 4x4 transform matrix that rebases all
output poses into an arbitrary target coordinate frame. When
provided, the matrix sent to the retargeting pipeline becomes
``target_T_world @ world_T_anchor`` instead of just
``world_T_anchor``, so all resulting poses are expressed in
the target frame rather than the simulation world frame.

Typical use case: pass ``robot_base_T_world`` so that an IK
controller receives end-effector poses in the robot's base
link frame.

Accepts :class:`numpy.ndarray`, :class:`torch.Tensor`, or any
object with a ``.numpy()`` method (e.g. ``wp.array``).

When ``None`` and
:attr:`~IsaacTeleopCfg.target_frame_prim_path` is set, the
transform is computed automatically by reading the prim's
world matrix from Fabric and inverting it.

Returns:
A flattened action :class:`torch.Tensor` ready for the Isaac Lab
environment, or ``None`` if the session has not started yet
Expand All @@ -163,9 +217,14 @@ def advance(self) -> torch.Tensor | None:
Raises:
RuntimeError: If called outside of a context manager.
"""
# Auto-compute target_T_world from config if not explicitly provided
if target_T_world is None and self._cfg.target_frame_prim_path is not None:
target_T_world = self._get_target_frame_T_world()

# Step the session (handles lazy start and action extraction)
action = self._session_lifecycle.step(
anchor_world_matrix_fn=self._anchor_manager.get_world_matrix,
target_T_world=target_T_world,
)

if action is not None:
Expand All @@ -174,6 +233,69 @@ def advance(self) -> torch.Tensor | None:

return action

# ------------------------------------------------------------------
# Target frame transform (config-driven rebase)
# ------------------------------------------------------------------

def _get_target_frame_T_world(self) -> np.ndarray | None:
"""Read the target-frame prim's world matrix from Fabric and return its inverse.

Uses USDRT to read the prim's hierarchical world matrix, matching the
pattern used by :class:`XrAnchorSynchronizer` for anchor prim reads.

Returns:
A (4, 4) float32 :class:`numpy.ndarray` representing the inverse
of the prim's world transform (i.e. ``target_T_world``), or
``None`` if the prim cannot be read.
"""
try:
import usdrt
from usdrt import Rt

from isaaclab.sim.utils.stage import get_current_stage_id

stage_id = get_current_stage_id()
rt_stage = usdrt.Usd.Stage.Attach(stage_id)
if rt_stage is None:
return None

rt_prim = rt_stage.GetPrimAtPath(self._cfg.target_frame_prim_path)
if rt_prim is None:
return None

rt_xformable = Rt.Xformable(rt_prim)
if rt_xformable is None:
return None

world_matrix_attr = rt_xformable.GetFabricHierarchyWorldMatrixAttr()
if world_matrix_attr is None:
return None

rt_matrix = world_matrix_attr.Get()
if rt_matrix is None:
return None

pos = rt_matrix.ExtractTranslation()
rt_quat = rt_matrix.ExtractRotationQuat()

from scipy.spatial.transform import Rotation

quat_xyzw = [
float(rt_quat.GetImaginary()[0]),
float(rt_quat.GetImaginary()[1]),
float(rt_quat.GetImaginary()[2]),
float(rt_quat.GetReal()),
]

mat = np.eye(4, dtype=np.float32)
mat[:3, :3] = Rotation.from_quat(quat_xyzw).as_matrix()
mat[:3, 3] = [float(pos[0]), float(pos[1]), float(pos[2])]

return np.linalg.inv(mat).astype(np.float32)
except Exception as e:
logger.warning(f"Failed to read target frame prim '{self._cfg.target_frame_prim_path}': {e}")
return None

# ------------------------------------------------------------------
# Controller button polling (glue between session and anchor manager)
# ------------------------------------------------------------------
Expand Down
Loading
Loading