ActionCodec2 / runtime /backends /actioncodec2 /batch_semantics.py
ZibinDong's picture
Publish audited ActionCodec2 release files
1bfc3f0 verified
Raw History Blame Contribute Delete
10.6 kB
"""Canonical adapters shared by episode fitting and batched inference."""
from __future__ import annotations
from typing import TYPE_CHECKING
import numpy as np
from ._so3 import (
conjugate_quaternion,
multiply_quaternions,
normalize_quaternion,
quaternion_to_rotvec,
)
from .native_backend import native_kernel
from .semantic import (
difference_additive,
eef_pose_absolute,
eef_pose_components,
eef_pose_deltas,
eef_pose_from_components,
integrate_additive,
integrate_eef_deltas,
normalize_gripper_actions,
resample_uniform,
wrap_to_pi,
)
if TYPE_CHECKING:
from .presets import ComponentSpec, PresetConfig
def _resample(values, source_hz, target_hz, method, *, kind="linear", target_length=None):
source_hz, target_hz = float(source_hz), float(target_hz)
if source_hz == target_hz and target_length in (None, values.shape[1]):
return values
kernel = native_kernel("resample_uniform_batch")
if kernel is not None:
return kernel(
values,
source_hz,
target_hz,
kind,
-1 if target_length is None else target_length,
method,
)
return np.stack(
[
resample_uniform(
row,
source_hz=source_hz,
target_hz=target_hz,
kind=kind,
target_length=target_length,
method=method,
)
for row in values
]
)
def _pose_map(values, kernel_name, reference):
kernel = native_kernel(kernel_name)
return kernel(values) if kernel is not None else np.stack([reference(row) for row in values])
def _rotvec_poses(values):
kernel = native_kernel("eef_pose_from_rotvec_batch")
if kernel is not None:
return kernel(values)
flat = values.reshape(-1, 6)
return eef_pose_from_components(flat[:, :3], flat[:, 3:], "rotation_vector").reshape(
*values.shape[:2], 7
)
def _poses(values: np.ndarray, component: ComponentSpec) -> np.ndarray:
position = np.take(values, component.position_dims, axis=-1)
rotation = np.take(values, component.rotation_dims, axis=-1)
if component.rotation_format == "rotation_vector":
return _rotvec_poses(np.concatenate((position, rotation), axis=-1))
return eef_pose_from_components(
position.reshape(-1, 3), rotation.reshape(-1, rotation.shape[-1]), component.rotation_format
).reshape(*values.shape[:2], 7)
def _local(poses, anchor):
quaternion = normalize_quaternion(poses[..., 3:7])
inverse = conjugate_quaternion(normalize_quaternion(anchor[..., 3:7]))
relative = multiply_quaternions(inverse, quaternion)
return np.concatenate(
(poses[..., :3].astype(np.float64) - anchor[..., :3], quaternion_to_rotvec(relative)),
axis=-1,
).astype(np.float32)
def _joint(preset, values, component, source_hz, current_state):
source = np.take(values, component.joint_dims, axis=-1)
if preset.input == "delta":
source = integrate_additive(source, 1)
anchor = np.zeros((len(source), 1, source.shape[-1]), np.float32)
else:
anchor = (
source[:, :1]
if current_state is None
else np.take(current_state, component.joint_dims, axis=-1)[:, None]
)
joined = np.concatenate((anchor, source), axis=1)
if component.joint_wrap == "pi":
outside = np.abs(joined) > np.pi + 1e-5
if outside.any():
row = int(np.flatnonzero(outside.any(axis=(1, 2)))[0])
column = component.joint_dims[int(np.argmax(outside[row].any(axis=0)))]
raise ValueError(
f"action space {preset.name!r} declares joint_wrap='pi' for component "
f"{component.name!r}, so its absolute joint columns must lie in (-pi, pi]; "
f"source column {column} reaches {joined[row][outside[row]][0]:.6g} rad. Wrap the "
"inputs, or register a layout with joint_wrap='none' for coordinates that "
"are continuous past one turn, such as a multi-turn or prismatic axis."
)
batch, steps, width = joined.shape
kernel = native_kernel("unwrap_angles")
unwrapped = (
(
kernel(joined.transpose(1, 0, 2).reshape(steps, batch * width), True)
.reshape(steps, batch, width)
.transpose(1, 0, 2)
)
if kernel is not None
else np.unwrap(joined, axis=1)
)
anchor, source = unwrapped[:, :1], unwrapped[:, 1:]
return np.asarray(
_resample(source, source_hz, preset.target_hz, preset.interpolation) - anchor,
dtype=np.float32,
)
def _eef(preset, values, component, source_hz, current_state):
if preset.input == "delta":
delta = np.concatenate(
(
np.take(values, component.position_dims, axis=-1),
np.take(values, component.rotation_dims, axis=-1),
),
axis=-1,
)
poses = _pose_map(delta, "integrate_eef_deltas_batch", integrate_eef_deltas)
sampled = _resample(
poses, source_hz, preset.target_hz, preset.interpolation, kind="pose_xyzw"
)
return _pose_map(sampled, "eef_pose_absolute_batch", eef_pose_absolute)
poses = _poses(values, component)
anchor = poses[:, :1] if current_state is None else _poses(current_state[:, None], component)
sampled = _resample(poses, source_hz, preset.target_hz, preset.interpolation, kind="pose_xyzw")
return _local(sampled, anchor)
def canonicalize_batch(preset: PresetConfig, values, *, source_hz, current_state):
"""Return component ``(B,T_codec,D_active)`` arrays from validated input."""
return {
component.name: canonicalize_component_batch(
preset, values, component, source_hz=source_hz, current_state=current_state
)
for component in preset.components
}
def canonicalize_component_batch(preset, values, component, *, source_hz, current_state):
"""Convert one component of validated ``(B,T,D)`` source actions."""
motion = (_joint if component.space == "joint" else _eef)(
preset, values, component, source_hz, current_state
)
if component.gripper_dim is None:
return motion
gripper = values[..., component.gripper_dim : component.gripper_dim + 1]
if component.gripper_threshold is not None:
gripper = ((gripper >= component.gripper_threshold) if component.gripper_open_is_high
else (gripper <= component.gripper_threshold)).astype(np.float32)
else:
gripper = normalize_gripper_actions(
gripper.reshape(-1, 1),
closed_value=component.gripper_closed_value,
open_value=component.gripper_open_value,
).reshape(*values.shape[:2], 1)
gripper = _resample(gripper, source_hz, preset.target_hz, preset.interpolation, kind="hold")
return np.concatenate((motion, gripper), axis=-1)
def _external_eef(motion, component, current_state):
poses = _rotvec_poses(motion)
if current_state is None:
return _pose_map(poses, "eef_pose_deltas_batch", eef_pose_deltas)
anchor = _poses(current_state[:, None], component)
quaternion = multiply_quaternions(
normalize_quaternion(anchor[..., 3:7]), normalize_quaternion(poses[..., 3:7])
)
attached = np.concatenate(
(poses[..., :3].astype(np.float64) + anchor[..., :3], quaternion), axis=-1
).astype(np.float32)
return eef_pose_components(attached.reshape(-1, 7), component.rotation_format).reshape(
*attached.shape[:2], 3 + len(component.rotation_dims)
)
def externalize_batch(
values, component: ComponentSpec, *, source_hz, target_hz, target_length, method, current_state
):
"""Resample decoded paths and return ``(B,T_output,D_component)`` arrays."""
sampled = resample_canonical_batch(
values,
component,
source_hz=source_hz,
target_hz=target_hz,
target_length=target_length,
method=method,
)
return externalize_canonical_batch(sampled, component, current_state=current_state)
def resample_canonical_batch(
values, component: ComponentSpec, *, source_hz, target_hz, target_length, method
):
"""Rate-convert local ``(B,T,D_active)`` paths, holding gripper commands."""
motion_width = len(component.joint_dims or ()) if component.space == "joint" else 6
motion = values[..., :motion_width]
if component.space == "eef":
poses = _rotvec_poses(motion)
poses = _resample(
poses, source_hz, target_hz, method, kind="pose_xyzw", target_length=target_length
)
motion = _pose_map(poses, "eef_pose_absolute_batch", eef_pose_absolute)
else:
motion = _resample(motion, source_hz, target_hz, method, target_length=target_length)
if component.gripper_dim is None:
return np.asarray(motion, dtype=np.float32)
gripper = _resample(
values[..., motion_width : motion_width + 1],
source_hz,
target_hz,
method,
kind="hold",
target_length=target_length,
)
return np.asarray(np.concatenate((motion, gripper), axis=-1), dtype=np.float32)
def externalize_canonical_batch(values, component: ComponentSpec, *, current_state):
"""Restore source coordinates or deltas from ``(B,T,D_active)`` local paths."""
motion_width = len(component.joint_dims or ()) if component.space == "joint" else 6
motion = values[..., :motion_width]
if component.space == "eef":
motion = _external_eef(motion, component, current_state)
elif current_state is None:
motion = difference_additive(motion, 1)
else:
motion = motion + np.take(current_state, component.joint_dims, axis=-1)[:, None]
if component.joint_wrap == "pi":
motion = wrap_to_pi(motion)
if component.gripper_dim is None:
return np.asarray(motion, dtype=np.float32)
gripper = values[..., motion_width : motion_width + 1]
if component.gripper_threshold is not None:
gripper = gripper if component.gripper_open_is_high else 1.0 - gripper
return np.asarray(np.concatenate((motion, gripper), axis=-1), dtype=np.float32)
gripper = component.gripper_closed_value + gripper * (
component.gripper_open_value - component.gripper_closed_value
)
return np.asarray(np.concatenate((motion, gripper), axis=-1), dtype=np.float32)