Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
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 .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ jobs:
# trials each) are heavy and run in the nightly instead. No marker filter, so
# both subprocess and unit tests run.
env:
TEST_PATH: "autodata_tests/e2e/ autodata_tests/interfaces/ autodata_tests/core/ autodata_tests/utils/"
TEST_PATH: "autodata_tests/e2e/ autodata_tests/interfaces/ autodata_tests/core/ autodata_tests/utils/ autodata_tests/retargeting/"

steps:
- name: nvidia-smi
Expand Down
38 changes: 38 additions & 0 deletions autodata_core/datagen_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,44 @@ def __init__(
self.target_eef_pose = target_eef_pose
self.passthrough_action = passthrough_action

@classmethod
def from_episode(cls, ep_grp: dict, eef_name_map: dict[str, str] | None = None) -> DatagenInfo:
"""Decode one episode's ``obs/datagen_info/*`` group into a record.

The single decode of this group shared by :meth:`DataGenInfoPool._add_episode` and
``autodata_retargeting``'s ``source_datagen_poses`` (both previously decoded it by hand, which
could drift apart). Every field is read with ``.get`` -- absent -> ``None`` on the returned
record, consistent with every field here being optional -- so a caller that only needs one or
two fields (e.g. just ``eef_pose``) does not require the dataset to carry the full set
:class:`DataGenInfoPool` does; it asserts its own required fields.

Args:
ep_grp: One episode's raw data mapping (``EpisodeData.data``), with an ``"obs"`` entry.
eef_name_map: Optional ``{source_eef: target_eef}`` renaming applied to the eef-keyed
fields (``eef_pose``, ``target_eef_pose``); identity when omitted.

Returns:
A populated record (fields absent from the episode are ``None``); no ``passthrough_action``
(decoding that needs an embodiment adapter, not this episode's ``datagen_info``).
"""
obs = ep_grp.get("obs", {})
datagen_info = obs.get("datagen_info") if isinstance(obs, dict) else None
if not isinstance(datagen_info, dict):
raise ValueError("Episode lacks 'datagen_info' obs annotations")

def _rename(poses: dict[str, torch.Tensor] | None) -> dict[str, torch.Tensor] | None:
if poses is None or not eef_name_map:
return poses
return {eef_name_map.get(name, name): pose for name, pose in poses.items()}

return cls(
eef_pose=_rename(datagen_info.get("eef_pose")),
object_poses=datagen_info.get("object_pose"),
subtask_term_signals=datagen_info.get("subtask_term_signals"),
subtask_start_signals=datagen_info.get("subtask_start_signals"),
target_eef_pose=_rename(datagen_info.get("target_eef_pose")),
)

def to_dict(self) -> dict[str, Any]:
"""Materialize a dict with only the populated fields."""
out: dict[str, Any] = {}
Expand Down
27 changes: 8 additions & 19 deletions autodata_core/pool.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,25 +120,14 @@ async def add_episode(self, episode: EpisodeData) -> None:
def _add_episode(self, episode: EpisodeData) -> None:
ep_grp = episode.data

if "datagen_info" not in ep_grp["obs"]:
raise ValueError("Episode lacks 'datagen_info' obs annotations")

eef_pose = ep_grp["obs"]["datagen_info"]["eef_pose"]
object_poses_dict = ep_grp["obs"]["datagen_info"]["object_pose"]
target_eef_pose = ep_grp["obs"]["datagen_info"]["target_eef_pose"]
subtask_term_signals_dict = ep_grp["obs"]["datagen_info"]["subtask_term_signals"]
subtask_start_signals_dict = ep_grp["obs"]["datagen_info"].get("subtask_start_signals")

passthrough_actions = self.embodiment_adapter.actions_to_passthrough_actions(ep_grp["actions"])

ep_datagen_info = DatagenInfo(
eef_pose=eef_pose,
object_poses=object_poses_dict,
subtask_start_signals=subtask_start_signals_dict,
subtask_term_signals=subtask_term_signals_dict,
target_eef_pose=target_eef_pose,
passthrough_action=passthrough_actions,
)
ep_datagen_info = DatagenInfo.from_episode(ep_grp)
missing = [
name
for name in ("eef_pose", "object_poses", "target_eef_pose", "subtask_term_signals")
if getattr(ep_datagen_info, name) is None
]
assert not missing, f"Episode's 'obs/datagen_info' is missing required field(s): {missing}"
ep_datagen_info.passthrough_action = self.embodiment_adapter.actions_to_passthrough_actions(ep_grp["actions"])
self._datagen_infos.append(ep_datagen_info)

for eef_name in self.subtask_term_signal_names.keys():
Expand Down
22 changes: 22 additions & 0 deletions autodata_examples/embodiments/g1_ik_abs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -27,20 +27,32 @@ type: absolute_pose_whole_body_bimanual
name: g1
description: Unitree G1 humanoid with whole-body IK and three-finger hands, absolute-pose control.

# Each EEF's `offset` is the SE(3) offset from the tracked control-link frame (wrist_yaw_link) to a
# canonical grasp frame. Rotate-then-translate: rotate by axis_angle [rad], then translate [m] in that
# grasp frame. Retargeting extracts the trajectory in the canonical frame (source_pose @ offset) and
# re-anchors to the target's control link (@ inv(target offset)), so different grippers align at the
# grasp point (handles gripper length + wrist convention). Seeded from the control-link -> grasped-
# object transform at grasp; translation is shared L/R (same gripper), rotation is per-hand.
eefs:
left:
pose_obs_keys:
pos: left_eef_pos
quat: left_eef_quat
# 7 left-hand joints (index/middle/thumb proximal + distal + thumb tip)
gripper_action_indices: [0, 1, 2, 6, 7, 8, 12]
offset:
axis_angle: [1.2092, -1.2092, 1.2092]
translation: [0.0, -0.105, 0.03]

right:
pose_obs_keys:
pos: right_eef_pos
quat: right_eef_quat
# 7 right-hand joints (index/middle/thumb proximal + distal + thumb tip)
gripper_action_indices: [3, 4, 5, 9, 10, 11, 13]
offset:
axis_angle: [-1.2092, -1.2092, -1.2092]
translation: [0.0, 0.105, 0.03]

action_layout:
left_pose_slice: [0, 7]
Expand All @@ -50,3 +62,13 @@ action_layout:
# Non-eef passthrough channels: name -> contiguous action slice. Add more here freely.
passthrough_channels:
body: [28, 32] # [vel_x, vel_y, rot_vel_z, hip_height]

# Per-EEF open and closed hand joint configurations (7 hand joints each, in gripper_action_indices
# order). Used by retargeting's 'binary' / 'interpolation' hand policies to score the source hand's
# closedness. Measured from the G1 demos.
hand_open:
left: [0.0, 0.0, 0.0, 0.0, 0.0, 0.72, 0.0]
right: [0.0, 0.0, 0.0, 0.0, 0.0, -0.72, 0.0]
hand_close:
left: [-1.57, -1.57, 0.0, -1.74, -1.74, 1.04, 1.74]
right: [1.57, 1.57, .0, 1.74, 1.74, -1.04, -1.74]
20 changes: 20 additions & 0 deletions autodata_examples/embodiments/gr1_ik_abs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -18,23 +18,43 @@ type: absolute_pose_whole_body_bimanual
name: gr1t2
description: GR1 humanoid with whole-body IK and dexterous hands, absolute-pose control.

# Each EEF's `offset` is the SE(3) offset from the tracked control-link frame (hand_pitch_link) to a
# canonical grasp frame. See g1_ik_abs.yaml for the convention (rotate then translate; translation
# shared L/R, rotation per-hand). Seeded from the control-link -> grasped-object transform from the
# GR1 demos.
eefs:
left:
pose_obs_keys:
pos: left_eef_pos
quat: left_eef_quat
# 11 left-hand joints: 5 proximal + 5 intermediate + 1 distal
gripper_action_indices: [0, 1, 2, 3, 4, 10, 11, 12, 13, 14, 20]
offset:
axis_angle: [1.5708, 0.0, 0.0]
translation: [0.01, -0.08, 0.035]

right:
pose_obs_keys:
pos: right_eef_pos
quat: right_eef_quat
# 11 right-hand joints: 5 proximal + 5 intermediate + 1 distal
gripper_action_indices: [5, 6, 7, 8, 9, 15, 16, 17, 18, 19, 21]
offset:
axis_angle: [-1.5708, -0.0, -0.0]
translation: [0.01, 0.08, 0.035]

action_layout:
left_pose_slice: [0, 7]
right_pose_slice: [7, 14]
hand_joints_slice: [14, 36]
canonicalize_quat: true

# Per-EEF open and closed hand joint configurations (11 hand joints each, in gripper_action_indices
# order). Used by retargeting's 'binary' / 'interpolation' hand policies to score the source hand's
# closedness and drive the target hand between these configs. Measured from the GR1 demos.
hand_open: # fully extended (robot default)
left: [0.0, 0.0, 0.0, 0.0, -1.57, 0.0, 0.0, 0.0, 0.0, 0.5, -0.238]
right: [0.0, 0.0, 0.0, 0.0, -1.57, 0.0, 0.0, 0.0, 0.0, 0.5, -0.238]
hand_close: # full grip (limits, matching what the demos actually reach)
left: [-1.57, -1.57, -1.57, -1.57, -1.74, 0.35, 0.35, 0.35, 0.35, 1.22, -0.238]
right: [-1.57, -1.57, -1.57, -1.57, -1.74, 0.35, 0.35, 0.35, 0.35, 1.22, -0.238]
63 changes: 63 additions & 0 deletions autodata_examples/retarget/g1_to_gr1_pick_place.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0

# Retarget descriptor: Unitree G1 -> Fourier GR1T2 pick-and-place (bimanual humanoids).
#
# The right arm picks the object and transports it; the left arm does a support motion. Self-contained:
# the per-EEF `subtasks` below segment the source demo and track the carried object during transport
# (replacing the old auto carry-detection).

name: g1_to_gr1_pick_place
description: G1 -> GR1T2 humanoid picks an object with the right arm and transfers it.
write_datagen_info: true # write full datagen_info -> output is a drop-in generate_dataset source

source_embodiment: ../embodiments/g1_ik_abs.yaml
target_embodiment: ../embodiments/gr1_ik_abs.yaml
target_env_name: IsaacContrib-PickPlace-GR1T2-Abs

hand_policy: binary
init_robot_from_ik: false
replay_speed: 2.0
reference_pose: eef_pose
eef_reference_link: controlled
retarget_frame: world
scene_translation: [0.0, 0.0, 0.30]

segment_settle_steps: 0

default_object_tracking:
interpolation_step_start: 15
interpolation_step_after: 5

subtasks:
right:
- object_ref: world
description: Approach and grasp the object with the right hand.
subtask_end:
method: gripper_close
offset: 10

- frame_ref: world
description: Place the object in the bin.
subtask_end:
method: gripper_open
object_tracking: object

- object_ref: world
description: Go back to idle

left:
- object_ref: object
description: Approach and grasp the object with the left hand.
subtask_end:
method: gripper_close
offset: 10

- frame_ref: world
description: Move the object next to the right end
subtask_end:
method: gripper_open
object_tracking: object

- object_ref: world
description: Go back to idle
66 changes: 66 additions & 0 deletions autodata_examples/retarget/gr1_to_g1_pick_place.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0

# Retarget descriptor: Fourier GR1T2 -> Unitree G1 pick-and-place (bimanual humanoids).
#
# The right arm picks the object and transports it; the left arm does a support motion. Self-contained:
# the per-EEF `subtasks` below segment the source demo and track the carried object during transport
# (replacing the old auto carry-detection).

name: gr1_to_g1_pick_place
description: GR1T2 -> G1 humanoid picks an object with the right arm and transfers it.
write_datagen_info: true # write full datagen_info -> output is a drop-in generate_dataset source

source_embodiment: ../embodiments/gr1_ik_abs.yaml
target_embodiment: ../embodiments/g1_ik_abs.yaml
target_env_name: IsaacContrib-PickPlace-Locomanipulation-G1-Abs

hand_policy: binary
# The G1 target's locomanipulation action has a body channel; hold it at this default during replay.
target_channel_defaults:
body: [0.0, 0.0, 0.0, 0.72]
init_robot_from_ik: false
replay_speed: 0.5
reference_pose: target_eef_pose
eef_reference_link: controlled
retarget_frame: world
scene_translation: [0.0, 0.0, -0.30]

segment_settle_steps: 0

default_object_tracking:
interpolation_step_start: 15
interpolation_step_after: 5

subtasks:
right:
- object_ref: world
description: Approach and grasp the object with the right hand.
subtask_end:
method: gripper_close
offset: 10

- frame_ref: world
description: Place the object in the bin.
subtask_end:
method: gripper_open
object_tracking: object

- object_ref: world
description: Go back to idle

left:
- object_ref: object
description: Approach and grasp the object with the left hand.
subtask_end:
method: gripper_close
offset: 10

- frame_ref: world
description: Move the object next to the right end
subtask_end:
method: gripper_open
object_tracking: object

- object_ref: world
description: Go back to idle
52 changes: 38 additions & 14 deletions autodata_interfaces/datastream/datastream.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,43 @@
from autodata_utils.tensor_utils import as_torch


def scene_object_poses(
scene: Any,
index: slice | Sequence[int],
robot_asset_name: str,
reference_inverse: torch.Tensor | None = None,
) -> dict[str, torch.Tensor]:
"""Non-robot rigid and articulated object poses (SE(3)) read live from ``scene``.

The single implementation shared by :meth:`Datastream.get_object_poses` and
:mod:`autodata_retargeting`'s per-step datagen-info recorder, which need the same live-scene
reading (env-local root poses, optionally expressed in an embodiment's reference frame) and must
not be allowed to drift apart again (see git history of this function).

Args:
scene: The env's ``InteractiveScene``.
index: Environment selector into each asset's batched root-pose ``(num_envs, ...)`` tensors
-- a slice (``slice(None)`` for every env) or a sequence of env indices.
robot_asset_name: Scene key of the robot articulation, excluded from the result.
reference_inverse: Optional inverse reference-frame pose(s), left-multiplied into the result;
``None`` leaves poses in the scene's env-local frame.

Returns:
``{object_name: (*, 4, 4)}`` SE(3) poses, batch shape matching ``index``.
"""
env_origins = as_torch(scene.env_origins)[index]
assets = {**scene.rigid_objects, **getattr(scene, "articulations", {})}
poses: dict[str, torch.Tensor] = {}
for name, obj in assets.items():
if name == robot_asset_name:
continue
pos_rel = as_torch(obj.data.root_pos_w)[index] - env_origins
quat = as_torch(obj.data.root_quat_w)[index]
pose = pose_math.make_pose(pos_rel, pose_math.matrix_from_quat(quat))
poses[name] = reference_inverse @ pose if reference_inverse is not None else pose
return poses


class Datastream:
"""Aggregates the env, task descriptor, embodiment adapter, and DataGenInfoPool
behind one interface. The data generator uses this interface to access all static
Expand Down Expand Up @@ -255,21 +292,8 @@ def get_object_poses(self, env_ids: Sequence[int] | None = None) -> dict[str, to
"""Get non-robot rigid and articulated object poses in the embodiment's reference frame."""

index: slice | Sequence[int] = slice(None) if env_ids is None else env_ids
scene = self.env.scene
env_origins = scene.env_origins[index]
object_pose_matrix: dict[str, torch.Tensor] = {}
assets = {**scene.rigid_objects, **getattr(scene, "articulations", {})}
reference_inverse = self._reference_frame_inverse(env_ids)
robot_name = getattr(self.embodiment_adapter, "robot_asset_name", "robot")
for obj_name, obj in assets.items():
if obj_name == robot_name:
continue
pos_rel = as_torch(obj.data.root_pos_w)[index] - env_origins
quat = as_torch(obj.data.root_quat_w)[index]
object_pose_matrix[obj_name] = pose_math.make_pose(pos_rel, pose_math.matrix_from_quat(quat))
if reference_inverse is not None:
object_pose_matrix[obj_name] = reference_inverse @ object_pose_matrix[obj_name]
return object_pose_matrix
return scene_object_poses(self.env.scene, index, robot_name, self._reference_frame_inverse(env_ids))

def get_subtask_term_signals(
self, env_ids: Sequence[int] | None = None, obs_group: str = "subtask_terms"
Expand Down
12 changes: 12 additions & 0 deletions autodata_interfaces/embodiments/embodiment_adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,22 @@ class EmbodimentAdapter(ABC):
env: Live env handle, bound post-construction via :meth:`bind_env`. ``None`` until bound.
robot_asset_name: Scene key of the robot articulation this embodiment drives. Used by
joint-state queries. Subclasses may override (e.g. expose it as a config field).
raw_config: The embodiment's full parsed YAML/dict config, attached once by
:func:`~.factory.embodiment_adapter_from_dict` (``None`` for an adapter built directly,
not through the factory). Lets a consumer read a section the typed adapter fields don't
model -- e.g. retargeting's ``hand_open``/``hand_close`` hand postures and ``eef_offset``/
``eef_rotation``/``eefs.<eef>.offset`` grasp-frame alignment (see
``autodata_retargeting.gripper_retargeting.hand_postures_from_adapter`` and
``autodata_retargeting.eef_offset.eef_offset_from_adapter``) -- without re-opening and
re-parsing the same YAML file a second time. Note ``eef_offset`` on
:class:`~.single_arm_embodiment_adapter.SingleArmEmbodimentAdapter` is this exact
control-link -> observed-EEF translation read back out of ``raw_config`` by retargeting,
not a second, unrelated value of the same name.
"""

env: Any = None
robot_asset_name: str = "robot"
raw_config: dict[str, Any] | None = None

def get_navigation_eef_name(self) -> str | None:
"""Return the configured navigation channel without requiring a live environment."""
Expand Down
Loading
Loading